plan: emit-ir-staticlib — M1 fieldtest spec_gap#2 scoped feature
This commit is contained in:
@@ -0,0 +1,471 @@
|
|||||||
|
# emit-ir --emit=staticlib — Implementation Plan
|
||||||
|
|
||||||
|
> **Parent spec:** `docs/specs/2026-05-18-fieldtest-embedding-abi-m1.md`
|
||||||
|
> (finding **[spec_gap]#2** "No public path emits the staticlib IR,
|
||||||
|
> though DESIGN.md Decision 5 makes IR readability a first-class LLM
|
||||||
|
> affordance"). Orchestrator resolution recorded in `docs/roadmap.md`
|
||||||
|
> P1 entry "[feature] ail emit-ir --emit=staticlib": **add** the flag
|
||||||
|
> (symmetric with `ail build`, reuse the M1-shipped `Target::StaticLib`
|
||||||
|
> path) — NOT narrow DESIGN.md. Scoped feature, no design fork — this
|
||||||
|
> is NOT a milestone (no `docs/specs/<milestone>.md`).
|
||||||
|
>
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: use `skills/implement`
|
||||||
|
> to run this plan. Steps use `- [ ]` checkboxes for tracking.
|
||||||
|
|
||||||
|
**Goal:** `ail emit-ir <module> --emit=staticlib` prints a
|
||||||
|
`main`-free kernel's LLVM IR (the external `@<sym>` forwarders)
|
||||||
|
instead of the executable-path `MissingEntryMain` rejection, so an
|
||||||
|
author can read the IR M1 introduced (Decision 5 affordance).
|
||||||
|
|
||||||
|
**Architecture:** Reuse the M1-shipped-and-audited `Target::StaticLib`
|
||||||
|
codegen path. Add a one-line symmetric codegen convenience
|
||||||
|
`lower_workspace_staticlib(ws)` (mirrors the existing
|
||||||
|
`lower_workspace(ws)`: `--emit=exe`↔`lower_workspace`,
|
||||||
|
`--emit=staticlib`↔`lower_workspace_staticlib`); add an `--emit`
|
||||||
|
clap field to `Cmd::EmitIr` symmetric with `Cmd::Build`'s; branch
|
||||||
|
the `emit-ir` match arm on it with a zero-export guard byte-identical
|
||||||
|
to `build_staticlib`'s. Recon resolved the integration risk negative:
|
||||||
|
both codegen entrypoints return `Result<String>` and the emit-ir
|
||||||
|
printer already consumes a `String` — no adapter. DESIGN.md gets a
|
||||||
|
present-tense current-state mirror (§"Embedding ABI (M1)" + the CLI
|
||||||
|
synopsis, which pre-existingly omitted M1's already-shipped
|
||||||
|
`build --emit=staticlib` too). No new regression pin: the E2E test
|
||||||
|
pins the behaviour; the architect's mandatory DESIGN.md read at
|
||||||
|
milestone close catches stale prose. Decision-5 prose
|
||||||
|
(`DESIGN.md:224-225`) is already present-tense correct — untouched.
|
||||||
|
|
||||||
|
**Tech Stack:** `crates/ailang-codegen/src/lib.rs` (one pub fn),
|
||||||
|
`crates/ail/src/main.rs` (clap field + arm branch + guard),
|
||||||
|
`crates/ail/tests/emit_ir_staticlib_cli.rs` (new E2E), `docs/DESIGN.md`
|
||||||
|
(current-state mirror).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files this plan creates or modifies
|
||||||
|
|
||||||
|
- Modify: `crates/ailang-codegen/src/lib.rs:275-279` — add the
|
||||||
|
symmetric `lower_workspace_staticlib` convenience between
|
||||||
|
`lower_workspace` (275-277) and `fn lower_workspace_inner` (279).
|
||||||
|
- Modify: `crates/ail/src/main.rs:114-119` — add the `emit` clap
|
||||||
|
field to the `Cmd::EmitIr` variant.
|
||||||
|
- Modify: `crates/ail/src/main.rs:640` + `:690` — destructure `emit`
|
||||||
|
in the `Cmd::EmitIr` arm; branch the lowering call on it with the
|
||||||
|
zero-export guard.
|
||||||
|
- Create: `crates/ail/tests/emit_ir_staticlib_cli.rs` — 3 E2E tests
|
||||||
|
(positive forwarder-IR, zero-export guard, default-exe still
|
||||||
|
requires main).
|
||||||
|
- Modify: `docs/DESIGN.md:2301-2304` — one-sentence §"Embedding ABI
|
||||||
|
(M1)" mirror after the canonical snippet.
|
||||||
|
- Modify: `docs/DESIGN.md:2609-2610` — CLI synopsis: both `emit-ir`
|
||||||
|
and `build` lines gain `[--emit=staticlib]` (build's was a
|
||||||
|
pre-existing M1 synopsis omission — current-state honest).
|
||||||
|
|
||||||
|
Files that do NOT change (verified by recon): the build-side
|
||||||
|
`build_staticlib` + its guard (`main.rs:2467-2473`) and
|
||||||
|
`crates/ail/tests/embed_staticlib_cli.rs` (the new guard reuses the
|
||||||
|
identical fixture `examples/embed_noentry_baseline.ail` and bail
|
||||||
|
string — no fixture minting); `Decision 5` prose
|
||||||
|
(`DESIGN.md:214-225`); any codegen lowering logic (the
|
||||||
|
`Target::StaticLib` forwarder emission `lib.rs:588-625` is M1-audited
|
||||||
|
and reused unchanged).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1: codegen — symmetric `lower_workspace_staticlib` convenience
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `crates/ailang-codegen/src/lib.rs:275-279`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the convenience fn**
|
||||||
|
|
||||||
|
Replace, verbatim (lines 275-279 — the `lower_workspace` fn, the
|
||||||
|
blank line, and the `fn lower_workspace_inner` opener):
|
||||||
|
|
||||||
|
old:
|
||||||
|
```rust
|
||||||
|
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||||
|
lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy, target: Target) -> Result<String> {
|
||||||
|
```
|
||||||
|
|
||||||
|
new:
|
||||||
|
```rust
|
||||||
|
pub fn lower_workspace(ws: &Workspace) -> Result<String> {
|
||||||
|
lower_workspace_inner(ws, AllocStrategy::Gc, Target::Executable)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Embedding-ABI M1 single-call entry point symmetric with
|
||||||
|
/// [`lower_workspace`]: lowers a [`Workspace`] for the static-library
|
||||||
|
/// target with the default `AllocStrategy::Gc`. This is what
|
||||||
|
/// `ail emit-ir --emit=staticlib` calls so an author can read a
|
||||||
|
/// `main`-free kernel's IR (the external `@<sym>` forwarders, no
|
||||||
|
/// `@main`) — the Decision-5 IR-readability affordance for the
|
||||||
|
/// artefact M1 introduced. Equivalent to
|
||||||
|
/// `lower_workspace_staticlib_with_alloc(ws, AllocStrategy::Gc)`.
|
||||||
|
pub fn lower_workspace_staticlib(ws: &Workspace) -> Result<String> {
|
||||||
|
lower_workspace_inner(ws, AllocStrategy::Gc, Target::StaticLib)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lower_workspace_inner(ws: &Workspace, alloc: AllocStrategy, target: Target) -> Result<String> {
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Compile gate**
|
||||||
|
|
||||||
|
Run: `cargo build -p ailang-codegen 2>&1 | tail -3`
|
||||||
|
Expected: builds, 0 errors (the new fn is `pub`, delegates to the
|
||||||
|
existing private `lower_workspace_inner` with the existing
|
||||||
|
`Target::StaticLib` — no signature change, no caller threading; a
|
||||||
|
new unused-within-crate `pub fn` is not dead-code-warned).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2: CLI — `--emit` on `emit-ir` + staticlib branch + zero-export guard
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `crates/ail/tests/emit_ir_staticlib_cli.rs`
|
||||||
|
- Modify: `crates/ail/src/main.rs:114-119`, `:640`, `:690`
|
||||||
|
|
||||||
|
> **Compile-unit note (planner Step-5 item 7):** Steps 3 and 4 are a
|
||||||
|
> single compile unit. After Step 3 adds the `emit` field to the
|
||||||
|
> variant, the Step-4 arm's `Cmd::EmitIr { path, out }` destructure
|
||||||
|
> is a non-exhaustive struct pattern (`error[E0027]`) until Step 4
|
||||||
|
> adds `emit` to it. Do NOT run `cargo build`/`cargo test` between
|
||||||
|
> Step 3 and Step 4 — the next build/test gate is Step 5, after both
|
||||||
|
> edits land.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the RED-first E2E test file**
|
||||||
|
|
||||||
|
Create `crates/ail/tests/emit_ir_staticlib_cli.rs` with exactly:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
//! Embedding-ABI M1 (M1 fieldtest spec_gap#2): `ail emit-ir
|
||||||
|
//! --emit=staticlib` prints a `main`-free kernel's LLVM IR (the
|
||||||
|
//! external `@<sym>` forwarders, no `@main`) instead of the
|
||||||
|
//! executable-path `MissingEntryMain` rejection — the Decision-5
|
||||||
|
//! IR-readability affordance for the artefact M1 introduced.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") }
|
||||||
|
fn ws_root() -> PathBuf {
|
||||||
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent().unwrap().parent().unwrap().to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_ir_staticlib_prints_kernel_forwarder_no_main() {
|
||||||
|
let fixture = ws_root().join("examples").join("embed_backtest_step.ail");
|
||||||
|
let out = Command::new(ail_bin())
|
||||||
|
.args(["emit-ir", fixture.to_str().unwrap(), "--emit=staticlib"])
|
||||||
|
.output().expect("ail emit-ir --emit=staticlib");
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let stderr = String::from_utf8_lossy(&out.stderr);
|
||||||
|
assert!(out.status.success(),
|
||||||
|
"emit-ir --emit=staticlib on a main-free kernel must succeed; stderr={stderr}");
|
||||||
|
assert!(!stderr.contains("has no `main` def"),
|
||||||
|
"must NOT hit the executable-path MissingEntryMain rejection; stderr={stderr}");
|
||||||
|
assert!(stdout.contains("@backtest_step("),
|
||||||
|
"kernel IR must contain the external `(export \"backtest_step\")` forwarder; stdout={stdout}");
|
||||||
|
assert!(stdout.contains("@ail_embed_backtest_step_step"),
|
||||||
|
"kernel IR must contain the internal mangled symbol the forwarder calls; stdout={stdout}");
|
||||||
|
assert!(!stdout.contains("@main("),
|
||||||
|
"staticlib kernel IR must NOT contain an @main trampoline; stdout={stdout}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_ir_staticlib_requires_an_export() {
|
||||||
|
// embed_noentry_baseline has no `(export …)` fn — symmetric with
|
||||||
|
// build's zero-export guard (embed_staticlib_cli.rs).
|
||||||
|
let fixture = ws_root().join("examples").join("embed_noentry_baseline.ail");
|
||||||
|
let out = Command::new(ail_bin())
|
||||||
|
.args(["emit-ir", fixture.to_str().unwrap(), "--emit=staticlib"])
|
||||||
|
.output().expect("ail emit-ir --emit=staticlib");
|
||||||
|
assert!(!out.status.success(),
|
||||||
|
"emit-ir --emit=staticlib with zero exports must fail");
|
||||||
|
assert!(String::from_utf8_lossy(&out.stderr)
|
||||||
|
.contains("at least one `(export"),
|
||||||
|
"expected the zero-export staticlib error; got {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn emit_ir_default_exe_still_requires_main() {
|
||||||
|
// Regression: --emit defaults to "exe"; emit-ir on a main-free
|
||||||
|
// module WITHOUT --emit=staticlib must still hit MissingEntryMain
|
||||||
|
// (the new staticlib branch must not alter the default path).
|
||||||
|
let fixture = ws_root().join("examples").join("embed_backtest_step.ail");
|
||||||
|
let out = Command::new(ail_bin())
|
||||||
|
.args(["emit-ir", fixture.to_str().unwrap()])
|
||||||
|
.output().expect("ail emit-ir");
|
||||||
|
assert!(!out.status.success(),
|
||||||
|
"default-exe emit-ir on a main-free kernel must still fail");
|
||||||
|
assert!(String::from_utf8_lossy(&out.stderr)
|
||||||
|
.contains("has no `main` def"),
|
||||||
|
"default path must still surface MissingEntryMain; got {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the new tests — RED**
|
||||||
|
|
||||||
|
Run: `cargo test -p ail --test emit_ir_staticlib_cli 2>&1 | tail -8`
|
||||||
|
Expected: **FAIL**. `emit_ir_staticlib_prints_kernel_forwarder_no_main`
|
||||||
|
and `emit_ir_staticlib_requires_an_export` FAIL because `ail emit-ir`
|
||||||
|
has no `--emit` flag yet — clap rejects `--emit=staticlib`
|
||||||
|
("unexpected argument '--emit=staticlib'", non-zero exit), so
|
||||||
|
`out.status.success()` is false / the guard message is absent.
|
||||||
|
`emit_ir_default_exe_still_requires_main` PASSES (that is the current
|
||||||
|
behaviour — the spec_gap itself: default-exe emit-ir on a main-free
|
||||||
|
kernel already fails with `has no \`main\` def`). Net: "2 failed; 1
|
||||||
|
passed". This is the RED.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the `emit` clap field to `Cmd::EmitIr`**
|
||||||
|
|
||||||
|
Replace, verbatim (`crates/ail/src/main.rs:114-119`):
|
||||||
|
|
||||||
|
old:
|
||||||
|
```rust
|
||||||
|
/// Writes LLVM IR (.ll) for the module.
|
||||||
|
EmitIr {
|
||||||
|
path: PathBuf,
|
||||||
|
#[arg(short, long)]
|
||||||
|
out: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
new:
|
||||||
|
```rust
|
||||||
|
/// Writes LLVM IR (.ll) for the module.
|
||||||
|
EmitIr {
|
||||||
|
path: PathBuf,
|
||||||
|
#[arg(short, long)]
|
||||||
|
out: Option<PathBuf>,
|
||||||
|
/// IR shape. `exe` (default): whole-program IR (requires an
|
||||||
|
/// entry `main`). `staticlib`: kernel IR for a `main`-free
|
||||||
|
/// module — one external C forwarder per `(export "<sym>")`
|
||||||
|
/// fn, no `@main` (Embedding ABI M1). Same `Target` the
|
||||||
|
/// `build --emit=staticlib` path uses; output is the IR text
|
||||||
|
/// (stdout, or `-o <file>`), not archives.
|
||||||
|
#[arg(long, default_value = "exe", value_parser = ["exe", "staticlib"])]
|
||||||
|
emit: String,
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Destructure `emit` + branch the lowering call**
|
||||||
|
|
||||||
|
Two verbatim replacements in the `Cmd::EmitIr` match arm. (Do not
|
||||||
|
run cargo between Step 3 and the end of Step 4 — see the
|
||||||
|
compile-unit note above.)
|
||||||
|
|
||||||
|
4a. The destructure line (`crates/ail/src/main.rs:640`):
|
||||||
|
|
||||||
|
old:
|
||||||
|
```rust
|
||||||
|
Cmd::EmitIr { path, out } => {
|
||||||
|
```
|
||||||
|
|
||||||
|
new:
|
||||||
|
```rust
|
||||||
|
Cmd::EmitIr { path, out, emit } => {
|
||||||
|
```
|
||||||
|
|
||||||
|
4b. The lowering call (`crates/ail/src/main.rs:690`):
|
||||||
|
|
||||||
|
old:
|
||||||
|
```rust
|
||||||
|
let ir = ailang_codegen::lower_workspace(&ws)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
new:
|
||||||
|
```rust
|
||||||
|
let ir = if emit == "staticlib" {
|
||||||
|
let has_export = ws.modules.values().any(|m| m.defs.iter().any(|d|
|
||||||
|
matches!(d, ailang_core::Def::Fn(f) if f.export.is_some())));
|
||||||
|
if !has_export {
|
||||||
|
anyhow::bail!(
|
||||||
|
"staticlib target needs at least one `(export \"<sym>\")` fn"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ailang_codegen::lower_workspace_staticlib(&ws)?
|
||||||
|
} else {
|
||||||
|
ailang_codegen::lower_workspace(&ws)?
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
(The `has_export` predicate + `anyhow::bail!` string are
|
||||||
|
byte-identical to `build_staticlib`'s guard at `main.rs:2467-2473`,
|
||||||
|
so the zero-export error message is symmetric across `build` and
|
||||||
|
`emit-ir` and the existing `embed_staticlib_cli.rs:54` substring
|
||||||
|
`at least one \`(export` matches both paths.)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Build + run all gates — GREEN**
|
||||||
|
|
||||||
|
Run: `cargo build -p ail 2>&1 | tail -3`
|
||||||
|
Expected: builds, 0 errors (variant field + arm destructure both
|
||||||
|
landed; `lower_workspace_staticlib` exists from Task 1).
|
||||||
|
|
||||||
|
Run: `cargo test -p ail --test emit_ir_staticlib_cli 2>&1 | tail -6`
|
||||||
|
Expected: **PASS**, "test result: ok. 3 passed; 0 failed".
|
||||||
|
|
||||||
|
Run: `cargo test -p ail --test embed_staticlib_cli 2>&1 | tail -4`
|
||||||
|
Expected: **PASS**, all build-side staticlib tests still green
|
||||||
|
(`staticlib_emit_produces_two_archives`,
|
||||||
|
`staticlib_emit_requires_an_export`, and any others in that file) —
|
||||||
|
the build path + its guard are untouched; this confirms symmetry,
|
||||||
|
not a regression risk.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3: DESIGN.md — present-tense current-state mirror
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/DESIGN.md:2301-2304`, `:2609-2610`
|
||||||
|
|
||||||
|
- [ ] **Step 1: §"Embedding ABI (M1)" — one sentence after the snippet**
|
||||||
|
|
||||||
|
Replace, verbatim (`docs/DESIGN.md:2301-2304` — the snippet's last
|
||||||
|
line, its closing fence, the blank line, and the `## Data model`
|
||||||
|
heading):
|
||||||
|
|
||||||
|
old:
|
||||||
|
```
|
||||||
|
(app + state (app * sample sample))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data model
|
||||||
|
```
|
||||||
|
|
||||||
|
new:
|
||||||
|
```
|
||||||
|
(app + state (app * sample sample))))
|
||||||
|
```
|
||||||
|
|
||||||
|
`ail emit-ir <module> --emit=staticlib` prints this kernel's LLVM
|
||||||
|
IR (the external `@<sym>` forwarders, no `@main`) instead of the
|
||||||
|
executable-path `main`-required rejection — the Decision-5
|
||||||
|
IR-readability affordance for a `main`-free kernel.
|
||||||
|
|
||||||
|
## Data model
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: CLI synopsis — both lines gain the M1 flag**
|
||||||
|
|
||||||
|
Replace, verbatim (`docs/DESIGN.md:2609-2610`):
|
||||||
|
|
||||||
|
old:
|
||||||
|
```
|
||||||
|
ail emit-ir <module> — writes .ll
|
||||||
|
ail build <module> — full pipeline → binary
|
||||||
|
```
|
||||||
|
|
||||||
|
new:
|
||||||
|
```
|
||||||
|
ail emit-ir <module> [--emit=staticlib] — writes .ll (staticlib: a main-free kernel's IR, no @main)
|
||||||
|
ail build <module> [--emit=staticlib] — full pipeline → binary (staticlib: lib<entry>.a + libailang_rt.a)
|
||||||
|
```
|
||||||
|
|
||||||
|
(The `build` line gains `[--emit=staticlib]` too: it shipped in M1
|
||||||
|
and the synopsis never reflected it — this corrects a pre-existing
|
||||||
|
current-state-honesty gap in the same edit, consistent with the
|
||||||
|
docs-as-current-state-mirror discipline.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Confirm no doc-pin regressed**
|
||||||
|
|
||||||
|
Run: `cargo test -p ailang-core --test docs_honesty_pin 2>&1 | tail -3`
|
||||||
|
Expected: **PASS**, "5 passed; 0 failed" — the `docs_honesty_pin`
|
||||||
|
suite (incl. the form-a carve-out pin) pins specific Wunschdenken/
|
||||||
|
present-tense anchors, none of which these two additive
|
||||||
|
present-tense edits touch. (This is a no-regression check, not a new
|
||||||
|
pin — per the architecture decision, this feature ships no doc pin.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4: Regression gate
|
||||||
|
|
||||||
|
**Files:** (none — verification only)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Full `ail` crate suite**
|
||||||
|
|
||||||
|
Run: `cargo test -p ail 2>&1 | grep -E "^test result:" | awk '{p+=$4; f+=$6} END {print "ail TOTAL: "p" passed, "f" failed"}'`
|
||||||
|
Expected: `ail TOTAL: <N> passed, 0 failed`. The delta vs. pre-iter
|
||||||
|
is **+3** (the new `emit_ir_staticlib_cli.rs` tests); every
|
||||||
|
pre-existing `ail` test (incl. `embed_staticlib_cli`,
|
||||||
|
`embed_e2e`, `embed_missing_main_baseline`, `e2e`, `ir_snapshot`)
|
||||||
|
still green.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Workspace-wide no-regression assertion**
|
||||||
|
|
||||||
|
Run: `cargo test --workspace 2>&1 | grep -E "test result:" | awk '{p+=$4} END {print "TOTAL_PASSED="p}'`
|
||||||
|
Expected: `TOTAL_PASSED=` a number **≥ the pre-iter `main`-HEAD
|
||||||
|
count + 3** (the only new tests are the 3 in
|
||||||
|
`emit_ir_staticlib_cli.rs`; the codegen convenience fn and the CLI
|
||||||
|
branch add behaviour exercised by those 3, no other suite loses a
|
||||||
|
test). A total below `pre-iter + 3` means a pre-existing test broke
|
||||||
|
— STOP and investigate. (Unfiltered suite + explicit count per
|
||||||
|
planner Step-5 item 8.)
|
||||||
|
|
||||||
|
- [ ] **Step 3: Scope check — only the expected files changed**
|
||||||
|
|
||||||
|
Run: `git diff --stat 2>&1`
|
||||||
|
Expected: exactly these modified/created paths (plus the
|
||||||
|
implement-skill journal + stats files): `crates/ailang-codegen/src/lib.rs`,
|
||||||
|
`crates/ail/src/main.rs`, `crates/ail/tests/emit_ir_staticlib_cli.rs`
|
||||||
|
(new), `docs/DESIGN.md`. No diff under `crates/ailang-check`,
|
||||||
|
`crates/ailang-surface`, `crates/ailang-core`, `runtime/`, or any
|
||||||
|
`examples/*.ail` (the new tests reuse existing public fixtures
|
||||||
|
`embed_backtest_step.ail` / `embed_noentry_baseline.ail` — no
|
||||||
|
fixture minting). Any path outside this set is out of scope — STOP.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-review (planner Step 5)
|
||||||
|
|
||||||
|
1. **Spec coverage:** [spec_gap]#2 resolved — the `--emit=staticlib`
|
||||||
|
flag (Task 2) routes through the M1 `Target::StaticLib` path
|
||||||
|
(Task 1's symmetric convenience), with a symmetric zero-export
|
||||||
|
guard (Task 2 Step 4b) and the DESIGN.md current-state mirror
|
||||||
|
(Task 3). Roadmap resolution ("add the flag, do not narrow
|
||||||
|
DESIGN.md") honoured: DESIGN.md is *widened* to state the
|
||||||
|
affordance, never narrowed. ✔
|
||||||
|
2. **Placeholder scan:** no "TBD/TODO/implement later/similar to/add
|
||||||
|
appropriate". Every code/text edit is given verbatim. ✔
|
||||||
|
3. **Type/name consistency:** `lower_workspace_staticlib` named
|
||||||
|
identically in Task 1 Step 1 and Task 2 Step 4b; clap field
|
||||||
|
`emit: String` matches `Cmd::Build`'s; test fn names
|
||||||
|
(`emit_ir_staticlib_prints_kernel_forwarder_no_main`,
|
||||||
|
`emit_ir_staticlib_requires_an_export`,
|
||||||
|
`emit_ir_default_exe_still_requires_main`) consistent across
|
||||||
|
Task 2 + Task 4. Fixtures `embed_backtest_step.ail` /
|
||||||
|
`embed_noentry_baseline.ail` are recon-verified existing paths. ✔
|
||||||
|
4. **Step granularity:** each step is one verbatim replacement, one
|
||||||
|
file create, or one command — 2-5 min. ✔
|
||||||
|
5. **No commit steps:** none. ✔
|
||||||
|
6. **Pin/replacement substring contiguity:** N/A — this feature
|
||||||
|
adds **no** presence-pin doc test (architecture decision: the
|
||||||
|
E2E pins behaviour, no `docs_honesty_pin` entry). The E2E's IR
|
||||||
|
substrings (`@backtest_step(`, `@ail_embed_backtest_step_step`,
|
||||||
|
`@main(`) are M1-audited codegen *output* (forwarder emission
|
||||||
|
`lib.rs:588-625`), not a verbatim-text-edit pair in this plan, so
|
||||||
|
the "two authoritative artefacts" failure mode does not arise.
|
||||||
|
Task 3 Step 3 is a no-regression check against the *existing*
|
||||||
|
pin, not a new pin. ✔
|
||||||
|
7. **Compile-gate vs. deferred-caller ordering:** Task 1 adds a new
|
||||||
|
`pub fn` (no signature change, no caller threading — its compile
|
||||||
|
gate is satisfiable standalone). Task 2's only crate-wide compile
|
||||||
|
coupling is the clap-variant field (Step 3) ↔ its single match
|
||||||
|
arm destructure (Step 4a): both are inside Task 2, the
|
||||||
|
compile/test gate is Step 5 *after both*, and an explicit
|
||||||
|
compile-unit note forbids running cargo between Step 3 and Step 4
|
||||||
|
(the `error[E0027]` window). Task 2's only caller of
|
||||||
|
`lower_workspace_staticlib` is the arm itself; Task 1 lands the
|
||||||
|
callee first. No deferred-caller contradiction. ✔
|
||||||
|
8. **Verification-command filter strings resolve:** `--test
|
||||||
|
emit_ir_staticlib_cli` is the file Task 2 Step 1 creates; `--test
|
||||||
|
embed_staticlib_cli` is recon-confirmed existing; `--test
|
||||||
|
docs_honesty_pin` is the existing pin file (5 tests). Task 4
|
||||||
|
Steps 1-2 use the **unfiltered** `-p ail` / `--workspace` suites
|
||||||
|
with explicit pass-count assertions so "0 ran" cannot read as "0
|
||||||
|
regressed". No filter guessed from a feature word. ✔
|
||||||
Reference in New Issue
Block a user