Laurel User Guide

3.4. Typing rules🔗

Each construct is given as a derivation. Γ is the current lexical scope (see ResolveState's scope); it threads identically through every premise and conclusion unless a rule explicitly extends it (written Γ, x : T).

Each rule is tagged with [⇒] (synthesis) or [⇐] (checking) to make the direction explicit. The Procedure rule is the one exception: it is a top-level well-formedness judgment and carries no direction tag.

The following notation recurs throughout the rules:

  • A <: B — subtyping (isSubtype); see Gradual typing above. In a checking premise or side condition (e.g. [⇐] Sub, [⇐] If-NoElse, [⇐] Assign, the check-mode operator rules, and [⇐] Hole-Some) the boundary check is the gradual consistent-subtype relation <:_\sim below — the implementation routes every such check through isConsistentSubtype, never bare <: — so \mathsf{Unknown} is admitted on either side.

  • A \sim B — the consistency relation isConsistent: symmetric, with \mathsf{Unknown} acting as a wildcard.

  • A <:_\sim B — the consistent-subtype relation isConsistentSubtype, the gradual combination of the two above.

  • \mathsf{Numeric}\;T — a predicate holding when T is consistent with one of \mathsf{TInt}, \mathsf{TReal}, \mathsf{TFloat64}, or \mathsf{TBv}_w (a bitvector of any width w), with \mathsf{Unknown} admitted as the gradual escape hatch.

  • \dashv \Gamma' — a rule's output scope: the judgment threads \Gamma in and produces \Gamma' out. Only the declaring rules extend the scope — [⇒] Var-Declare / Var-Declare-Infer, the Decl-Synth pair, and [⇒]/[⇐] Assign when a target is a Declare; the block rules thread it statement-to-statement (the \Gamma_{i-1} \to \Gamma_i chain in [⇐] Block / [⇒] Block-Synth).

  • \rightsquigarrow \text{error: …} — the rule emits an error and aborts; no type is produced.

  • [\text{emits …}] — the rule produces its type but also emits a diagnostic.

  • \mapsto — elaboration: the construct is rewritten to the form on the right.

The Index below links to each construct's subsection.

3.4.1. Index🔗

  • Subsumption — [⇐] Sub

  • Literals — [⇒] Lit-Int, [⇒] Lit-Bool, [⇒] Lit-String, [⇒] Lit-Decimal

  • Variables — [⇒] Var-Local, [⇒] Var-Field, [⇒] Var-Declare, [⇒] Var-Declare-Infer

  • Control flow — [⇐] If, [⇐] If-NoElse, [⇒] If-Synth, [⇒] If-Synth-NoElse; [⇐] Block, [⇒] Block-Synth, [⋄] Stmt, [⇒] Empty-Block; [⇒] Exit; [⇒] Return-None-Void, [⇒] Return-None-Single, [⇒] Return-None-Multi, [⇒] Return-Some, [⇒] Return-Void-Error, [⇒] Return-Multi-Error; [⇒] While

  • Verification statements — [⇒] Assert, [⇒] Assume

  • Assignment — [⇒] Assign, [⇐] Assign, [⇒] Decl-Synth, [⇐] Decl-Synth

  • Calls — [⇒] Static-Call, [⇒] Static-Call-Multi, [⇒] Instance-Call, [⇒] Instance-Call-Multi

  • Operators — no operator-specific rules: operators are calls, typed by [⇒] Static-Call. Equality is the one special case: [⇒] Op-Eq

  • Object forms — [⇒] New-Ok, [⇒] New-Fallback; [⇒] AsType; [⇒] IsType; [⇒] RefEq; [⇒] PureFieldUpdate

  • Verification expressions — [⇒] Quantifier, [⇒] Assigned, [⇐] Old, [⇒] Old-Synth, [⇒] Fresh, [⇐] ProveBy, [⇒] ProveBy-Synth

  • Self reference — [⇒] This-Inside, [⇒] This-Outside

  • Untyped forms — [⇒] Abstract / All

  • ContractOf — [⇒] ContractOf-Bool, [⇒] ContractOf-Set, [⇒] ContractOf-Error

  • Holes — [⇐] Hole-Some, [⇐] Hole-None, [⇒] Hole-Synth-None, [⇒] Hole-Synth-Some

  • Procedure — Procedure

3.4.2. Subsumption🔗

\frac{\Gamma \vdash e \Rightarrow A \quad A <: B}{\Gamma \vdash e \Leftarrow B} \quad \text{([⇐] Sub)}

Fallback in Check.resolveStmtExpr whenever no bespoke check rule applies.

3.4.3. Literals🔗

\frac{}{\Gamma \vdash \mathsf{LiteralInt}\;n \Rightarrow \mathsf{TInt}} \quad \text{([⇒] Lit-Int)}

🔗def
Strata.Laurel.Resolution.Synth.litInt (v : Int) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd
Strata.Laurel.Resolution.Synth.litInt (v : Int) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd

Γ ⊢ LiteralInt n ⇒ TInt

\frac{}{\Gamma \vdash \mathsf{LiteralBool}\;b \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Lit-Bool)}

🔗def
Strata.Laurel.Resolution.Synth.litBool (v : Bool) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd
Strata.Laurel.Resolution.Synth.litBool (v : Bool) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd

Γ ⊢ LiteralBool b ⇒ TBool

\frac{}{\Gamma \vdash \mathsf{LiteralString}\;s \Rightarrow \mathsf{TString}} \quad \text{([⇒] Lit-String)}

🔗def
Strata.Laurel.Resolution.Synth.litString (v : String) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd
Strata.Laurel.Resolution.Synth.litString (v : String) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd

Γ ⊢ LiteralString s ⇒ TString

\frac{}{\Gamma \vdash \mathsf{LiteralDecimal}\;d \Rightarrow \mathsf{TReal}} \quad \text{([⇒] Lit-Decimal)}

🔗def
Strata.Laurel.Resolution.Synth.litDecimal (v : StrataDDM.Decimal) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd
Strata.Laurel.Resolution.Synth.litDecimal (v : StrataDDM.Decimal) (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd

Γ ⊢ LiteralDecimal d ⇒ TReal

3.4.4. Variables🔗

\frac{\Gamma(x) = T}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Local}\;x) \Rightarrow T} \quad \text{([⇒] Var-Local)}

🔗def
Strata.Laurel.Resolution.Synth.varLocal (ref : Strata.Laurel.Identifier) (source : Strata.FileRange) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.varLocal (ref : Strata.Laurel.Identifier) (source : Strata.FileRange) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Var-Local)

Γ(x) = T
──────────────────────
Γ ⊢ Var (.Local x) ⇒ T

Resolves ref against the lexical scope and reads its declared type.

\frac{\Gamma \vdash e \Rightarrow \_ \quad \Gamma(f) = T_f}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Field}\;e\;f) \Rightarrow T_f} \quad \text{([⇒] Var-Field)}

🔗def
Strata.Laurel.Resolution.Synth.varField (exprMd target : Strata.Laurel.StmtExprMd) (fieldName : Strata.Laurel.Identifier) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Var (Strata.Laurel.Variable.Field target fieldName)) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.varField (exprMd target : Strata.Laurel.StmtExprMd) (fieldName : Strata.Laurel.Identifier) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Var (Strata.Laurel.Variable.Field target fieldName)) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Var-Field)

Γ ⊢ e ⇒ _
Γ(f) = T_f
───────────────────────────
Γ ⊢ Var (.Field e f) ⇒ T_f

f is looked up against the type of e.

\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, \mathsf{some}\;T_x\rangle) \Rightarrow \mathsf{TVoid} \quad \dashv \quad \Gamma, x : T_x} \quad \text{([⇒] Var-Declare)}

\frac{x \notin \mathrm{dom}(\Gamma)}{\Gamma \vdash \mathsf{Var}\;(\mathsf{.Declare}\;\langle x, \mathsf{none}\rangle) \Rightarrow \mathsf{TVoid} \quad [\text{emits “cannot infer a type …”}] \quad \dashv \quad \Gamma, x : \mathsf{Unknown}} \quad \text{([⇒] Var-Declare-Infer)}

The type annotation is optional in the AST (type : Option). A bare var x (annotation none) has neither an annotation nor an initializer to read a type from, so [⇒] Var-Declare-Infer diagnoses it and binds x : \mathsf{Unknown} so later uses of x don't cascade further type errors. An unannotated declaration with an initializer (var x := e) never reaches these rules: it parses as an Assign with a sole Declare target and is handled by the [⇒]/[⇐] Decl-Synth rules (see Assignment), which recover the binding's type from the initializer. Either way the node is rewritten to a fully-annotated Declare x (some T), so no none annotation survives resolution.

x \notin \mathrm{dom}(\Gamma) is a soft side condition rather than a hard premise: when x is already bound in the current scope the rule still fires, [\text{emits “Duplicate definition …”}], and extends the scope — but with an unresolved placeholder instead of x : T_x, so later uses of x don't cascade further type errors.

🔗def
Strata.Laurel.Resolution.Check.varDeclare (param : Strata.Laurel.Parameter?) (source : Strata.FileRange) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.varDeclare (param : Strata.Laurel.Parameter?) (source : Strata.FileRange) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Var-Declare)

x ∉ dom(Γ)                                       (annotated, type = some T_x)
────────────────────────────────────────────────────
Γ ⊢ Var (.Declare x (some T_x)) ⇒ TVoid ⊣ Γ, x : T_x

x ∉ dom(Γ)                                       (Var-Declare-Infer, type = none)
────────────────────────────────────────────────────
Γ ⊢ Var (.Declare x none) ⇒ TVoid ⊣ Γ, x : Unknown

⊣ Γ, x : T_x records that the surrounding scope is extended with the new binding for the remainder of the enclosing block. The declaration itself does no work other than registering x : T_x, and yields no value, so it synthesizes TVoid.

When the annotation is absent (type = none, i.e. surface var x), there is neither an annotation nor an initializer to read a type from — var x := e is handled in Synth.declInfer/Check.declInfer by synthesizing e — so this rule emits a "cannot infer a type" diagnostic (binding x : Unknown, so that later uses of x do not cascade further type errors). Either way the node is rewritten to a fully-typed Declare x (some T), so no none annotation survives resolution.

