16%

Section 15D: Bindings & Types

Goal: Implement binding syntax changes and type system simplifications across six approved proposals.

Source proposals (all approved):

  • compiler_repo/docs/ori_lang/proposals/approved/checks-proposal.md
  • compiler_repo/docs/ori_lang/proposals/approved/as-conversion-proposal.md
  • compiler_repo/docs/ori_lang/proposals/approved/simplified-bindings-proposal.md
  • compiler_repo/docs/ori_lang/proposals/approved/remove-dyn-keyword-proposal.md
  • compiler_repo/docs/ori_lang/proposals/approved/index-assignment-proposal.md
  • compiler_repo/docs/ori_lang/proposals/approved/mutable-self-proposal.md

Phase contracts (load-bearing, do NOT relocate work across phases without amending these SSOTs first):

  • typeck.md §EX-17 — index/field assignment desugar runs in the type checker, not canon.
  • canon.md §2 row 3-4 — same; surface desugars are eliminated before canonical IR.
  • canon.md §7.1 Invariant 5 — new capabilities extend the unified model, never spawn parallel paths.
  • section-03-traits.md §3.11 — object-safety machinery (already shipped) is reused by §15D.4, not re-implemented.
  • spec/08-types.mdAs<T> / TryAs<T> semantics; lossy as rejection.

Ownership map (owns_crates: [] is correct — this section is a phase-local audit; cross-cutting work is owned upstream — verified against each upstream section’s frontmatter owns_crates: 2026-05-06):

  • ori_lexer_core, ori_lexer, ori_parse — owned by §00-parser
  • ori_ir, ori_types — owned by §01-type-system
  • ori_canon, ori_arc (AIMS pipeline), ori_repr, ori_llvm — owned by §21A-llvm
  • ori_eval, ori_patterns — owned by §23-evaluator
  • ori_registry — owned by §03-traits
  • ori_fmt, ori_diagnostic, ori_test_harness, ori_stack, ori_compiler, oric, ori_lsp — owned by §22-tooling

Test-file convention: any tests/spec/... or tests/compile-fail/... path that does not yet exist on disk is a plan deliverable — the implementer creates the file as part of the corresponding checkbox. The path is the WHERE annotation, not a citation of existing code.

Subsection dependency (encoded in body prose since SubsectionEntry schema does not yet carry inter-subsection edges): §15D.6 (Mutable Self) is BLOCKED-BY §15D.5 (Index/Field Assignment) because state.f = v inside method bodies depends on §15D.5’s field-assign desugar landing first. /continue-roadmap MUST execute §15D.5 to completion before opening §15D.6.


15D.1 Function-Level Contracts: pre() / post()

Proposal: proposals/approved/checks-proposal.md (semantics), proposals/approved/block-expression-syntax.md (syntax)

Function-level pre() and post() contract declarations for defensive programming. Contracts go between the return type and =:

@divide (a: int, b: int) -> int
    pre(b != 0)
    post(r -> r * b <= a)
= a div b

// Multiple conditions
@transfer (from: Account, to: Account, amount: int) -> (Account, Account)
    pre(amount > 0 | "amount must be positive")
    pre(from.balance >= amount | "insufficient funds")
    post((f, t) -> f.balance == from.balance - amount)
    post((f, t) -> t.balance == to.balance + amount)
= {
    // ... body ...
}

Key Design Decisions

  • Function-level placement: Contracts go on the declaration between return type and =
  • Multiple declarations: Use multiple pre() / post() declarations (not list syntax)
  • | for messages: Custom messages use condition | "message" syntax
  • Scope constraints: pre() can only access function parameters and module-level bindings; post() can access the result via lambda parameter
  • Void return: Compile error if post() used on a function returning void
  • Check modes deferred: check_mode: (enforce/observe/ignore) deferred to future proposal

Implementation

Status note (2026-05-06): Parser is shipped — call site at ori_parse/src/grammar/item/function/mod.rs:131 invokes parse_contracts() whose definition begins at line 221 of the same file. Blast-radius producer gap is the type-checker validation pass at ori_types/src/check/bodies/functions.rs (currently does NOT enforce pre() is bool, post() is a lambda, or void-return rejection). Implementation order below MUST be: (a) failing-test matrix → (b) typeck validation → (c) desugar/codegen → (d) interpreter+LLVM parity verification.


  • TDD-first: Write failing test matrix BEFORE implementation

    • Matrix dimensions: contract type (pre / post) × condition shape (literal-true / literal-false / param-ref / lambda-with-result / void-return / scope-violation) × backend (interpreter / LLVM)
    • WHERE: tests/spec/patterns/checks.ori, tests/spec/patterns/check_messages.ori, tests/compile-fail/checks/{pre_not_bool,post_not_lambda,post_void_return,pre_scope}.ori
    • Semantic pin: at least one positive pre() test that ONLY passes when the panic on false fires; one negative pin that rejects a pre() returning int instead of bool.
  • Implement (DONE): Parser parses pre() and post() on function declarations

    • ori_parse/src/grammar/item/function/mod.rsparse_contracts() at line 221 returns (Vec<PreContract>, Vec<PostContract>). Contracts parsed between return type and =.
    • AST: PreContract { condition, message, span } and PostContract { params: Vec<Name>, condition, message, span } in ori_ir/src/ast/items/function.rs (verified — PostContract carries params: Vec<Name> because the lambda may be (a, b) -> cond with multiple binders for tuple-returning functions). FunctionDef has pre_contracts and post_contracts fields.
    • Parser tests: ori_parse/src/grammar/item/function/tests.rs:144 (test_pre_contract_basic), :159 (test_pre_contract_with_message), :172 (test_post_contract_basic), :187 (test_post_contract_tuple_params). Plan items previously labelled NEEDS TESTS were stale per blind-spots Round 1.
  • Implement (DONE): Parser supports | "message" custom message syntax

    • PreContract.message: Option<Name>. PostContract.message: Option<Name>. Parser handles | separator.
  • Implement: Type checker validates pre() condition is bool

    • WHERE: ori_types/src/check/bodies/functions.rs — add validate_pre_contracts() at the end of body-checking
    • Diagnostic: new error code in ori_diagnostic/src/error_code/mod.rs (E2xxx range — coordinate with §00-parser owner before allocating)
    • Rust Tests: ori_types/src/check/bodies/tests.rs — pre type validation
    • Ori Tests: tests/compile-fail/checks/pre_not_bool.ori
  • Implement: Type checker validates post() is T -> bool lambda where T matches return type

    • WHERE: same module as the pre() validator; share helper that asserts Tag::Function shape
    • Rust Tests: ori_types/src/check/bodies/tests.rs — post type validation
    • Ori Tests: tests/compile-fail/checks/post_not_lambda.ori
  • Implement: Type checker errors when post() used on void-returning function

    • Rust Tests: same module; void-return rejection test
    • Ori Tests: tests/compile-fail/checks/post_void_return.ori
  • Implement: Scope checker — pre() can only access parameters and module-level bindings (no body locals leak in via shadowing)

    • WHERE: ori_types/src/check/bodies/functions.rs — restricted scope walker on the contract expression tree
    • Rust Tests: ori_types/src/check/bodies/tests.rs — pre scope validation
    • Ori Tests: tests/compile-fail/checks/pre_scope.ori
  • Implement: Desugar pre() / post() contracts to conditional-panic blocks at function entry/exit — performed ONCE in the type checker (or ori_canon if typeck-rewrite tooling cannot host it; choose at implementation time but commit to a single phase). Per canon.md §7.1 Invariant 5 (unified model) and canon.md §1 phase ordering, BOTH backends consume the desugared CanExpr form — no per-backend desugar logic.

    • WHERE: ori_types/src/check/bodies/functions.rs (preferred — typeck rewrite, same module as the validators above) OR ori_canon/src/desugar/ (fallback — canonical-IR rewrite if typeck rewrite tooling cannot host it). Pick ONE; do NOT split across both.
    • Backend rule: ori_eval and ori_llvm see the post-desugar CanExpr (entry-panic-block + exit-panic-block already inlined into the function body) and emit it identically — NO contract-aware code in either backend.
    • Rust Tests: typeck/canon desugar tests in the chosen module’s tests.rs; ori_llvm/tests/aot/contracts.rs verifies the post-desugar shape compiles without backend-specific contract handling.
    • Ori Tests: tests/spec/patterns/checks_desugaring.ori
  • Implement: Desugar embeds source text for default error messages

    • WHERE: same module as the desugar above; thread the contract expression’s Span source text into the synthesized panic call before the rewrite reaches CanExpr.
    • Rust Tests: same test module — source-text embedding present in the rewritten body
    • Ori Tests: tests/spec/patterns/checks_error_messages.ori
  • Verify: All new tests pass in BOTH debug and release builds (matrix verification)

  • Verify: Interpreter and LLVM produce identical observable behavior for every test (ORI_CHECK_LEAKS=1 clean for memory-touching cases)

  • /tpr-review passed (this subsection only) — see §15D.shared-close-out

  • /impl-hygiene-review passed (after TPR clean) — see §15D.shared-close-out

  • Subsection close-out (15D.1) — see §15D.shared-close-out

  • /sync-claude section-close doc sync — see §15D.shared-close-out

  • Repo hygiene check — see §15D.shared-close-out

15D.2 as Conversion Syntax

Proposal: proposals/approved/as-conversion-proposal.md

