From 818177d835efd9a9f0f1d6d5dbe869ee3cf71e76 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 18 May 2026 14:32:39 +0200 Subject: [PATCH] iter embedding-abi-m1.1 (PARTIAL 3/7): schema + surface + baseline prerequisites; plan Repair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tasks 1-3 of docs/plans/embedding-abi-m1.1.md, a verified known-good subset (cargo build --workspace exit 0; full workspace green; the roundtrip_cli all-examples gate green): - Task 1: CLI build-path MissingEntryMain baseline pin (examples/embed_noentry_baseline.ail + embed_missing_main_baseline.rs; triple-assertion, layered on the green codegen-unit pin lib.rs:3314). - Task 2: additive FnDef.export: Option (serde default + skip_serializing_if, modelled on FnDef.doc); compile-driven thread of export: None across ~85 FnDef/AstFnDef literals / 18 files incl. all in-source #[cfg(test)] mod tests + drift/hash/spec-drift literals + the codegen lib.rs:3320 in-source pin; hash-stability golden pin; DESIGN.md fn-JSON "export" line. DONE_WITH_CONCERNS (plan symbol content_hash_fn -> def_hash per hash_pin.rs, plan- pseudo-vs-reality class, plan corrected). - Task 3: Form-A (export "") modifier — parse_export + parse_fn thread + write_fn_def emission + form_a.md grammar; byte-identical round-trip. Task 4 BLOCKED (Boss plan-defect: fixtures declared module != file stem; AILang's loader hard-enforces module==stem at workspace.rs:438, so ail check panicked on load before the gate). Boss resolution (Option 2, overriding the orchestrator's Option 1, rationale in the journal + plan Design-decision 6): keep descriptive embed_* filenames, rename fixture MODULES to their stems — the C symbol backtest_step stays via (export ...), demonstrating spec Decision 2's mangling- decoupling rather than violating it; archive becomes libembed_backtest_step.a, internal symbol @ail_embed_backtest_step_step, host.c unchanged. Plan fixed (Design-decision 6 + Task-4 module==stem + Task-5/6/7 dependent strings + the no-*. Float-head + content_hash_fn concerns folded). Headline fixture corrected; blocked-Task-4 WIP discarded (recreated by the [4,7] re-dispatch from the fixed plan). PARTIAL commit (not the iter's final commit): the implement Repair path mandates committing the known-good subset so the [4,7] re-dispatch starts from a clean tree that includes Tasks 1-3 (Tasks 4-7 structurally depend on the schema field + surface). INDEX line added when the full iter lands post-re-dispatch. --- .../2026-05-18-iter-embedding-abi-m1.1.json | 23 ++ .../ail/tests/embed_missing_main_baseline.rs | 61 ++++ crates/ailang-check/src/lib.rs | 4 + crates/ailang-check/src/lift.rs | 1 + crates/ailang-check/src/linearity.rs | 4 + crates/ailang-check/src/mono.rs | 3 + crates/ailang-check/src/reuse_shape.rs | 1 + crates/ailang-check/src/suppress_filter.rs | 1 + crates/ailang-check/src/uniqueness.rs | 1 + crates/ailang-check/tests/workspace.rs | 8 + crates/ailang-codegen/src/lib.rs | 32 ++ crates/ailang-core/specs/form_a.md | 9 + crates/ailang-core/src/ast.rs | 13 + crates/ailang-core/src/desugar.rs | 16 + crates/ailang-core/src/pretty.rs | 1 + .../ailang-core/tests/design_schema_drift.rs | 1 + .../tests/embed_export_hash_stable.rs | 35 ++ crates/ailang-core/tests/hash_pin.rs | 1 + crates/ailang-core/tests/spec_drift.rs | 1 + crates/ailang-prose/src/lib.rs | 7 + crates/ailang-surface/src/parse.rs | 35 +- crates/ailang-surface/src/print.rs | 8 + docs/DESIGN.md | 1 + .../2026-05-18-iter-embedding-abi-m1.1.md | 317 ++++++++++++++++++ docs/plans/embedding-abi-m1.1.md | 134 ++++++-- examples/embed_backtest_step.ail | 20 ++ examples/embed_noentry_baseline.ail | 9 + 27 files changed, 709 insertions(+), 38 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m1.1.json create mode 100644 crates/ail/tests/embed_missing_main_baseline.rs create mode 100644 crates/ailang-core/tests/embed_export_hash_stable.rs create mode 100644 docs/journals/2026-05-18-iter-embedding-abi-m1.1.md create mode 100644 examples/embed_backtest_step.ail create mode 100644 examples/embed_noentry_baseline.ail diff --git a/bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m1.1.json b/bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m1.1.json new file mode 100644 index 0000000..17786b7 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m1.1.json @@ -0,0 +1,23 @@ +{ + "iter_id": "embedding-abi-m1.1", + "date": "2026-05-18", + "mode": "standard", + "outcome": "PARTIAL", + "tasks_total": 7, + "tasks_completed": 3, + "reloops_per_task": { "1": 0, "2": 0, "3": 0, "4": 0 }, + "review_loops_spec": 0, + "review_loops_quality": 0, + "blocked_reason": "spec-ambiguous", + "notes": { + "blocked_task": 4, + "blocked_detail": "load_workspace stem-match (workspace.rs:182 ModuleNameMismatch) is mutually unsatisfiable with the plan's verbatim fixtures (spec-mandated module `backtest` in file embed_backtest_step.ail; bad/bad_io/... in embed_export_*_rejected.ail) plus the verbatim load_workspace-based gate pin. Structural spec/plan defect rippling Tasks 4-7; Boss/spec decision required (Option 1: rename fixture files to module stems, keep module backtest).", + "concerns": [ + "Task 2: plan symbol content_hash_fn(&FnDef) nonexistent; mirrored hash_pin.rs def_hash(&Def) per the plan NOTE's own resolution clause (planner pseudo-vs-reality class).", + "Task 4: plan fixture `*.` Float-multiply head nonexistent; used the typeclass-generic `*` head per the plan NOTE (not yet exercised — gate BLOCKED before run).", + "INFRA: docs/roadmap.md externally modified mid-session; path-disjoint from iter; left untouched (Boss-owned)." + ], + "fnDef_threading_sites": "~85 export:None literals across 18 files via compile-driven enumeration (2 dependency-ordered waves), cargo build --workspace exits 0", + "tests_state": "Tasks 1-3 + pre-existing all green; only the deliberately-RED embed_export_gate (6 tests) fails workspace-wide" + } +} diff --git a/crates/ail/tests/embed_missing_main_baseline.rs b/crates/ail/tests/embed_missing_main_baseline.rs new file mode 100644 index 0000000..884648e --- /dev/null +++ b/crates/ail/tests/embed_missing_main_baseline.rs @@ -0,0 +1,61 @@ +//! Baseline pin (Embedding-ABI M1, spec Testing-strategy item 0): +//! `ail build` (default executable mode) on a `main`-free module +//! surfaces `CodegenError::MissingEntryMain` at the CLI build-path +//! layer. The staticlib emit-mode (Task 5/6) is *defined* as +//! suppressing exactly this rejection; this pin guards the baseline +//! at the layer the suppression branches on. The codegen-unit layer +//! is independently pinned by +//! `crates/ailang-codegen/src/lib.rs` `missing_entry_main_is_error`. +//! +//! Pure reader: writes nothing to the repo. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn ail_bin() -> &'static str { + env!("CARGO_BIN_EXE_ail") +} + +fn workspace_root() -> PathBuf { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + Path::new(manifest_dir).parent().unwrap().parent().unwrap().to_path_buf() +} + +#[test] +fn ail_build_on_main_free_module_is_rejected_at_cli() { + let fixture = workspace_root() + .join("examples") + .join("embed_noentry_baseline.ail"); + assert!(fixture.exists(), "missing fixture {fixture:?}"); + + let out_bin = std::env::temp_dir().join(format!( + "ailang-embed-baseline-{}", std::process::id() + )); + let output = Command::new(ail_bin()) + .args([ + "build", + fixture.to_str().unwrap(), + "-o", + out_bin.to_str().unwrap(), + ]) + .output() + .expect("run `ail build`"); + + assert!( + !output.status.success(), + "ail build of a main-free module unexpectedly succeeded; \ + stdout={} stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("has no `main` def"), + "expected the MissingEntryMain message \ + (`entry module ... has no `main` def`); got stderr: {stderr}", + ); + assert!( + !out_bin.exists(), + "no binary should be produced for a main-free module", + ); +} diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 332cb42..aa3069f 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1754,6 +1754,7 @@ fn check_instance( body: subst_body, doc: None, suppress: Vec::new(), + export: None, }; check_fn(&synthetic, env, out_warnings)?; } @@ -4260,6 +4261,7 @@ mod tests { body, suppress: vec![], doc: None, + export: None, }) } @@ -4828,6 +4830,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, }); // Use unbox at Int and at Bool — both must succeed and not // cross-contaminate the polymorphic variable. @@ -6933,6 +6936,7 @@ mod tests { body: Term::Lit { lit: Literal::Str { value: "_".into() } }, doc: None, suppress: vec![], + export: None, })], }; let mut modules = BTreeMap::new(); diff --git a/crates/ailang-check/src/lift.rs b/crates/ailang-check/src/lift.rs index e7a8e39..79f9c03 100644 --- a/crates/ailang-check/src/lift.rs +++ b/crates/ailang-check/src/lift.rs @@ -658,6 +658,7 @@ impl<'a> Lifter<'a> { body: body_full, suppress: vec![], doc: Some(doc), + export: None, })); // Update env globals so any outer LetRec lifted later diff --git a/crates/ailang-check/src/linearity.rs b/crates/ailang-check/src/linearity.rs index 9d843b3..1848ca3 100644 --- a/crates/ailang-check/src/linearity.rs +++ b/crates/ailang-check/src/linearity.rs @@ -1106,6 +1106,7 @@ mod tests { body, suppress: vec![], doc: None, + export: None, }) } @@ -1259,6 +1260,7 @@ mod tests { body: Term::Lit { lit: Literal::Int { value: 0 } }, suppress: vec![], doc: None, + export: None, }); let f_body = Term::App { callee: Box::new(Term::Var { name: "read".into() }), @@ -1425,6 +1427,7 @@ mod tests { body, suppress: vec![], doc: None, + export: None, }) } @@ -1574,6 +1577,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, }); let m = Module { diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index b0e9a27..61e696a 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -305,6 +305,7 @@ fn const_as_pseudo_fn(c: &ConstDef) -> AstFnDef { body: c.value.clone(), doc: None, suppress: Vec::new(), + export: None, } } @@ -944,6 +945,7 @@ pub fn synthesise_mono_fn( body, doc: None, suppress: Vec::new(), + export: None, }) } @@ -1043,6 +1045,7 @@ pub fn synthesise_mono_fn_for_free_fn( body: new_body, doc: source_fn.doc.clone(), suppress: Vec::new(), + export: None, }) } diff --git a/crates/ailang-check/src/reuse_shape.rs b/crates/ailang-check/src/reuse_shape.rs index 9bfcaaf..480e043 100644 --- a/crates/ailang-check/src/reuse_shape.rs +++ b/crates/ailang-check/src/reuse_shape.rs @@ -597,6 +597,7 @@ mod tests { body, suppress: vec![], doc: None, + export: None, }) } diff --git a/crates/ailang-check/src/suppress_filter.rs b/crates/ailang-check/src/suppress_filter.rs index bcbffc6..ffd29fb 100644 --- a/crates/ailang-check/src/suppress_filter.rs +++ b/crates/ailang-check/src/suppress_filter.rs @@ -126,6 +126,7 @@ mod tests { body: Term::Lit { lit: Literal::Int { value: 0 } }, doc: None, suppress, + export: None, })], } } diff --git a/crates/ailang-check/src/uniqueness.rs b/crates/ailang-check/src/uniqueness.rs index 5749165..81bbbd0 100644 --- a/crates/ailang-check/src/uniqueness.rs +++ b/crates/ailang-check/src/uniqueness.rs @@ -445,6 +445,7 @@ mod tests { body, suppress: vec![], doc: None, + export: None, }) } diff --git a/crates/ailang-check/tests/workspace.rs b/crates/ailang-check/tests/workspace.rs index 46c15ce..cf0ca41 100644 --- a/crates/ailang-check/tests/workspace.rs +++ b/crates/ailang-check/tests/workspace.rs @@ -155,6 +155,7 @@ fn body_errors_accumulate_across_defs() { }, suppress: vec![], doc: None, + export: None, }); let bad_b = Def::Fn(FnDef { name: "bad_b".into(), @@ -171,6 +172,7 @@ fn body_errors_accumulate_across_defs() { }, suppress: vec![], doc: None, + export: None, }); let m = Module { @@ -228,6 +230,7 @@ fn use_after_consume_on_own_param_is_reported() { body: Term::Lit { lit: Literal::Int { value: 0 } }, suppress: vec![], doc: None, + export: None, }); // The offending fn. Body is `(+ (sum_list xs) (sum_list xs))` — both @@ -263,6 +266,7 @@ fn use_after_consume_on_own_param_is_reported() { }, suppress: vec![], doc: None, + export: None, }); // List ADT (referenced by both fn types via `Type::Con`); a real @@ -356,6 +360,7 @@ fn consume_while_borrowed_in_sibling_arg_is_reported() { body: Term::Lit { lit: Literal::Int { value: 0 } }, suppress: vec![], doc: None, + export: None, }); let bad = Def::Fn(FnDef { @@ -378,6 +383,7 @@ fn consume_while_borrowed_in_sibling_arg_is_reported() { }, suppress: vec![], doc: None, + export: None, }); let m = Module { @@ -517,6 +523,7 @@ fn reuse_as_happy_path_in_map_inc_is_linearity_clean() { body: map_inc_body, suppress: vec![], doc: None, + export: None, }); let m = Module { @@ -629,6 +636,7 @@ fn reuse_as_shape_mismatch_is_reported_on_cons_to_nil() { body, suppress: vec![], doc: None, + export: None, }); let m = Module { diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 51393bd..02f1d92 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -3168,6 +3168,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, }), // Entry module needs a `main`, otherwise // `lower_workspace` returns `MissingEntryMain`. @@ -3184,6 +3185,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -3248,6 +3250,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -3299,6 +3302,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let err = emit_ir(&m).expect_err( @@ -3332,6 +3336,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let err = emit_ir(&m).unwrap_err(); @@ -3402,6 +3407,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -3471,6 +3477,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -3502,6 +3509,7 @@ mod tests { body, suppress: vec![], doc: None, + export: None, }) } let plus_int = Term::App { @@ -3562,6 +3570,7 @@ mod tests { body, suppress: vec![], doc: None, + export: None, }) } fn cmp(op: &str, a: Term, b: Term) -> Term { @@ -3583,6 +3592,7 @@ mod tests { }, params: vec![], body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }); let m = Module { schema: ailang_core::SCHEMA.to_string(), @@ -3624,6 +3634,7 @@ mod tests { param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body, suppress: vec![], doc: None, + export: None, }) } fn app1(callee: &str, arg: Term) -> Term { @@ -3674,6 +3685,7 @@ mod tests { param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body, suppress: vec![], doc: None, + export: None, }) } let main_def = Def::Fn(FnDef { @@ -3684,6 +3696,7 @@ mod tests { }, params: vec![], body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }); let m = Module { schema: ailang_core::SCHEMA.to_string(), @@ -3731,6 +3744,7 @@ mod tests { body: Term::Lit { lit: Literal::Bool { value: false } }, suppress: vec![], doc: None, + export: None, }), Def::Fn(FnDef { name: "main".into(), @@ -3745,6 +3759,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -3788,6 +3803,7 @@ mod tests { body: Term::Lit { lit: Literal::Bool { value: false } }, suppress: vec![], doc: None, + export: None, }), Def::Fn(FnDef { name: "main".into(), @@ -3802,6 +3818,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -3849,6 +3866,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); @@ -3947,6 +3965,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, // placeholder; intercept overrides suppress: vec![], doc: None, + export: None, }); let main_def = Def::Fn(FnDef { name: "main".into(), @@ -3961,6 +3980,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }); Module { schema: SCHEMA.into(), @@ -4003,6 +4023,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -4041,6 +4062,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); @@ -4077,6 +4099,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); @@ -4113,6 +4136,7 @@ mod tests { body: Term::Lit { lit: Literal::Bool { value: false } }, suppress: vec![], doc: None, + export: None, }), Def::Fn(FnDef { name: "main".into(), @@ -4127,6 +4151,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -4208,6 +4233,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, }), Def::Fn(FnDef { name: "main".into(), @@ -4222,6 +4248,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }), ], }; @@ -4272,6 +4299,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); @@ -4311,6 +4339,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); @@ -4351,6 +4380,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); @@ -4391,6 +4421,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); @@ -4434,6 +4465,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let ir = emit_ir(&m).unwrap(); diff --git a/crates/ailang-core/specs/form_a.md b/crates/ailang-core/specs/form_a.md index aa0e119..77414ae 100644 --- a/crates/ailang-core/specs/form_a.md +++ b/crates/ailang-core/specs/form_a.md @@ -70,6 +70,7 @@ Five kinds, matched on the leading keyword: ``` (fn NAME (doc STRING)? + (export STRING)? (suppress (code STRING) (because STRING))* (type FN-TYPE) (params NAME*) @@ -79,6 +80,14 @@ Five kinds, matched on the leading keyword: `doc` is optional but recommended — it appears in the prose projection and helps downstream readers (human and LLM). +`export` is optional. When present, the string is the C symbol under +which codegen's `--emit=staticlib` mode exposes this fn as an +externally-callable entrypoint (Embedding ABI M1). The symbol is +author-chosen and decoupled from the internal `ail__` +mangling. M1 requires the fn's parameter and return types to be +scalar (`Int`/`Float`) and its effect set to be empty — see +DESIGN.md §"Embedding ABI (M1)". + `suppress` clauses silence advisory diagnostics for this def. The `code` MUST be one of the registered codes (today: `over-strict-mode`). The `because` MUST be a non-empty justification — empty / whitespace- diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index 932f6df..2536d89 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -205,6 +205,19 @@ pub struct FnDef { /// Optional source-level documentation string. #[serde(default, skip_serializing_if = "Option::is_none")] pub doc: Option, + /// Iter embedding-abi-m1: when `Some(sym)`, this fn is the + /// embedding boundary. Codegen's `Target::StaticLib` mode + /// additionally emits an externally-visible C entrypoint + /// `@` (scalar signature, provisional until M3) forwarding + /// to `@ail__`. The symbol is author-chosen and + /// decoupled from the `ail__` mangling so the + /// M3-frozen ABI survives module/fn refactors. + /// + /// Serialised `skip_serializing_if = "Option::is_none"` so every + /// pre-M1 fixture's canonical-JSON hash stays bit-identical — + /// the same additive-schema pattern as [`FnDef::doc`]. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub export: Option, /// Iter 19b: structured-diagnostic suppressions opted into for this /// fn. Each entry names a diagnostic code and an author-asserted /// reason. Currently the only consumer is `over-strict-mode` diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index 993c498..317f162 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -910,6 +910,7 @@ impl Desugarer { body: body_full, suppress: vec![], doc: None, + export: None, })); in_full } @@ -1733,6 +1734,7 @@ mod tests { body: body_match, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -1789,6 +1791,7 @@ mod tests { body: original.clone(), suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -1846,6 +1849,7 @@ mod tests { body: fact_letrec_term(), suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -1934,6 +1938,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -2059,6 +2064,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -2173,6 +2179,7 @@ mod tests { body: outer_body, suppress: vec![], doc: None, + export: None, }), ], }; @@ -2251,6 +2258,7 @@ mod tests { body: letrec, suppress: vec![], doc: None, + export: None, })], }; let _ = desugar_module(&m); @@ -2312,6 +2320,7 @@ mod tests { body: letrec, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -2450,6 +2459,7 @@ mod tests { body: letrec, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -2553,6 +2563,7 @@ mod tests { body: letrec, suppress: vec![], doc: None, + export: None, })], }; let _ = desugar_module(&m); @@ -2634,6 +2645,7 @@ mod tests { body: outer, suppress: vec![], doc: None, + export: None, })], }; let _ = desugar_module(&m); @@ -2714,6 +2726,7 @@ mod tests { body: outer, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -2839,6 +2852,7 @@ mod tests { body: body_match, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -2912,6 +2926,7 @@ mod tests { body: body_match, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); @@ -2993,6 +3008,7 @@ mod tests { body: body_match, suppress: vec![], doc: None, + export: None, })], }; let out = desugar_module(&m); diff --git a/crates/ailang-core/src/pretty.rs b/crates/ailang-core/src/pretty.rs index 732dd99..49cef5a 100644 --- a/crates/ailang-core/src/pretty.rs +++ b/crates/ailang-core/src/pretty.rs @@ -191,6 +191,7 @@ mod tests { }, suppress: vec![], doc: None, + export: None, }), ], } diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index 6e607a1..5cc97cd 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -282,6 +282,7 @@ fn design_md_anchors_every_def_kind() { ty: Type::fn_implicit(vec![], Type::int(), vec![]), params: vec![], body: Term::Lit { lit: Literal::Int { value: 0 } }, + export: None, }; let const_def = ConstDef { name: "k".into(), diff --git a/crates/ailang-core/tests/embed_export_hash_stable.rs b/crates/ailang-core/tests/embed_export_hash_stable.rs new file mode 100644 index 0000000..69265bc --- /dev/null +++ b/crates/ailang-core/tests/embed_export_hash_stable.rs @@ -0,0 +1,35 @@ +//! Additive-field hash stability (Embedding-ABI M1): adding +//! `FnDef.export: Option` with +//! `#[serde(default, skip_serializing_if = "Option::is_none")]` +//! must leave every pre-M1 canonical-JSON hash bit-identical when +//! `export` is `None`. A round-trip-able fixture WITHOUT `export` +//! hashes identically to a hand-frozen pre-M1 golden. +//! +//! The hashing entry point is `ailang_core::hash::def_hash(&Def)` — +//! the exact one `crates/ailang-core/tests/hash_pin.rs` uses (the +//! plan's `content_hash_fn(&FnDef)` symbol does not exist; the plan +//! NOTE authorises mirroring `hash_pin.rs`'s entry point when the +//! name differs). The golden below was captured pre-field via +//! `def_hash` on the same `f : (Int)->Int / body=x` shape. + +use ailang_core::ast::*; +use ailang_core::hash::def_hash; + +#[test] +fn fn_without_export_hash_is_unchanged() { + // A minimal pre-M1-shaped fn (no export). Its content hash must + // equal the value captured before the field existed. + let def = Def::Fn(FnDef { + name: "f".into(), + ty: Type::fn_implicit(vec![Type::int()], Type::int(), vec![]), + params: vec!["x".into()], + body: Term::Var { name: "x".into() }, + doc: None, + suppress: vec![], + export: None, + }); + // GOLDEN: captured pre-field (Step 2) from `def_hash` on this shape. + let golden = "b4662aa70839f60b"; + assert_eq!(def_hash(&def), golden, + "pre-M1 fn hash drifted — additive-field invariant violated"); +} diff --git a/crates/ailang-core/tests/hash_pin.rs b/crates/ailang-core/tests/hash_pin.rs index 1869bf1..9e066d7 100644 --- a/crates/ailang-core/tests/hash_pin.rs +++ b/crates/ailang-core/tests/hash_pin.rs @@ -45,6 +45,7 @@ fn sample_fn() -> Def { }, suppress: vec![], doc: None, + export: None, }) } diff --git a/crates/ailang-core/tests/spec_drift.rs b/crates/ailang-core/tests/spec_drift.rs index 72c180a..5cc7d9e 100644 --- a/crates/ailang-core/tests/spec_drift.rs +++ b/crates/ailang-core/tests/spec_drift.rs @@ -260,6 +260,7 @@ fn spec_mentions_every_def_kind() { ty: Type::fn_implicit(vec![], Type::int(), vec![]), params: vec![], body: Term::Lit { lit: Literal::Int { value: 0 } }, + export: None, }; let const_def = ConstDef { name: "k".into(), diff --git a/crates/ailang-prose/src/lib.rs b/crates/ailang-prose/src/lib.rs index d275167..c4b14e6 100644 --- a/crates/ailang-prose/src/lib.rs +++ b/crates/ailang-prose/src/lib.rs @@ -1759,6 +1759,7 @@ mod tests { body: Term::Lit { lit: Literal::Int { value: 0 } }, suppress: vec![], doc: None, + export: None, }; let mut out = String::new(); write_fn_def(&mut out, &fd, 0, ""); @@ -1775,6 +1776,7 @@ mod tests { body: Term::Lit { lit: Literal::Unit }, suppress: vec![], doc: None, + export: None, }; let mut out = String::new(); write_fn_def(&mut out, &fd, 0, ""); @@ -1807,6 +1809,7 @@ mod tests { because: "RC codegen test fixture".into(), }], doc: Some("Take ownership.".into()), + export: None, }; let mut out = String::new(); write_fn_def(&mut out, &fd, 0, ""); @@ -1847,6 +1850,7 @@ mod tests { }, ], doc: None, + export: None, }; let mut out = String::new(); write_fn_def(&mut out, &fd, 0, ""); @@ -1869,6 +1873,7 @@ mod tests { body: Term::Lit { lit: Literal::Int { value: 0 } }, suppress: vec![], doc: Some("just a doc".into()), + export: None, }; let mut out = String::new(); write_fn_def(&mut out, &fd, 0, ""); @@ -2297,6 +2302,7 @@ mod tests { }, doc: None, suppress: vec![], + export: None, })], }; @@ -2344,6 +2350,7 @@ mod tests { }, doc: None, suppress: vec![], + export: None, })], }; diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 1c5d79d..fecc47e 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -460,6 +460,31 @@ impl<'a> Parser<'a> { Ok(s) } + fn parse_export(&mut self) -> Result { + self.expect_lparen("export-attr")?; + self.expect_keyword("export")?; + let s = match self.peek().cloned() { + Some(Token { tok: Tok::Str(s), .. }) => { + self.cur += 1; + s + } + Some(t) => { + return Err(ParseError::Unexpected { + expected: "string literal (export C symbol)".into(), + got: tok_label(&t.tok), + pos: t.span.start, + }); + } + None => { + return Err(ParseError::UnexpectedEof { + expected: "string literal (export C symbol)".into(), + }); + } + }; + self.expect_rparen("export-attr")?; + Ok(s) + } + fn parse_ctor(&mut self) -> Result { self.expect_lparen("ctor-decl")?; self.expect_keyword("ctor")?; @@ -479,6 +504,7 @@ impl<'a> Parser<'a> { self.expect_keyword("fn")?; let name = self.expect_ident("fn name")?; let mut doc: Option = None; + let mut export: Option = None; let mut ty: Option = None; let mut params: Option> = None; let mut body: Option = None; @@ -493,6 +519,12 @@ impl<'a> Parser<'a> { } doc = Some(self.parse_doc()?); } + Some("export") => { + if export.is_some() { + return Err(self.duplicate_clause_err("fn-def", &format!("fn `{name}`"), "export")); + } + export = Some(self.parse_export()?); + } Some("suppress") => suppress.push(self.parse_suppress_attr()?), Some("type") => { if ty.is_some() { @@ -517,7 +549,7 @@ impl<'a> Parser<'a> { return Err(ParseError::Production { production: "fn-def", message: format!( - "unknown fn attribute `{other}`; expected `doc`, `suppress`, `type`, `params`, or `body`" + "unknown fn attribute `{other}`; expected `doc`, `export`, `suppress`, `type`, `params`, or `body`" ), pos, }); @@ -547,6 +579,7 @@ impl<'a> Parser<'a> { params, body, doc, + export, suppress, }) } diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index 36abd75..fbfa217 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -156,6 +156,13 @@ fn write_fn_def(out: &mut String, fd: &FnDef, level: usize) { write_string_lit(out, doc); out.push(')'); } + if let Some(export) = &fd.export { + out.push('\n'); + indent(out, level + 1); + out.push_str("(export "); + write_string_lit(out, export); + out.push(')'); + } // Iter 19b: emit one `(suppress ...)` clause per entry, after the // doc string and before the type. Round-trip stable: parser // re-reads each clause back into [`FnDef::suppress`]. @@ -779,6 +786,7 @@ mod tests { params: vec!["x".into()], body: Term::Var { name: "x".into() }, doc: None, + export: None, suppress: vec![], })], }; diff --git a/docs/DESIGN.md b/docs/DESIGN.md index bf32a4c..d90eaf5 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -2295,6 +2295,7 @@ are real surface forms. "params": [""...], // names bound in body, in type.params order "body": Term, "doc": "", + "export": "", // omitted when absent (hash-stable when omitted); see §"Embedding ABI (M1)" "suppress": [Suppress...] // omitted when empty } diff --git a/docs/journals/2026-05-18-iter-embedding-abi-m1.1.md b/docs/journals/2026-05-18-iter-embedding-abi-m1.1.md new file mode 100644 index 0000000..7c735a7 --- /dev/null +++ b/docs/journals/2026-05-18-iter-embedding-abi-m1.1.md @@ -0,0 +1,317 @@ +# iter embedding-abi-m1.1 — Embedding ABI M1: linkable artifact + scalar C entrypoint + +**Date:** 2026-05-18 +**Started from:** b0bd7efeaff137af22bb12f5e751260804e770c3 +**Status:** PARTIAL — Tasks 1–3 committed as a known-good subset +(see "Boss resolution" below); plan defect fixed; Tasks 4–7 +re-dispatched on the corrected plan. +**Tasks completed:** 3 of 7 (Tasks 1–3 DONE + committed; Task 4 +BLOCKED on a Boss plan-defect — fixture module≠stem — before any +gate code was written; resolved by Boss + re-dispatched; Tasks +5–7 follow in the re-dispatch) + +## Summary + +Tasks 1–3 of the Embedding-ABI M1 plan landed cleanly: the CLI +build-path `MissingEntryMain` baseline pin (Task 1), the additive +`FnDef.export: Option` schema field threaded across the +whole workspace via compile-driven enumeration with a hash-stability +golden pin (Task 2), and the Form-A `(export "")` surface +modifier with full parse/print/grammar lockstep and a byte-identical +round-trip (Task 3). Task 4 (the check-side export-signature gate) +is BLOCKED before any production code: the plan's verbatim gate-pin +test (`crates/ailang-check/tests/embed_export_gate.rs`, using +`ailang_surface::load_workspace`) is mutually unsatisfiable with the +plan's verbatim fixtures, because `load_workspace` hard-enforces +`module-name == file-stem` (`crates/ailang-core/src/workspace.rs:182` +`ModuleNameMismatch`) while the fixtures (spec-mandated module +`backtest` in file `embed_backtest_step.ail`; `bad`/`bad_io`/… in +`embed_export_*_rejected.ail`) deliberately diverge module name from +filename. This is not an implementer-judgement cross-check (the six +named ones were executed where reached) — it is a structural +contradiction that ripples across Tasks 4–7 and can only be resolved +by a Boss/spec-level decision. Per the Iron Law (`unclear` spec +ambiguity → BLOCKED, no implementing on a hunch), the iter stopped. +Tasks 1–3 sit clean and green in the working tree; only the +deliberately-RED Task 4 pin fails workspace-wide. + +## Per-task notes + +- iter embedding-abi-m1.1.1: CLI build-path `MissingEntryMain` + baseline pin. Created `examples/embed_noentry_baseline.ail` + (main-free module) + `crates/ail/tests/embed_missing_main_baseline.rs` + (triple-assertion: non-zero exit + verbatim `has no \`main\` def` + Display substring [codegen lib.rs:129] + absent binary). GREEN on + write; not-a-no-op confirmed; `roundtrip_cli` green with the new + fixture. DONE. +- iter embedding-abi-m1.1.2: additive `FnDef.export` schema field + + workspace-wide threading. Added the field to `FnDef` after `doc` + (verbatim plan doc-comment, `#[serde(default, + skip_serializing_if = "Option::is_none")]`). Compile-driven + enumeration via `cargo build --workspace --all-targets + --keep-going` over two dependency-ordered waves threaded + `export: None` into ~85 `FnDef`/`AstFnDef` literals across 18 + files incl. all named blind-spot sites (codegen lib.rs:3320 + `missing_entry_main_is_error`, design_schema_drift.rs:278, + spec_drift.rs:256, hash_pin.rs, the in-source `#[cfg(test)] + mod tests` literals, the `mono.rs` `AstFnDef` alias, the + `parse.rs`/`print.rs` literals). `cargo build --workspace` + exits 0 (planner self-review item 7 gate met). DESIGN.md + fn-JSON `"export"` line + drift test green (Recon Q2 confirmed: + no nested-struct-key anchor forced). Hash pin green (golden + `b4662aa70839f60b`). Full workspace test gate: 611/0. + DONE_WITH_CONCERNS. +- iter embedding-abi-m1.1.3: Form-A `(export "")` surface + modifier. `parse_export` helper (faithful `parse_doc` mirror); + `parse_fn` threaded (`let mut export`, `Some("export")` arm with + duplicate-clause guard, unknown-attr string updated, `Ok(FnDef{…})` + uses parsed `export`); `write_fn_def` export emission block (doc → + export → suppress → type order); form_a.md production + + `(export STRING)?` + prose paragraph. RED verified + (`unknown fn attribute \`export\``), then GREEN: round-trip gate + byte-identical, surface crate 72/0, ailang-core 111/0. DONE. +- iter embedding-abi-m1.1.4: check-side export-signature gate. + Wrote the five plan fixtures + the verbatim gate pin; ran it → + RED for the WRONG reason (a `ModuleNameMismatch` load failure, + not the absence of the gate). BLOCKED — see Blocked detail. + No `CheckError` variant, no `code()` arm, no `check_fn` gate + written. + +## Concerns + +- iter embedding-abi-m1.1.2: the plan's RED-pin symbol + `ailang_core::hash::content_hash_fn(&FnDef)` does not exist. The + real entry point is `def_hash(&Def) -> String` (the one + `crates/ailang-core/tests/hash_pin.rs` uses). Resolved by + mirroring `hash_pin.rs` exactly (wrap the `FnDef` in + `Def::Fn(...)`, call `def_hash`) — this is explicitly authorised + by the plan's own Step-1 NOTE ("the implementer mirrors the exact + hashing entry point that `hash_pin.rs` uses if the name differs"). + Property and shape unchanged; recorded because the plan's literal + symbol was wrong (planner pseudo-vs-reality class — the + `feedback_specs_need_concrete_code` / `feedback_plan_pseudo_vs_reality` + family). +- iter embedding-abi-m1.1.4: the plan fixture `embed_export_float_ok.ail` + uses `(app *. a b)`. There is no `*.` Float-multiply head in + AILang — Float arithmetic uses the same typeclass-generic head as + Int (`examples/floats.ail` / `examples/mut_sum_floats.ail` both + use `(app + …)`; codegen lowers to `fadd/fmul double` by type). + Per the plan NOTE ("if the Float-multiply surface differs, use the + same head that example uses"), the fixture as written uses + `(app * a b)`. The fixture's role ("scalar+pure Float export + passes the gate") is preserved. Recorded; not yet exercised + (Task 4 BLOCKED before the gate ran). +- INFRA (not iter-authored): `docs/roadmap.md` became dirty in the + working tree during this run (a ~98-line P2 "DESIGN.md → design/ + role-split" milestone proposal, mtime mid-session). Phase 0's + `git status --porcelain` was empty (clean start). Nothing in this + iter writes `roadmap.md`; it was modified by an external/concurrent + writer. It is path-disjoint from every iter file. Left untouched + (roadmap.md is Boss-owned per CLAUDE.md; main HEAD sacrosanct). + The Boss must decide independently whether to keep or discard it; + it is NOT part of this iter's commit. + +## Known debt + +- Tasks 5 (codegen `Target::StaticLib`), 6 (CLI `--emit=staticlib`), + 7 (E2E host harness + DESIGN.md §"Embedding ABI (M1)") not reached + — they depend on Task 4's gate (Task 5's forwarder loop trusts the + gate; Task 6/7 build on the headline fixture). Not touched because + the iter is BLOCKED upstream of them; no speculative partial work. +- E2E coverage (Phase 3) not written — the iter did not reach + completion; the Task 7 host-harness is the milestone's coherent- + stop proof and is downstream of the block. + +## Blocked detail + +**Task:** 4 (check-side export-signature gate) +**Reason:** spec-ambiguous (a spec-internal contradiction that +rippling-binds Tasks 4–7, surfaced as `unclear` per the per-task +sub-status table → BLOCKED to Boss). + +**Worker says:** The plan's Task-4 gate pin +(`crates/ailang-check/tests/embed_export_gate.rs`) is verbatim and +correct Rust; it calls `ailang_surface::load_workspace(&path)` on +each fixture (the exact call shape `crates/ail/src/main.rs:580–581` +uses). `load_workspace` → +`ailang_core::workspace::load_modules_with` → +`module_name_from_path` (`workspace.rs:1566`) hard-enforces +`in-file module name == file stem`, returning +`WorkspaceLoadError::ModuleNameMismatch` (`workspace.rs:182`) +otherwise. The plan's verbatim fixtures deliberately violate this: + + - `examples/embed_backtest_step.ail` has `(module backtest …)` — + spec-mandated, because the entry module name `backtest` is what + produces `libbacktest.a` (Task 6), `@ail_backtest_step` and the + exported C symbol `backtest_step` (Tasks 5/7, the spec headline + host `host.c` declares `extern int64_t backtest_step(...)`). + Stem is `embed_backtest_step` ⇒ `ModuleNameMismatch`. + - `embed_export_str_param_rejected.ail` → module `bad_str_param`; + `embed_export_io_rejected.ail` → `bad_io`; + `embed_export_effectful_rejected.ail` → `bad`; + `embed_export_adt_ret_rejected.ail` → `bad_adt_ret`; + `embed_export_float_ok.ail` → `embed_float_ok`. All + stem-mismatched ⇒ every `check_codes()` call panics on load + before the gate is ever consulted. + +These three plan facts cannot all hold simultaneously: (a) fixture +files named `examples/embed_*` (every Task 4/5/6/7 path cites these +exact names), (b) the spec-headline module/C-symbol naming +(`backtest` → `libbacktest.a` / `backtest_step`), (c) a +`load_workspace`-based check-side gate pin. `roundtrip_cli` (Task 1/3) +sidesteps this only because it drives `ail parse` (single-file, no +workspace stem-match). The contradiction is therefore invisible until +the first `load_workspace`-based fixture test — exactly Task 4. + +This is not one of the six named implementer-judgement cross-checks +(Float `*.` head, public-symbol confirmation, `Type::Con` field set, +`declared_effs` element type, `run_cmd` pre-existence, `llvm_scalar` +callee-ABI). It is a structural spec/plan defect requiring a +Boss/spec decision among at least: + + 1. Rename each fixture **file** to its module stem + (`examples/backtest.ail`, `examples/bad_str_param.ail`, …) and + update every Task 4/5/6/7 path citation accordingly (preserves + the spec headline module/C-symbol naming; ripples ~12 plan + path references + the Task-6/7 `libbacktest.a` math still holds + since module stays `backtest`). + 2. Rename each fixture **module** to its filename stem + (`(module embed_backtest_step …)`, …) and re-derive the spec's + C-symbol / `lib.a` story from `embed_backtest_step` + (changes the spec headline; `libembed_backtest_step.a`, + `@ail_embed_backtest_step_step` — almost certainly not what the + spec wants). + 3. Change the Task-4 gate pin to a loader that does not + stem-enforce (e.g. parse the single file directly), which + diverges from the plan's verbatim `load_workspace`-based test + and from how `ail check` actually loads (`main.rs:580`). + +Option 1 is the most spec-faithful (it keeps the headline module +`backtest` and only moves files), but it is a cross-task plan edit +(≥12 path citations) and therefore the planner's / Boss's call, not +an inline implementer substitution. I did NOT silently pick one +(reverted my own speculative fixture-module renames so the Boss sees +the contradiction unobscured); no gate production code was written. + +**Suggested next step:** Boss adopts Option 1 — re-issue the plan +for Tasks 4–7 with fixture files renamed to their module stems +(`examples/backtest.ail` etc.) and the ~12 path citations updated; +keep module `backtest` so the spec's `libbacktest.a` / +`backtest_step` headline is preserved — then re-dispatch this iter +with `task_range: [4, 7]` (Tasks 1–3 are clean and can be committed +now or carried). + +## Boss resolution (2026-05-18) + +The BLOCK was verified accurate against source: +`crates/ailang-core/src/workspace.rs:438` (`entry_module.name != +module_name_from_path(entry_path)` → `ModuleNameMismatch`, no +exception) — module name MUST equal file stem. The defect is a +**Boss plan-write error**: the plan's headline + gate fixtures +declared `(module backtest)` / `(module bad)` / … in `embed_*.ail` +files, malformed under that invariant. The orchestrator correctly +stopped at the Iron Law instead of guessing. Recon maps file +structure, not fixture-content invariants; the Boss source- +verification focused on code-edit anchors, not cross-checking each +fixture's module name against its filename against the loader — +this is the `feedback_specs_need_concrete_code` / +`feedback_plan_pseudo_vs_reality` family (the fixture as written +would not load), now also a fixture-well-formedness lesson. + +**Decision: Option 2, overriding the orchestrator-recommended +Option 1.** (Agent recommendations are not directives — own +judgement formed first.) Keep the descriptive `embed_*` filenames; +rename the fixture *modules* to their file stems +(`embed_backtest_step`, `embed_export_str_param_rejected`, …). +Rationale from semantics, not effort: + +- The spec's load-bearing contract (Decision 2, spec lines + 52–59 / 167–169) is that the exported C symbol is *author-chosen + and decoupled from `ail_` mangling* — + `(export "backtest_step")` is the contract; `host.c` asserts on + `backtest_step`, which is invariant under module rename. So + spec-faithfulness does **not** discriminate between the options; + `libbacktest.a` / `@ail_backtest_step` are *illustrative* + (build-output filename + internal mangling — the latter is + exactly what the spec decouples from), not frozen-ABI contracts. + The orchestrator's "Option 1 most spec-faithful" over-weighted + the illustrative `libbacktest.a` string. +- `examples/` is a flat shared corpus auto-enrolled in whole-corpus + tests (`roundtrip_cli`); Option 1's `examples/bad.ail` / + `backtest.ail` / `bad_io.ail` pollute it with collision-prone, + non-self-describing names. Option 2 keeps the self-describing + grouped `embed_*` set — a real maintainability argument in a + ~100-file flat dir read by future LLM authors. +- Option 2 (module = stem, C symbol stays `backtest_step` via the + `(export …)` string) is a *concrete demonstration of the spec's + own Decision-2 decoupling thesis* — the first fixture proving + module-name ≠ C-symbol — not a violation of it. The spec's + `(module backtest)` / `(module bad)` blocks are shape- + illustrative (planner contract: spec owns shape, not bytes). + +Consequence: entry module `embed_backtest_step` ⇒ archive +`libembed_backtest_step.a`, internal symbol +`@ail_embed_backtest_step_step`; the external C symbol +`backtest_step` and `host.c` are UNCHANGED (the actual coherent- +stop contract). Architect-at-close note recorded in the plan +(Design-decision 6): a fixture module name differing from the +spec's illustrative block is expected under module==stem + +Decision 2, not drift. + +Actions taken: (1) plan `docs/plans/embedding-abi-m1.1.md` fixed — +Design-decision 6 added, Task-4 fixtures → module==stem, Task-5/6/7 +dependent strings updated (`libembed_backtest_step.a`, +`@ail_embed_backtest_step_step`; C symbol `@backtest_step` +unchanged), the two iter-1 concerns folded (no `*.` Float head → +`(app * a b)`; `content_hash_fn` → `def_hash(&Def)`); (2) the +already-created headline `examples/embed_backtest_step.ail` +corrected to `(module embed_backtest_step)`; (3) the 5 blocked- +Task-4 fixtures + the RED `embed_export_gate.rs` discarded +(untracked WIP — recreated correctly by the re-dispatch from the +fixed plan); (4) Tasks 1–3 committed as a verified known-good +partial subset (`cargo build --workspace` exit 0, full workspace +green with zero FAILED after the WIP discard, `roundtrip_cli` +green); (5) orchestrator re-dispatched with `task_range: [4, 7]` +on the clean tree + corrected plan. The user's path-disjoint +roadmap addition (P2 "DESIGN.md → `design/` role-split") was +committed separately as Boss roadmap maintenance, not bundled +into this iter. + +## Files touched + +Schema / canonical docs: +- crates/ailang-core/src/ast.rs (FnDef.export field) +- docs/DESIGN.md (fn-JSON "export" line) +- crates/ailang-core/specs/form_a.md (grammar production + prose) + +Workspace-wide compile-gate threading (~85 `export: None,` literals): +- crates/ailang-core/src/desugar.rs, pretty.rs +- crates/ailang-core/tests/{design_schema_drift,hash_pin,spec_drift}.rs +- crates/ailang-check/src/{lib,lift,linearity,mono,reuse_shape,suppress_filter,uniqueness}.rs +- crates/ailang-check/tests/workspace.rs +- crates/ailang-codegen/src/lib.rs +- crates/ailang-prose/src/lib.rs + +Surface modifier (Task 3): +- crates/ailang-surface/src/parse.rs (parse_export + parse_fn thread) +- crates/ailang-surface/src/print.rs (write_fn_def export block) + +New files (untracked): +- examples/embed_noentry_baseline.ail +- examples/embed_backtest_step.ail +- examples/embed_export_str_param_rejected.ail +- examples/embed_export_adt_ret_rejected.ail +- examples/embed_export_io_rejected.ail +- examples/embed_export_effectful_rejected.ail +- examples/embed_export_float_ok.ail +- crates/ail/tests/embed_missing_main_baseline.rs +- crates/ailang-core/tests/embed_export_hash_stable.rs +- crates/ailang-check/tests/embed_export_gate.rs (deliberately RED — + blocks on the spec contradiction; do NOT commit as-is) + +NOT iter-authored, present in tree, Boss-decides separately: +- docs/roadmap.md (external mid-session modification; path-disjoint) + +## Stats + +bench/orchestrator-stats/2026-05-18-iter-embedding-abi-m1.1.json diff --git a/docs/plans/embedding-abi-m1.1.md b/docs/plans/embedding-abi-m1.1.md index c22a761..a5cd64e 100644 --- a/docs/plans/embedding-abi-m1.1.md +++ b/docs/plans/embedding-abi-m1.1.md @@ -95,6 +95,45 @@ finance/`data-server` knowledge or dependency (Invariant 1). ABI (M1)" subsection documents a capability that only fully exists after codegen+CLI+E2E → Task 7 (DESIGN.md = current-state mirror: document the ABI once it demonstrably works). +6. **Fixture module name == file stem (Option 2; revised + post-Task-4-BLOCK 2026-05-18).** AILang's workspace loader hard- + enforces `module-name == file-stem` + (`crates/ailang-core/src/workspace.rs:438`, + `WorkspaceLoadError::ModuleNameMismatch`, no exception) — so every + fixture's `(module …)` MUST equal its filename stem or `ail check` + panics on load before any gate runs. The original plan's headline + used `(module backtest)` in `embed_backtest_step.ail` (and + `bad`/`bad_io`/… in the gate fixtures) — malformed under that + invariant; the implement-orchestrator correctly BLOCKED Task 4 on + it. Resolution: **rename the fixture *modules* to their file + stems, keep the descriptive `embed_*` filenames** (NOT the + orchestrator-suggested Option 1 of renaming files to bare module + stems). Rationale, from semantics not effort: (a) the spec's + load-bearing contract (Decision 2, spec lines 52–59/167–169) is + that the exported C symbol is *author-chosen and decoupled from + `ail_` mangling* — `(export "backtest_step")` is the + contract; the host asserts on `backtest_step`, which is invariant + under module rename. So spec-faithfulness does not discriminate; + `libbacktest.a`/`@ail_backtest_step` are *illustrative* (build- + output filename + internal mangling — the latter is exactly what + the spec decouples from), not frozen-ABI contracts. (b) `examples/` + is a flat shared corpus auto-enrolled in whole-corpus tests; + Option 1's `examples/bad.ail`/`backtest.ail` pollute it with + collision-prone non-self-describing names — Option 2 keeps the + self-describing grouped `embed_*` set. (c) Option 2 *is* a + concrete demonstration of the spec's own Decision-2 decoupling + thesis (module `embed_backtest_step`, C symbol `backtest_step`), + not a violation of it. The spec's `(module backtest)`/`(module + bad)` blocks are shape-illustrative (planner contract: spec owns + shape, not bytes — same as the spec naming a filename while the + contract is the `(export …)` symbol). Consequences threaded below: + entry module `embed_backtest_step` ⇒ archive + `libembed_backtest_step.a`, internal symbol + `@ail_embed_backtest_step_step`; the external C symbol + `backtest_step` and `host.c` are UNCHANGED. **Architect-at-close + note:** a fixture module name differing from the spec's + illustrative block is expected and correct under the module==stem + invariant + Decision 2 — not drift. --- @@ -266,9 +305,15 @@ fn fn_without_export_hash_is_unchanged() { } ``` -> NOTE: the `content_hash_fn` symbol + `Type::fn_implicit` / -> `Term::Var` constructors are the ones already used by -> `crates/ailang-core/tests/hash_pin.rs` and +> NOTE (corrected post-iter-1): there is **no** +> `ailang_core::hash::content_hash_fn`. The real entry point is +> `def_hash(&Def) -> String` — wrap the `FnDef` in `Def::Fn(...)` +> and call `def_hash`, exactly as `crates/ailang-core/tests/hash_pin.rs` +> does. (Task 2 already executed this correctly per this NOTE's +> original "mirror hash_pin.rs" license; the symbol name above is +> left only so the diff to the as-executed test is legible.) +> `Type::fn_implicit` / `Term::Var` constructors are the ones +> already used by `hash_pin.rs` and > `design_schema_drift.rs:282`; the implementer mirrors the exact > hashing entry point that `hash_pin.rs` uses if the name differs. > The `<>` is resolved deterministically in @@ -381,11 +426,13 @@ constructs/serialises identically because `export: None` is omitted). - [ ] **Step 1: Write the headline round-trip RED fixture** -Create `examples/embed_backtest_step.ail` (the spec headline, -verbatim): +Create `examples/embed_backtest_step.ail` (the spec headline; module +name == file stem per Design-decision 6 — the C symbol stays +`backtest_step` via the `(export …)` string, demonstrating spec +Decision 2's mangling-decoupling): ``` -(module backtest +(module embed_backtest_step (fn step (export "backtest_step") @@ -577,7 +624,7 @@ Expected: PASS (form_a.md is not hashed; this confirms no `examples/embed_export_str_param_rejected.ail` (Str param, else scalar+pure → `export-non-scalar-signature`): ``` -(module bad_str_param +(module embed_export_str_param_rejected (fn f (export "f") (type @@ -591,7 +638,7 @@ scalar+pure → `export-non-scalar-signature`): `examples/embed_export_adt_ret_rejected.ail` (ADT return → `export-non-scalar-signature`): ``` -(module bad_adt_ret +(module embed_export_adt_ret_rejected (data Pair (ctor Pair (con Int) (con Int))) (fn f @@ -607,7 +654,7 @@ scalar+pure → `export-non-scalar-signature`): `examples/embed_export_io_rejected.ail` (scalar params+ret but `!IO` → `export-has-effects`): ``` -(module bad_io +(module embed_export_io_rejected (fn f (export "f") (type @@ -624,7 +671,7 @@ scalar+pure → `export-non-scalar-signature`): Str param **and** `!IO`; gate-order params→ret→effects ⇒ fails `export-non-scalar-signature`): ``` -(module bad +(module embed_export_effectful_rejected (fn log_step (export "log_step") (type @@ -640,7 +687,7 @@ Str param **and** `!IO`; gate-order params→ret→effects ⇒ fails `examples/embed_export_float_ok.ail` (positive — `(Float,Float)->Float`, pure; the second scalar type beyond the Int headline): ``` -(module embed_float_ok +(module embed_export_float_ok (fn scale (export "embed_scale") (type @@ -649,13 +696,15 @@ pure; the second scalar type beyond the Int headline): (ret (con Float)))) (params a b) (body - (app *. a b)))) + (app * a b)))) ``` -> The implementer verifies `*.` is the Form-A Float-multiply head -> against an existing Float example (`examples/floats.ail`); if the -> Float-multiply surface differs, use the same head that example -> uses. The fixture's *role* is "scalar+pure export passes the gate". +> NOTE (folded from the iter-1 BLOCK concern): AILang has **no** +> `*.` Float-multiply head — Float arithmetic uses the same +> typeclass-generic head as Int (`examples/floats.ail` uses +> `(app + …)`; codegen lowers to `fmul double` by type). The +> fixture above already uses `(app * a b)`. The fixture's *role* is +> "scalar+pure Float export passes the gate". - [ ] **Step 2: Write the gate pin — verify RED** @@ -858,9 +907,12 @@ Create `crates/ailang-codegen/tests/embed_staticlib_lowering.rs`: ```rust //! Embedding-ABI M1 codegen (spec §4): `Target::StaticLib` lowering //! emits NO `@main` trampoline, NO `MissingEntryMain`, and one -//! external `define i64 @backtest_step(...)` forwarding to -//! `@ail_backtest_step`. Executable-target lowering is byte-unchanged -//! (the `missing_entry_main_is_error` in-source pin still holds). +//! external `define i64 @backtest_step(...)` (the author-chosen C +//! symbol from `(export "backtest_step")`) forwarding to the +//! internal `@ail_embed_backtest_step_step` (module +//! `embed_backtest_step` == file stem, per Design-decision 6). +//! Executable-target lowering is byte-unchanged (the +//! `missing_entry_main_is_error` in-source pin still holds). use ailang_core::Workspace; use std::path::PathBuf; @@ -880,9 +932,11 @@ fn staticlib_emits_forwarder_no_main() { assert!(!ir.contains("define i32 @main()"), "staticlib IR must not emit the @main trampoline"); assert!(ir.contains("@backtest_step("), - "staticlib IR must emit the external @backtest_step entrypoint"); - assert!(ir.contains("@ail_backtest_step("), - "the forwarder must call the internal @ail_backtest_step"); + "staticlib IR must emit the external @backtest_step entrypoint \ + (the author-chosen C symbol — invariant under module rename)"); + assert!(ir.contains("@ail_embed_backtest_step_step("), + "the forwarder must call the internal \ + @ail_embed_backtest_step_step (module embed_backtest_step)"); } #[test] @@ -1077,8 +1131,9 @@ fn llvm_scalar(t: &Type) -> &'static str { Run: `cargo test -p ailang-codegen --test embed_staticlib_lowering` Expected: PASS (2 passed): staticlib IR has no `@main`, has external -`@backtest_step`, forwards to `@ail_backtest_step`; exe target still -returns `MissingEntryMain`. +`@backtest_step` (C symbol), forwards to the internal +`@ail_embed_backtest_step_step`; exe target still returns +`MissingEntryMain`. - [ ] **Step 7: Codegen in-source baseline pin still green** @@ -1138,8 +1193,8 @@ fn staticlib_emit_produces_two_archives() { String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)); - assert!(outdir.join("libbacktest.a").exists(), - "expected libbacktest.a in {outdir:?}"); + assert!(outdir.join("libembed_backtest_step.a").exists(), + "expected libembed_backtest_step.a in {outdir:?}"); assert!(outdir.join("libailang_rt.a").exists(), "expected separate libailang_rt.a in {outdir:?}"); } @@ -1328,8 +1383,9 @@ fn run_cmd(bin: &str, args: &[&str], ctx: &str) -> Result<()> { - [ ] **Step 5: Run the CLI pin — verify GREEN** Run: `cargo test -p ail --test embed_staticlib_cli` -Expected: PASS (2 passed): the headline produces `libbacktest.a` + -`libailang_rt.a`; the export-less module is rejected with the +Expected: PASS (2 passed): the headline produces +`libembed_backtest_step.a` + `libailang_rt.a`; the export-less +module is rejected with the zero-export error. - [ ] **Step 6: Default-exe path regression** @@ -1373,8 +1429,8 @@ Create `crates/ail/tests/embed_e2e.rs`: ```rust //! Embedding-ABI M1 coherent-stop proof (spec §"Testing strategy" 5 //! + Acceptance criteria): build `examples/embed_backtest_step.ail` -//! as a staticlib, link the C host against `libbacktest.a` + -//! `libailang_rt.a`, run it, assert the `s == 25` assertion holds +//! as a staticlib, link the C host against `libembed_backtest_step.a` +//! + `libailang_rt.a`, run it, assert the `s == 25` assertion holds //! (exit 0). This is the whole-milestone existence proof. use std::path::{Path, PathBuf}; @@ -1405,7 +1461,7 @@ fn c_host_calls_exported_scalar_kernel() { let host_bin = outdir.join("host"); let cc = Command::new("cc") .arg(&host_c) - .arg(outdir.join("libbacktest.a")) + .arg(outdir.join("libembed_backtest_step.a")) .arg(outdir.join("libailang_rt.a")) .arg("-o").arg(&host_bin) .output().expect("cc host link"); @@ -1427,8 +1483,9 @@ Task 5's forwarder loop and the link is undefined-symbol). Then PASS. - [ ] **Step 3: Run the E2E — verify GREEN** Run: `cargo test -p ail --test embed_e2e` -Expected: PASS — `libbacktest.a` + `libailang_rt.a` link with the C -host; `backtest_step(0,3)=9`, `backtest_step(9,4)=25`; assertion +Expected: PASS — `libembed_backtest_step.a` + `libailang_rt.a` link +with the C host; `backtest_step(0,3)=9`, `backtest_step(9,4)=25`; +assertion holds; exit 0. This is the coherent stop. - [ ] **Step 4: DESIGN.md §"Embedding ABI (M1)" — current-state honesty** @@ -1518,10 +1575,13 @@ plan-level gate. `lower_workspace_staticlib_with_alloc` / `Target::StaticLib` / `ExportNonScalarSignature` / `export-non-scalar-signature` / `ExportHasEffects` / `export-has-effects` / `build_staticlib` / - `libbacktest.a` / `libailang_rt.a` / `backtest_step` — used - identically across every task that references them. Module - `backtest` ⇒ entry `backtest` ⇒ `lib backtest.a` = `libbacktest.a` - (spec-exact). + `libembed_backtest_step.a` / `libailang_rt.a` / `backtest_step` + (the C symbol) / `@ail_embed_backtest_step_step` (internal) — + used identically across every task that references them. Module + == file stem `embed_backtest_step` ⇒ entry `embed_backtest_step` + ⇒ `lib embed_backtest_step.a` = `libembed_backtest_step.a` + (Design-decision 6; the C symbol `backtest_step` is decoupled + and invariant per spec Decision 2). 4. **Step granularity.** Every step is one action ≤5 min; the only long one (Task 2 Step 5, the ~95-site thread) is a single compile-driven mechanical loop with a hard terminating gate — it diff --git a/examples/embed_backtest_step.ail b/examples/embed_backtest_step.ail new file mode 100644 index 0000000..5e82834 --- /dev/null +++ b/examples/embed_backtest_step.ail @@ -0,0 +1,20 @@ +(module embed_backtest_step + + (fn step + (export "backtest_step") + (type + (fn-type + (params (con Int) (con Int)) + (ret (con Int)))) + (params state sample) + (body + (app + state (app * sample sample)))) + + (fn helper + (type + (fn-type + (params (con Int)) + (ret (con Int)))) + (params x) + (body + (app * x x)))) diff --git a/examples/embed_noentry_baseline.ail b/examples/embed_noentry_baseline.ail new file mode 100644 index 0000000..2e36c2d --- /dev/null +++ b/examples/embed_noentry_baseline.ail @@ -0,0 +1,9 @@ +(module embed_noentry_baseline + (fn helper + (type + (fn-type + (params (con Int)) + (ret (con Int)))) + (params x) + (body + (app * x x))))