x ∉ dom(Γ) is a soft side condition, not a hard premise: when x is already bound in the current scope, defineNameCheckDup emits a "Duplicate definition '<x>' is already defined in this scope" diagnostic and still extends the scope — but with an unresolved placeholder rather than x : T_x, so later uses of x do not cascade further type errors.

3.4.5. Control flow🔗

\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \Gamma \vdash \mathit{elseBr} \Leftarrow T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Leftarrow T} \quad \text{([⇐] If)}

\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Leftarrow T \quad \mathsf{TVoid} <: T}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Leftarrow T} \quad \text{([⇐] If-NoElse)}

🔗def
Strata.Laurel.Resolution.Check.ifThenElse (exprMd cond thenBr : Strata.Laurel.StmtExprMd) (elseBr : Option Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.IfThenElse cond thenBr elseBr) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.ifThenElse (exprMd cond thenBr : Strata.Laurel.StmtExprMd) (elseBr : Option Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.IfThenElse cond thenBr elseBr) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(If / If-NoElse)

Γ ⊢ cond ⇐ TBool                                            (If)
Γ ⊢ thenBr ⇐ T
Γ ⊢ elseBr ⇐ T
──────────────────────────────────────────────────────────────────
Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇐ T

Γ ⊢ cond ⇐ TBool                                            (If-NoElse)
Γ ⊢ thenBr ⇐ T
TVoid <: T
──────────────────────────────────────────────────────────────────
Γ ⊢ IfThenElse cond thenBr none ⇐ T

Pushes the surrounding T into both branches (rather than going through If-Synth + Sub at the boundary): errors fire at the offending branch instead of at the if, and the expectation propagates through nested Block / IfThenElse / Hole constructs that have their own check rules.

When T = TVoid (the if is a statement, see Check.statement) each arm is a statement as well (Γ ⊢ thenBr ⋄, Γ ⊢ elseBr ⋄), exactly as Check.block treats the last statement of a block in statement position.

Without an else, the implicit branch is an empty block of type TVoid, so the rule degenerates to require TVoid <: T — the standard [⇐] Sub boundary check that Resolution.Synth.emptyBlock composes with for an empty block.

When an if appears in operand position — where no expected type is available to push down (e.g. as an operand of == / < / +\!+, whose operands are synthesized) — the synth counterpart fires instead. With an else, both branches are synthesized and their types must be mutually consistent (\sim, the symmetric gradual relation); inconsistent branches [\text{emits “'if' branches have incompatible types X and Y”}] and synthesize \mathsf{Unknown}. The result is the join T_t \sqcup T_e of the two branch types, so when one branch is a hole (\mathsf{Unknown}) the join promotes to the other branch's concrete type, and the synthesized type is independent of branch order. Without an else, the missing branch cannot produce a value, so the if synthesizes \mathsf{TVoid}.

`if` in operand position
  • (if c then 1 else 2) == y — both branches \mathsf{TInt}, so the if synthesizes \mathsf{TInt}

  • if c then 1 else <?> — the hole branch promotes; synthesizes \mathsf{TInt}

  • if c then 1 else "x" — incompatible branches: 'if' branches have incompatible types 'int' and 'string', synthesizes \mathsf{Unknown}

  • if c then 1 (no else) — synthesizes \mathsf{TVoid}

\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow T_t \quad \Gamma \vdash \mathit{elseBr} \Rightarrow T_e \quad T_t \sim T_e}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;(\mathsf{some}\;\mathit{elseBr}) \Rightarrow T_t \sqcup T_e} \quad \text{([⇒] If-Synth)}

\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{thenBr} \Rightarrow \_}{\Gamma \vdash \mathsf{IfThenElse}\;\mathit{cond}\;\mathit{thenBr}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] If-Synth-NoElse)}

🔗def
Strata.Laurel.Resolution.Synth.ifThenElse (exprMd cond thenBr : Strata.Laurel.StmtExprMd) (elseBr : Option Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.IfThenElse cond thenBr elseBr) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.ifThenElse (exprMd cond thenBr : Strata.Laurel.StmtExprMd) (elseBr : Option Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.IfThenElse cond thenBr elseBr) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(If-Synth)

Γ ⊢ cond ⇐ TBool   Γ ⊢ thenBr ⇒ T_t   Γ ⊢ elseBr ⇒ T_e
T_t ~ T_e   T = T_t ⨆ T_e (consistency join)                (If-Synth)
──────────────────────────────────────────────────────────────────────────
Γ ⊢ IfThenElse cond thenBr (some elseBr) ⇒ T

Γ ⊢ cond ⇐ TBool   Γ ⊢ thenBr ⇒ _                          (If-Synth-NoElse)
──────────────────────────────────────────────────────────────────────────
Γ ⊢ IfThenElse cond thenBr none ⇒ TVoid

Synth-mode rule for an if used where no expected type is available (e.g. as an operand of ==/</++, whose operands are synthesized). cond is checked against TBool; both branches are synthesized. With an else, the two branch types must be mutually consistent (isConsistent, the symmetric gradual relation — Unknown flows freely either way); when consistent, the result is their symmetric join (Unknown ⊔ T = T), so a hole branch promotes to the other branch's concrete type and the synthesized type is independent of branch order. (isConsistent stays the accept/reject gate: it admits a gradual corner where join is none, for which the result falls back to the then-branch type, leaving that boundary unchanged.) Inconsistent branches (e.g. if c then 1 else "x") emit a diagnostic and synthesize Unknown to suppress cascading errors. Without an else, the if cannot produce a value on the missing branch, so it synthesizes TVoid.

This is the synth counterpart to Check.ifThenElse: when an expected type is available the dispatcher prefers the check rule (pushing the type into both branches); this rule fires only at the synth wildcard.

expected? is the caller's slot, threaded into both branches exactly as Check.ifThenElse pushes its expected. An if is a check-capable wrapper, so a generic call inside a branch must see the slot that a direct one would (identity(if c then setEmpty() else setEmpty())). It only sharpens inference; the join below is still what types the if.

A non-empty block is typed by splitting its statement list into the last statement and the statements before it. The last statement carries the block's value and inherits the surrounding expected type; each earlier statement runs only for its effect — written \Gamma \vdash s\;\diamond (effect position: the statement's value is discarded). The check and synth rules share this shape, differing only in how the last statement is treated:

\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \;\diamond \;\dashv\; \Gamma_i \;\;(1 \le i \le n) \quad \Gamma_n \vdash \mathit{last} \Leftarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n; \mathit{last}]\;\mathit{label} \Leftarrow T} \quad \text{([⇐] Block)}

\frac{\Gamma_0 = \Gamma \quad \Gamma_{i-1} \vdash s_i \;\diamond \;\dashv\; \Gamma_i \;\;(1 \le i \le n) \quad \Gamma_n \vdash \mathit{last} \Rightarrow T}{\Gamma \vdash \mathsf{Block}\;[s_1; \ldots; s_n; \mathit{last}]\;\mathit{label} \Rightarrow T} \quad \text{([⇒] Block-Synth)}

[⇐] Block fires whenever an expected type T is supplied (procedure bodies, branches, loop bodies, assignment RHS, call arguments); [⇒] Block-Synth fires in operand position, where no expected type is available (e.g. \{\,x := 1;\; x\,\} == y), synthesizing the last statement's type as the block's value type.

When the block itself sits in statement position (T = \mathsf{TVoid}) the last statement is in effect position too: its premise becomes \mathit{last}\;\diamond rather than \mathit{last} \Leftarrow \mathsf{TVoid}, so a trailing call discards its result and \{\ldots;\,\mathit{foo}()\} type-checks as a statement even when foo returns a non-void type.

The effect-position judgment \Gamma \vdash s\;\diamond has two cases. An IfThenElse or a non-empty Block is checked against \mathsf{TVoid}, the marker for statement position, so that its arms (rule [⇐] If) and its last statement (rule [⇐] Block) are in effect position in turn and no type is ever joined or compared for them. Every other form is synthesized and its result discarded:

\frac{\Gamma \vdash s \Leftarrow \mathsf{TVoid} \;\dashv\; \Gamma'}{\Gamma \vdash s \;\diamond \;\dashv\; \Gamma'} \quad s \text{ an } \mathit{if} \text{ or a non-empty block} \qquad \frac{\Gamma \vdash s \Rightarrow \_ \;\dashv\; \Gamma'}{\Gamma \vdash s \;\diamond \;\dashv\; \Gamma'} \quad \text{otherwise} \qquad \text{([⋄] Stmt)}

Statement-shaped forms (Var-Declare, Assert, Assume, While, Exit, Return) synthesize \mathsf{TVoid}; value-producing forms (calls, Assign, IncrDecr, literals, etc.) synthesize their natural type, which is then discarded. This means any expression is accepted in statement position — the f(x); idiom works regardless of f's return type, and x++; is admitted even though ++ synthesizes the target's type. A statement if c then { go := false } else { steps := steps + 1 } is accepted whatever the arms' types, because nobody consumes its value; only an if whose value is used ([⇒] If-Synth) joins its branch types. A trailing generic call such as mk() is synthesized with no expected type, so a type argument that nothing determines is still reported.

Only declarations actually extend the scope \Gamma_i — Var (.Declare …) and Assign statements with Declare targets (var x := e, assign var x, y := call()); every other statement leaves it unchanged. The block opens a fresh nested scope, so declarations made inside don't leak out — once the block ends, the surrounding \Gamma is restored. It also emits a "dead code after '<terminator>'" diagnostic when an Exit or Return is followed by further statements in the same block.

Pushing T into the last statement (rather than synthesizing the whole block and applying [⇐] Sub at the boundary) means a type mismatch is reported at the offending subexpression's source location, and the expectation keeps propagating through nested Block / IfThenElse / Hole / Quantifier constructs that have their own check rules.

\frac{}{\Gamma \vdash \mathsf{Block}\;[]\;\mathit{label} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Empty-Block)}

