iter embedding-abi-m1.1 (PARTIAL 3/7): schema + surface + baseline prerequisites; plan Repair

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<String> (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 "<sym>") 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.
This commit is contained in:
2026-05-18 14:32:39 +02:00
parent 123ae1be5c
commit 818177d835
27 changed files with 709 additions and 38 deletions
@@ -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"
}
}
@@ -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",
);
}
+4
View File
@@ -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();
+1
View File
@@ -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
+4
View File
@@ -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 {
+3
View File
@@ -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,
})
}
+1
View File
@@ -597,6 +597,7 @@ mod tests {
body,
suppress: vec![],
doc: None,
export: None,
})
}
@@ -126,6 +126,7 @@ mod tests {
body: Term::Lit { lit: Literal::Int { value: 0 } },
doc: None,
suppress,
export: None,
})],
}
}
+1
View File
@@ -445,6 +445,7 @@ mod tests {
body,
suppress: vec![],
doc: None,
export: None,
})
}
+8
View File
@@ -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 {
+32
View File
@@ -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();
+9
View File
@@ -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_<module>_<def>`
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-
+13
View File
@@ -205,6 +205,19 @@ pub struct FnDef {
/// Optional source-level documentation string.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
/// 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
/// `@<sym>` (scalar signature, provisional until M3) forwarding
/// to `@ail_<module>_<fn>`. The symbol is author-chosen and
/// decoupled from the `ail_<module>_<def>` 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<String>,
/// 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`
+16
View File
@@ -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);
+1
View File
@@ -191,6 +191,7 @@ mod tests {
},
suppress: vec![],
doc: None,
export: None,
}),
],
}
@@ -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(),
@@ -0,0 +1,35 @@
//! Additive-field hash stability (Embedding-ABI M1): adding
//! `FnDef.export: Option<String>` 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");
}
+1
View File
@@ -45,6 +45,7 @@ fn sample_fn() -> Def {
},
suppress: vec![],
doc: None,
export: None,
})
}
+1
View File
@@ -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(),
+7
View File
@@ -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,
})],
};
+34 -1
View File
@@ -460,6 +460,31 @@ impl<'a> Parser<'a> {
Ok(s)
}
fn parse_export(&mut self) -> Result<String, ParseError> {
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<Ctor, ParseError> {
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<String> = None;
let mut export: Option<String> = None;
let mut ty: Option<Type> = None;
let mut params: Option<Vec<String>> = None;
let mut body: Option<Term> = 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,
})
}
+8
View File
@@ -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![],
})],
};
+1
View File
@@ -2295,6 +2295,7 @@ are real surface forms.
"params": ["<id>"...], // names bound in body, in type.params order
"body": Term,
"doc": "<optional string>",
"export": "<optional C symbol>", // omitted when absent (hash-stable when omitted); see §"Embedding ABI (M1)"
"suppress": [Suppress...] // omitted when empty
}
@@ -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 13 committed as a known-good subset
(see "Boss resolution" below); plan defect fixed; Tasks 47
re-dispatched on the corrected plan.
**Tasks completed:** 3 of 7 (Tasks 13 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
57 follow in the re-dispatch)
## Summary
Tasks 13 of the Embedding-ABI M1 plan landed cleanly: the CLI
build-path `MissingEntryMain` baseline pin (Task 1), the additive
`FnDef.export: Option<String>` schema field threaded across the
whole workspace via compile-driven enumeration with a hash-stability
golden pin (Task 2), and the Form-A `(export "<sym>")` 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 47 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 13 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 "<sym>")` 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 47, 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:580581`
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<entry>.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 47 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 13 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
5259 / 167169) is that the exported C symbol is *author-chosen
and decoupled from `ail_<module>` 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 13 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
+97 -37
View File
@@ -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 5259/167169) is
that the exported C symbol is *author-chosen and decoupled from
`ail_<module>` 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 `<<GOLDEN-PLACEHOLDER>>` 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
+20
View File
@@ -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))))
+9
View File
@@ -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))))