4fc65ccb99
`Term::Lam`'s two camelCase JSON tags become kebab-case, matching
the convention every other compound-key tag in the AST schema
already follows. After this iter the canonical JSON has zero
camelCase outliers.
## Schema swap (atomic, Task 2)
- `crates/ailang-core/src/ast.rs:492,494` — two
`#[serde(rename)]` strings: `"paramTypes" → "param-types"`,
`"retType" → "ret-type"`. Rust field names unchanged.
- `crates/ailang-core/src/workspace.rs:1925-1926` — in-source
JSON literal in the `ct1_validator_walks_lam_embedded_types`
test.
- `design/contracts/data-model.md:142-147` — fenced JSON block
in the canonical data-model contract. Honesty-Rule touch-point:
the contract document must describe the actual present-state
schema.
- `examples/test_loop_binder_captured_by_lambda.ail.json` and
`experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json`
— the two `.ail.json` fixtures whose canonical-JSON embeds
the renamed tags.
The five files moved together within one task because `Term::Lam`
has no `#[serde(default)]` on `param_tys` / `ret_ty` — a
renamed-away key is a hard deserialise error. Splitting the swap
would have left the workspace untestable mid-step.
## RED → GREEN pin (Tasks 1, 2)
`crates/ailang-core/tests/design_schema_drift.rs` gains
`lam_serialises_with_kebab_keys`: pins that `serde_json::to_value(&Term::Lam{...})`
emits `"param-types"` / `"ret-type"` AND does not emit the old
camelCase keys. Companion: the existing
`design_md_anchors_every_term_variant` test now also asserts the
kebab anchors appear inside `data-model.md`'s `lam` fenced block.
Both confirmed RED on entry, GREEN after Task 2.
## Rustdoc honesty pass (Task 3)
Three pure-prose edits keep production rustdoc consistent with
the new schema vocabulary: `crates/ailang-core/src/ast.rs:8` (the
module-level rename enumeration), `crates/ailang-surface/src/parse.rs:81`
(prose mention of `lam`'s carry), `crates/ailang-check/src/lib.rs:1707`
(InstanceMethod routing helper rustdoc). No compile or test
consequence — Honesty-Rule maintenance.
## Plan-pseudo-vs-reality finding: under-audited hash blast radius
The brainstorm spec audited `crates/ailang-core/tests/hash_pin.rs`
exhaustively (5 pinned modules, zero `(lam ...)` occurrences,
"no hash refresh required"). It missed
`crates/ailang-surface/tests/prelude_module_hash_pin.rs` — a
separate hash-pin file in a different crate that pins `prelude.ail`,
which contains 11 `(lam ...)` forms. The prelude module hash
drifted (`6d0577ff0d4e50ac` → `562a03fc57e7e017`); the implementer
refreshed it with a Honesty-Rule provenance comment matching the
precedent set in commit `26fb345`. Form-A is unchanged — only the
canonical-JSON byte stream differs, which is exactly what the
rename targets. The spec-side learning is: schema-rename
brainstorms must walk *every* test crate for hash-pin files, not
just the home crate of the AST.
## Why this shape, and not the alternatives
- *`params-ty` / `ret-ty` (Rust-field-name vibe)* — rejected. The
AST JSON schema follows its own kebab/single-word convention,
not Rust field names. The other multi-word tag (`reuse-as`) is
kebab; abbreviating `params-ty` mixes singular+plural and has
no precedent.
- *Inline typed params as a sub-tag* (e.g. `params: [{name, type}, ...]`)
— rejected. Stronger semantic locality, but it changes schema
topology rather than tag spelling. Out of scope for a camelCase
correction; would have re-pinned far more than this milestone.
- *Bundle with #27 (arith-rename)* — rejected. #27 carries four
open brainstorm questions (mod vs rem, neg vs sub 0, Num class,
deprecation window) and is structurally a separate milestone.
Each gets one re-pin wave with its own rationale; no churn
saving from bundling.
## Honest call on the feature-acceptance gate
Clause 1 (LLM author naturally uses the new form) is the weakest
of the three. Direct empirical evidence is absent — the 2026-05-21
naming-A/B run measured a different axis. The argument is
indirect: a future LLM author generalises from the rest of the
schema ("compound keys are kebab"), and the two camelCase
outliers are precisely the sites where that generalisation
diverged from reality. Clause 2 (redundancy reduction) and
Clause 3 (no semantic surface touched) are direct.
## Forbidden touches verified untouched
`docs/plans/*.md` historic plans (which embed old tag names in
example JSON), `experiments/2026-05-12-cross-model-authoring/master/spec.md`,
`experiments/.../rendered/*.md`, and `experiments/.../runs/**` —
all left as frozen historical artefacts per Honesty-Rule
analogue (describe state at time of writing, not present state).
Verified via `git diff --name-only HEAD` (Task 4 Step 4).
## Verification
- `cargo test --workspace`: all test groups OK, 0 failed,
2 ignored (pre-existing).
- `cargo test -p ailang-surface --test round_trip`: GREEN
(Form-A unchanged invariant).
- New schema-shape pin and the extended data-model anchor walk:
GREEN after Task 2.
- Negative grep across `crates/`, `examples/`, `design/`,
`runtime/`: the only remaining `paramTypes` / `retType`
occurrences in checked-in code are the load-bearing
negative-assertion strings inside the new pin (intentional —
they are what makes the pin RED-able on regression).
- Stats: `bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json`
— 4/4 tasks DONE, no re-loops, no review-loops.
closes #30
43 lines
1.4 KiB
JSON
43 lines
1.4 KiB
JSON
{
|
|
"schema": "ailang/v0",
|
|
"name": "test_loop_binder_captured_by_lambda",
|
|
"imports": [],
|
|
"defs": [
|
|
{
|
|
"kind": "fn",
|
|
"name": "main",
|
|
"type": {
|
|
"k": "fn",
|
|
"params": [],
|
|
"ret": { "k": "con", "name": "Int" },
|
|
"effects": []
|
|
},
|
|
"params": [],
|
|
"doc": "loop-recur follow-on: a lambda inside a loop body that references the enclosing loop binder `acc` is rejected with loop-binder-captured-by-lambda. Loop binders are alloca-resident and lexically scoped (iter-3 reuses the mut_var_allocas registry, emptied at the lambda frame boundary); lifting them into a heap-closure env is the same deferred work as the mut-var case. Symmetric to mut-var-captured-by-lambda.",
|
|
"body": {
|
|
"t": "loop",
|
|
"binders": [
|
|
{
|
|
"name": "acc",
|
|
"type": { "k": "con", "name": "Int" },
|
|
"init": { "t": "lit", "lit": { "kind": "int", "value": 0 } }
|
|
}
|
|
],
|
|
"body": {
|
|
"t": "let",
|
|
"name": "f",
|
|
"value": {
|
|
"t": "lam",
|
|
"params": [],
|
|
"param-types": [],
|
|
"ret-type": { "k": "con", "name": "Int" },
|
|
"effects": [],
|
|
"body": { "t": "var", "name": "acc" }
|
|
},
|
|
"body": { "t": "app", "fn": { "t": "var", "name": "f" }, "args": [] }
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|