The empty block has a fixed type and is the only block-level rule that synthesizes unconditionally. [⇐] Block and [⇒] Block-Synth always split off a last statement, so they never reach an empty list; the empty case is hit only when the block is literally empty at the dispatch site. When an empty block appears in check position with expected ≠ TVoid, the standard [⇐] Sub rule fires at the boundary (Check.resolveStmtExpr's subsumption-fallback wildcard arm, requiring \mathsf{TVoid} <: \mathit{expected}).

🔗def
Strata.Laurel.Resolution.Synth.emptyBlock (source : Strata.FileRange) : Strata.Laurel.HighTypeMd
Strata.Laurel.Resolution.Synth.emptyBlock (source : Strata.FileRange) : Strata.Laurel.HighTypeMd

(Empty-Block)

─────────────────────────────────
Γ ⊢ Block [] label ⇒ TVoid

The empty block has a fixed type TVoid. This is the only block-level rule that synthesizes unconditionally: non-empty blocks are typed structurally by Resolution.Check.block (last statement carries the value, non-last positions via Check.statement), which always splits off a last statement and so never reaches an empty list. When an empty block appears in check position, Resolution.Check.resolveStmtExpr's wildcard arm synth-then-subsumes via the standard [⇐] Sub fallback.

🔗def
Strata.Laurel.Resolution.Synth.block (exprMd : Strata.Laurel.StmtExprMd) (stmts : List Strata.Laurel.StmtExprMd) (label : Option String) (invs : List Strata.Laurel.StmtExprMd) (dec : Option Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Block stmts label invs dec) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.block (exprMd : Strata.Laurel.StmtExprMd) (stmts : List Strata.Laurel.StmtExprMd) (label : Option String) (invs : List Strata.Laurel.StmtExprMd) (dec : Option Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Block stmts label invs dec) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Block-Synth)

Γ ⊢ sᵢ ⋄ (1 ≤ i ≤ n)   Γ ⊢ last ⇒ T          (Block-Synth)
──────────────────────────────────────────────────────────────
Γ ⊢ Block [s₁; … ; sₙ; last] label ⇒ T

Synth-mode rule for a non-empty block used where no expected type is available (e.g. { x := 1; x } == y). Mirrors Check.block's structure — fresh scope, optional label, non-last statements in effect position (Check.statement), dead-code-after-terminator diagnostic — but synthesizes the last statement instead of checking it against an expected type, and returns that synthesized type as the block's value type. The empty block is handled by Synth.emptyBlock at the dispatch site; this rule only runs on a non-empty block.

expected? is the caller's slot, threaded into the last statement only — the same statement Check.block pushes its expected into. A block is a check-capable wrapper, so a generic call in its value position must see the slot that a direct one would (identity({ setEmpty() })).

🔗def
Strata.Laurel.Resolution.Check.block (exprMd : Strata.Laurel.StmtExprMd) (stmts : List Strata.Laurel.StmtExprMd) (label : Option String) (invs : List Strata.Laurel.StmtExprMd) (dec : Option Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Block stmts label invs dec) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.block (exprMd : Strata.Laurel.StmtExprMd) (stmts : List Strata.Laurel.StmtExprMd) (label : Option String) (invs : List Strata.Laurel.StmtExprMd) (dec : Option Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Block stmts label invs dec) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Block) Check-mode typing rule for a non-empty block.

A block's value is the value of its last statement; every earlier statement is run only for its effect. The rule splits the statement list into [s₁; … ; sₙ] (all but the last) and last, handling each part as follows:

  • non-last — Γ ⊢ s ⋄. A non-last statement is in effect position (see Check.statement): an IfThenElse or a non-empty Block is checked against TVoid, so its arms and its own last statement are statements in turn; every other form is synthesized and its type discarded. Any expression is accepted — statement-shaped forms synthesize TVoid, value-producing forms (calls, IncrDecr, etc.) synthesize their natural type which is then discarded.

  • last — Γ ⊢ last ⇐ T. The surrounding expected type T is routed to the last statement, so a check-only trailing form (IfThenElse, a nested Block, Hole, Return, …) still receives its expected type. When T = TVoid (the block is in statement position), the last statement is also in effect position and goes through Check.statement.

A block most often occurs in check position (procedure bodies, branches, loop bodies, assignment RHS, and call arguments all supply an expected type). When one appears in synth-only operand position with no contextual type, Resolution.Synth.block handles it with the same structure, synthesizing the last statement instead.

The block opens a fresh nested scope (declarations made inside don't leak), and emits a "dead code after exit/return" diagnostic when a terminator is followed by further statements. When label is some l, l is registered in ResolveState.labelScope (via withLabel) for the block's extent so nested exit l checks can see it.

The \Gamma \vdash s\;\diamond judgment — the [⋄] Stmt rule above — is the single definition of what counts as a statement in effect position, factored out into Check.statement:

🔗def
Strata.Laurel.Resolution.Check.statement (s : Strata.Laurel.StmtExprMd) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.statement (s : Strata.Laurel.StmtExprMd) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Stmt) Check a statement in effect position, written Γ ⊢ s ⋄.

Laurel has no syntactic statement/expression split — everything is a StmtExpr — so "what may appear where its value is discarded" is defined by this rule rather than by the grammar. A statement's value is never consumed, so it is accepted whatever its type:

Γ ⊢ s ⇐ TVoid            s is an `IfThenElse` or a non-empty `Block`
──────────────────────
Γ ⊢ s ⋄

Γ ⊢ s ⇒ _                otherwise
──────────────────────
Γ ⊢ s ⋄

The two check-capable compound forms are checked against TVoid, the file's marker for statement position (Check.block routes its last statement back through this rule under TVoid; Check.ifThenElse does the same for its arms; Check.assign/Check.declInfer skip their Sub check under it). So the whole statement chain — body → block → if → arm → block → last statement — ends here, and an if in statement position never has to synthesize a type: Synth.ifThenElse's branch join is for an if whose value is used ((if c then 1 else 2) + x), and for a statement if c then { go := false } else { steps := steps + 1 } the join would only compare bool with int for nobody. TVoid rather than Unknown because the marker is never handed to synthesis as an expected type: a trailing generic call mk() is synthesized with expected? = none and still reports "cannot infer type argument", whereas Unknown would silently bind the type argument. Every other form is synthesized and its type discarded: statement-shaped forms (Assert, Assume, While, Exit, Return) synthesize TVoid, value-producing forms (calls, Assign, IncrDecr, literals, etc.) synthesize their natural type, which is then dropped. So any expression is accepted in statement position — the f(x); idiom works regardless of f's return type, and x++; is admitted even though ++ synthesizes the target's type.

This is the single definition of "what counts as a statement". It is used by Check.block for every non-last statement (and for the last one when the block itself sits in statement position, expected = TVoid), and by resolveBody for a procedure body.

\frac{l \in \Gamma_{\mathrm{lbl}}}{\Gamma \vdash \mathsf{Goto}\;l\;d \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Goto)}

exit is an unconditional jump out of the enclosing labeled block, and goto an unconditional jump back to its start; both are a \mathsf{Goto}, differing in the destination d. It synthesizes \mathsf{TVoid} unconditionally. Labels live in their own namespace \Gamma_{\mathrm{lbl}}, populated by the surrounding Block rule when its \mathit{label} is some l. A \mathsf{Goto}\;l\;d targeting a label not in \Gamma_{\mathrm{lbl}} is rejected. A block's invariants resolve as bool in the scope enclosing the block, and only a labelled block may carry them.

🔗def
Strata.Laurel.Resolution.Check.goto (target : String) (destination : Strata.Laurel.GotoDestination) (source : Strata.FileRange) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.goto (target : String) (destination : Strata.Laurel.GotoDestination) (source : Strata.FileRange) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Goto)

l ∈ Γ_lbl
───────────────────
Γ ⊢ Goto l d ⇒ TVoid

exit l and goto l are control-flow terminators — unconditional jumps past the end of, or back to the start of, the enclosing labeled block. Because they never fall through, they never deliver a value, so they synthesize TVoid.

The premise l ∈ Γ_lbl requires the target label to name an enclosing labeled block; labels live in their own namespace (ResolveState.labelScope, populated by Check.block via withLabel). An unknown label is reported here as "label '<l>' is not in scope".

In the Return rules below, \overline{T_o} denotes the declared output-parameter type list of the enclosing procedure (an implicit parameter of the rules — the procedure binds it once on entry).

\frac{\overline{T_o} = []}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None-Void)}

\frac{\overline{T_o} = [T]}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None-Single)}

\frac{\overline{T_o} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;\mathsf{none} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-None-Multi)}

\frac{\overline{T_o} = [T] \quad \Gamma \vdash e \Leftarrow T}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Return-Some)}

\frac{\overline{T_o} = []}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “void procedure cannot return a value”}} \quad \text{([⇒] Return-Void-Error)}

\frac{\overline{T_o} = [T_1; \ldots; T_n] \quad (n \ge 2)}{\Gamma \vdash \mathsf{Return}\;(\mathsf{some}\;e) \rightsquigarrow \text{error: “multi-output procedure cannot use 'return e'; assign to named outputs instead”}} \quad \text{([⇒] Return-Multi-Error)}

return is the only rule whose premises depend on the enclosing procedure's declared outputs. The rule synthesizes \mathsf{TVoid} because return is a control-flow terminator: it never falls through and produces no value for the surrounding context. The returned value (if any) is checked against the procedure's declared output. The error arms fire when \overline{T_o}'s arity does not match the syntactic shape of return e.

Regardless of which arm fires, e is always elaborated — it is checked against the declared output in the single-output case, otherwise synthesized — so any errors inside e are reported in addition to the arity diagnostic.

The three Return-None rules all accept return; unconditionally. Void-output procedures accept it naturally (Return-None-Void); single-output procedures accept it without a subtype check (Return-None-Single); multi-output procedures accept it as an early-exit shorthand that leaves the named outputs at whatever they were last assigned to (Return-None-Multi).

When the surrounding context has no enclosing procedure body (e.g. inside a constant initializer), answerType = none and all Return checks are skipped; well-formed input never produces this case.

🔗def
Strata.Laurel.Resolution.Check.return (exprMd : Strata.Laurel.StmtExprMd) (vals : List Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Return vals) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.return (exprMd : Strata.Laurel.StmtExprMd) (vals : List Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Return vals) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Return)

Below, T_o-bar denotes the enclosing procedure's declared output-type list (bound on entry to a procedure body, stored on ResolveState.answerType).

T_o-bar = []                                           (Return-None-Void)
─────────────────────────
Γ ⊢ Return [] ⇒ TVoid

T_o-bar = [T]                                          (Return-None-Single)
──────────────────────────────────
Γ ⊢ Return [] ⇒ TVoid

