iter schema-camelcase-fix.1 (DONE 4/4): paramTypes/retType → param-types/ret-type — closes #30
`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
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"iter_id": "schema-camelcase-fix.1",
|
||||
"date": "2026-05-21",
|
||||
"mode": "standard",
|
||||
"outcome": "DONE",
|
||||
"tasks_total": 4,
|
||||
"tasks_completed": 4,
|
||||
"reloops_per_task": { "1": 0, "2": 0, "3": 0, "4": 0 },
|
||||
"review_loops_spec": 0,
|
||||
"review_loops_quality": 0,
|
||||
"blocked_reason": null
|
||||
}
|
||||
@@ -1704,7 +1704,7 @@ fn check_def(
|
||||
|
||||
/// Route each `InstanceMethod` body through `check_fn` by wrapping
|
||||
/// it in a synthetic `FnDef`. The InstanceMethod body is by
|
||||
/// convention a `Term::Lam` carrying its own `paramTypes` / `retType`
|
||||
/// convention a `Term::Lam` carrying its own `param-types` / `ret-type`
|
||||
/// (the canonical class-form shape); we extract its `param_tys` /
|
||||
/// `ret_ty` / `params` / `body` into an `FnDef` shape and reuse the
|
||||
/// existing fn-body-check machinery.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! `crates/ailang-core/tests/design_schema_drift.rs` fires.
|
||||
//!
|
||||
//! The serde attributes carry the schema: field renames (`as`, `type`,
|
||||
//! `fn`, `paramTypes`, `retType`), enum tags (`kind`, `t`, `k`, `p`),
|
||||
//! `fn`, `param-types`, `ret-type`), enum tags (`kind`, `t`, `k`, `p`),
|
||||
//! and `skip_serializing_if` predicates that keep the canonical-JSON
|
||||
//! representation backwards compatible across schema extensions.
|
||||
//!
|
||||
@@ -489,9 +489,9 @@ pub enum Term {
|
||||
/// the inferred shape.
|
||||
Lam {
|
||||
params: Vec<String>,
|
||||
#[serde(rename = "paramTypes")]
|
||||
#[serde(rename = "param-types")]
|
||||
param_tys: Vec<Type>,
|
||||
#[serde(rename = "retType")]
|
||||
#[serde(rename = "ret-type")]
|
||||
ret_ty: Box<Type>,
|
||||
#[serde(default)]
|
||||
effects: Vec<String>,
|
||||
|
||||
@@ -1922,8 +1922,8 @@ mod tests {
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "con", "name": "Ordering" }],
|
||||
"retType": { "k": "con", "name": "Unit" },
|
||||
"param-types": [{ "k": "con", "name": "Ordering" }],
|
||||
"ret-type": { "k": "con", "name": "Unit" },
|
||||
"effects": [],
|
||||
"body": { "t": "lit", "lit": { "kind": "unit" } }
|
||||
}
|
||||
|
||||
@@ -186,6 +186,18 @@ fn design_md_anchors_every_term_variant() {
|
||||
add it to design/contracts/data-model.md"
|
||||
);
|
||||
}
|
||||
|
||||
// The two Term::Lam compound-key tags are kebab-case (closes #30).
|
||||
// Pin the spellings inside data-model.md's fenced jsonc blocks so a
|
||||
// future edit cannot silently revert the contract document to
|
||||
// camelCase while ast.rs ships kebab.
|
||||
for anchor in [r#""param-types""#, r#""ret-type""#] {
|
||||
assert!(
|
||||
anchor_in_jsonc_block(DATA_MODEL, anchor),
|
||||
"design/contracts/data-model.md is missing Term::Lam kebab-key anchor `{anchor}` — \
|
||||
check the `lam` fenced JSON block"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `Pattern` variant must have its JSON-schema anchor present in
|
||||
@@ -451,6 +463,45 @@ End.
|
||||
);
|
||||
}
|
||||
|
||||
/// Schema-shape pin: the JSON serialisation of `Term::Lam` must
|
||||
/// emit kebab-case tags `"param-types"` and `"ret-type"`, and
|
||||
/// must NOT emit the old camelCase `"paramTypes"` / `"retType"`.
|
||||
/// Pinned because the closes-#30 milestone made the rename a
|
||||
/// schema-stability invariant: a future regression that flips
|
||||
/// the `#[serde(rename)]` string back to camelCase must fire RED
|
||||
/// here, not slip through.
|
||||
#[test]
|
||||
fn lam_serialises_with_kebab_keys() {
|
||||
let lam = Term::Lam {
|
||||
params: vec!["x".into()],
|
||||
param_tys: vec![Type::int()],
|
||||
ret_ty: Box::new(Type::int()),
|
||||
effects: vec![],
|
||||
body: Box::new(Term::Var { name: "x".into() }),
|
||||
};
|
||||
let v = serde_json::to_value(&lam).expect("Term::Lam serialises");
|
||||
let obj = v.as_object().expect("Term::Lam serialises as object");
|
||||
|
||||
assert!(
|
||||
obj.contains_key("param-types"),
|
||||
"Term::Lam must emit `param-types` key; got keys {:?}",
|
||||
obj.keys().collect::<Vec<_>>(),
|
||||
);
|
||||
assert!(
|
||||
obj.contains_key("ret-type"),
|
||||
"Term::Lam must emit `ret-type` key; got keys {:?}",
|
||||
obj.keys().collect::<Vec<_>>(),
|
||||
);
|
||||
assert!(
|
||||
!obj.contains_key("paramTypes"),
|
||||
"Term::Lam must NOT emit old camelCase `paramTypes` key",
|
||||
);
|
||||
assert!(
|
||||
!obj.contains_key("retType"),
|
||||
"Term::Lam must NOT emit old camelCase `retType` key",
|
||||
);
|
||||
}
|
||||
|
||||
/// Nested struct key anchors must be present in design/contracts/data-model.md.
|
||||
/// These keys appear inside `Suppress`, `ClassMethod`, `InstanceMethod`,
|
||||
/// and `Type::Forall` — they are not discriminators but they ARE part
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
//! Notes on the form (the binding constraints are in
|
||||
//! design/contracts/authoring-surface.md):
|
||||
//!
|
||||
//! - The `lam` form carries `paramTypes`, a `ret` type, and an
|
||||
//! - The `lam` form carries `param-types`, a `ret` type, and an
|
||||
//! optional `effects` clause. The AST stores them per-lambda and
|
||||
//! so the form must round-trip them.
|
||||
//! - The `import` form admits an optional `as` alias to round-trip
|
||||
|
||||
@@ -16,8 +16,15 @@ fn prelude_parse_yields_canonical_hash() {
|
||||
// 3abe0d3fa3c11c99): Eq Int/Bool/Str bodies flip to placeholder
|
||||
// `false`; Eq Unit added; six float_* fns added; class-Eq doc
|
||||
// rewritten away from the "P2 follow-up" comment.
|
||||
// 2026-05-21 schema-camelcase-fix re-pinned (prior:
|
||||
// 6d0577ff0d4e50ac): `Term::Lam`'s JSON tags renamed from
|
||||
// `paramTypes`/`retType` to `param-types`/`ret-type`. The
|
||||
// prelude contains 11 `(lam ...)` forms, so each lambda's
|
||||
// canonical-JSON bytes change and the module hash drifts.
|
||||
// Form-A surface unchanged — only the canonical-JSON byte
|
||||
// stream differs, which is what the rename targets.
|
||||
assert_eq!(
|
||||
h, "6d0577ff0d4e50ac",
|
||||
h, "562a03fc57e7e017",
|
||||
"prelude module hash drifted; if intentional, capture the new \
|
||||
hex below + record the why in the commit body."
|
||||
);
|
||||
|
||||
@@ -141,8 +141,8 @@ narrative — defaults, superclasses, diagnostics — lives in
|
||||
// Anonymous fn value; free vars captured from enclosing scope.
|
||||
{ "t": "lam",
|
||||
"params": ["<id>"...],
|
||||
"paramTypes": [Type...],
|
||||
"retType": Type,
|
||||
"param-types": [Type...],
|
||||
"ret-type": Type,
|
||||
"effects": ["<id>"...],
|
||||
"body": Term }
|
||||
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
"value": {
|
||||
"t": "lam",
|
||||
"params": [],
|
||||
"paramTypes": [],
|
||||
"retType": { "k": "con", "name": "Int" },
|
||||
"param-types": [],
|
||||
"ret-type": { "k": "con", "name": "Int" },
|
||||
"effects": [],
|
||||
"body": { "t": "var", "name": "acc" }
|
||||
},
|
||||
|
||||
+2
-2
@@ -22,8 +22,8 @@
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["y"],
|
||||
"paramTypes": [ { "k": "con", "name": "Int" } ],
|
||||
"retType": { "k": "con", "name": "Int" },
|
||||
"param-types": [ { "k": "con", "name": "Int" } ],
|
||||
"ret-type": { "k": "con", "name": "Int" },
|
||||
"effects": [],
|
||||
"body": {
|
||||
"t": "app",
|
||||
|
||||
Reference in New Issue
Block a user