Replace int(), float(), str(), byte() with as / as? keyword syntax backed by As<T> and TryAs<T> traits.

AOT GAP: lower_cast() at ori_arc/src/lower/collections/mod.rs:320 interns __cast and emits Apply(dst, __cast, [val]), but __cast has zero registration in ori_ir/src/builtin_constants/protocol/ (verified — no entry in protocol/mod.rs) and zero handler in ori_llvm/src/codegen/arc_emitter/apply_protocols.rs (no match arm for "cast"). Every AOT as silently link-fails or crashes at runtime. This subsection’s primary deliverable is closing that gap.

DISPOSITION DRIFT: tests/spec/expressions/type_conversion.ori is 458 lines, 457 of which are //-prefixed (one substantive // TODO comment). All test cases commented out. Re-enabling them is a §15D.2 deliverable, NOT a separate #skip — the file is currently inert.

EVALUATOR/SPEC DRIFT: eval_can_cast at ori_eval/src/interpreter/can_eval/operators.rs permits float -> int truncation. spec/08-types.md:1299-1308 REJECTS lossy float → int at compile time under both as and as? — the spec migration path is explicit truncate() / round() / floor() / ceil() methods, NOT as?. The hardcoded primitive cast path in the evaluator MUST move to trait dispatch via As<T> / TryAs<T> AND drop the float → int truncation entirely (no As<int> for float, no TryAs<int> for float impl); reversing that order cements LEAK:duplicated-dispatch between eval (hardcoded) and LLVM (whatever lands in __cast).

// Before (special-cased positional args)
let x = int("42")
let y = float(value)

// After (consistent keyword syntax)
let x = "42" as? int
let y = value as float

Implementation Order — Trait First, Then Backends

The architecturally correct order (per canon.md §7.1 Invariant 5 and typeck.md §EX-2 registry-driven dispatch):

  1. Define As<T> / TryAs<T> traits in prelude.
  2. Type checker resolves as / as? against the trait registry — single dispatch path.
  3. Evaluator routes through the same registry lookup; the hardcoded eval_can_cast primitive ladder is removed.
  4. LLVM codegen registers __cast protocol + handler so the resolved trait method has a real backend.
  5. Spec test corpus is uncommented; all paths verified in BOTH backends.

Reversing steps 2 and 3 (or skipping step 2 entirely as the current shipped code does) cements parallel dispatch.

TDD-first Matrix (write BEFORE implementation)

  • TDD-first: Failing test matrix

    • Dimensions: source type × target type × as vs as? × backend (interpreter / LLVM)
    • Source × target pairs (per spec/08-types.md §8.11.2-§8.11.5):
      • int → float (lossless via As<float>)
      • int → byte (range-checked via TryAs<byte>)
      • byte → int (lossless via As<int>)
      • char → int (codepoint value, lossless via As<int>)
      • int → char (valid-codepoint check, fallible via TryAs<char>)
      • str → int (fallible via TryAs<int>)
      • str → float (fallible via TryAs<float>)
      • str → bool (fallible via TryAs<bool>)
    • Lossy float → int is REJECTED at compile time by both as and as? per spec/08-types.md:1299-1308. NOT a positive matrix cell. Verified via dedicated negative pin: tests/compile-fail/as_lossy_float_to_int.ori rejects both f as int and f as? int; spec migration hint points users at f.truncate() / f.round() / f.floor() / f.ceil().
    • Each pair: positive test (works) + negative pin (as rejected when only TryAs is implemented; as? rejected when only As is implemented)
    • WHERE: tests/spec/expressions/type_conversion.ori (un-comment + extend), tests/compile-fail/{as_fallible_conversion,as_not_implemented,try_as_not_implemented,as_lossy_float_to_int}.ori

Lexer

  • Implement (DONE): as keyword token

    • TokenKind::As in ori_lexer/src/keywords/mod.rs:50. Test at line 183.

Parser

  • Implement (DONE): Parse expression as Type and expression as? Type

    • ori_parse/src/grammar/expr/postfix.rs:120 checks for TokenKind::As; ? after as sets fallible: true. Produces ExprKind::Cast { expr, ty, fallible }.

Trait Definition

  • Implement: Define As<T> and TryAs<T> traits in compiler_repo/library/std/prelude.ori

    • Required signatures: @as (self) -> T for As<T>; @try_as (self) -> Option<T> for TryAs<T> (per spec/08-types.md and as-conversion-proposal.md)
    • WHERE: prelude additions; register in ori_registry::BUILTIN_TYPES per canon.md §6 “Builtin type behavior”
    • Rust Tests: ori_types/src/check/registration/tests.rs — As/TryAs registration

Type Checker

  • Implement: infer_cast() in ori_types/src/infer/expr/operators.rs (currently line 466) routes through TraitRegistry::lookup_method() for As<T> / TryAs<T> — replace the type-resolution-only path with full trait dispatch.

    • Rust Tests: ori_types/src/infer/expr/tests.rs — cast trait resolution
    • Ori Tests: included in the §15D.2 matrix above
  • Implement: Validate as only used with As<T> impls (lossless); as? required when only a TryAs<T> impl exists (fallible). Lossy conversions (e.g. float → int) are REJECTED by both as and as? per spec/08-types.md:1299-1308; the diagnostic for these MUST emit the spec migration hint (f.truncate() / .round() / .floor() / .ceil()).

    • Ori Tests: tests/compile-fail/as_not_implemented.ori, tests/compile-fail/as_fallible_conversion.ori, tests/compile-fail/as_lossy_float_to_int.ori
  • Implement: Validate as? only used with TryAs<T> impls

    • Ori Tests: tests/compile-fail/try_as_not_implemented.ori

Evaluator (remove parallel dispatch)

  • Implement: eval_can_cast() at ori_eval/src/interpreter/can_eval/operators.rs (line ~119–142+) routes through the resolved trait method instead of the hardcoded primitive ladder. The hardcoded conversions move into the trait As<T> / TryAs<T> impls for primitives, registered via ori_registry.

    • Rust Tests: ori_eval/src/interpreter/can_eval/tests.rs — cast via trait dispatch
    • Remove the float -> int infallible path entirely (NO As<int> for float and NO TryAs<int> for float impl); both as and as? MUST reject per spec/08-types.md:1299-1308. Users migrate to f.truncate() / .round() / .floor() / .ceil().

LLVM / AOT (close the __cast gap)

  • Implement: Register __cast as a protocol builtin in ori_ir/src/builtin_constants/protocol/mod.rs

    • Add __cast to the protocol enumeration; thread through protocol/tests.rs registration tests.
    • Rust Tests: ori_ir/src/builtin_constants/protocol/tests.rs__cast registration
  • Implement: Handle __cast in ori_llvm/src/codegen/arc_emitter/apply_protocols.rs

    • Emit appropriate LLVM cast instructions per source/target type pair (per spec/08-types.md §8.11.2-§8.11.5 — lossy float → int is spec-rejected and has no LLVM emission path):
      • sitofp (int → float) — lossless As<float>
      • trunc + range-check helper (int → byte) — fallible TryAs<byte> ONLY; as form is compile-rejected (no emission path), as? returns Option<byte> per the TDD matrix’s lossy-rejection rule
      • zext (byte → int) — lossless As<int>
      • zext (char → int) — codepoint value, lossless As<int> (Ori char is a 32-bit codepoint scalar; widen to i64 via zext, NOT bitcast — different bit widths)
      • Codepoint validity helper + trunc (int → char) — fallible TryAs<char>; emit Some(char) / None per Option<char> ABI when valid, else None
      • Native FFI calls into ori_rt for str → int / str → float / str → bool parsing helpers (fallible TryAs<*>)
    • Rust Tests: ori_llvm/tests/aot/conversions.rs — as / as? AOT codegen
    • AOT Tests: cover all matrix dimensions in §15D.2 TDD matrix above; ORI_VERIFY_ARC=1 clean.

Migration

  • Implement: Migration-hint phase — keep the int() / float() / str() / byte() single-arg recognizer in the parser, but emit E1xxx migration-hint diagnostic pointing at x as int / x as? int syntax. Mirrors the dyn keyword removal pattern in §15D.4 (recognized-but-rejected token producing a migration hint).

    • WHERE: ori_parse/src/grammar/expr/primary/ — keep the single-arg call recognizer; route it to a diagnostic-only path that produces no AST node (or a synthetic Cast node when recovery is required) and emits the migration hint.
    • Ori Tests: tests/compile-fail/old_conversion_function_syntax.ori
  • Implement: Removal phase — once one full release cycle has passed with the migration-hint phase shipping (tracked separately at /add-bug time when the cycle elapses), drop the int() / float() / str() / byte() recognizer entirely. NOT a §15D deliverable; documented here so the implementer of the migration-hint phase knows the recognizer is intentionally retained.

Verification


  • Verify: All matrix tests pass in interpreter AND LLVM; debug AND release; ORI_CHECK_LEAKS=1 clean
  • Verify: tests/spec/expressions/type_conversion.ori fully uncommented and green; the file’s previous state of 457/458 commented is closed.
  • /tpr-review passed (this subsection only) — see §15D.shared-close-out
  • /impl-hygiene-review passed (after TPR clean) — see §15D.shared-close-out
  • Subsection close-out (15D.2) — see §15D.shared-close-out
  • /sync-claude section-close doc sync — see §15D.shared-close-out
  • Repo hygiene check — see §15D.shared-close-out

15D.3 Simplified Bindings with $ for Immutability