T_o-bar = [T_1;…;T_n]  n ≥ 2                           (Return-None-Multi)
──────────────────────────────────
Γ ⊢ Return [] ⇒ TVoid

T_o-bar = [T_1;…;T_n]    Γ ⊢ e_i ⇐ T_i                 (Return-Values)
──────────────────────────────────
Γ ⊢ Return [e_1;…;e_n] ⇒ TVoid

T_o-bar = []                                           (Return-Void-Error)
───────────────────────────────────────────────────────────
Γ ⊢ Return [e_1;…;e_m] ↝ "void procedure cannot return a value"

T_o-bar = [T_1;…;T_n]  m ≠ n  m ≥ 1                    (Return-Arity-Error)
───────────────────────────────────────────────────────────
Γ ⊢ Return [e_1;…;e_m] ↝ "procedure declares n outputs, so 'return' takes n values, not m"

return is the only rule whose premises depend on the enclosing procedure's declared outputs. It is a control-flow terminator: it transfers control out of the enclosing procedure and never falls through, so it synthesizes TVoid. The returned value, if any, is checked against the procedure's declared output. Anything after return in the same block is dead code, flagged by Resolution.Check.block.

When answerType = none we are not inside any procedure body (e.g. resolving a constant initializer), so all Return checks are skipped — Return should not occur there in well-formed input.

return; (no payload) is unconditionally accepted in all cases: void-output procedures (Return-None-Void), single-output procedures (Return-None-Single), and multi-output procedures (Return-None-Multi). In the multi-output case it acts as an early-exit shorthand — each declared output retains whatever was last assigned to it via named-output assignment.

A valued return supplies one value per declared output, left to right, each checked against the type of the output it fills. Every value is elaborated even when the count is wrong — synthesized rather than checked — so errors inside a value are reported in addition to the arity diagnostic.

\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{invs}_i \Leftarrow \mathsf{TBool} \quad \Gamma \vdash \mathit{decreases} \Rightarrow U \quad \mathsf{Numeric}\;U \quad \Gamma \vdash \mathit{body} \Leftarrow \mathsf{Unknown}}{\Gamma \vdash \mathsf{While}\;\mathit{cond}\;\mathit{invs}\;\mathit{decreases}\;\mathit{body} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] While)}

The body is checked at \mathsf{Unknown}: control either re-enters the loop or falls through, so the body's value type is never observed by the surrounding context. A loop is a statement and yields no value, so the rule synthesizes \mathsf{TVoid}.

The optional \mathit{decreases} clause is synthesized and required to have a numeric type via the same \mathsf{Numeric} predicate used by the arithmetic primitive operations. \mathsf{Numeric} is a predicate (it admits \mathsf{TInt}, \mathsf{TReal}, \mathsf{TFloat64}, \mathsf{TBv}_w (a bitvector of any width), and \mathsf{Unknown} as the gradual escape hatch), not a single type, so the clause runs in synth mode rather than check mode.

🔗def
Strata.Laurel.Resolution.Check.while (exprMd cond : Strata.Laurel.StmtExprMd) (invs : List Strata.Laurel.StmtExprMd) (dec : Option Strata.Laurel.StmtExprMd) (body : Strata.Laurel.StmtExprMd) (postTest : Bool) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.While cond invs dec body postTest) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.while (exprMd cond : Strata.Laurel.StmtExprMd) (invs : List Strata.Laurel.StmtExprMd) (dec : Option Strata.Laurel.StmtExprMd) (body : Strata.Laurel.StmtExprMd) (postTest : Bool) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.While cond invs dec body postTest) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(While)

Γ ⊢ cond ⇐ TBool
Γ ⊢ invs_i ⇐ TBool
Γ ⊢ decreases ⇒ U
Numeric U
Γ ⊢ body ⇐ Unknown
─────────────────────────────────────────────────
Γ ⊢ While cond invs decreases body ⇒ TVoid

cond is checked against TBool, and each invariant against TBool. The body's value type is discarded — control either re-enters the loop or falls through, so the body is checked at Unknown (the gradual wildcard) and any value the body's tail might produce is ignored. A loop is a statement: it yields no value, so it synthesizes TVoid.

The optional decreases clause is synthesized and required to have a numeric type, via the same Numeric U predicate used by the arithmetic primitive ops. Numeric is a predicate, not a single type, so the clause runs in synth mode rather than check mode.

3.4.6. Verification statements🔗

\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assert}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assert)}

🔗def
Strata.Laurel.Resolution.Check.assert (exprMd condExpr : Strata.Laurel.StmtExprMd) (summary : Option String) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assert condExpr summary) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.assert (exprMd condExpr : Strata.Laurel.StmtExprMd) (summary : Option String) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assert condExpr summary) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Assert)

Γ ⊢ cond ⇐ TBool
──────────────────────────────────
Γ ⊢ Assert cond ⇒ TVoid

cond is checked against TBool. assert is a statement: it yields no value, so it synthesizes TVoid.

\frac{\Gamma \vdash \mathit{cond} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Assume}\;\mathit{cond} \Rightarrow \mathsf{TVoid}} \quad \text{([⇒] Assume)}

🔗def
Strata.Laurel.Resolution.Check.assume (exprMd cond : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assume cond) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.assume (exprMd cond : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assume cond) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Assume)

Γ ⊢ cond ⇐ TBool
──────────────────────────────────
Γ ⊢ Assume cond ⇒ TVoid

cond is checked against TBool. assume is a statement: it yields no value, so it synthesizes TVoid.

3.4.7. Assignment🔗

\frac{\Gamma \vdash \mathit{targets}_i \Rightarrow T_i \quad \Gamma \vdash e \Leftarrow \mathit{ExpectedTy}}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathit{ExpectedTy}} \quad \text{([⇒] Assign)}

where ExpectedTy = T_1 if |targets| = 1 and MultiValuedExpr [T_1; …; T_n] otherwise. The target's declared type T_i comes from the variable's scope entry (for Local and Field) or from the Declare-bound parameter type. The RHS receives ExpectedTy via Check.resolveStmtExpr, so bidirectional rules in the RHS propagate the assignment's type into nested constructs. The assignment synthesizes ExpectedTy — populating the surrounding context with the target's type while the RHS is checked against it.

🔗def
Strata.Laurel.Resolution.Synth.assign (exprMd : Strata.Laurel.StmtExprMd) (targets : List Strata.Laurel.VariableMd) (value : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign targets value) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.assign (exprMd : Strata.Laurel.StmtExprMd) (targets : List Strata.Laurel.VariableMd) (value : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign targets value) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Assign)

Γ ⊢ targets_i ⇒ T_i
Γ ⊢ e ⇐ ExpectedTy
─────────────────────────────────────────────────────────
Γ ⊢ Assign targets e ⇒ ExpectedTy

where ExpectedTy = T_1 if |targets| = 1 and otherwise MultiValuedExpr [T_1; …; T_n]. The target tuple type is pushed into the RHS via Check.resolveStmtExpr, so bidirectional rules in the RHS receive the assignment's type. The assignment synthesizes ExpectedTy — the LHS-derived target tuple type — so the surrounding context sees the type the RHS was checked against.

\frac{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Rightarrow \mathit{ExpectedTy} \quad T = \mathsf{TVoid} \lor \mathit{ExpectedTy} <: T}{\Gamma \vdash \mathsf{Assign}\;\mathit{targets}\;e \Leftarrow T} \quad \text{([⇐] Assign)}

The check rule synthesizes the assignment's type via [⇒] Assign and then runs the standard [⇐] Sub boundary check ExpectedTy <: T — unless T = TVoid, the marker for statement position. Pushing TVoid through subsumption would only succeed when the LHS is itself void, which would reject every non-void assignment used as a statement, so the subsumption is skipped and the synthesized value is discarded.

🔗def
Strata.Laurel.Resolution.Check.assign (exprMd : Strata.Laurel.StmtExprMd) (targets : List Strata.Laurel.VariableMd) (value : Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign targets value) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.assign (exprMd : Strata.Laurel.StmtExprMd) (targets : List Strata.Laurel.VariableMd) (value : Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign targets value) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

Check-mode rule for assignment. Synthesizes the assignment's type by inlining the same work as Synth.assign (resolving targets, pushing the LHS-derived ExpectedTy into the RHS via Check.resolveStmtExpr), then runs the standard [⇐] Sub boundary check ExpectedTy <: T against the surrounding expected — unless T = TVoid, the marker for statement position (e.g. last statement of a block whose value is being discarded). Sub against TVoid would only succeed when ExpectedTy = TVoid, which would reject every non-void assignment used as a statement, so the subsumption is skipped there. The synthesized value is discarded in statement position, exactly as for calls.

An unannotated declaring assignment — var x := e, i.e. an Assign whose sole target is Declare x none — is dispatched to a dedicated rule pair before [⇒]/[⇐] Assign. The target has no declared type to push into the RHS, so the direction flips: the initializer is synthesized and the binding adopts its type.

\frac{x \notin \mathrm{dom}(\Gamma) \quad \Gamma \vdash e \Rightarrow T}{\Gamma \vdash \mathsf{Assign}\;[\mathsf{.Declare}\;\langle x, \mathsf{none}\rangle]\;e \Rightarrow T \quad \dashv \quad \Gamma, x : T} \quad \text{([⇒] Decl-Synth)}

\frac{x \notin \mathrm{dom}(\Gamma) \quad \Gamma \vdash e \Rightarrow T \quad T' = \mathsf{TVoid} \lor T <:_\sim T'}{\Gamma \vdash \mathsf{Assign}\;[\mathsf{.Declare}\;\langle x, \mathsf{none}\rangle]\;e \Leftarrow T' \quad \dashv \quad \Gamma, x : T} \quad \text{([⇐] Decl-Synth)}

The adopted type T must be a value type: a \mathsf{TVoid} initializer (a void call, a while, …) or a \mathsf{MultiValuedExpr} (a multi-output call) [\text{emits “cannot infer a type …”}] and binds x : \mathsf{Unknown} instead, suppressing cascades on later uses. As in [⇒] Var-Declare, the node is rewritten to carry \mathsf{some}\;T, so no none annotation survives resolution. Unannotated declared targets of a multi-target assign var x, y := call() don't take this rule; they are recovered component-wise from the synthesized RHS tuple inside [⇒]/[⇐] Assign (see the docstrings above).

🔗def
Strata.Laurel.Resolution.Synth.declInfer (exprMd : Strata.Laurel.StmtExprMd) (name : Strata.Laurel.Identifier) (vs : Strata.FileRange) (value : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign [{ val := Strata.Laurel.Variable.Declare { name := name, type := none }, source := vs }] value) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.declInfer (exprMd : Strata.Laurel.StmtExprMd) (name : Strata.Laurel.Identifier) (vs : Strata.FileRange) (value : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign [{ val := Strata.Laurel.Variable.Declare { name := name, type := none }, source := vs }] value) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Decl-Synth, synth mode)

