abcdd05991
Hygiene sweep across the workspace under `cargo clippy --workspace
--all-targets`. Before: 61 warnings. After: zero. Tests stay 563
green, `cargo doc` stays at zero warnings, and all three bench
scripts exit 0 against existing baselines.
Twelve lint classes, three fix shapes:
- Documentation hygiene (~32 hits): `doc_lazy_continuation` resolved
by adding blank lines before continuation paragraphs or rephrasing
the offending line; plus four `empty_line_after_doc_comments` hits
in workspace.rs where eight `///` blocks left orphan by form-a.1
T5 test relocation were converted to plain `//`.
- Idiomatic refactors (~16 hits): `.err().expect()` → `.expect_err()`,
redundant `.into_iter()` and `.into()` removed, `|f| g(f)` →
`g`, `.trim().split_whitespace()` → `.split_whitespace()`,
`push_str("x")` → `push('x')`, two manual `impl Default` flipped
to `#[derive(Default)]` + `#[default]`, two nested `if let Some(_)
= …` collapsed.
- Block extraction (1 hit): a 13-line block inside an `else if`
condition in synth's Var-arm extracted to a new helper
`is_class_method_dispatch(name, env)` next to
`qualifier_is_class_shape`.
Three `#[allow]`s with inline rationale where clippy's suggestion
would lose meaning: `only_used_in_recursion` on walk_pattern (the
parameter preserves the five-fn walker-framework signature
uniformity), 2× `if_same_then_else` on qualify_local_types shapes
(two branches encode semantically distinct disqualification
reasons), `too_many_arguments` on `Emitter::new` (8 workspace-flat
tables; bundling would just rename boilerplate).
119 lines
4.8 KiB
Rust
119 lines
4.8 KiB
Rust
//! Surface-side loader: dispatches by file extension.
|
|
//!
|
|
//! `.ail` → `surface::parse` (Form A); `.ail.json` → `core::load_module`
|
|
//! (Form B). Workspace import resolution prefers `.ail` over `.ail.json`.
|
|
|
|
use ailang_surface::{load_module, load_workspace};
|
|
use std::fs;
|
|
|
|
/// Property: `load_module` on a `.ail` (Form A) source file dispatches
|
|
/// to `ailang_surface::parse` and returns the parsed module. Guards
|
|
/// against the dispatch silently falling through to the JSON loader
|
|
/// (which would fail on Form-A bytes with the misleading
|
|
/// `json: expected value at line 1 column 1` error the rest of this
|
|
/// iter exists to eliminate).
|
|
#[test]
|
|
fn load_module_dispatches_ail_to_surface_parse() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let path = tmp.path().join("hello.ail");
|
|
// Smallest valid Form-A module: header + no defs.
|
|
fs::write(&path, "(module hello)\n").expect("write");
|
|
|
|
let module = load_module(&path).expect("surface parses .ail");
|
|
assert_eq!(module.name, "hello");
|
|
}
|
|
|
|
/// Property: `load_module` on a `.ail.json` (Form B) source file
|
|
/// delegates to `ailang_core::load_module` and produces the same
|
|
/// `Module` value the core entry point would. Guards against the
|
|
/// surface loader silently shadowing or rewriting the JSON path.
|
|
#[test]
|
|
fn load_module_dispatches_ail_json_to_core_loader() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let path = tmp.path().join("dummy.ail.json");
|
|
fs::write(
|
|
&path,
|
|
r#"{"schema":"ailang/v0","name":"dummy","imports":[],"defs":[]}"#,
|
|
)
|
|
.expect("write");
|
|
|
|
let surface_loaded = load_module(&path).expect("surface dispatches .ail.json to core");
|
|
let core_loaded = ailang_core::load_module(&path).expect("core loads directly");
|
|
|
|
assert_eq!(surface_loaded.name, core_loaded.name);
|
|
assert_eq!(surface_loaded.schema, core_loaded.schema);
|
|
}
|
|
|
|
/// Property: workspace import resolution accepts mixed extensions —
|
|
/// a `.ail` entry module can import a sibling provided as `.ail.json`,
|
|
/// and both modules end up in the loaded workspace. Guards against
|
|
/// the `.ail`-first / `.ail.json`-fallback precedence rule in
|
|
/// `workspace::visit` regressing back to JSON-only resolution.
|
|
#[test]
|
|
fn load_workspace_resolves_ail_imports_first_then_ail_json() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
let entry = tmp.path().join("main.ail");
|
|
// Surface grammar uses one `(import <name>)` clause per import.
|
|
fs::write(&entry, "(module main (import leaf))\n").expect("write entry");
|
|
|
|
let leaf = tmp.path().join("leaf.ail.json");
|
|
fs::write(
|
|
&leaf,
|
|
r#"{"schema":"ailang/v0","name":"leaf","imports":[],"defs":[]}"#,
|
|
)
|
|
.expect("write leaf");
|
|
|
|
let ws = load_workspace(&entry).expect("workspace loads with mixed extensions");
|
|
assert!(ws.modules.contains_key("main"));
|
|
assert!(ws.modules.contains_key("leaf"));
|
|
}
|
|
|
|
/// Property: when BOTH `<module>.ail` (Form A) and `<module>.ail.json`
|
|
/// (Form B) exist in the workspace root, the `.ail` sibling wins —
|
|
/// the loaded module's content reflects Form A, not Form B. Pins the
|
|
/// `surface_path.is_file()` precedence branch in
|
|
/// `workspace::visit`'s import-path construction. Regression would
|
|
/// look like the loader silently shadowing a freshly-edited `.ail`
|
|
/// with a stale `.ail.json` snapshot of the same module name.
|
|
#[test]
|
|
fn load_workspace_prefers_ail_when_both_extensions_exist() {
|
|
let tmp = tempfile::tempdir().expect("tempdir");
|
|
|
|
// Entry imports `leaf`.
|
|
let entry = tmp.path().join("main.ail");
|
|
fs::write(&entry, "(module main (import leaf))\n").expect("write entry");
|
|
|
|
// Both leaf.ail and leaf.ail.json exist, with different in-source content.
|
|
// The `.ail` carries a `(const winner ...)`; the `.ail.json` carries a
|
|
// `(const loser ...)`. The loader must pick `winner`.
|
|
let leaf_ail = tmp.path().join("leaf.ail");
|
|
fs::write(
|
|
&leaf_ail,
|
|
"(module leaf (const winner (type (con Int)) (body 1)))\n",
|
|
)
|
|
.expect("write leaf.ail");
|
|
|
|
let leaf_json = tmp.path().join("leaf.ail.json");
|
|
fs::write(
|
|
&leaf_json,
|
|
r#"{"schema":"ailang/v0","name":"leaf","imports":[],"defs":[{"kind":"const","name":"loser","type":{"k":"con","name":"Int"},"value":{"t":"lit","lit":{"kind":"int","value":2}}}]}"#,
|
|
)
|
|
.expect("write leaf.ail.json");
|
|
|
|
let ws = load_workspace(&entry).expect("workspace loads with both extensions present");
|
|
let leaf = &ws.modules["leaf"];
|
|
let names: Vec<&str> = leaf
|
|
.defs
|
|
.iter()
|
|
.map(ailang_core::def_name)
|
|
.collect();
|
|
assert!(
|
|
names.contains(&"winner"),
|
|
"expected `.ail` content (def `winner`); got defs: {names:?}"
|
|
);
|
|
assert!(
|
|
!names.contains(&"loser"),
|
|
"must NOT have loaded `.ail.json` content (def `loser`); got defs: {names:?}"
|
|
);
|
|
}
|