Proposal: proposals/approved/simplified-bindings-proposal.md

Simplified binding model:

  • let x is mutable.
  • let $x is immutable.
  • The mut keyword is removed.
  • Module-level bindings require the $ prefix.
// Before
let x = 5         // immutable
let mut x = 5     // mutable
$timeout = 30s    // config variable

// After
let x = 5         // mutable
let $x = 5        // immutable
let $timeout = 30s // module-level constant (let and $ required)

TDD-first Matrix

  • TDD-first: Failing test matrix BEFORE implementation

    • Dimensions: binding kind (let x / let $x / let $name = expr module-level) × usage (read / reassign / shadow) × scope (block / module / import) × backend (interpreter / LLVM)
    • Each cell: positive (works) + negative (rejected with the right error code)

Lexer

  • Implement (DONE): Remove mut from reserved keywords

    • No mut in ori_lexer/src/keywords/mod.rs. No KwMut token.

Parser

  • Implement (DONE): Update let_expr to accept $ prefix in binding pattern (2026-02-20)

    • Rust Tests: ori_parse/src/tests/parser.rs — block let binding with $ prefix
    • Ori Tests: tests/spec/expressions/immutable_bindings.ori
  • Implement (DONE): Remove mut from let_expr grammar

    • Ori Tests: All 151 let mut occurrences migrated to let across 25 test files + AOT tests
    • AOT Tests: ori_llvm/tests/aot/mutations.rs — 21 tests use let x (mutable-by-default) with reassignment
  • Implement: Update constant_decl to require let $name = expr (no bare $name = expr form)

    • WHERE: ori_parse/src/dispatch.rs (declaration dispatch — the bare $ constant path) and ori_parse/src/grammar/item/config/mod.rs (config-style constant parsing); coordinate with ori_parse/src/grammar/ty/mod.rs and ori_types/src/check/imports.rs (note plural imports.rs) if any imports of bare-$ constants need to be reconciled
    • Rust Tests: dispatch / config parser tests — constant declaration parsing
    • Ori Tests: tests/spec/declarations/constants.ori
    • AOT Tests: cover constant declaration in LLVM AOT corpus
  • Implement: Remove old const function syntax $name (params) -> Type

    • WHERE: ori_parse/src/dispatch.rs and the function-form parser under ori_parse/src/grammar/item/function/ — drop the const-function parse path (post-proposal it is redundant with let $name = (params) -> Type = body)
    • Ori Tests: tests/compile-fail/old_const_function_syntax.ori (migration error)
  • Implement (DONE): Support $ prefix in destructuring patterns (2026-02-20)

    • Rust Tests: ori_parse/src/grammar/expr/primary/parse_binding_pattern handles $ for Name, Tuple, Struct, List
    • Ori Tests: tests/spec/expressions/immutable_bindings.ori — tuple, struct, list destructuring with $
  • Implement (DONE): List rest binding ..rest tracks $ mutability (2026-02-20)

    • IR: BindingPattern::List.rest is Option<(Name, Mutability)> (ori_ir/src/ast/patterns/binding/mod.rs:58 — verified; Mutability is the canonical newtype enum, not a raw bool)
    • IR canon: CanBindingPattern::List.rest is Option<(Name, Mutability)> (ori_ir/src/canon/patterns.rs:33 — verified)
    • Parser: handles $ before rest identifier (ori_parse/src/grammar/expr/primary/ — directory; parse_binding_pattern is in the primary submodule)
    • Type checker: bind_pattern() uses bind_with_mutability() for rest (ori_types/src/infer/expr/sequences.rs:489)
    • Evaluator: bind_can_pattern() uses per-binding rest_mutable (ori_eval/src/interpreter/can_eval/ — directory; can_eval.rs does not exist as a single file, the audit’s reference is path-stale)
    • Canon: lowering passes through (Name, Mutability) tuple (ori_canon/src/lower/patterns.rs)
    • Formatter: emits $ prefix on immutable rest bindings (ori_fmt/src/formatter/patterns.rs)
    • Grammar: grammar.ebnf updated to allow [ "$" ] on rest identifier (line 581)

Semantic Analysis

  • Implement (DONE): Track $ modifier separately from identifier name (2026-02-20)

    • ori_types/src/infer/env/mod.rsTypeEnvInner::mutability FxHashMap tracks per-binding mutability
    • tests/spec/expressions/mutable_vs_immutable.ori — verifies $ bindings preserve immutability
  • Implement: Prevent $x and x coexisting in same scope (same-name conflict)

    • WHERE: ori_types/src/check/bindings.rs (ensure module exists; if not, add it; coordinate with §01-type-system owner)
    • Rust Tests: same-name conflict detection
    • Ori Tests: tests/compile-fail/dollar_and_non_dollar_conflict.ori
  • Implement: Enforce module-level bindings require $ prefix

    • WHERE: ori_types/src/check/mod.rs (module-level binding enforcement entry point — module.rs does not exist; module-binding logic lives in the check crate’s root)
    • Rust Tests: module binding immutability test
    • Ori Tests: tests/compile-fail/module_level_mutable.ori
  • Implement (DONE): Enforce $-prefixed bindings cannot be reassigned (2026-02-20)

    • ori_types/src/infer/expr/operators.rs — immutability check in infer_assign
    • tests/compile-fail/assign_to_immutable.ori, assign_to_immutable_in_loop.ori, assign_to_immutable_destructured.ori

Imports

  • Implement: Require $ in import statements for immutable bindings (use module { $name } or use module { name } based on the binding’s declared mutability)

    • WHERE: ori_types/src/check/imports.rs
    • Ori Tests: tests/spec/modules/import_immutable.ori
  • Implement: Error when importing $x as x or vice versa (mismatch)

    • Ori Tests: tests/compile-fail/import_dollar_mismatch.ori

Shadowing

  • Implement: Allow shadowing to change mutability (per spec)

    • Ori Tests: tests/spec/expressions/shadow_mutability.ori

Error Messages

  • Implement (DONE): Clear error for reassignment to immutable binding (2026-02-20)

    • ori_types/src/type_error/check_error/mod.rsAssignToImmutable variant + formatting
    • tests/compile-fail/assign_to_immutable.ori
  • Implement: Clear error for module-level mutable binding (no $)

    • Ori Tests: tests/compile-fail/module_mutable_message.ori
  • Implement: Migration hint when old let mut syntax appears (now lex-rejected, but the diagnostic should suggest let directly)

    • Ori Tests: tests/compile-fail/let_mut_migration.ori

Verification


  • Verify: All matrix tests green in BOTH interpreter and LLVM (debug + release)
  • /tpr-review passed (this subsection only) — see §15D.shared-close-out
  • /impl-hygiene-review passed (after TPR clean) — see §15D.shared-close-out
  • Subsection close-out (15D.3) — see §15D.shared-close-out
  • /sync-claude section-close doc sync — see §15D.shared-close-out
  • Repo hygiene check — see §15D.shared-close-out

15D.4 Remove dyn Keyword for Trait Objects

Proposal: proposals/approved/remove-dyn-keyword-proposal.md

Remove the dyn keyword for trait objects. Trait names used directly as types mean “any value implementing this trait.”

// Before
@process (item: dyn Printable) -> void = ...
let items: [dyn Serializable] = ...

// After
@process (item: Printable) -> void = ...
let items: [Serializable] = ...

Re-use existing object-safety machinery — DO NOT duplicate. section-03-traits.md §3.11 already shipped object-safety checking (ObjectSafetyViolation enum + TraitEntry::is_object_safe() at ori_types/src/registry/traits/mod.rs; compute_object_safety_violations() at registration; check_parsed_type_object_safety() at signature sites). §15D.4’s job is to wire trait-as-type parsing into the existing checker, never to re-implement object-safety logic. Plumbing changes touch the parser and the type-resolution-of-named-types path; safety enforcement is unchanged.

Status note (2026-05-06): §15D.4 is in-progress — the object-safety enforcement at ori_types/src/check/object_safety.rs is already shipped (per §3.11). The parser surface at ori_parse/src/grammar/ty/mod.rs already accepts trait-as-type forms. Remaining work is the dyn keyword removal + migration-hint diagnostic and the matrix verification across both backends.

  • Already shipped: Object-safety enforcement infrastructure at ori_types/src/check/object_safety.rs (per §3.11) — check_parsed_type_object_safety() at line 35, invoked from signature-site lowering. §15D.4 reuses this; the remaining work is parser/resolver plumbing to route trait-as-type identifiers through it.

TDD-first Matrix

  • TDD-first: Failing test matrix BEFORE implementation

    • Dimensions: type position (param / return / field / list element / map value) × trait shape (object-safe / non-object-safe — Clone / Eq / Iterator) × dyn presence (legacy dyn Trait / new Trait) × generic-bound vs trait-object disambiguation × backend
    • Each cell: positive (compiles) + negative (E2024 for non-object-safe trait used as type)