x ∉ dom(Γ)    Γ ⊢ e ⇒ T
──────────────────────────────────────────────
Γ ⊢ (var x := e) ⇒ T ⊣ Γ, x : T

var x := e, an unannotated declaration (Declare x none) with an initializer. With no annotation to push into the RHS, we synthesize the initializer's type T and adopt it for the binding. The node is rewritten to Assign [⟨.Declare x (some T)⟩] e, so no none annotation survives resolution. This rule handles the sole-target form; unannotated declared targets of a multi-target assign var x, y := call() are recovered component-wise inside Synth.assign/Check.assign. The synthesized type is T, matching Synth.assign's single-target case.

Scoping: the initializer is synthesized before defineNameCheckDup introduces the binding, so e cannot see the x being declared — a self-referential var x := x + 1 reports "'x' is not defined" (or reads an outer x if one is in scope).

Synth.assign/Check.assign follow the same scoping for annotated declarations (var x : int := x + 1): they resolve the value while the declared names are still out of scope, and bind them afterwards. That symmetry is what keeps resolution idempotent — this function rewrites var x := e into the annotated form, so re-resolving an already-resolved declaration still binds its initializer in the enclosing scope rather than to the declaration itself. See the comment on Synth.assign.

Pinned by selfRefNoOuter/selfRefOuterShadow for the unannotated form and annotatedSelfRefOuterShadow for the annotated form, in ResolutionTypeCheckTests.

🔗def
Strata.Laurel.Resolution.Check.declInfer (exprMd : Strata.Laurel.StmtExprMd) (name : Strata.Laurel.Identifier) (vs : Strata.FileRange) (value : Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign [{ val := Strata.Laurel.Variable.Declare { name := name, type := none }, source := vs }] value) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.declInfer (exprMd : Strata.Laurel.StmtExprMd) (name : Strata.Laurel.Identifier) (vs : Strata.FileRange) (value : Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assign [{ val := Strata.Laurel.Variable.Declare { name := name, type := none }, source := vs }] value) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Decl-Synth, check mode) var x := e (Declare x none with initializer) where a type A is expected (e.g. as the value-producing last statement of a checked block). Synthesizes the initializer's type and adopts it for the binding (see Synth.declInfer), then runs the standard [⇐] Sub boundary check T <: A against the surrounding expected — unless A = TVoid (statement position), exactly as in Check.assign.

3.4.8. Calls🔗

\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and output } [T'] \text{ (single output)} \quad \Gamma \vdash \mathit{args}_i \Leftarrow Ts_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{Call}\;\mathsf{none}\;\mathit{callee}\;\mathit{args} \Rightarrow T'} \quad \text{([⇒] Static-Call)}

\frac{\Gamma(\mathit{callee}) = \text{static-procedure with inputs } Ts \text{ and outputs } [T_1; \ldots; T_n],\; n \ge 2 \quad \Gamma \vdash \mathit{args}_i \Leftarrow Ts_i \text{ (pairwise)}}{\Gamma \vdash \mathsf{Call}\;\mathsf{none}\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Static-Call-Multi)}

🔗def
Strata.Laurel.Resolution.Synth.staticCall (exprMd : Strata.Laurel.StmtExprMd) (callee : Strata.Laurel.Identifier) (args : List Strata.Laurel.StmtExprMd) (priorTypeArgs : List Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Call none callee args priorTypeArgs) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.staticCall (exprMd : Strata.Laurel.StmtExprMd) (callee : Strata.Laurel.Identifier) (args : List Strata.Laurel.StmtExprMd) (priorTypeArgs : List Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Call none callee args priorTypeArgs) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

Cases on the arity of the callee's declared outputs.

Γ(callee) = static-procedure with inputs Ts                  (Static-Call)
  and output [T'] (single output)
Γ ⊢ args_i ⇐ Ts_i (pairwise)
──────────────────────────────────────────────────────
Γ ⊢ Call none callee args ⇒ T'

Γ(callee) = static-procedure with inputs Ts                  (Static-Call-Multi)
  and outputs [T_1; …; T_n] (n ≥ 2)
Γ ⊢ args_i ⇐ Ts_i (pairwise)
──────────────────────────────────────────────────────
Γ ⊢ Call none callee args ⇒ MultiValuedExpr [T_1; …; T_n]

A callee with zero outputs synthesizes TVoid (the n = 0 case). The two rules differ only in output arity — argument checking is identical. Callee is resolved against the expected kinds (parameter, static procedure, datatype constructor, datatype destructor, constant); each argument is checked against the corresponding parameter type. The bidirectional push lets impure-expression arguments ({x := 1; x}, if c then …, holes) flow through their own check rules instead of bottoming out at the synth wildcard.

When the callee resolves to a static procedure with a known parameter count and the call supplies more arguments than it declares, an over-arity diagnostic is emitted (the surplus arguments are still resolved first, against Unknown, so errors inside them are reported too). The check fires only for genuine procedures (procArity); for an unresolved name (where paramTypes = [] purely because the name was not found), a datatype constructor/tester, a parameter, or a constant, no arity diagnostic is emitted — surplus arguments are checked against Unknown, the gradual escape hatch, exactly as before, so no spurious/duplicate diagnostic is produced. Under-arity (too few arguments) is deliberately not flagged.

The result type is the (possibly multi-valued) declared output type from getCallInfo.

priorTypeArgs is whatever a PREVIOUS resolution stamped on this node. It is deliberately ignored and recomputed: the post-pass re-resolution (LaurelCompilationPipeline) re-runs this rule on an already-resolved tree, and the instantiation must be re-derived from the current types rather than trusted, since an intervening pass may have changed them. Present only so the shape of h matches the node.

\frac{\Gamma \vdash \mathit{target} \Rightarrow C \quad \Gamma(\mathit{Owner}\$\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and output } [T'] \text{ (single output)} \quad \Gamma \vdash \mathit{args}_i \Leftarrow Ts_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{Call}\;(\mathsf{some}\;\mathit{target})\;\mathit{callee}\;\mathit{args} \Rightarrow T'} \quad \text{([⇒] Instance-Call)}

\frac{\Gamma \vdash \mathit{target} \Rightarrow C \quad \Gamma(\mathit{Owner}\$\mathit{callee}) = \text{instance-procedure with inputs } [\mathit{self}; Ts] \text{ and outputs } [T_1; \ldots; T_n],\; n \ge 2 \quad \Gamma \vdash \mathit{args}_i \Leftarrow Ts_i \text{ (pairwise; self dropped)}}{\Gamma \vdash \mathsf{Call}\;(\mathsf{some}\;\mathit{target})\;\mathit{callee}\;\mathit{args} \Rightarrow \mathsf{MultiValuedExpr}\;[T_1; \ldots; T_n]} \quad \text{([⇒] Instance-Call-Multi)}

The receiver synthesizes a composite C, and \mathit{Owner} is the most specific of C and its ancestors that declares the callee. Output arity is forwarded identically to Synth.staticCall's single-vs-multi split. In both call families the single- and multi-output rules differ only in the output arity; argument checking is the same, and surplus arguments (beyond the declared parameters, or when the callee is unresolved) are checked against \mathsf{Unknown} rather than flagged as an arity error. A zero-output (n = 0) procedure call is the third case in the arity split: it synthesizes \mathsf{TVoid} rather than a \mathsf{MultiValuedExpr}.

🔗def
Strata.Laurel.Resolution.Synth.instanceCall (exprMd target : Strata.Laurel.StmtExprMd) (callee : Strata.Laurel.Identifier) (args : List Strata.Laurel.StmtExprMd) (priorTypeArgs : List Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Call (some target) callee args priorTypeArgs) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.instanceCall (exprMd target : Strata.Laurel.StmtExprMd) (callee : Strata.Laurel.Identifier) (args : List Strata.Laurel.StmtExprMd) (priorTypeArgs : List Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Call (some target) callee args priorTypeArgs) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

Cases on the arity of the callee's declared outputs.

Γ ⊢ target ⇒ C                                            (Instance-Call)
Γ(Owner$callee) = instance-procedure
  with inputs [self; Ts] and output [T'] (single output)
Γ ⊢ args_i ⇐ Ts_i (pairwise; self dropped)
─────────────────────────────────────────
Γ ⊢ Call (some target) callee args ⇒ T'

Γ ⊢ target ⇒ C                                            (Instance-Call-Multi)
Γ(Owner$callee) = instance-procedure
  with inputs [self; Ts] and outputs [T_1; …; T_n] (n ≥ 2)
Γ ⊢ args_i ⇐ Ts_i (pairwise; self dropped)
─────────────────────────────────────────
Γ ⊢ Call (some target) callee args ⇒ MultiValuedExpr [T_1; …; T_n]

A callee with zero outputs synthesizes TVoid (the n = 0 case). The two rules differ only in output arity. Target synthesizes a composite C, and Owner is the most specific of C and its ancestors that declares callee; arguments are checked pairwise against the callee's parameter types after dropping self. As in Synth.staticCall, supplying more arguments than the callee declares (compared against the post-self parameter count) emits an over-arity diagnostic when the callee resolves to an instance procedure, while surplus arguments against any other resolution kind are still checked against Unknown with no arity diagnostic. Like Synth.staticCall, the push is bidirectional so block- and conditional-shaped arguments route through their own check rules.

Generic composites and methods. A method of a generic composite, or a generic method, is resolved instead the way Synth.staticCall's generic path will resolve the static call Owner$m(obj, args) that LiftInstanceProcedures turns it into. Only its errors differ: where that call is rejected, what the receiver binds a type parameter to is kept, so that a mismatched argument is the one blamed (on a Box<int>, mix(true, 5) reports true, not a conflict over T). A method that redeclares a type parameter of its composite follows the rules above.

Ambiguity. With no single most-specific declarer (MemberResolution.ambiguous) the call reports the ambiguity and stops, typed Unknown, binding no candidate: binding one would propagate a return type that contradicts the ambiguity. A call on a receiver that is not a composite stops the same way, and is reported unless an error already rejects the program: one for the receiver, or, for an Unknown receiver other than a hole or the name of a procedure or a type, any earlier one, which may be what typed it (var x; does). Arguments are still checked, against Unknown, so an error in one is not masked.

priorTypeArgs is ignored and recomputed, as in Synth.staticCall.

3.4.9. Operators🔗

Operators are not a distinct kind of expression, and there are no operator-specific typing rules. x + y parses as \mathsf{Call}\;\mathsf{none}\;\$\mathsf{add}\;[x; y], a call to an overloaded built-in wrapper procedure declared in CoreDefinitionsForLaurel and prepended to every program, so operators are typed entirely by [⇒] Static-Call above. What used to be an operator's admissible operand types is now just the set of declared overloads:

procedure $add(x: int, y: int) : int    return intAdd(x, y);
procedure $add(x: real, y: real) : real return realAdd(x, y);

Each wrapper is a thin transparent procedure delegating to a type-specific external (intAdd, realAdd, …) that LaurelToCoreSchemaPass recognizes and lowers to the corresponding Core operator. The wrappers of one operator must all share a name — the parser cannot know which overload a + denotes — while the externals they delegate to do not.

Two consequences of typing operators as calls:

Operand admissibility is overload selection

There is no Numeric side-condition. 1 + 2.0 is rejected not because a rule demands equal operand types, but because neither the int nor the real overload of $add accepts an (\mathsf{TInt}, \mathsf{TReal}) pair — reported as no overload of '$add' matches the argument types. Likewise < on bitvectors resolves only at the widths Core provides operators for (1, 8, 16, 32, 64), rather than silently mistranslating other widths.

Preconditions come from the wrapper

Because a wrapper is an ordinary procedure it can carry a contract. $div declares requires y != 0 and delegates to Core's safe division, so a possible division by zero surfaces as a failed precondition on the call.

The gradual \mathsf{Unknown} still flows freely: it is a consistent subtype of every parameter type, so it never rules an overload out. An \mathsf{Unknown} argument therefore cannot discriminate between overloads, but the other arguments still can — selection runs on the informative arguments alone, and only an unresolved result caused by an \mathsf{Unknown} argument is passed over silently (the argument's own error already covers it) instead of being reported as a no-match or an ambiguity.

Operator overload selection
  • 1 + 2 selects the int overload and synthesizes \mathsf{TInt}

  • 1.5 + 2.5 selects the real overload and synthesizes \mathsf{TReal}

  • <?> + 1 selects the int overload — the informative operand decides

  • <?> + <?> is unresolved and synthesizes \mathsf{Unknown}; no error is reported

  • 1 + 2.0 is rejected: no overload of '$add' matches the argument types

Equality is the one operator that is not a transparent wrapper. $eq / $neq are declared external, because equality is polymorphic and Laurel has no polymorphic types: a wrapper body would carry a placeholder \mathsf{int} \to \mathsf{int} \to \mathsf{bool} signature into Core and fail to unify against a composite, a datatype, or a bool. Synth.staticCall special-cases these two names to require only that the operands be consistent (T_l \sim T_r), and LaurelToCoreSchemaPass lowers them straight to Core's polymorphic equality.

\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad T_l \sim T_r \quad T_l \neq \mathsf{TVoid} \quad T_r \neq \mathsf{TVoid} \quad \mathit{callee} \in \{\$\mathsf{eq}, \$\mathsf{neq}\}}{\Gamma \vdash \mathsf{Call}\;\mathsf{none}\;\mathit{callee}\;[\mathit{lhs}; \mathit{rhs}] \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Op-Eq)}

The \neq \mathsf{TVoid} premises reject void operands even though \mathsf{TVoid} \sim \mathsf{TVoid} holds: a void expression carries no value to compare.

Since an operator is a call, the check-mode rule for one is [⇐] Sub applied to [⇒] Static-Call: the call's synthesized result type is compared against the expected type. There is no separate operand-pushing rule, so a mixed-type operator expression reports one overload-resolution failure over the whole call rather than a mismatch against an individual operand.

3.4.10. Object forms🔗

\frac{\mathit{ref} \text{ is a composite or datatype, or is unresolved, or is absent from } \Gamma}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{UserDefined}\;\mathit{ref}} \quad \text{([⇒] New-Ok)}

\frac{\mathit{ref} \text{ resolves to a non-type kind}}{\Gamma \vdash \mathsf{New}\;\mathit{ref} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] New-Fallback)}

