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:
@@ -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",
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,6 +445,7 @@ mod tests {
|
||||
body,
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
export: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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-
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -45,6 +45,7 @@ fn sample_fn() -> Def {
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
export: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
})],
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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![],
|
||||
})],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user