Implementation

  • Implement: Remove "dyn" type from grammar type production

    • WHERE: ori_parse/src/grammar/ty/mod.rs
    • Update compiler_repo/docs/ori_lang/v2026/spec/grammar.ebnf via /sync-grammar (NOT direct edit per CLAUDE.md §Spec & Grammar Changes)
    • Rust Tests: ori_parse/src/grammar/ty/mod.rs parser tests — dyn removal
    • Ori Tests: tests/spec/types/trait_objects.ori
  • Implement: Parser emits a single Type::Named node for any bare identifier in type position; trait-vs-nominal disambiguation is the type checker’s job (NOT the parser’s — parser MUST stay syntax-only per compiler.md §Phase-Specific Purity and canon.md §5; consulting ori_types::TraitRegistry from the parser is LEAK:phase-bleeding).

    • WHERE: ori_parse/src/grammar/ty/mod.rs — already emits Type::Named; verify no special-casing for “trait” identifiers leaks in.
    • WHERE (resolution): ori_types/src/check/signatures/mod.rs — the resolver uses TraitRegistry::contains_trait(name) (or get_trait_by_name(name) when the entry is needed) at compiler_repo/compiler/ori_types/src/registry/traits/lookup.rs:22,38 when it lowers Type::Named to a Tag; if the name is a trait, lowering proceeds via the existing trait-object machinery (check_parsed_type_object_safety() at ori_types/src/check/object_safety.rs:35, invoked from signatures/mod.rs per §3.11) without naming a specific Tag variant — the concrete representation is determined by the type system’s existing trait-object handling, not introduced as a new tag here.
    • Ori Tests: tests/spec/types/trait_objects.ori
  • Implement: Type checker distinguishes item: Trait (trait object) vs <T: Trait> (generic bound)

    • WHERE: ori_types/src/check/signatures/mod.rs — disambiguate by syntactic position; Trait in type position is trait-object, <T: Trait> adds bound
    • Rust Tests: ori_types/src/check/signatures/tests.rs
    • Ori Tests: tests/spec/types/trait_vs_bound.ori
  • Implement: Wire object-safety check at every trait-as-type site (NO new check_object_safety module — re-use check_parsed_type_object_safety() from ori_types/src/check/signatures/mod.rs per §3.11)

    • Rust Tests: ori_types/src/check/signatures/tests.rs — adds at-type-site object-safety enforcement
    • Ori Tests: tests/compile-fail/non_object_safe_trait.ori — verifies E2024 fires for Clone/Eq/Iterator in type position
  • Implement: Error if dyn keyword is used (helpful migration message pointing at the new bare-trait syntax)

    • WHERE: ori_parse/src/grammar/ty/mod.rs — keep dyn as a recognized-but-rejected token to produce the migration hint
    • Ori Tests: tests/compile-fail/dyn_keyword_removed.ori

LLVM / AOT

  • Implement: LLVM trait-object codegen path validated for trait-as-type (no change vs dyn Trait — same vtable layout, same pointer size; verify the parser change does not regress codegen for any object-safe trait)

    • AOT Tests: cover all dimensions of the §15D.4 matrix in LLVM

Verification


  • Verify: All matrix tests green; dyn migration hint surfaces on legacy code
  • /tpr-review passed (this subsection only) — see §15D.shared-close-out
  • /impl-hygiene-review passed (after TPR clean) — see §15D.shared-close-out
  • Subsection close-out (15D.4) — see §15D.shared-close-out
  • /sync-claude section-close doc sync — see §15D.shared-close-out
  • Repo hygiene check — see §15D.shared-close-out

15D.5 Index and Field Assignment

Proposal: proposals/approved/index-assignment-proposal.md (+ proposals/approved/index-trait-proposal.md for Index / IndexSet trait pair)

Supersedes: bug-tracker/section-04-codegen-llvm.md::BUG-04-070 (E4003 ARC-lowering symptom), bug-tracker/section-07-tooling-cli.md::BUG-07-016 (typeck-layer naming of the same fix), bug-tracker/section-04-codegen-llvm.md::BUG-04-100 (phase-purity-violation naming of the same fix; surfaced via BUG-02-023 §04 Plan TPR R10-1). All three bugs are symptoms of the unimplemented typeck.md §EX-17 desugar; §15D.5’s 5-phase plan IS the fix.

Phase placement (load-bearing): per typeck.md §EX-17 and canon.md §2 row 3-4, the desugar runs in the type checker, NOT in canon. Phase ordering rationale: AIMS sees post-desugar IR where mutation has been rewritten as pure reassignment, which is the shape the lattice is designed for; placing the desugar in canon would let pre-desugar IR reach AIMS and emit RC ops on the mutation expression instead of the COW form, violating canon.md §7.1 Invariant 5 (unified model).

Blocks §15D.6 — mutable-self desugar at state.f = v inside method bodies depends on field-assignment lowering shipping first. Dependency encoded in body prose (per §15D.0 “Subsection dependency” note) since SubsectionEntry schema does not yet carry inter-subsection edges.

Extend assignment targets to support index expressions (list[i] = x), field access (state.name = x), mixed chains (state.items[i] = x, list[i].name = x), and compound assignment on all forms (list[i] += 1). All forms desugar to copy-on-write reassignment via IndexSet trait (for index) or struct spread (for fields).

let list = [1, 2, 3]
list[0] = 10                          // list = list.updated(key: 0, value: 10)

let state = GameState { score: 0, level: 1 }
state.score = 100                     // state = { ...state, score: 100 }
state.items[i] = new_item             // mixed chain
list[i].name = "new"                  // index then field

list[0] += 5                          // compound: list[0] = list[0] + 5

TDD-first Matrix (write BEFORE Phase 1)

  • TDD-first: Failing test matrix

    • Dimensions: target shape (x[i] / x.f / x.f[i] / x[i].f / x.f.g[i].h) × root binding (mutable / immutable / parameter / loop var) × operator (= / += / -= / *= / /= / %= / **=) × index expression (literal / variable / f() side-effecting) × backend
    • Side-effect cell (load-bearing — caught by codex outlier R1): arr[f()] += 1 where f() MUST run exactly once. Pin the count via dbg!()-ed counter in test fixture; reject any desugar that double-evaluates the index expression.
    • Each row: positive + negative pin

Phase 1: IndexSet Trait and updated Method

  • Implement: Define IndexSet<Key, Value> trait in compiler_repo/library/std/prelude.ori@updated (self, key: Key, value: Value) -> Self

    • Coordinate with proposals/approved/index-trait-proposal.md for the trait pair (Index<Key, Value> already exists per prelude reference; IndexSet is the symmetric write side)
    • Rust Tests: ori_types/src/check/registration/tests.rs — IndexSet trait registration
    • Ori Tests: tests/spec/traits/index_set/basic.ori
  • Implement: Register updated as built-in method on [T], {K: V}, [T, max N] in evaluator + ori_registry

    • Rust Tests: ori_eval/src/method_dispatch/tests.rs — updated method dispatch
    • Ori Tests: tests/spec/traits/index_set/updated_method.ori
  • Implement: updated with ARC-aware copy-on-write in ori_patterns / ori_eval (single-RC fast-path: when the receiver is uniquely owned, mutate in place per AIMS lattice; otherwise clone-then-mutate)

    • Rust Tests: ori_patterns/src/value/tests.rs — copy-on-write behavior matrix
    • Ori Tests: tests/spec/traits/index_set/cow_behavior.ori

Phase 2: Parser Changes

  • Implement: Extend parser to accept assignment_target (identifier + index/field chains) on LHS of = and compound operators

    • WHERE: ori_parse/src/grammar/expr/mod.rs (the assignment-target dispatcher) and ori_parse/src/grammar/expr/postfix.rs (chain resolution)
    • Side-effect rule: parser MUST emit a single AST node capturing the chain; per-step temporaries are introduced at typeck rewrite time, NOT at parse time. Parser-level desugaring would duplicate complex targets per the codex outlier R1 finding.
    • Rust Tests: ori_parse/src/grammar/expr/mod.rs parser tests — assignment target parsing
    • Ori Tests: tests/spec/expressions/index_assignment_syntax.ori
  • Implement: AST node AssignTarget { root: ExprId, steps: Vec<AccessStep> } capturing chain of index/field accesses in assignment target

    • WHERE: ori_ir/src/ast/expr.rsAccessStep::Index(ExprId) / AccessStep::Field(Name)
    • Rust Tests: ori_ir/src/ast/tests.rs
    • Ori Tests: tests/spec/expressions/field_assignment_syntax.ori

Phase 3: Type-Directed Desugaring (in type checker — NOT canon)

Phase contract reminder: this rewrite happens in ori_types, NOT ori_canon. See canon.md §2 row 3-4.

  • Implement: Desugar [key] steps to updated() calls (requires IndexSet trait resolution at the receiver type)

    • WHERE: ori_types/src/infer/expr/operators.rs — extend infer_assign / add desugar_assign_target
    • Side-effect rule: when the index expression has side effects (anything other than a Literal or pure Path), bind to a fresh let first, then use the bound name in the updated(key: ..., value: ...) call. Single evaluation guaranteed by typeck rewrite.
    • Rust Tests: ori_types/src/infer/expr/tests.rs — index assignment desugaring
    • Ori Tests: tests/spec/expressions/index_assignment_desugar.ori
  • Implement: Desugar .field steps to struct spread reconstruction (requires struct type info)

    • WHERE: same module
    • Rust Tests: same tests file — field assignment desugaring
    • Ori Tests: tests/spec/expressions/field_assignment_desugar.ori
  • Implement: Handle nested cases, mixed field-index chains, and compound assignment

    • Rust Tests: ori_types/src/infer/expr/tests.rs — mixed chain desugaring
    • Ori Tests: tests/spec/expressions/mixed_chain_assignment.ori