The \mathsf{Unknown} fallback fires only when \mathit{ref} resolves to a present definition whose kind is neither composite nor datatype. An unresolved or out-of-scope \mathit{ref} takes the New-Ok branch instead, so the kind diagnostic that resolveRef already emitted is not duplicated.

🔗def
Strata.Laurel.Resolution.Synth.new (ref : Strata.Laurel.Identifier) (typeArgs : List Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.new (ref : Strata.Laurel.Identifier) (typeArgs : List Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

Cases on whether ref resolves to a composite/datatype.

ref is a composite or datatype,                (New-Ok)
  or is unresolved, or is absent from Γ
──────────────────────────────────────
Γ ⊢ New ref ⇒ UserDefined ref

ref resolves to a non-type kind               (New-Fallback)
──────────────────────────────────────
Γ ⊢ New ref ⇒ Unknown

When ref resolves to a composite or datatype, the type is UserDefined ref — or, for an explicit instantiation new C<τ…>, the applied type Applied (UserDefined ref) [τ…], so the type checker and MonomorphizeComposites see the concrete instantiation (allocatedType). A bare new C carries no type args and keeps the plain UserDefined type. The Unknown fallback fires only when ref resolves to a present definition whose kind is neither composite nor datatype (e.g. a variable or procedure name); this suppresses cascading errors after the kind diagnostic has already fired. An unresolved ref, or one absent from scope, takes the UserDefined branch instead — resolveRef has already reported the name, so re-flagging it here would only duplicate that diagnostic. The explicit type args are resolved (so a .TVar inside is reclassified and a bad arg reported) and their count is checked against the composite's declared type-arg arity.

A bare new C for a GENERIC C recovers its instantiation from expected? (var b: Box<int> := new Box ⇒ typeArgs = [int]), so the allocation site carries the concrete arguments the monomorphizer needs instead of leaving it to recover them from surrounding context. When nothing supplies them it is a resolution error, since an un-instantiated generic composite cannot be monomorphized.

\frac{\Gamma \vdash \mathit{target} \Rightarrow U \quad U \sim T \lor U <: T \lor T <: U}{\Gamma \vdash \mathsf{AsType}\;\mathit{target}\;T \Rightarrow T} \quad \text{([⇒] AsType)}

🔗def
Strata.Laurel.Resolution.Synth.asType (exprMd target : Strata.Laurel.StmtExprMd) (ty : Strata.Laurel.HighTypeMd) (h : exprMd.val = Strata.Laurel.StmtExpr.AsType target ty) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.asType (exprMd target : Strata.Laurel.StmtExprMd) (ty : Strata.Laurel.HighTypeMd) (h : exprMd.val = Strata.Laurel.StmtExpr.AsType target ty) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(AsType)

Γ ⊢ target ⇒ U
U ~ T  ∨  U <: T  ∨  T <: U  ∨  ∃ D. D <: U ∧ D <: T  (D a non-generic composite)
──────────────────────────────────────────────
Γ ⊢ AsType target T ⇒ T

target synthesizes some type U; the cast is allowed when U and T sit in the same lineage modulo gradual Unknown — either consistent after unfolding aliases/constrained types (e.g. 5 as Int where Int is a wrapper over int), or a subtype in either direction (downcast animal as Cat when Cat extends Animal, upcast cat as Animal), or a non-generic composite descends from both (multiple inheritance, so an A may be a B). Sibling casts (Dog as Cat) and casts between unrelated primitives ("hi" as int) are rejected. The synthesized type is T — the user's claim is honored once the relation check passes.

is/as are supported only for COMPOSITE targets (see castTargetRejection?); a target not known to be one is rejected up front.

\frac{\Gamma \vdash \mathit{target} \Rightarrow U \quad U \sim T \lor U <: T \lor T <: U}{\Gamma \vdash \mathsf{IsType}\;\mathit{target}\;T \Rightarrow \mathsf{TBool}} \quad \text{([⇒] IsType)}

🔗def
Strata.Laurel.Resolution.Synth.isType (exprMd target : Strata.Laurel.StmtExprMd) (ty : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.IsType target ty) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.isType (exprMd target : Strata.Laurel.StmtExprMd) (ty : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.IsType target ty) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(IsType)

Γ ⊢ target ⇒ U
U ~ T  ∨  U <: T  ∨  T <: U  ∨  ∃ D. D <: U ∧ D <: T  (D a non-generic composite)
──────────────────────────────────────────────
Γ ⊢ IsType target T ⇒ TBool

Same lineage check as AsType — is only makes sense between types that share a lineage modulo gradual Unknown; testing 5 is Cat is statically nonsense. The synthesized type is TBool.

\frac{\Gamma \vdash \mathit{lhs} \Rightarrow T_l \quad \Gamma \vdash \mathit{rhs} \Rightarrow T_r \quad \mathsf{isReference}\;T_l \quad \mathsf{isReference}\;T_r \quad T_l \sim T_r}{\Gamma \vdash \mathsf{ReferenceEquals}\;\mathit{lhs}\;\mathit{rhs} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] RefEq)}

isReference T holds when T is a UserDefined or Unknown type. ~ is the consistency relation isConsistent — symmetric, with the Unknown wildcard.

