b9ca8894a5
Adds form-A surface (borrow T) / (own T) wrappers in fn-type
params and ret slots. Internally these are not new Type variants
but per-position metadata on Type::Fn:
Type::Fn { params, param_modes, ret, ret_mode, effects }
enum ParamMode { Implicit, Own, Borrow }
This keeps Type itself unchanged, so unification, occurs, apply,
and ~190 other Type match-arms in the typechecker need no new
branch. Fields use serde skip-if-default predicates so canonical
JSON hashes for every pre-18a fixture stay bit-identical (sum,
list, hof, closure, list_map, etc.: zero diff under git).
Iter 18a is purely additive: typechecker treats modes as
transparent (mode-compat check deferred to 18c), no codegen
change (--alloc=gc / Boehm path unchanged), no linearity
enforcement. The annotation info flows into the JSON side-table
that 18c will consume.
Equality of mode slices is length-tolerant: a vec![] (the form
written by mechanically-updated construction sites that elide
modes) compares equal to vec![Implicit; n]. This kept the patch
from being a 100-site mechanical migration through the
typechecker. 18c will choose: keep the slack, or normalise to
full-length on construction.
New fixture examples/borrow_own_demo.{ailx,ail.json} exercises
both modes. list_length declares (borrow (con List)); sum_list
declares (own (con List)). Stdout: 3 then 6.
Documentation: DESIGN.md schema-additions block clarified that
modes are Type::Fn metadata (not Type variants); migration plan
flag rename --memory=rc → --alloc=rc to match the existing CLI
flag.
Verification:
- cargo build --workspace green
- cargo test --workspace green (154 tests, +1 vs baseline 153)
- git diff examples/{sum,list,hof,closure,list_map,...}.ail.json: empty
- borrow_own_demo runtime stdout: "3\n6\n"
- canonical JSON contains "param_modes":["borrow"] and ["own"]
176 lines
5.7 KiB
Rust
176 lines
5.7 KiB
Rust
//! Integration tests for `check_workspace` (Iter 5b).
|
|
//!
|
|
//! These tests drive the canonical `examples/ws_*.ail.json` files;
|
|
//! the loader and the checker together form the pipeline that `ail check`
|
|
//! runs in JSON mode against a workspace.
|
|
|
|
use ailang_check::{check_workspace, Severity};
|
|
use ailang_core::load_workspace;
|
|
use std::path::Path;
|
|
|
|
fn examples_dir() -> std::path::PathBuf {
|
|
let manifest = env!("CARGO_MANIFEST_DIR");
|
|
Path::new(manifest).parent().unwrap().parent().unwrap().join("examples")
|
|
}
|
|
|
|
#[test]
|
|
fn happy_path_resolves_qualified_import() {
|
|
// ws_main imports ws_lib and calls `ws_lib.add` — fully typed.
|
|
// Expected: no diagnostics.
|
|
let entry = examples_dir().join("ws_main.ail.json");
|
|
let ws = load_workspace(&entry).expect("load ws_main");
|
|
let diags = check_workspace(&ws);
|
|
assert!(
|
|
diags.is_empty(),
|
|
"expected no diagnostics; got: {}",
|
|
serde_json::to_string_pretty(&diags).unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_import_is_reported() {
|
|
// ws_broken references `ws_lib.bogus` — module is there, def isn't.
|
|
let entry = examples_dir().join("ws_broken.ail.json");
|
|
let ws = load_workspace(&entry).expect("load ws_broken");
|
|
let diags = check_workspace(&ws);
|
|
assert_eq!(diags.len(), 1, "got: {:?}", diags);
|
|
assert!(matches!(diags[0].severity, Severity::Error));
|
|
assert_eq!(diags[0].code, "unknown-import");
|
|
// Context must point structurally to module + def name.
|
|
assert_eq!(
|
|
diags[0].ctx.get("module").and_then(|v| v.as_str()),
|
|
Some("ws_lib")
|
|
);
|
|
assert_eq!(
|
|
diags[0].ctx.get("name").and_then(|v| v.as_str()),
|
|
Some("bogus")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_module_prefix_is_reported() {
|
|
// ws_unknown_module has no imports but references `nope.x`.
|
|
let entry = examples_dir().join("ws_unknown_module.ail.json");
|
|
let ws = load_workspace(&entry).expect("load ws_unknown_module");
|
|
let diags = check_workspace(&ws);
|
|
assert_eq!(diags.len(), 1, "got: {:?}", diags);
|
|
assert!(matches!(diags[0].severity, Severity::Error));
|
|
assert_eq!(diags[0].code, "unknown-module");
|
|
assert_eq!(
|
|
diags[0].ctx.get("module").and_then(|v| v.as_str()),
|
|
Some("nope")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_def_name_with_dot_is_reported() {
|
|
// Synthetic: a module with a def whose name contains a dot.
|
|
// We construct this as a Module directly and feed it into a
|
|
// trivial workspace, because the canonical convention should not
|
|
// let this through to disk in the first place.
|
|
use ailang_core::ast::*;
|
|
use std::collections::BTreeMap;
|
|
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
imports: vec![],
|
|
defs: vec![Def::Const(ConstDef {
|
|
name: "weird.name".into(),
|
|
ty: Type::int(),
|
|
value: Term::Lit {
|
|
lit: Literal::Int { value: 0 },
|
|
},
|
|
doc: None,
|
|
})],
|
|
};
|
|
let mut modules = BTreeMap::new();
|
|
modules.insert(m.name.clone(), m.clone());
|
|
let ws = ailang_core::Workspace {
|
|
entry: m.name.clone(),
|
|
modules,
|
|
root_dir: std::path::PathBuf::from("."),
|
|
};
|
|
let diags = check_workspace(&ws);
|
|
assert_eq!(diags.len(), 1, "got: {:?}", diags);
|
|
assert_eq!(diags[0].code, "invalid-def-name");
|
|
assert_eq!(
|
|
diags[0].ctx.get("reason").and_then(|v| v.as_str()),
|
|
Some("contains-dot")
|
|
);
|
|
}
|
|
|
|
/// Iter 6: body-check is multi-diagnose. A module with two independent
|
|
/// errors in different defs must produce two diagnostics — the first
|
|
/// error must not short-circuit the second.
|
|
#[test]
|
|
fn body_errors_accumulate_across_defs() {
|
|
use ailang_core::ast::*;
|
|
use std::collections::BTreeMap;
|
|
|
|
// Two fns, each with a different body error:
|
|
// bad_a: arity mismatch — calls `+` with three arguments.
|
|
// bad_b: references a name that does not exist anywhere.
|
|
let bad_a = Def::Fn(FnDef {
|
|
name: "bad_a".into(),
|
|
ty: Type::Fn {
|
|
params: vec![],
|
|
ret: Box::new(Type::int()),
|
|
effects: vec![],
|
|
param_modes: vec![],
|
|
ret_mode: ParamMode::Implicit,
|
|
},
|
|
params: vec![],
|
|
body: Term::App {
|
|
callee: Box::new(Term::Var { name: "+".into() }),
|
|
args: vec![
|
|
Term::Lit { lit: Literal::Int { value: 1 } },
|
|
Term::Lit { lit: Literal::Int { value: 2 } },
|
|
Term::Lit { lit: Literal::Int { value: 3 } },
|
|
],
|
|
tail: false,
|
|
},
|
|
doc: None,
|
|
});
|
|
let bad_b = Def::Fn(FnDef {
|
|
name: "bad_b".into(),
|
|
ty: Type::Fn {
|
|
params: vec![],
|
|
ret: Box::new(Type::int()),
|
|
effects: vec![],
|
|
param_modes: vec![],
|
|
ret_mode: ParamMode::Implicit,
|
|
},
|
|
params: vec![],
|
|
body: Term::Var {
|
|
name: "this_does_not_exist".into(),
|
|
},
|
|
doc: None,
|
|
});
|
|
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "two_errors".into(),
|
|
imports: vec![],
|
|
defs: vec![bad_a, bad_b],
|
|
};
|
|
let mut modules = BTreeMap::new();
|
|
modules.insert(m.name.clone(), m.clone());
|
|
let ws = ailang_core::Workspace {
|
|
entry: m.name.clone(),
|
|
modules,
|
|
root_dir: std::path::PathBuf::from("."),
|
|
};
|
|
let diags = check_workspace(&ws);
|
|
|
|
assert_eq!(diags.len(), 2, "want both errors, got: {:#?}", diags);
|
|
|
|
// Each diagnostic carries the offending def name as `def`.
|
|
let defs: Vec<&str> = diags
|
|
.iter()
|
|
.filter_map(|d| d.def.as_deref())
|
|
.collect();
|
|
assert!(defs.contains(&"bad_a"), "missing bad_a; got {defs:?}");
|
|
assert!(defs.contains(&"bad_b"), "missing bad_b; got {defs:?}");
|
|
}
|