Phase 4: Type Checker Integration (validation)

  • Implement: Validate mutability of root binding (not $, not parameter, not loop variable)

    • Ori Tests: tests/spec/expressions/assignment_mutability.ori
  • Implement: Validate field names against struct types in assignment chains

    • Ori Tests: tests/compile-fail/assignment/invalid_field.ori
  • Implement: Validate key and value types against IndexSet impl

    • Ori Tests: tests/compile-fail/assignment/type_mismatch.ori
  • Implement: Emit diagnostics for all error cases (immutable binding, parameter, loop var, missing IndexSet, field mismatch, type mismatch)

    • WHERE: ori_diagnostic/src/error_code/mod.rs — coordinate diagnostic codes with §00-parser owner
    • Ori Tests: tests/compile-fail/assignment/all_errors.ori

Phase 5: Backend Verification (NOT a new lowering pass)

Phase ordering: by Phase 5, the desugar is already complete — CanExpr reaching ori_arc and ori_eval is pure reassignment. There is no separate “LLVM index-assignment codegen” — what backends see is let list = list.updated(...), which already works. Phase 5 verifies that.


  • Verify: LLVM AOT compiles list[i] = v end-to-end (parses → typeck rewrites → canon → arc → llvm) producing identical observable behavior to the interpreter

    • AOT Tests: cover the full §15D.5 matrix
  • Verify: Field assignment AOT path identical-behavior with interpreter

  • Verify: Compound assignment on extended targets (list[i] += 1) produces identical eval/LLVM behavior; side-effect-pinning test (arr[f()] += 1 with single-evaluation counter) passes in both backends

  • Verify: BUG-04-070 / BUG-04-100 / BUG-07-016 superseded entries flip to complete once the desugar runs in typeck (the E4003 emission site at ori_arc/src/lower/control_flow/mod.rs:391-408 becomes dead code; remove it as a litter-pickup per CLAUDE.md §Scope Expansion)

  • /tpr-review passed (this subsection only) — see §15D.shared-close-out

  • /impl-hygiene-review passed (after TPR clean) — see §15D.shared-close-out

  • Subsection close-out (15D.5) — see §15D.shared-close-out

  • /sync-claude section-close doc sync — see §15D.shared-close-out

  • Repo hygiene check — see §15D.shared-close-out

15D.6 Mutable Self

Proposal: proposals/approved/mutable-self-proposal.md

Blocked by §15D.5state.f = v inside method bodies relies on §15D.5’s field-assignment desugar. Dependency encoded in body prose (per the §15D.0 “Subsection dependency” note above) since SubsectionEntry schema does not yet carry inter-subsection edges.

Capability coordination: any mutating-self diagnostics that touch capability annotations align with section-06-capabilities.md (NOT §22 Tooling — §22 is the tooling subsystem). Mutable-self does NOT introduce a new capability; the alignment is for shared error-code conventions only.

Make self a mutable binding in method bodies with implicit mutation propagation back to the caller. Extends the existing field/index assignment desugaring pattern to method calls.

impl Cursor {
    @advance (self) -> void = {
        self.pos += 1
    }
}

cursor.advance()   // cursor is updated via desugaring

TDD-first Matrix

  • TDD-first: Failing test matrix BEFORE implementation

    • Dimensions: receiver mutability (x / $x / parameter / loop var) × method shape (-> void / -> Self / -> T / -> (Self, T)) × mutation depth (direct self.f = v / nested self.inner.advance()) × trait vs inherent vs extension × backend
    • Each row: positive + negative pin (calling mutating method on $x MUST reject; calling non-mutating method on $x MUST pass)

Phase 1: Self Mutability

  • Implement: Make self a mutable binding in method bodies (type checker)

    • WHERE: ori_types/src/check/bodies/impls.rsbind_self_with_mutability analogue (impl-method body checking lives here; infer/methods/ does not exist in the shipped tree)
    • Rust Tests: ori_types/src/check/bodies/tests.rs — self mutability in methods
    • Ori Tests: tests/spec/methods/mutable_self_basic.ori
  • Implement: Mutation detection dataflow analysis (classify methods as mutating/non-mutating)

    • WHERE: ori_types/src/check/bodies/impls.rs — walk the method body for any self.f = ... / self[i] = ... / self.inner.<mutating-method>() site
    • Ori Tests: tests/spec/methods/mutation_detection.ori