🔗def
Strata.Laurel.Resolution.Synth.refEq (exprMd : Strata.Laurel.StmtExprMd) (expr : Strata.Laurel.StmtExpr) (lhs rhs : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h_expr : expr = Strata.Laurel.StmtExpr.ReferenceEquals lhs rhs) (h : exprMd.val = Strata.Laurel.StmtExpr.ReferenceEquals lhs rhs) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.refEq (exprMd : Strata.Laurel.StmtExprMd) (expr : Strata.Laurel.StmtExpr) (lhs rhs : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h_expr : expr = Strata.Laurel.StmtExpr.ReferenceEquals lhs rhs) (h : exprMd.val = Strata.Laurel.StmtExpr.ReferenceEquals lhs rhs) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(RefEq)

Γ ⊢ lhs ⇒ T_l
Γ ⊢ rhs ⇒ T_r
isReference T_l
isReference T_r
T_l ~ T_r
──────────────────────────────────────────────────
Γ ⊢ ReferenceEquals lhs rhs ⇒ TBool

Both operands must be reference types (UserDefined or Unknown) — reference equality is meaningless on primitives. They must also be mutually CONSISTENT (isConsistent, symmetric), which for two UserDefined types is NAME equality (not subtyping): Cat and Dog are rejected, and even Cat/Animal (subtype) is rejected — only an Unknown operand flows freely against the other via the gradual wildcard.

\frac{\Gamma \vdash \mathit{target} \Rightarrow T_t \quad \Gamma(f) = T_f \quad \Gamma \vdash \mathit{newVal} \Leftarrow T_f}{\Gamma \vdash \mathsf{PureFieldUpdate}\;\mathit{target}\;f\;\mathit{newVal} \Rightarrow T_t} \quad \text{([⇒] PureFieldUpdate)}

🔗def
Strata.Laurel.Resolution.Synth.pureFieldUpdate (exprMd target : Strata.Laurel.StmtExprMd) (fieldName : Strata.Laurel.Identifier) (newVal : Strata.Laurel.StmtExprMd) (h : exprMd.val = Strata.Laurel.StmtExpr.PureFieldUpdate target fieldName newVal) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.pureFieldUpdate (exprMd target : Strata.Laurel.StmtExprMd) (fieldName : Strata.Laurel.Identifier) (newVal : Strata.Laurel.StmtExprMd) (h : exprMd.val = Strata.Laurel.StmtExpr.PureFieldUpdate target fieldName newVal) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(PureFieldUpdate)

Γ ⊢ target ⇒ T_t
Γ(f) = T_f
Γ ⊢ newVal ⇐ T_f
─────────────────────────────────────────────────────
Γ ⊢ PureFieldUpdate target f newVal ⇒ T_t

target is synthesized, f resolved against T_t, and newVal checked against the field's declared type. The synthesized type is T_t — updating a field on a pure type produces a new value of the same type.

3.4.11. Verification expressions🔗

\frac{\Gamma, x : T \vdash \mathit{body} \Leftarrow \mathsf{TBool}}{\Gamma \vdash \mathsf{Quantifier}\;\mathit{mode}\;\langle x, T\rangle\;\mathit{trig}\;\mathit{body} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Quantifier)}

🔗def
Strata.Laurel.Resolution.Synth.quantifier (exprMd : Strata.Laurel.StmtExprMd) (mode : Strata.Laurel.QuantifierMode) (param : Strata.Laurel.Parameter) (trigger : Option Strata.Laurel.StmtExprMd) (body : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Quantifier mode param trigger body) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.quantifier (exprMd : Strata.Laurel.StmtExprMd) (mode : Strata.Laurel.QuantifierMode) (param : Strata.Laurel.Parameter) (trigger : Option Strata.Laurel.StmtExprMd) (body : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Quantifier mode param trigger body) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Quantifier)

Γ, x : T ⊢ body ⇐ TBool
────────────────────────────────────────────
Γ ⊢ Quantifier mode ⟨x, T⟩ trig body ⇒ TBool

Opens a fresh scope, binds x : T (in scope only for the body and trigger), resolves the optional trigger, and checks the body against TBool since a quantifier is a proposition. Without that body check, forall x: int :: x + 1 would be silently accepted. The construct itself synthesizes TBool.

\frac{\Gamma \vdash \mathit{name} \Rightarrow \_}{\Gamma \vdash \mathsf{Assigned}\;\mathit{name} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Assigned)}

🔗def
Strata.Laurel.Resolution.Synth.assigned (exprMd name : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assigned name) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.assigned (exprMd name : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Assigned name) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Assigned)

Γ ⊢ name ⇒ _
────────────────────────────
Γ ⊢ Assigned name ⇒ TBool

assigned x is a verification predicate that holds when x has been definitely assigned. The construct unconditionally synthesizes TBool; the operand's synthesized type is discarded, and Assigned imposes no constraint on it.

The operand is still resolved (via Synth.resolveStmtExpr) purely for its name-resolution side effects — its identifier must point at a definition so that downstream passes can reason about the binding — but the result type is thrown away. Assigned is meant to name a variable or field, yet its AST field is an arbitrary StmtExpr (Assigned (name : StmtExprMd)), so this rule does not enforce that shape: it is not correct-by-construction, and the type checker deliberately leaves the operand unconstrained rather than rejecting, say, assigned (a + b).

\frac{\Gamma \vdash v \Leftarrow T}{\Gamma \vdash \mathsf{Old}\;v \Leftarrow T} \quad \text{([⇐] Old)}

🔗def
Strata.Laurel.Resolution.Check.old (exprMd val : Strata.Laurel.StmtExprMd) (label? : Option Strata.Laurel.Identifier) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Old val label?) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.old (exprMd val : Strata.Laurel.StmtExprMd) (label? : Option Strata.Laurel.Identifier) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Old val label?) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Old)

Γ ⊢ v ⇐ T
───────────────
Γ ⊢ Old v ⇐ T

old(v) refers to the pre-state value of v in a postcondition. It has the same type as v, so the surrounding expectation propagates straight through: v is checked against the same T, and the result is wrapped back up as Old v'.

The rule is type-transparent and deliberately does not restrict v to an identifier or lvalue. old wraps an arbitrary expression (Old (value : StmtExprMd)), matching Dafny, where old(this.f + g()) is legal — the pre-state is taken of the whole expression. Whether v denotes something whose pre-state is meaningful is a well-formedness question for the verifier's heap model, not a typing one, so resolution only resolves names inside v and checks its type; it imposes no syntactic shape on v.

old is type-transparent, so it also synthesizes: in operand position (e.g. the postcondition pattern ensures counter.value == old(counter.value) + 1, where == synthesizes its operands) v is synthesized and its type returned unchanged.

\frac{\Gamma \vdash v \Rightarrow T}{\Gamma \vdash \mathsf{Old}\;v \Rightarrow T} \quad \text{([⇒] Old-Synth)}

🔗def
Strata.Laurel.Resolution.Synth.old (exprMd val : Strata.Laurel.StmtExprMd) (label? : Option Strata.Laurel.Identifier) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Old val label?) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.old (exprMd val : Strata.Laurel.StmtExprMd) (label? : Option Strata.Laurel.Identifier) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.Old val label?) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Old-Synth)

Γ ⊢ v ⇒ T
───────────────
Γ ⊢ Old v ⇒ T

old is a universal morphism: it is fully type-transparent, so old(v) has exactly the type of v and passes through every operation. When old(...) appears in a synthesis position (e.g. as an operand of ==/</++, which synthesize their operands — the documented postcondition pattern ensures counter.value == old(counter.value) + 1), v is synthesized and its type T is returned unchanged, wrapped back up as Old v'. Without this rule the construct would fall into the synth wildcard and spuriously report that its type cannot be synthesized.

expected? is the caller's slot, threaded into v exactly as Check.old pushes its expected. old is a check-capable wrapper, so a generic call inside it must see the slot that a direct one would (identity(old(setEmpty()))). Type direction only: the snapshot is unchanged.

\frac{\Gamma \vdash v \Rightarrow T \quad \mathsf{isReference}\;T}{\Gamma \vdash \mathsf{Fresh}\;v \Rightarrow \mathsf{TBool}} \quad \text{([⇒] Fresh)}

🔗def
Strata.Laurel.Resolution.Synth.fresh (exprMd : Strata.Laurel.StmtExprMd) (expr : Strata.Laurel.StmtExpr) (val : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h_expr : expr = Strata.Laurel.StmtExpr.Fresh val) (h : exprMd.val = Strata.Laurel.StmtExpr.Fresh val) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.fresh (exprMd : Strata.Laurel.StmtExprMd) (expr : Strata.Laurel.StmtExpr) (val : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h_expr : expr = Strata.Laurel.StmtExpr.Fresh val) (h : exprMd.val = Strata.Laurel.StmtExpr.Fresh val) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(Fresh)

Γ ⊢ v ⇒ T
isReference T
────────────────────────────
Γ ⊢ Fresh v ⇒ TBool

v is synthesized and must have a reference type (UserDefined or Unknown) — Fresh only makes sense on heap-allocated references, so fresh(5) is rejected. The construct itself synthesizes TBool.

\frac{\Gamma \vdash v \Leftarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Leftarrow T} \quad \text{([⇐] ProveBy)}

🔗def
Strata.Laurel.Resolution.Check.proveBy (exprMd val proof : Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.ProveBy val proof) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.proveBy (exprMd val proof : Strata.Laurel.StmtExprMd) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.ProveBy val proof) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(ProveBy)

Γ ⊢ v ⇐ T
Γ ⊢ proof ⇒ _
────────────────────────────
Γ ⊢ ProveBy v proof ⇐ T

ProveBy v proof has the same type as v (the proof is just a hint for downstream verification), so the surrounding expectation propagates into v. The proof itself has no constraint on its type and is still synthesized.

Like old, ProveBy is type-transparent in v, so it also synthesizes: in operand position v is synthesized for its type T, \mathit{proof} is synthesized only for its name-resolution side effects (its type discarded), and T is returned.

\frac{\Gamma \vdash v \Rightarrow T \quad \Gamma \vdash \mathit{proof} \Rightarrow \_}{\Gamma \vdash \mathsf{ProveBy}\;v\;\mathit{proof} \Rightarrow T} \quad \text{([⇒] ProveBy-Synth)}

🔗def
Strata.Laurel.Resolution.Synth.proveBy (exprMd val proof : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.ProveBy val proof) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.proveBy (exprMd val proof : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.ProveBy val proof) (expected? : Option Strata.Laurel.HighTypeMd := none) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

(ProveBy-Synth)

