Skip to content

Compiler and Validation Rules

theirish81 edited this page Sep 3, 2026 · 1 revision

Compiler and Validation Rules

The FML compiler enforces strict static and runtime validation rules to ensure deterministic execution, prevent infinite loops, and eliminate runtime LLM formatting errors.


The 11 Core Compiler Constraints

1. Built-in Search Declaration Rule

  • Rule: Never declare require mcp search or require search at the file level.
  • Explanation: Web search is a native, built-in capability of the FML runtime.
  • Correction: Use use search inside the individual session blocks where search is needed.
# ❌ INCORRECT:
require mcp search

# ✅ CORRECT:
session("research") {
    use search
}

2. Mandatory Upstream Dependency Declaration (after)

  • Rule: Any session that references context.<session_name> inside its expect, iterate, or template strings MUST declare after="<session_name>".
  • Explanation: FML builds an execution DAG. If a dependency is undeclared, the runtime cannot guarantee that the upstream session has completed before the downstream session begins.
# ❌ INCORRECT:
session("format_data") {
    context "Previous: {{ .context.fetch_data | json }}" # ERROR: fetch_data not declared in after!
}

# ✅ CORRECT:
session("format_data", after="fetch_data") {
    context "Previous: {{ .context.fetch_data | json }}"
}

3. Mandatory Array Schema for iterate Sessions

  • Rule: If a session declares the iterate attribute, its schema must be an array type (e.g., schema type[] or schema { ... }[]).
  • Explanation: When iterating over a collection, the runtime executes the session once for each element and sequentially appends each output to an array in the exact order of the input items.
# ❌ INCORRECT:
session("process_batch", iterate=context.items) {
    schema { id: string } # ERROR: Must be an array schema!
}

# ✅ CORRECT:
session("process_batch", iterate=context.items) {
    schema { id: string }[] # OR schema string[]
}

4. Strict Prompt Limits: Exactly One - per Session

  • Rule: Each session must have exactly one prompt prefixed with -.
  • Explanation: Pre-prompts (+) allow tool use and free-form reasoning. The single prompt (-) triggers the final schema-constrained generation where tool schemas are stripped. Having multiple prompts would create ambiguity regarding which prompt governs the schema output.
# ❌ INCORRECT:
session("example") {
    - First prompt.
    - Second prompt. # ERROR: Only one prompt allowed!
    schema { id: string }
}

# ✅ CORRECT:
session("example") {
    + Free-form pre-prompt (Tools enabled)
    + Another pre-prompt (Tools enabled)
    - Final prompt (Tools disabled)
    schema { id: string }
}

5. Flat Root Schemas (Avoid Redundant Nesting)

  • Rule: Do not wrap schema properties inside a redundant parent object named after the session.
  • Explanation: Session outputs are already automatically namespaced under context.<session_name>. Wrapping fields in an extra nested object creates unnecessary property depth (e.g., context.user.user.name).
# ❌ INCORRECT:
session("user_details") {
    schema {
        user_details: {
            username: string
        }
    }
}

# ✅ CORRECT:
session("user_details") {
    schema {
        username: string
    }
}

6. Transformer Exclusivity

  • Rule: A transformer block can define either jmesPath or code, but never both.
  • Explanation: A tool output can only undergo one primary transformation pipeline.
# ❌ INCORRECT:
transformer("bad_transformer") {
    onFunctionOutput = "query"
    jmesPath = "[*].id"
    code((args.map(x => x.id))) # ERROR: Mutually exclusive!
}

7. Cascading Skips on expect Evaluation

  • Rule: If a session's expect condition evaluates to false, that session is skipped completely. Any downstream sessions depending on it via after are automatically skipped in cascade.
  • Explanation: Downstream sessions cannot satisfy their data requirements if the upstream provider was skipped.
session("optional_stage", expect="params.enable_deep_scan") {
    # If enable_deep_scan is false, this is skipped...
}