Phase 2: Call-Site Desugaring (depends on §15D.5 field-assign desugar)

  • Implement: Void-returning mutating methods desugar to -> Self + implicit reassignment at the call site

    • Ori Tests: tests/spec/methods/mutable_self_void.ori
  • Implement: Value-returning mutating methods desugar to -> (Self, T) + tuple split

    • ARC invariant: implicit reassignment must trigger the AIMS uniqueness check before mutation to preserve COW invariants per canon.md §7.1 Invariant 5 (verified by gemini Round 1 — architectural risk #3)
    • Ori Tests: tests/spec/methods/mutable_self_value_return.ori
  • Implement: No desugaring for -> Self methods (already return Self)

    • Ori Tests: tests/spec/methods/mutable_self_returns_self.ori
  • Implement: Nested field mutation cascading (self.inner.advance())

    • Ori Tests: tests/spec/methods/mutable_self_nested.ori

Phase 3: Caller Validation

  • Implement: Error when calling mutating method on immutable ($) binding

    • Ori Tests: tests/compile-fail/methods/mutating_on_immutable.ori
  • Implement: Diagnostics for mutation-related errors

    • Ori Tests: tests/compile-fail/methods/mutation_errors.ori

Phase 4: Trait Integration

  • Implement: Infer mutation classification across trait implementations

    • Ori Tests: tests/spec/traits/mutable_self_trait.ori
  • Implement: Conservative mutating classification for trait objects without local impls

    • Ori Tests: tests/spec/traits/mutable_self_trait_object.ori

Phase 5: Extension Support

  • Implement: Extension methods follow same mutation propagation rules

    • Ori Tests: tests/spec/methods/mutable_self_extension.ori

Phase 6: Evaluator

  • Implement: Evaluator support for mutable self in method bodies

    • WHERE: ori_eval/src/interpreter/ (directory — no single can_eval.rs file)
    • Ori Tests: tests/spec/methods/mutable_self_eval.ori
  • Implement: Evaluator call-site desugaring (implicit reassignment) — single dispatch path shared with type-checker desugar; the evaluator does NOT re-implement the rewrite

    • Ori Tests: tests/spec/methods/mutable_self_propagation.ori

Phase 7: LLVM Support

  • Implement: LLVM codegen for mutable self desugaring (no special LLVM path — backends see post-desugar IR per Phase 2)

    • AOT Tests: tests/spec/methods/mutable_self_aot.ori
  • Implement: COW integration verified for self-mutating methods (AIMS lattice drives RC emission; verify no regressions in ori_arc snapshots)

    • AOT Tests: tests/spec/methods/mutable_self_cow.ori

Verification


  • Verify: All matrix tests green in both backends; ORI_VERIFY_ARC=1 clean; ORI_CHECK_LEAKS=1 clean
  • /tpr-review passed (this subsection only) — see §15D.shared-close-out
  • /impl-hygiene-review passed (after TPR clean) — see §15D.shared-close-out
  • Subsection close-out (15D.6) — see §15D.shared-close-out
  • /sync-claude section-close doc sync — see §15D.shared-close-out
  • Repo hygiene check — see §15D.shared-close-out

15D.7 Section Completion Checklist + Routing Migration Note

Section-level exit criteria

  • Mission Success Criteria in this section’s frontmatter ALL satisfied (six rows)
  • All six approved proposals (checks / as-conversion / simplified-bindings / remove-dyn / index-assignment / mutable-self) implemented end-to-end across all backends
  • All spec docs updated via /sync-spec (NEVER direct edit per CLAUDE.md §Spec & Grammar Changes)
  • Grammar updated via /sync-grammar for dyn removal, pre() / post() syntax, assignment-target extension
  • CLAUDE.md updated with syntax changes (mut-removal, $ immutability, as casts, mutating self) — verified via /sync-claude
  • All tests pass: ./test-all.sh (debug + release; ORI_VERIFY_ARC=1 clean; ORI_CHECK_LEAKS=1 clean)
  • BUG-04-070 / BUG-04-100 / BUG-07-016 closed as dead code per §15D.5 supersession
  • Bug-tracker entries for §15D.2 evaluator/spec drift (float→int) and §15D.4 dyn migration lint filed via /add-bug if not already represented in scope-expansion litter-pickups
  • Section-level /tpr-review passed
  • Section-level /impl-hygiene-review passed (AFTER TPR clean)
  • Section-level /improve-tooling retrospective completed (sweep mode — covers tooling cross-cutting, plan structure, knowledge persistence, approach pattern)

Plan-sync line items

  • Section frontmatter status flipped to complete
  • Section frontmatter subsection_statuses all complete
  • 00-overview.md Quick Reference + mission success criteria checkboxes flipped
  • index.md section status updated
  • Cross-link verification: every Supersedes: / blocked_by: / blocks: in this section’s frontmatter and prose resolves
  • Next section’s depends_on (if any) verified clean

Routing migration note (PLAN_ROUTING_DRIFT — DOCUMENTED, NOT FIXED HERE)

Acknowledged routing drift (per blind-spots Round 1, gemini + opencode + codex consensus): this section’s owns_crates: [] plus the original raw-checkbox layout matched the PLAN_ROUTING_DRIFT:roadmap-implementation-leak pattern documented in CLAUDE.md §Plan Routing and .claude/rules/roadmap.md §Lazy Migration. The architecturally-correct long-term fix is to lift §15D’s six proposals into per-feature plan dirs:

  • plans/checks/ (function-level contracts)
  • plans/as-conversion/
  • plans/simplified-bindings/
  • plans/dyn-removal/
  • plans/index-assignment/ (with field-assignment as a sibling section, since they ship together)
  • plans/mutable-self/

Each lift uses /migrate-feature <name> and replaces this section’s content with <!-- blocked-by:plans/<feature>/section-NN.md --> pointers, preserving phase-coverage audit but moving implementation tracking to the feature plans.

The migration is NOT performed in this editor pass. Per CLAUDE.md §Plan Routing “Lazy Migration”, structural moves are /migrate-feature’s responsibility, not /review-plan’s. The migration runs when /continue-roadmap next picks up §15D and scripts/feature-cluster-detect.py flags one of these clusters; that workflow opens a separate session with the migration as its single deliverable.

Until then, this section retains raw checkboxes as a documented exception, with the editor pass having narrowed the dimensions (matrix-first, side-effect pins, phase-contract citations, blocked_by frontmatter) so that the lazy migration has clean targets to lift.

Exit Criteria: All six proposals shipped in both backends with dual-execution parity; routing migration scheduled (NOT performed here).


§15D.shared-close-out

SSOT for per-subsection close-out — every subsection’s last five checkboxes reference this single block by name to eliminate the LEAK:algorithmic-duplication flagged in blind-spots Round 1. Implementer copies these five protocol entries into the active subsection’s commit body when running close-out; the section file itself stays single-instance.

  1. /tpr-review passed — independent third-party review found no critical or major issues (or all findings triaged into - [ ] items in this section). Run AFTER all subsection implementation items are checked.
  2. /impl-hygiene-review passed — hygiene review clean (phase boundaries, SSOT, algorithmic DRY, naming, dispositions, scope-expansion). Run AFTER /tpr-review is clean.
  3. Subsection close-out (/improve-tooling retrospective) — Run on this subsection’s debugging journey per .claude/skills/improve-tooling/SKILL.md “Per-Subsection Workflow”. What diagnostics/ scripts ran, where dbg! / tracing calls were added, where output was hard to interpret, where test failures gave unhelpful messages, where command sequences repeated. Forward-look: what tool / log / diagnostic would shorten the next regression in this code path by 10 minutes? Implement improvements NOW (zero deferral) and commit each via SEPARATE /commit-push using a valid conventional-commit type (build(diagnostics): ... — surfaced by section-15D.<N> retrospectivebuild / test / chore / ci / docs are valid; tools(...) is rejected by the lefthook commit-msg hook). Mandatory even when nothing felt painful. If genuinely no gaps, document briefly: “Retrospective 15D.: no tooling gaps”. Then update this subsection’s status in section frontmatter to complete.
  4. /sync-claude section-close doc sync — verify Claude artifacts across all section commits. Map changed crates to rules files, check CLAUDE.md, canon.md. Fix drift NOW.
  5. Repo hygiene check — run diagnostics/repo-hygiene.sh --check and clean any detected temp files.

  • Migration trigger — when /continue-roadmap next picks up §15D, run /migrate-feature checks (or whichever proposal cluster surfaces first); confirm cleanly lifts to plans/<feature>/. Do NOT lift all six at once unless the migration tooling supports batch.

15D.R Third Party Review Findings

Round 0 Reviewers: codex (HIGH trust), gemini (LOWER trust), opencode (MEDIUM trust). 17 reviewer findings + 2 §3.5 plan-coherence pre-gate findings. After verification: 9 applied inline, 4 filed below for execution at appropriate workflow phase, 4 dropped (gemini confabulations on lines 727 / 585 — claimed text not present in file; opencode line-number drift already correct in plan).

  • [TPR-15D-001-codex+gemini+opencode][critical] plans/roadmap/section-15D-bindings-types.md:710PLAN_ROUTING_DRIFT:roadmap-implementation-leak: six approved proposals (checks / as-conversion / simplified-bindings / remove-dyn / index-assignment / mutable-self) carry raw implementation checkboxes inside this roadmap section. Per CLAUDE.md §Plan Routing + .claude/rules/roadmap.md §Lazy Migration the roadmap is an audit-index, not an implementation tracker for cross-cutting features.

    Evidence: §15D.7 Routing migration note already documents the drift (lines 732-749 — line range refreshes when editor passes shift content). Section retains raw checkboxes pending lazy migration. Impact: §15D will keep accumulating raw checkboxes until lifted; downstream /continue-roadmap touching feature work re-triggers PLAN_ROUTING_DRIFT. Required plan update: when /continue-roadmap next picks up §15D, run /migrate-feature for each of plans/checks/, plans/as-conversion/, plans/simplified-bindings/, plans/dyn-removal/, plans/index-assignment/, plans/mutable-self/. Replace each subsection’s body with <!-- blocked-by:plans/<feature>/section-NN.md#item --> pointers per .claude/skills/migrate-feature/SKILL.md. Do NOT lift all six at once unless the migration tooling supports batch. Basis: fresh_verification (codex F1 line:710 + gemini F4 + opencode finding clustered). Confidence: high.

  • [TPR-15D-002-gemini][critical] compiler_repo/tests/spec/expressions/type_conversion.oriDISPOSITION_DRIFT:untracked-annotation-shape (file is 458 lines, 457 prefixed with //); the disposition scanner cannot track the gap because //-comment lines are not annotation-form. Replace the wholesale //-comment with #skip("BUG-XX-NNN: …") on each test once the underlying tracking bug is filed, OR uncomment as part of §15D.2 implementation.

    Evidence: wc -l reports 458 lines; grep -c "^//" reports 457. §15D.2 already documents this state at line 179 (“DISPOSITION DRIFT” callout). Impact: state.sh refresh --dispositions-only cannot enumerate the per-test gap; every test in the file is invisible to disposition tracking. §15D.2 close-out depends on this file being green. Required plan update: anchored to §15D.2 verification checkbox at line 268 (“tests/spec/expressions/type_conversion.ori fully uncommented and green”). Out of scope for /review-plan (.ori file edit not permitted in plan-only mode); the §15D.2 implementer addresses this when un-commenting the test corpus. If a tracking bug is needed for partial enablement, file via /add-bug at that point. Basis: fresh_verification (gemini F3 confirmed via shell). Confidence: high.

  • [TPR-15D-003-codex][low] plans/roadmap/section-15D-bindings-types.md — 15D.R Third Party Review Findings block was missing on Round 0 entry. ACTION: created in this fix-and-commit cycle (this block).

    Evidence: codex F8 explicitly noted the missing 15D.R block. Impact: Round 0 had no host for filed findings. Required plan update: NONE (resolved by adding this block). Basis: fresh_verification. Confidence: high.

Verification surprises

  • Gemini F1 (claimed “Evaluator assigned syntactic desugaring duties” at line 727) — DROPPED. Line 727 is **Exit Criteria**: All six proposals shipped... — no such evaluator-desugar claim present. Gemini confabulation.
  • Gemini F2 (claimed “Interpreter using AIMS static analysis” at line 585) — DROPPED. Line 585 is **Blocked by §15D.5** — state.f = v inside method bodies relies... — no AIMS-static-analysis claim. Gemini confabulation. (Codex F5 referenced the same line for a DIFFERENT, verifiable finding — frontmatter blocked_by mismatch — which was applied inline.)
  • Opencode F3 (claimed line numbers drift: parse_contracts at 221 not 129; tests at 531+ not 143) — DROPPED. Plan line 119 already cites parse_contracts() at line 221 (correct: grep -n "fn parse_contracts" confirms line 221). Plan line 121’s “line:143” reference is to historical parser tests; verification would require deeper test-file inspection beyond this fix-cycle scope. No drift to fix at the cited plan lines.

Round 1 audit log

Round 1 against post-Round-0 plan state. Reviewers: codex (HIGH trust), gemini (LOWER trust), opencode (MEDIUM trust). 13 reviewer findings. After verification: 10 applied inline (path corrections + matrix corrections + phase-bleeding rewrite), 1 dropped (gemini F1 — out of /review-plan scope; should be filed via /add-bug separately if real), 1 classified as meta-duplicate (codex F1 — exact duplicate of TPR-15D-001 already filed in Round 0; not re-filed), 1 dropped (codex F1 same).

  • [TPR-15D-R1-codex-F1][meta-duplicate] Re-raised PLAN_ROUTING_DRIFT at line 750 — DUPLICATE of TPR-15D-001 (Round 0). The §15D.7 Routing migration note + TPR-15D-001 already document this; no new filing.
  • [TPR-15D-R1-gemini-F1][dropped] Major @ compiler_repo/compiler/ori_arc/src/lower/collections/mod.rs:320lower_cast quality concern. DROPPED from /review-plan scope (compiler-code quality, not plan-doc). If the concern is real, file via /add-bug separately; this finding is out of scope for the current /review-plan fix-and-commit cycle.
  • [TPR-15D-R1-codex-F2][high] §15D.2 cast matrix at line 210 — removed float → int as? positive cell (spec-rejected per spec/08-types.md:1299-1308); added explicit lossy-rejection negative pin as_lossy_float_to_int.ori and migration-hint reference (f.truncate() / .round() / .floor() / .ceil()).
  • [TPR-15D-R1-codex-F3][high] §15D.2 LLVM cast checklist at line 256 — deleted fptosi (no spec path), replaced char-bitcast guidance with zext for char → int (codepoint widening, different bit widths), explicit TryAs<char> valid-codepoint helper for int → char, and ori_rt FFI references for str-parsing fallible casts.
  • [TPR-15D-R1-codex-F4][critical] §15D.4 trait-as-type at line 439 — rewrote task. Parser now emits bare Type::Named for identifiers in type position; trait-object disambiguation moved to ori_types/src/check/signatures/mod.rs consulting TraitRegistry::is_trait() and running existing check_parsed_type_object_safety(). Parser purity (compiler.md §Phase-Specific Purity + canon.md §5) preserved. The original task description contained a LEAK:phase-bleeding (parser would consult ori_types::TraitRegistry), now removed.
  • [TPR-15D-R1-codex-F5 + gemini-F4][medium] §15D.1 line 121 — PostContract { param, ... } (singular) → PostContract { params: Vec<Name>, ... } matching shipped AST (ori_ir/src/ast/items/function.rs:53params: Vec<Name> because lambda binders may be (a, b) -> cond for tuple-returning functions).
  • [TPR-15D-R1-codex-F6 + opencode-F3][low] §15D.3 line 370 (and §15D.3 line 324 mention) — ori_types/src/check/import.rs (singular) → ori_types/src/check/imports.rs (plural — verified path).
  • [TPR-15D-R1-gemini-F2][minor] §15D.3 lines 338-343 — Option<(Name, bool)>Option<(Name, Mutability)> matching shipped IR (ori_ir/src/ast/patterns/binding/mod.rs:58, ori_ir/src/canon/patterns.rs:33). Mutability is the canonical newtype enum, not a raw bool.
  • [TPR-15D-R1-gemini-F3][minor] §15D.6 lines 612-619 — ori_types/src/infer/methods/ (nonexistent) → ori_types/src/check/bodies/impls.rs (impl-method body checking lives here in the shipped tree).
  • [TPR-15D-R1-opencode-F1][critical] §15D.1 line 129 + §15D.5 line 563 — ori_diagnostic/src/error_codes.rs (nonexistent) → ori_diagnostic/src/error_code/mod.rs (verified directory + mod.rs).
  • [TPR-15D-R1-opencode-F2][high] §15D.4 lines 434, 440, 452 — ori_parse/src/grammar/ty.rs (file does not exist) → ori_parse/src/grammar/ty/mod.rs (verified directory layout). All 3 occurrences fixed.
  • [TPR-15D-R1-opencode-F4][medium] §15D.3 line 359 — ori_types/src/check/module.rs (nonexistent) → ori_types/src/check/mod.rs (module-level binding enforcement entry point lives in the check crate’s root).

Round 1 verification surprises

  • Codex F1 (PLAN_ROUTING_DRIFT) — meta-duplicate of TPR-15D-001 filed Round 0; no new action. Reviewers should reach consensus that §15D.7 routing-migration note + TPR-15D-001 are the canonical accepted state pending lazy migration.
  • Gemini F1 (lower_cast compiler-code concern) — out of /review-plan scope. If real, the Implementer should /add-bug separately during §15D.2 implementation.

Inline fixes applied (Round 0 same commit)

  • [codex F2][high] Ownership map at lines 64-69 — corrected against verified upstream owns_crates: (verified §00, §01, §03, §21A, §22, §23 frontmatter). ori_canon moved from §01 to §21A. ori_diagnostic + ori_fmt moved from §00 to §22. ori_patterns added to §23. Added §22-tooling row.
  • [codex F3][high] §15D.2 line 226 — @as_to/@try_as_to@as/@try_as (per spec/08-types.md and as-conversion-proposal.md).
  • [codex F4][medium] §15D.4 frontmatter status not-startedin-progress (object-safety machinery shipped at ori_types/src/check/object_safety.rs; trait-as-type parsing shipped at ori_parse/src/grammar/ty/mod.rs). Added status note in body.
  • [codex F5 + opencode F1][medium] Lines 478 + 585 — replaced false blocks: ["15D.6"] / blocked_by: ["15D.5"] frontmatter claim with “Dependency encoded in body prose” matching the existing §15D.0 acknowledgment at line 73.
  • [codex F6][medium] §15D.1 line 124 — PreContract.message: Option<ExprId>Option<Name> (verified in compiler_repo/compiler/ori_ir/src/ast/items/function.rs:28).
  • [codex F7][medium] §15D.3 lines 322-329 — ori_parse/src/grammar/decl.rs (does not exist) replaced with actual paths: ori_parse/src/dispatch.rs, ori_parse/src/grammar/item/config/mod.rs, ori_parse/src/grammar/item/function/.
  • [opencode F2][major] index.md:522 — Status “Not Started” → “In Progress” (matches §15D frontmatter status: in-progress).
  • [PLAN-COHERENCE-001 + 002][high] 00-overview.md:144 and :168 — bare “Section 7 (Stdlib)” → “Section 7A-E (Stdlib)” matching the actual section-07A...07E files (no section-7.md exists).

Round 2 audit log

Round 2 against post-Round-1 plan state. Reviewers: codex (HIGH trust), gemini (LOWER trust), opencode (MEDIUM trust). 9 reviewer findings (codex 5 incl. duplicate F4 id, gemini 3, opencode 1). After verification: 7 applied inline (frontmatter fix + line-number drift + API-name correction + cast-language alignment + contract-desugar relocation + migration-hint disambiguation), 1 classified meta-duplicate (codex F1 — PLAN_ROUTING_DRIFT, exact duplicate of TPR-15D-001 filed Round 0; not re-filed), 1 dropped (gemini F3 — pre-existing language at line 682 already satisfies the “typeck owns desugar; eval consumes rewritten form” requirement; no edit needed).

  • [TPR-15D-R2-codex-F1][meta-duplicate] Re-raised PLAN_ROUTING_DRIFT at line 730 — DUPLICATE of TPR-15D-001 (Round 0). The §15D.7 routing-migration note + TPR-15D-001 already document this; no new filing.
  • [TPR-15D-R2-codex-F2 + opencode-F1][high] §15D.2 lines 185, 249, 259 — float→int cast remediation contradicted the TDD matrix at line 222 (“rejected by both as and as?”). Rewrote EVALUATOR/SPEC DRIFT note (line 185 area) to align with spec/08-types.md:1299-1308 (lossy rejected, migrate to truncate() / round() / floor() / ceil()); rewrote validator task (line 249) to make lossy rejection explicit and add as_lossy_float_to_int.ori negative pin; rewrote evaluator task (line 259) to remove “must become as? only” and require dropping the float → int truncation entirely (no As<int> for float, no TryAs<int> for float).
  • [TPR-15D-R2-codex-F3][medium] §15D.4 frontmatter status — Round 0 codex F4 audit log claimed not-startedin-progress was applied, but only the body status note was added; the YAML status: not-started was left in place. PARTIAL-FIX VERIFIED. Now flipped to status: in-progress to complete the Round 0 fix.
  • [TPR-15D-R2-codex-F4][high] §15D.4 line 459 trait-object resolver task — original task cited TraitRegistry::is_trait() (does not exist on the shipped registry). Real APIs verified at ori_types/src/registry/traits/lookup.rs:22,38 are get_trait_by_name(name) and contains_trait(name). Rewrote task to cite contains_trait (and get_trait_by_name when the entry is needed) and added the verified path to check_parsed_type_object_safety at ori_types/src/check/object_safety.rs:35.
  • [TPR-15D-R2-codex-F5][low] §15D.1 line 115 contract status note — claimed “shipped at parse_contracts:129” but the actual call site is at line 131 and the function definition begins at line 221 of ori_parse/src/grammar/item/function/mod.rs (verified via grep -n "fn parse_contracts\|parse_contracts("). Rewrote the status note with both line numbers (call site 131, definition 221).
  • [TPR-15D-R2-gemini-F1][critical] §15D.1 line 150 contract codegen — task assigned the pre()/post() desugar to BOTH ori_eval/src/interpreter/can_eval/control_flow.rs AND ori_llvm/src/codegen/arc_emitter/emit_function.rs, violating canon.md §7.1 Invariant 5 (unified model — one shape, all backends consume identically) and canon.md §1 phase ordering. Rewrote to relocate desugar into the type checker (preferred — ori_types/src/check/bodies/functions.rs) or ori_canon/src/desugar/ (fallback) — pick ONE phase, never split. Both backends consume the post-desugar CanExpr with no contract-aware code.
  • [TPR-15D-R2-gemini-F2][high] §15D.2 lines 280-282 migration — task said “Remove int(), float(), str(), byte() from parser” AND “when int(x) form parses, emit migration hint” (mutually exclusive). Rewrote into two phases: (1) migration-hint phase keeps the recognizer and routes to a diagnostic-only path emitting E1xxx migration hint, (2) removal phase (out of §15D scope; documented for the implementer’s awareness) drops the recognizer once one release cycle has elapsed. Mirrors the dyn removal pattern in §15D.4 (recognized-but-rejected token).
  • [TPR-15D-R2-gemini-F3][dropped] §15D.6 line 682 evaluator desugar — verification of pre-existing text shows the task already states “single dispatch path shared with type-checker desugar; the evaluator does NOT re-implement the rewrite”. Pre-existing language already satisfies gemini’s concern (typeck SOLE owner of the rewrite, eval consumes the post-rewrite shape). No edit needed.

Round 2 verification surprises

  • Round 0 codex F4 partial fix: Round 0 audit log (line 819) claimed §15D.4 frontmatter status: not-started → in-progress was applied. Verification of the post-Round-0 file showed only the body status note was written; the YAML status: not-started line was left untouched. The Round 0 fix was incomplete; Round 2 codex F3 caught it. Lesson: when a “frontmatter + body” fix is logged, verify BOTH locations changed before accepting the audit-log claim.
  • Round 0 opencode F3 (line 115 line numbers): Round 0 verification surprises (line 791) noted opencode F3 was DROPPED with “Plan line 119 already cites parse_contracts() at line 221 (correct)”. That assessment was correct for line 119, but missed line 115’s separate parse_contracts:129 reference (which was wrong — actual call site at 131). Round 2 codex F5 caught the line-115 drift. Lesson: when a plan has multiple locations citing the same symbol, verify ALL of them.
  • Codex F2 vs opencode F1 overlap: both reviewers surfaced the same float→int contradiction across lines 184/185, 248/249, 258/259. Treated as ONE finding with combined attribution; line 184 (cited by opencode) is the EVALUATOR/SPEC DRIFT note that contained the contradictory “requires as?” wording — verified and fixed.
  • plan-cleanup auto-revert of §15D.4 status: scripts/plan-cleanup.py auto-derived §15D.4 status: not-started from checkbox-state heuristics during the Round 2 fix-and-commit Step 4 pre-commit run, reverting the codex-F3 frontmatter fix. Re-applied in-progress because the body Status note (line 442) documents that object-safety machinery is already shipped via §3.11 (ori_types/src/check/object_safety.rs) and trait-as-type parsing is already shipped at ori_parse/src/grammar/ty/mod.rs — the section IS in progress on upstream work, just not visible in checkbox state. Future plan-cleanup runs may attempt the revert again until §15D.4 has at least one [x] checkbox; if so, re-flip and refer back to this entry. Filed as a tooling improvement candidate (plan-cleanup should consult body Status notes before deriving subsection status from checkboxes alone) rather than re-engaging /add-bug here.

Round 3 audit log

Round 3 against post-Round-2 plan state. Reviewers: codex (HIGH trust, 2 findings), gemini (LOWER trust, 2 findings), opencode (MEDIUM trust, status: clean — first reviewer to return zero findings). Strong convergence trajectory: 17 → 14 → 8 → 4 findings across rounds 0-3. After verification: 4 applied inline.

  • [TPR-15D-R3-codex-F1][medium] §15D.4 frontmatter status REAPPEARED at not-started — plan-cleanup auto-revert struck again post-Round-2 commit. Re-flipped to in-progress. Recurring tooling drift documented in Round 2 audit log; will recur on every commit until §15D.4 has at least one [x] checkbox or plan-cleanup is improved (tooling candidate, not /add-bug).
  • [TPR-15D-R3-codex-F2][medium] §15D.4 line 460 trait-object resolver task — Round 2 rewrite cited “lowers to the trait-object tag” but no such Tag variant exists in the shipped enum (compiler_repo/compiler/ori_types/src/tag/mod.rs:25). Rewrote to defer the concrete representation to “the type system’s existing trait-object handling” without naming a specific Tag variant; preserves the verified contains_trait()/get_trait_by_name()/check_parsed_type_object_safety() API references.
  • [TPR-15D-R3-gemini-F1][high] §15D ownership map line 74 — ori_lsp was missing from the §22-tooling row. Verified §22-tooling.md frontmatter owns_crates: [ori_fmt, ori_test_harness, ori_stack, ori_diagnostic, ori_compiler, oric, ori_lsp]. Added ori_lsp to the map.
  • [TPR-15D-R3-gemini-F2-revised][medium] §15D.1 line 126 — claimed parser tests at ori_parse/src/grammar/item/function/mod.rs:143. Verified: tests DO exist but at tests.rs not mod.rs, specifically function/tests.rs:144 (test_pre_contract_basic), :159 (test_pre_contract_with_message), :172 (test_post_contract_basic), :187 (test_post_contract_tuple_params). Gemini’s claim that “neither mod.rs nor tests.rs contains contract tests” was confabulated; orchestrator-corrected fix: cite the actual tests.rs paths/line numbers.

Round 3 verification surprises

  • opencode returned status: clean — first reviewer to return zero findings across the convergence loop. Confirms the section’s substantive content is now well-aligned with the codebase; remaining drift is narrow (status state-machine, ownership map gap, line-number drift, one Tag-variant overreach).
  • gemini F2 partially confabulated: reviewer claimed “neither mod.rs nor tests.rs contains contract tests”; verification showed 4 contract tests in tests.rs:144+. The plan’s path/line citation WAS wrong (mod.rs:143 → should be tests.rs:144), so the underlying drift was real, but reviewer’s evidence was partially wrong. Orchestrator-corrected fix lands the right path/lines.
  • plan-cleanup recurrence-pattern continues: §15D.4 frontmatter auto-reverted between Round 2 commit and Round 3 dispatch. The fix in this round will likely be re-reverted at this round’s commit too unless an [x] checkbox is added to §15D.4 first. Adding the [x] checkbox satisfies plan-cleanup’s mechanical heuristic without misrepresenting work state.

Round 4 audit log (FINAL — iter_cap_reached at iteration_counter=5)

Round 4 against post-Round-3 plan state. Reviewers: codex (HIGH trust, 4 findings incl. 1 meta-duplicate), gemini (LOWER trust, status: clean — informational only confirming code-state alignment), opencode (MEDIUM trust, 2 minor findings). Convergence trajectory complete: 17 → 14 → 8 → 4 → 5 findings across rounds 0-4. After verification: 5 applied inline, 1 meta-duplicate (TPR-15D-001 re-raise), 1 informational (gemini’s “plan aligns” confirmation).

  • [TPR-15D-R4-codex-F2][high] review_pipeline frontmatter rounds_completed: 2 + last_round_findings: "Round 2..." was stale post-Round-3 commit. Updated to rounds_completed: 4 with Round 4 summary; stage: tpr-done and next_step: 7.
  • [TPR-15D-R4-codex-F3][high] §15D.2 line 269 — int→byte AOT task said “fallible TryAs; emit panic-on-fail wrapper for as-form rejection”. Spec/08-types.md §8.11.2-§8.11.3 + TDD matrix line 222 reject as for lossy. Rewrote: as form compile-rejected (no emission path), as? returns Option<byte> only.
  • [TPR-15D-R4-codex-F4][medium] §15D.7 routing migration note + TPR-15D-001 had two slugs for the remove-dyn proposal: plans/remove-dyn/ (one location) and plans/dyn-removal/ (other location). Canonicalized to plans/dyn-removal/ everywhere via replace_all.
  • [TPR-15D-R4-opencode-F1][medium] success_criteria #2 said “via a registered __cast protocol builtin” (singular handler implication) but implementation plan describes per-type-pair LLVM dispatch. Reworded to: “via a registered __cast protocol builtin (whose handler dispatches per type-pair to the appropriate LLVM cast instruction — sitofp, trunc+range-check, zext, fptosi if/where applicable)”.
  • [TPR-15D-R4-opencode-F2][low] TPR-15D-001 evidence cited routing-migration note “lines 708-723” but editor passes shifted content. Updated to “lines 732-749” with note that line range refreshes when editor passes shift content.
  • [TPR-15D-R4-codex-F1][meta-duplicate] PLAN_ROUTING_DRIFT re-raised at line 732 — exact duplicate of TPR-15D-001 already filed Round 0. Not re-filed; documented here.
  • [TPR-15D-R4-gemini-F1][informational] “Plan aligns precisely with shipped parser and arc codegen state” — gemini returned status: clean with 1 informational confirmation rather than findings. Treated as meta per §6 (no actionable content). Strong terminal-state signal.

Round 4 verification surprises (FINAL)

  • gemini status: clean (informational-only) — second reviewer to converge to clean; in combination with opencode Round 3 clean, this is strong terminal-state confirmation.
  • codex still raising substantive findings (review_pipeline lag, int→byte semantics, slug mismatch) — high-trust reviewer’s residual findings indicate genuine drift that previous rounds didn’t surface (review_pipeline.rounds_completed only became wrong AFTER Round 3 audit log was added without marker update; int→byte rewrite was Round 0 collateral that survived Rounds 1-3 unrevealed; slug mismatch arose from Round 0 TPR-15D-001 filing using one slug while §15D.7 routing note used another).
  • iter_cap_reached at iteration_counter=5meta_only_streak never reached 2 (codex Critical/High kept resetting it). Per /tpr-review §5 stop condition #3, this exits with exit_reason: cap_reached_with_substantive. Caller (/review-plan → /continue-roadmap) presents Branch 2 escalation envelope to user.
  • Skipped /independent-review tie-breaker for TPR-15D-002: The disposition-drift finding was singleton-gemini in Round 0 (technically contested per §5.5 classification rule), but the finding is verified-by-construction (file IS 457/458 commented — git ground truth) and out-of-/review-plan-scope (cannot edit .ori files; correct fix is /add-bug for #skip annotations). Arbiter dispatch would consume 25-45 min wall-clock to confirm what’s mechanically known and prescribe a fix the orchestrator cannot apply within /review-plan’s authorized scope. Pragmatic skip; documented for future-loop /improve-tooling consideration (§5.5 may want an “out-of-scope-but-verifiable” exception class).