Γ ⊢ v ⇒ T
Γ ⊢ proof ⇒ _
────────────────────────────
Γ ⊢ ProveBy v proof ⇒ T

Like old, ProveBy v proof is type-transparent in v — the proof is just a hint for downstream verification and carries no typing constraint. In a synthesis position v is synthesized for its type T, proof is synthesized only for its name-resolution side effects (its type is discarded), and T is returned.

expected? is the caller's slot, threaded into v only — the same subexpression Check.proveBy pushes its expected into. The proof is not value-producing, so it keeps its unconstrained synth position and the obligation it states is untouched.

3.4.12. Self reference🔗

\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{some}\;T}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{UserDefined}\;T} \quad \text{([⇒] This-Inside)}

\frac{\Gamma.\mathit{instanceTypeName} = \mathsf{none}}{\Gamma \vdash \mathsf{This} \Rightarrow \mathsf{Unknown} \quad [\text{emits “‘this’ is not allowed outside instance methods”}]} \quad \text{([⇒] This-Outside)}

🔗def
Strata.Laurel.Resolution.Synth.this (source : Strata.FileRange) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.this (source : Strata.FileRange) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

Cases on whether instanceTypeName is set (i.e., we're inside an instance method).

Γ.instanceTypeName = some T      (This-Inside)
───────────────────────────
Γ ⊢ This ⇒ UserDefined T

Γ.instanceTypeName = none        (This-Outside)
───────────────────────────
Γ ⊢ This ⇒ Unknown               (emits "'this' is not allowed outside instance methods")

When instanceTypeName is set (we're inside an instance method, populated on ResolveState by resolveInstanceProcedure for the duration of an instance method body), This synthesizes UserDefined T. With it, this.field and instance-method dispatch synthesize real types instead of being wildcarded through Unknown. Otherwise an error is emitted ("'this' is not allowed outside instance methods") and the type collapses to Unknown to suppress cascading errors.

3.4.13. Untyped forms🔗

\frac{}{\Gamma \vdash \mathsf{Abstract}\,/\,\mathsf{All}\;\ldots \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Abstract / All)}

🔗def
Strata.Laurel.Resolution.Synth.abstract (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd
Strata.Laurel.Resolution.Synth.abstract (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd

Γ ⊢ Abstract ⇒ Unknown

🔗def
Strata.Laurel.Resolution.Synth.all (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd
Strata.Laurel.Resolution.Synth.all (source : Strata.FileRange) : Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd

Γ ⊢ All ⇒ Unknown

3.4.14. ContractOf🔗

\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}, \mathit{unresolved}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Precondition}\;\mathit{fn} \Rightarrow \mathsf{TBool} \qquad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{PostCondition}\;\mathit{fn} \Rightarrow \mathsf{TBool}} \quad \text{([⇒] ContractOf-Bool)}

\frac{\mathit{fn} = \mathsf{Var}\;(\mathsf{.Local}\;\mathit{id}) \quad \Gamma(\mathit{id}) \in \{\mathit{staticProcedure}, \mathit{instanceProcedure}, \mathit{unresolved}\}}{\Gamma \vdash \mathsf{ContractOf}\;\mathsf{Reads}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown} \qquad \Gamma \vdash \mathsf{ContractOf}\;\mathsf{Modifies}\;\mathit{fn} \Rightarrow \mathsf{TSet}\;\mathsf{Unknown}} \quad \text{([⇒] ContractOf-Set)}

\frac{\mathit{fn} \text{ is not a } \mathsf{Var}\;(\mathsf{.Local}) \text{ resolving to a procedure or unresolved name}}{\Gamma \vdash \mathsf{ContractOf}\;\ldots\;\mathit{fn} \rightsquigarrow \text{error: “‘contractOf’ expected a procedure reference”}} \quad \text{([⇒] ContractOf-Error)}

The \mathit{unresolved} kind is admitted so an already-reported name-resolution error is not duplicated; ContractOf-Error fires only when \mathit{fn} resolves to a present non-procedure definition (or is not a local reference at all).

🔗def
Strata.Laurel.Resolution.Synth.contractOf (exprMd : Strata.Laurel.StmtExprMd) (ty : Strata.Laurel.ContractType) (fn : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.ContractOf ty fn) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)
Strata.Laurel.Resolution.Synth.contractOf (exprMd : Strata.Laurel.StmtExprMd) (ty : Strata.Laurel.ContractType) (fn : Strata.Laurel.StmtExprMd) (source : Strata.FileRange) (h : exprMd.val = Strata.Laurel.StmtExpr.ContractOf ty fn) : Strata.Laurel.ResolveM (Strata.Laurel.StmtExpr × Strata.Laurel.HighTypeMd)

Cases on the contract type ty and on whether fn is a procedure reference.

fn = Var (.Local id)                                       (ContractOf-Bool)
Γ(id) ∈ {staticProcedure, instanceProcedure, unresolved}
────────────────────────────────────────────
Γ ⊢ ContractOf Precondition fn ⇒ TBool
Γ ⊢ ContractOf PostCondition fn ⇒ TBool

fn = Var (.Local id)                                       (ContractOf-Set)
Γ(id) ∈ {staticProcedure, instanceProcedure, unresolved}
────────────────────────────────────────────
Γ ⊢ ContractOf Reads fn ⇒ TSet Unknown
Γ ⊢ ContractOf Modifies fn ⇒ TSet Unknown

fn is not a Var (.Local) resolving to a procedure          (ContractOf-Error)
  or unresolved name
────────────────────────────────────────────
Γ ⊢ ContractOf _ fn ↝ error: "'contractOf' expected a procedure reference"

ContractOf ty fn extracts a procedure's contract clause as a value: its preconditions (Precondition), postconditions (PostCondition), reads set (Reads), or modifies set (Modifies). fn must be a direct identifier reference resolving to a procedure — a contract belongs to a named procedure, not an arbitrary expression. The diagnostic "'contractOf' expected a procedure reference" fires (and the construct synthesizes Unknown to suppress cascading errors) when fn is anything other than a Var (.Local id), or resolves to a present definition that is not a procedure. An unresolved id, or one absent from scope, is accepted without firing the diagnostic — its name-resolution error was already reported.

Precondition and PostCondition are propositions, hence TBool. Reads and Modifies are sets of heap-allocated locations — composite/datatype references and fields. The element type is left as Unknown for now since the rule doesn't yet recover it from fn's declared modifies/reads clauses.

The constructor is reserved for future use — Laurel's grammar has no contractOf production today, and the translator emits "not yet implemented" for it. The typing rule exists so resolution remains exhaustive over StmtExpr.

3.4.15. Holes🔗

\frac{T_h <: T}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T_h) \Leftarrow T} \quad \text{([⇐] Hole-Some)}

🔗def
Strata.Laurel.Resolution.Check.holeSome (det : Bool) (ty expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.holeSome (det : Bool) (ty expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) : Strata.Laurel.ResolveM Strata.Laurel.StmtExprMd

(Hole-Some)

T_h <: T
────────────────────────────
Γ ⊢ Hole d (some T_h) ⇐ T

A typed hole carries the user's annotation T_h. The annotation is resolved and verified against the surrounding expected type via subsumption; the resolved annotation is preserved on the node so downstream passes (hole elimination) can generate correctly typed uninterpreted functions.

\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Leftarrow T \quad \mapsto \quad \mathsf{Hole}\;d\;(\mathsf{some}\;T)} \quad \text{([⇐] Hole-None)}

🔗def
Strata.Laurel.Resolution.Check.holeNone (det : Bool) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) : Strata.Laurel.StmtExprMd
Strata.Laurel.Resolution.Check.holeNone (det : Bool) (expected : Strata.Laurel.HighTypeMd) (source : Strata.FileRange) : Strata.Laurel.StmtExprMd

(Hole-None)

────────────────────────────────────────
Γ ⊢ Hole d none ⇐ T  ↦  Γ ⊢ Hole d (some T)

An untyped hole in check mode records the expected type on the node so downstream passes (hole elimination) don't have to infer it again.

In synth position no expected type is available to push into the hole, so an unannotated hole synthesizes the gradual \mathsf{Unknown} while an annotated hole synthesizes its annotation T_h (this is what lets <?> + 1 synthesize \mathsf{TInt}).

\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;\mathsf{none} \Rightarrow \mathsf{Unknown}} \quad \text{([⇒] Hole-Synth-None)}

\frac{}{\Gamma \vdash \mathsf{Hole}\;d\;(\mathsf{some}\;T_h) \Rightarrow T_h} \quad \text{([⇒] Hole-Synth-Some)}

3.4.16. Procedure🔗

A procedure body is synthesized (not checked against a computed expected type) and is resolved under a scope that includes the procedure's input and output parameters. The Return rules above refer to the same output list \overline{T_o} that the procedure binds here.

\frac{\overline{T_o} = \mathit{proc}.\mathit{outputs}.\mathit{types} \quad \Gamma_\mathit{global},\,\mathit{params}(\mathit{proc}) \vdash \mathit{proc}.\mathit{body} \Rightarrow \_}{\Gamma_\mathit{global} \vdash \mathsf{Procedure}\;\mathit{proc}} \quad \text{(Procedure)}

The body is synthesized and its type is discarded — there is no constraint from the output list pushed into the body. Outputs are matched only via return e (checked against \overline{T_o} by Check.return) or via named-output assignment.

🔗def

(Procedure)

T_o-bar = proc.outputs.types
Γ_global, params(proc) ⊢ proc.body ⋄
──────────────────────────────────────────────────────────
Γ_global ⊢ Procedure proc

The body is resolved in statement position (Check.statement: its value is discarded) under a scope that includes the procedure's input and output parameters. Outputs are matched only via return e (checked against the declared output by Check.return) or via named-output assignment. The procedure's declared output list T_o-bar is stored on ResolveState.answerType, set on entry and restored on exit.

🔗def
Strata.Laurel.resolveInstanceProcedure (typeName : Strata.Laurel.Identifier) (proc : Strata.Laurel.Procedure) : Strata.Laurel.ResolveM Strata.Laurel.Procedure
Strata.Laurel.resolveInstanceProcedure (typeName : Strata.Laurel.Identifier) (proc : Strata.Laurel.Procedure) : Strata.Laurel.ResolveM Strata.Laurel.Procedure

Resolve an instance procedure on a composite type.