session("report_stage", after="optional_stage") {
    # ...which automatically skips this stage as well!
}

8. Schema Enum / Union Syntax

  • Rule: Enum or string union fields must use the vertical bar (|) syntax without quotes around individual choices.
  • Explanation: Standardized syntax for categorical fields.
schema {
    severity: low|medium|high|critical # Valid enum
    tags: ai|cloud|security[]          # Valid array of enum items
}

9. Target Requirement on Global Calls

  • Rule: All global file-level call statements must define an explicit output target routing (-> vars:name or -> name).
  • Explanation: Session-level calls can omit target routing because the engine injects their output into the session's ambient context. At the root level, there is no ambient session context, so target routing is mandatory.
# ❌ INCORRECT:
call("pre_warm") { # ERROR: Missing target routing!
    env = "prod"
}

# ✅ CORRECT:
call("pre_warm") -> vars:prewarm_result {
    env = "prod"
}

10. The context Block Scope & Antipattern

  • Rule: The context block (e.g., context "...") is evaluated during session initialization. It must only reference completed upstream session outputs ({{ .context.completed_session }}).
  • Antipattern: Referencing local variables populated by PreCalls in the current session inside a context statement is an antipattern. PreCalls execute after session initialization, so local variables are not yet defined when context is evaluated.
  • Correction: Reference local PreCall variables directly inside pre-prompts (+) or prompts (-), or omit the PreCall's target routing so it is injected automatically.
# ❌ INCORRECT:
session("audit") {
    call("fetch_logs") -> vars:local_logs { limit = 100 }
    context "Logs: {{ .vars.local_logs | json }}" # FAIL: Evaluated before fetch_logs runs!
}

# ✅ CORRECT:
session("audit") {
    call("fetch_logs") -> vars:local_logs { limit = 100 }
    + Review the fetched logs: {{ .vars.local_logs | json }}
    - Summarize anomalies.
    schema { anomalies: string[] }
}

11. Context Routing Restriction in Plan Generation

  • Rule: When generating plans, avoid routing PreCalls directly to the context namespace (-> context:key).
  • Explanation: -> context:key tightly couples tool outputs to the public session bus, bypassing session boundary validation. Use -> vars:name, implicit routing -> name, or omit routing instead.

Target Routing Syntax: Colon vs. Dot

Routing Expression Validity Consequence
-> vars:result Valid Stored in vars.result
-> result Valid Implicitly stored in vars.result
-> context:result Valid Stored in context.result
-> vars.result INVALID Syntax Parse Error (dots not permitted in target arrows)
-> context.result INVALID Syntax Parse Error (dots not permitted in target arrows)

Expression Leading Dot Rules

Go Templates ({{ ... }}):
  ✔ {{ .params.x }}       (Leading dot REQUIRED)
  ✖ {{ params.x }}        (Syntax error / evaluates to empty)

Antonmedv Expr ($( ... ), expect, iterate):
  ✔ $(vars.x)             (No leading dot)
  ✔ expect="params.x > 0" (No leading dot)
  ✖ $(.vars.x)            (Syntax error)
  ✖ expect=".params.x > 0"(Syntax error)

Diagnostic & Error Troubleshooting Checklist

Error / Symptom Probable Cause Fix
Unknown tool 'search' Used require mcp search Remove require, use use search inside session.
Unsatisfied dependency: foo Missing after="foo" on session Add after="foo" attribute.
Iterate session must produce array Schema is an object (schema { ... }) Change schema to array (schema { ... }[] or schema string[]).
Multiple prompts defined Defined more than one - prompt Change upstream prompts to pre-prompts (+).
Global call missing target call("...") { ... } at file root Add -> vars:name output routing.
Empty context variable Referenced local PreCall variable in context statement Reference variable in + or - prompt instead.
Tool call failed during prompt Instructed LLM to use tools in - prompt Move tool instructions to + pre-prompt.

Clone this wiki locally