diff --git a/bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json b/bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json new file mode 100644 index 0000000..8299b44 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-21-iter-schema-camelcase-fix.json @@ -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 +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index c34f80d..0e61b9f 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -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. diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index d1e77a0..77ef23a 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -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, - #[serde(rename = "paramTypes")] + #[serde(rename = "param-types")] param_tys: Vec, - #[serde(rename = "retType")] + #[serde(rename = "ret-type")] ret_ty: Box, #[serde(default)] effects: Vec, diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index a67ff37..3025840 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -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" } } } diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index 3e9f912..b111b64 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -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::>(), + ); + assert!( + obj.contains_key("ret-type"), + "Term::Lam must emit `ret-type` key; got keys {:?}", + obj.keys().collect::>(), + ); + 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 diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 8c0fc36..7d2b56b 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -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 diff --git a/crates/ailang-surface/tests/prelude_module_hash_pin.rs b/crates/ailang-surface/tests/prelude_module_hash_pin.rs index 24d80c7..1ee0cc0 100644 --- a/crates/ailang-surface/tests/prelude_module_hash_pin.rs +++ b/crates/ailang-surface/tests/prelude_module_hash_pin.rs @@ -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." ); diff --git a/design/contracts/data-model.md b/design/contracts/data-model.md index 8dab9b8..d693ea9 100644 --- a/design/contracts/data-model.md +++ b/design/contracts/data-model.md @@ -141,8 +141,8 @@ narrative — defaults, superclasses, diagnostics — lives in // Anonymous fn value; free vars captured from enclosing scope. { "t": "lam", "params": [""...], - "paramTypes": [Type...], - "retType": Type, + "param-types": [Type...], + "ret-type": Type, "effects": [""...], "body": Term } diff --git a/examples/test_loop_binder_captured_by_lambda.ail.json b/examples/test_loop_binder_captured_by_lambda.ail.json index 1b8e296..aad2e6c 100644 --- a/examples/test_loop_binder_captured_by_lambda.ail.json +++ b/examples/test_loop_binder_captured_by_lambda.ail.json @@ -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" } }, diff --git a/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json b/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json index ff2e6d9..f5d426f 100644 --- a/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json +++ b/experiments/2026-05-12-cross-model-authoring/master/examples/fn_with_lambda.ail.json @@ -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",