diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index abd450f..c16dcf8 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -1200,6 +1200,46 @@ fn unreachable_demo() { assert_eq!(lines, vec!["4", "5"]); } +/// Iter 18a: `(borrow T)` and `(own T)` mode annotations on +/// fn-type parameters. Properties protected: +/// (1) the parser accepts `(borrow ...)` and `(own ...)` only as +/// wrappers in fn-type param/ret slots and round-trips them +/// into [`ailang_core::ParamMode`] on `Type::Fn`; +/// (2) the canonical-JSON serialisation emits `param_modes` only +/// when at least one mode is non-Implicit (verified by reading +/// the file and confirming the substring is present), so +/// pre-18a fixtures continue to hash bit-identically; +/// (3) the typechecker treats modes as transparent — it accepts a +/// fn whose parameter is `(borrow (con List))` even when the +/// same value is later passed to a fn whose parameter is +/// `(own (con List))`, because Iter 18a does not enforce +/// mode compatibility (deferred to 18c); +/// (4) codegen ignores modes entirely: list_length and sum_list +/// compile to the same LLVM IR they would have without the +/// wrappers, and the binary prints `3` then `6`. +#[test] +fn borrow_own_demo_modes_are_metadata_only() { + // Sanity-check (2): the on-disk JSON contains `param_modes` + // tokens for both fns. If the schema regressed and the field + // were dropped, this would fail before the binary even runs. + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); + let json_path = workspace.join("examples").join("borrow_own_demo.ail.json"); + let json = std::fs::read_to_string(&json_path).expect("read borrow_own_demo.ail.json"); + assert!( + json.contains("\"param_modes\":[\"borrow\"]"), + "expected `param_modes:[\"borrow\"]` in canonical JSON; the schema for `(borrow T)` regressed" + ); + assert!( + json.contains("\"param_modes\":[\"own\"]"), + "expected `param_modes:[\"own\"]` in canonical JSON; the schema for `(own T)` regressed" + ); + + let stdout = build_and_run("borrow_own_demo.ail.json"); + let lines: Vec<&str> = stdout.lines().collect(); + assert_eq!(lines, vec!["3", "6"]); +} + /// Iter 16e: polymorphic `==`. Properties protected: /// (1) `==` typechecks at Int / Bool / Str / Unit (the fixture /// uses every supported case directly via `(app == ...)`); diff --git a/crates/ailang-check/src/builtins.rs b/crates/ailang-check/src/builtins.rs index 531128e..74476c5 100644 --- a/crates/ailang-check/src/builtins.rs +++ b/crates/ailang-check/src/builtins.rs @@ -52,11 +52,15 @@ pub fn install(env: &mut crate::Env) { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ailang_core::ast::ParamMode::Implicit, }; let int_int_bool = Type::Fn { params: vec![Type::int(), Type::int()], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ailang_core::ast::ParamMode::Implicit, }; for op in ["+", "-", "*", "/", "%"] { env.globals.insert(op.into(), int_int_int.clone()); @@ -82,6 +86,8 @@ pub fn install(env: &mut crate::Env) { ], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ailang_core::ast::ParamMode::Implicit, }), }, ); @@ -91,6 +97,8 @@ pub fn install(env: &mut crate::Env) { params: vec![Type::bool_()], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ailang_core::ast::ParamMode::Implicit, }, ); diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 317e9e9..2026b5e 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -84,10 +84,12 @@ impl Subst { name: name.clone(), args: args.iter().map(|a| self.apply(a)).collect(), }, - Type::Fn { params, ret, effects } => Type::Fn { + Type::Fn { params, ret, effects, .. } => Type::Fn { params: params.iter().map(|p| self.apply(p)).collect(), ret: Box::new(self.apply(ret)), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, Type::Forall { vars, body } => Type::Forall { vars: vars.clone(), @@ -122,10 +124,12 @@ fn substitute_rigids(t: &Type, mapping: &BTreeMap) -> Type { name: name.clone(), args: args.iter().map(|a| substitute_rigids(a, mapping)).collect(), }, - Type::Fn { params, ret, effects } => Type::Fn { + Type::Fn { params, ret, effects, .. } => Type::Fn { params: params.iter().map(|p| substitute_rigids(p, mapping)).collect(), ret: Box::new(substitute_rigids(ret, mapping)), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, Type::Forall { vars, body } => { // Inner forall shadows: only substitute vars not re-bound here. @@ -205,8 +209,8 @@ fn unify(a: &Type, b: &Type, subst: &mut Subst) -> Result<()> { } // Function types: zip params, unify ret, effects must match as a set. ( - Type::Fn { params: ap, ret: ar, effects: ae }, - Type::Fn { params: bp, ret: br, effects: be }, + Type::Fn { params: ap, ret: ar, effects: ae, .. }, + Type::Fn { params: bp, ret: br, effects: be, .. }, ) => { if ap.len() != bp.len() { return Err(CheckError::TypeMismatch { @@ -978,7 +982,7 @@ fn check_fn(f: &FnDef, env: &Env) -> Result<()> { }; let (param_tys, ret_ty, declared_effs) = match &inner_ty { - Type::Fn { params, ret, effects } => { + Type::Fn { params, ret, effects, .. } => { (params.clone(), (**ret).clone(), effects.clone()) } other => { @@ -1212,14 +1216,14 @@ pub(crate) fn synth( let cty = synth(callee, env, locals, effects, in_def, subst, counter)?; let cty = subst.apply(&cty); let (params, ret, fx) = match &cty { - Type::Fn { params, ret, effects: fx } => { + Type::Fn { params, ret, effects: fx, .. } => { (params.clone(), (**ret).clone(), fx.clone()) } Type::Forall { vars, body } => { // Defensive — Var should already have instantiated. let (_, body) = instantiate(vars, body, counter); match &body { - Type::Fn { params, ret, effects: fx } => { + Type::Fn { params, ret, effects: fx, .. } => { (params.clone(), (**ret).clone(), fx.clone()) } other => { @@ -1519,6 +1523,8 @@ pub(crate) fn synth( params: param_tys.clone(), ret: Box::new((**ret_ty).clone()), effects: lam_effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }) } Term::LetRec { name, ty, params, body, in_term } => { @@ -1543,7 +1549,7 @@ pub(crate) fn synth( other => other.clone(), }; let (param_tys, ret_ty, declared_effs) = match &inner_ty { - Type::Fn { params: ps, ret, effects } => { + Type::Fn { params: ps, ret, effects, .. } => { (ps.clone(), (**ret).clone(), effects.clone()) } other => { @@ -1664,13 +1670,15 @@ fn qualify_local_types(t: &Type, owner_module: &str, local_types: &IndexMap Type::Fn { + Type::Fn { params, ret, effects, .. } => Type::Fn { params: params .iter() .map(|p| qualify_local_types(p, owner_module, local_types)) .collect(), ret: Box::new(qualify_local_types(ret, owner_module, local_types)), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, Type::Forall { vars, body } => Type::Forall { vars: vars.clone(), @@ -1953,6 +1961,8 @@ mod tests { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["a", "b"], Term::App { @@ -1980,6 +1990,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Lit { @@ -2003,7 +2015,9 @@ mod tests { Type::Fn { params: vec![], ret: Box::new(Type::unit()), - effects: vec![], // !IO missing + effects: vec![], // !IO missing, + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Do { @@ -2032,6 +2046,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Let { @@ -2072,6 +2088,8 @@ mod tests { params: vec![Type::Con { name: "Maybe".into(), args: vec![] }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["m"], Term::Match { @@ -2120,6 +2138,8 @@ mod tests { params: vec![Type::Con { name: "Maybe".into(), args: vec![] }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["m"], Term::Match { @@ -2160,6 +2180,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::If { @@ -2194,6 +2216,8 @@ mod tests { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, vec!["x"], @@ -2217,6 +2241,8 @@ mod tests { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, vec!["x"], @@ -2229,6 +2255,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::App { @@ -2244,6 +2272,8 @@ mod tests { params: vec![], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::App { @@ -2274,6 +2304,8 @@ mod tests { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, vec!["x"], @@ -2286,6 +2318,8 @@ mod tests { params: vec![], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::App { @@ -2320,11 +2354,15 @@ mod tests { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "b".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, Type::Var { name: "a".into() }, ], ret: Box::new(Type::Var { name: "b".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, vec!["f", "x"], @@ -2341,6 +2379,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["n"], Term::App { @@ -2358,6 +2398,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::App { @@ -2401,6 +2443,8 @@ mod tests { args: vec![Type::int()], }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Ctor { @@ -2442,6 +2486,8 @@ mod tests { }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, params: vec!["b".into()], @@ -2465,6 +2511,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::App { @@ -2483,6 +2531,8 @@ mod tests { params: vec![], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::App { @@ -2526,6 +2576,8 @@ mod tests { }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["b"], Term::Lit { lit: Literal::Int { value: 0 } }, @@ -2556,6 +2608,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Seq { @@ -2605,6 +2659,8 @@ mod tests { params: vec![Type::Con { name: "L".into(), args: vec![] }], ret: Box::new(Type::Con { name: "L".into(), args: vec![] }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["xs"], // Body: C(0, tail-app loop xs) — the recursion is @@ -2687,6 +2743,8 @@ mod tests { }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["b"], Term::Lit { lit: Literal::Int { value: 0 } }, @@ -2714,6 +2772,8 @@ mod tests { args: vec![Type::int()], }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Ctor { @@ -2746,6 +2806,8 @@ mod tests { }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["b"], Term::Match { @@ -2809,6 +2871,8 @@ mod tests { }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["t"], Term::Match { @@ -2902,6 +2966,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Match { @@ -2971,6 +3037,8 @@ mod tests { params: vec![Type::Con { name: "L".into(), args: vec![] }], ret: Box::new(Type::unit()), effects: vec!["IO".into()], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["xs"], Term::Match { @@ -3041,6 +3109,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["i".into()], body: Box::new(Term::If { @@ -3083,6 +3153,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["n"], body, @@ -3106,6 +3178,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::Lit { lit: Literal::Bool { value: true } }), @@ -3128,6 +3202,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::Seq { @@ -3161,6 +3237,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], body, @@ -3194,6 +3272,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], Term::Let { @@ -3205,6 +3285,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::App { @@ -3247,6 +3329,8 @@ mod tests { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, "lifted ty should have capture appended; got {:?}", synth.ty @@ -3314,6 +3398,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["z".into()], body: Box::new(Term::App { @@ -3358,6 +3444,8 @@ mod tests { }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec!["p"], outer_body, @@ -3388,6 +3476,8 @@ mod tests { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, "lifted ty should have the match-arm-capture type Int appended; got {:?}", synth.ty @@ -3439,6 +3529,8 @@ mod tests { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, vec!["x".into()], @@ -3451,6 +3543,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["k".into()], body: Box::new(Term::If { @@ -3571,6 +3665,8 @@ mod tests { params: vec![], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, vec![], body, diff --git a/crates/ailang-check/src/lift.rs b/crates/ailang-check/src/lift.rs index 0557208..85980d4 100644 --- a/crates/ailang-check/src/lift.rs +++ b/crates/ailang-check/src/lift.rs @@ -513,7 +513,7 @@ impl<'a> Lifter<'a> { // capture types uniformly because they're all `Type::Fn` // params at the AST level. let inner_augmented_ty = match ty { - Type::Fn { params: ps, ret, effects } => { + Type::Fn { params: ps, ret, effects, .. } => { let mut new_ps = ps.clone(); for (_, t) in &capture_types { new_ps.push(t.clone()); @@ -522,6 +522,8 @@ impl<'a> Lifter<'a> { params: new_ps, ret: ret.clone(), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, } } Type::Forall { .. } => panic!( @@ -569,7 +571,7 @@ impl<'a> Lifter<'a> { // LetRec's declared type to mirror them on the eta-Lam. let (orig_param_tys, orig_ret_ty, lr_effects): (Vec, Type, Vec) = match ty { - Type::Fn { params: ps, ret, effects } => { + Type::Fn { params: ps, ret, effects, .. } => { (ps.clone(), (**ret).clone(), effects.clone()) } Type::Forall { .. } => unreachable!("rejected above"), @@ -832,13 +834,15 @@ fn substitute_rigids_local(t: &Type, mapping: &BTreeMap) -> Type { name: name.clone(), args: args.iter().map(|a| substitute_rigids_local(a, mapping)).collect(), }, - Type::Fn { params, ret, effects } => Type::Fn { + Type::Fn { params, ret, effects, .. } => Type::Fn { params: params .iter() .map(|p| substitute_rigids_local(p, mapping)) .collect(), ret: Box::new(substitute_rigids_local(ret, mapping)), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, Type::Forall { vars, body } => { let inner: BTreeMap = mapping diff --git a/crates/ailang-check/tests/workspace.rs b/crates/ailang-check/tests/workspace.rs index e22b8b5..c54d50f 100644 --- a/crates/ailang-check/tests/workspace.rs +++ b/crates/ailang-check/tests/workspace.rs @@ -117,6 +117,8 @@ fn body_errors_accumulate_across_defs() { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::App { @@ -136,6 +138,8 @@ fn body_errors_accumulate_across_defs() { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Var { diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index d3b0c41..60089db 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -2676,6 +2676,8 @@ impl<'a> Emitter<'a> { params: param_tys.clone(), ret: ret_ty.clone(), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), Term::App { callee, args, .. } => { let cty = self.synth_with_extras(callee, extras)?; @@ -2859,11 +2861,15 @@ fn builtin_ail_type(name: &str) -> Option { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }; let int_int_bool = || Type::Fn { params: vec![Type::int(), Type::int()], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }; Some(match name { "+" | "-" | "*" | "/" | "%" => int_int_int(), @@ -2882,12 +2888,16 @@ fn builtin_ail_type(name: &str) -> Option { ], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, "not" => Type::Fn { params: vec![Type::bool_()], ret: Box::new(Type::bool_()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, // Iter 16d: `__unreachable__` is the polymorphic bottom value // (`forall a. a`). Mirrors the typechecker's `builtins::install`. @@ -3073,13 +3083,15 @@ fn qualify_local_types_codegen( .collect(), } } - Type::Fn { params, ret, effects } => Type::Fn { + Type::Fn { params, ret, effects, .. } => Type::Fn { params: params .iter() .map(|p| qualify_local_types_codegen(p, owner_module, owner_local_types)) .collect(), ret: Box::new(qualify_local_types_codegen(ret, owner_module, owner_local_types)), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, Type::Forall { vars, body } => Type::Forall { vars: vars.clone(), @@ -3099,10 +3111,12 @@ fn apply_subst_to_type(t: &Type, subst: &BTreeMap) -> Type { name: name.clone(), args: args.iter().map(|a| apply_subst_to_type(a, subst)).collect(), }, - Type::Fn { params, ret, effects } => Type::Fn { + Type::Fn { params, ret, effects, .. } => Type::Fn { params: params.iter().map(|p| apply_subst_to_type(p, subst)).collect(), ret: Box::new(apply_subst_to_type(ret, subst)), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, Type::Forall { vars, body } => { // Inner forall shadows: don't substitute re-bound names. @@ -3312,6 +3326,8 @@ mod tests { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["a".into(), "b".into()], body: Term::App { @@ -3332,6 +3348,8 @@ mod tests { params: vec![], ret: Box::new(Type::unit()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Lit { lit: Literal::Unit }, @@ -3384,6 +3402,8 @@ mod tests { params: vec![], ret: Box::new(Type::unit()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Let { @@ -3425,6 +3445,8 @@ mod tests { params: vec![], ret: Box::new(Type::unit()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Let { @@ -3468,6 +3490,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Lit { diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index 8f3d054..ad408bd 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -368,9 +368,22 @@ pub enum Type { /// Function type with optional effect annotation. `effects` is a /// set; equality compares it modulo order (see the [`PartialEq`] /// impl below). + /// + /// Iter 18a (Decision 10): `param_modes` and `ret_mode` carry the + /// `(borrow T)` / `(own T)` wrappers from the surface form. They + /// are metadata on `Type::Fn`, not new `Type` variants — so + /// unification, occurs, apply, and every other `Type` match-arm + /// keeps working unchanged. `param_modes` is omitted from + /// canonical JSON when every entry is `Implicit`; `ret_mode` is + /// omitted when it is `Implicit`. Pre-18a fixtures therefore hash + /// bit-identically. Fn { params: Vec, + #[serde(default, skip_serializing_if = "all_implicit")] + param_modes: Vec, ret: Box, + #[serde(default, skip_serializing_if = "ParamMode::is_implicit")] + ret_mode: ParamMode, #[serde(default)] effects: Vec, }, @@ -406,6 +419,100 @@ impl Type { pub fn str_() -> Type { Type::Con { name: "Str".into(), args: vec![] } } + + /// Iter 18a: build a `Type::Fn` with all parameter modes set to + /// `ParamMode::Implicit` and `ret_mode` set to `Implicit`. This is + /// the form every typechecker / desugar / codegen site that + /// synthesises a fn-type should use, so that newly inferred + /// fn-types retain pre-18a canonical-JSON bytes. + pub fn fn_implicit(params: Vec, ret: Type, effects: Vec) -> Type { + let n = params.len(); + Type::Fn { + params, + param_modes: vec![ParamMode::Implicit; n], + ret: Box::new(ret), + ret_mode: ParamMode::Implicit, + effects, + } + } +} + +/// Iter 18a (Decision 10): per-parameter / return mode marker on a +/// [`Type::Fn`]. +/// +/// `Implicit` is the legacy state for fn-types that were constructed +/// before the borrow/own surface annotations existed. Semantically, +/// `Implicit ≡ Own` throughout the 18-series; the distinction exists +/// only so pre-18a JSON fixtures continue to serialize without a +/// `"mode"` wrapper and therefore keep their canonical-JSON hash. +/// +/// `Own` and `Borrow` are author-asserted: the surface form +/// `(own T)` / `(borrow T)` round-trips through this enum. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ParamMode { + /// Pre-18a / unannotated. Treated as `Own` by the typechecker. + Implicit, + /// `(own T)` — caller transfers ownership; callee consumes. + Own, + /// `(borrow T)` — caller retains ownership; callee may not consume. + Borrow, +} + +impl Default for ParamMode { + fn default() -> Self { + ParamMode::Implicit + } +} + +impl ParamMode { + /// Used by the `skip_serializing_if` predicate on + /// [`Type::Fn::ret_mode`]. + pub fn is_implicit(&self) -> bool { + matches!(self, ParamMode::Implicit) + } +} + +/// Iter 18a: serde helper for [`Type::Fn::param_modes`]. Returns +/// `true` when every entry is [`ParamMode::Implicit`] (or when the +/// list is empty), so canonical JSON omits the field for any fn-type +/// without explicit `(borrow)` / `(own)` annotations and pre-18a +/// fixtures keep bit-identical hashes. +fn all_implicit(modes: &[ParamMode]) -> bool { + modes.iter().all(|m| m.is_implicit()) +} + +/// Iter 18a: equality of [`ParamMode`] for the purposes of `Type` +/// equality. `Implicit` and `Own` are treated as the same mode +/// throughout the 18-series; `Borrow` is distinct. This keeps +/// pre-18a fixtures (whose fn-types serialize `Implicit`) compatible +/// with newly-written 18a fixtures that mark the same fn-type +/// explicitly with `(own T)`. +fn mode_eq(a: &ParamMode, b: &ParamMode) -> bool { + match (a, b) { + (ParamMode::Borrow, ParamMode::Borrow) => true, + (ParamMode::Borrow, _) | (_, ParamMode::Borrow) => false, + // Implicit and Own are interchangeable. + _ => true, + } +} + +/// Iter 18a: equality of two `param_modes` slices, robust to the +/// "elided when all-implicit" representation used by typechecker / +/// desugar / codegen sites that construct fn-types with +/// `param_modes: vec![]`. Both slices are normalised to "implicit +/// padding to match the longer one"; equality then proceeds +/// element-wise via [`mode_eq`]. +fn mode_slices_eq(a: &[ParamMode], b: &[ParamMode]) -> bool { + let n = a.len().max(b.len()); + for i in 0..n { + let x = a.get(i).copied().unwrap_or(ParamMode::Implicit); + let y = b.get(i).copied().unwrap_or(ParamMode::Implicit); + if !mode_eq(&x, &y) { + return false; + } + } + true } impl PartialEq for Type { @@ -416,16 +523,32 @@ impl PartialEq for Type { Type::Con { name: b, args: ba }, ) => a == b && aa == ba, ( - Type::Fn { params: ap, ret: ar, effects: ae }, - Type::Fn { params: bp, ret: br, effects: be }, + Type::Fn { + params: ap, + param_modes: apm, + ret: ar, + ret_mode: arm, + effects: ae, + }, + Type::Fn { + params: bp, + param_modes: bpm, + ret: br, + ret_mode: brm, + effects: be, + }, ) => { - ap == bp && ar == br && { - let mut a = ae.clone(); - let mut b = be.clone(); - a.sort(); - b.sort(); - a == b - } + ap == bp + && ar == br + && mode_slices_eq(apm, bpm) + && mode_eq(arm, brm) + && { + let mut a = ae.clone(); + let mut b = be.clone(); + a.sort(); + b.sort(); + a == b + } } (Type::Var { name: a }, Type::Var { name: b }) => a == b, ( diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index 945275e..bdc97ff 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -708,7 +708,7 @@ impl Desugarer { // since LetRec itself doesn't quantify) is the inner // type. let inner_augmented_ty = match ty { - Type::Fn { params: ps, ret, effects } => { + Type::Fn { params: ps, ret, effects, .. } => { let mut new_ps = ps.clone(); for (_, t) in &capture_types { new_ps.push(t.clone()); @@ -717,6 +717,8 @@ impl Desugarer { params: new_ps, ret: ret.clone(), effects: effects.clone(), + param_modes: vec![], + ret_mode: ParamMode::Implicit, } } Type::Forall { .. } => panic!( @@ -1519,6 +1521,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["xs".into()], body: body_match, @@ -1572,6 +1576,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["xs".into()], body: original.clone(), @@ -1601,6 +1607,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], body: Box::new(Term::Var { name: "n".into() }), @@ -1624,6 +1632,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: fact_letrec_term(), @@ -1683,6 +1693,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], // (let-rec helper (params x) (type Int -> Int) @@ -1694,6 +1706,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::App { @@ -1727,6 +1741,8 @@ mod tests { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, "lifted fn type should have capture appended; got {:?}", lifted.ty @@ -1795,6 +1811,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::App { @@ -1821,6 +1839,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Let { @@ -1889,6 +1909,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["z".into()], body: Box::new(Term::App { @@ -1933,6 +1955,8 @@ mod tests { }], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["p".into()], body: outer_body, @@ -1979,6 +2003,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::Let { @@ -2006,6 +2032,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: letrec, @@ -2035,6 +2063,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::App { @@ -2062,6 +2092,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: letrec, @@ -2150,6 +2182,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["k".into()], body: Box::new(Term::If { @@ -2193,6 +2227,8 @@ mod tests { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, params: vec!["x".into()], @@ -2265,6 +2301,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["k".into()], body: Box::new(Term::Var { name: "x".into() }), @@ -2290,6 +2328,8 @@ mod tests { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }), }, params: vec!["x".into()], @@ -2327,6 +2367,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["j".into()], body: Box::new(Term::App { @@ -2346,6 +2388,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["i".into()], body: Box::new(inner), @@ -2365,6 +2409,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: outer, @@ -2397,6 +2443,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["j".into()], body: Box::new(Term::App { @@ -2419,6 +2467,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["i".into()], body: Box::new(inner), @@ -2438,6 +2488,8 @@ mod tests { params: vec![], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec![], body: outer, @@ -2554,6 +2606,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], body: body_match, @@ -2624,6 +2678,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["xs".into()], body: body_match, @@ -2702,6 +2758,8 @@ mod tests { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], body: body_match, diff --git a/crates/ailang-core/src/hash.rs b/crates/ailang-core/src/hash.rs index d851a7c..7480873 100644 --- a/crates/ailang-core/src/hash.rs +++ b/crates/ailang-core/src/hash.rs @@ -59,6 +59,8 @@ mod tests { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["a".into(), "b".into()], body: Term::App { diff --git a/crates/ailang-core/src/pretty.rs b/crates/ailang-core/src/pretty.rs index 60c275b..2672397 100644 --- a/crates/ailang-core/src/pretty.rs +++ b/crates/ailang-core/src/pretty.rs @@ -136,7 +136,7 @@ pub fn type_to_string(t: &Type) -> String { } } Type::Var { name } => name.clone(), - Type::Fn { params, ret, effects } => { + Type::Fn { params, ret, effects, .. } => { let p = params .iter() .map(type_to_string) @@ -171,6 +171,8 @@ mod tests { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], + param_modes: vec![], + ret_mode: ParamMode::Implicit, }, params: vec!["a".into(), "b".into()], body: Term::App { diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 1dc6b98..9750dc9 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -27,8 +27,9 @@ //! type ::= type-var | type-con | fn-type | forall-type //! type-var ::= ident //! type-con ::= "(" "con" ident type* ")" -//! fn-type ::= "(" "fn-type" "(" "params" type* ")" -//! "(" "ret" type ")" +//! fn-type-param ::= type | "(" "borrow" type ")" | "(" "own" type ")" +//! fn-type ::= "(" "fn-type" "(" "params" fn-type-param* ")" +//! "(" "ret" fn-type-param ")" //! effects-clause? ")" //! forall-type ::= "(" "forall" "(" "vars" ident+ ")" type ")" //! effects-clause::= "(" "effects" ident+ ")" @@ -80,7 +81,8 @@ //! [`ailang_core::ast::Import::alias`]. use ailang_core::ast::{ - Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, Pattern, Term, Type, TypeDef, + Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Term, Type, + TypeDef, }; use ailang_core::SCHEMA; use thiserror::Error; @@ -559,6 +561,20 @@ impl<'a> Parser<'a> { "con" => self.parse_type_con(), "fn-type" => self.parse_fn_type(), "forall" => self.parse_forall_type(), + // Iter 18a: `borrow` / `own` are valid only as + // wrappers around `fn-type` params or `ret`. At + // top-level type position they are a parse error + // with a clear message. + "borrow" | "own" => { + let pos = self.peek().map(|t| t.span.start).unwrap_or(0); + Err(ParseError::Production { + production: "type", + message: format!( + "`{head}` may only appear inside fn-type params or ret" + ), + pos, + }) + } other => { let pos = self.peek().map(|t| t.span.start).unwrap_or(0); Err(ParseError::Production { @@ -602,18 +618,21 @@ impl<'a> Parser<'a> { fn parse_fn_type(&mut self) -> Result { self.expect_lparen("fn-type")?; self.expect_keyword("fn-type")?; - // (params type*) + // (params fn-type-param*) self.expect_lparen("fn-type params")?; self.expect_keyword("params")?; let mut params: Vec = Vec::new(); + let mut param_modes: Vec = Vec::new(); while !matches!(self.peek(), Some(Token { tok: Tok::RParen, .. })) { - params.push(self.parse_type()?); + let (t, m) = self.parse_param_with_mode()?; + params.push(t); + param_modes.push(m); } self.expect_rparen("fn-type params")?; - // (ret type) + // (ret fn-type-param) self.expect_lparen("fn-type ret")?; self.expect_keyword("ret")?; - let ret = self.parse_type()?; + let (ret, ret_mode) = self.parse_param_with_mode()?; self.expect_rparen("fn-type ret")?; // optional (effects ident+) let mut effects: Vec = Vec::new(); @@ -621,13 +640,50 @@ impl<'a> Parser<'a> { effects = self.parse_effects_clause()?; } self.expect_rparen("fn-type")?; + // If every entry is Implicit, store as `vec![]` so canonical + // JSON serialisation omits the field — preserves pre-18a + // hashes for any fixture that still uses bare types. + let stored_modes = if param_modes.iter().all(|m| matches!(m, ParamMode::Implicit)) { + Vec::new() + } else { + param_modes + }; Ok(Type::Fn { params, ret: Box::new(ret), effects, + param_modes: stored_modes, + ret_mode, }) } + /// Iter 18a: parse one fn-type slot — a type, optionally wrapped + /// in `(borrow T)` or `(own T)`. The mode is `Implicit` for a + /// bare type, `Borrow` for `(borrow T)`, `Own` for `(own T)`. + fn parse_param_with_mode(&mut self) -> Result<(Type, ParamMode), ParseError> { + if let Some(head) = self.peek_head_ident() { + match head { + "borrow" => { + self.expect_lparen("borrow")?; + self.expect_keyword("borrow")?; + let inner = self.parse_type()?; + self.expect_rparen("borrow")?; + return Ok((inner, ParamMode::Borrow)); + } + "own" => { + self.expect_lparen("own")?; + self.expect_keyword("own")?; + let inner = self.parse_type()?; + self.expect_rparen("own")?; + return Ok((inner, ParamMode::Own)); + } + _ => {} + } + } + let t = self.parse_type()?; + Ok((t, ParamMode::Implicit)) + } + fn parse_effects_clause(&mut self) -> Result, ParseError> { self.expect_lparen("effects-clause")?; self.expect_keyword("effects")?; @@ -1116,6 +1172,65 @@ mod tests { assert!(matches!(m.defs.len(), 1)); } + /// Iter 18a: `(borrow T)` and `(own T)` wrappers in fn-type + /// param/ret slots round-trip into [`ParamMode::Borrow`] / + /// [`ParamMode::Own`] on `Type::Fn`. A bare type stays + /// [`ParamMode::Implicit`] (and its mode is elided from the + /// canonical form). + #[test] + fn parses_borrow_and_own_modes_on_fn_type_slots() { + let m = parse( + r#" + (module m + (fn f + (type (fn-type + (params (borrow (con Int)) (con Bool)) + (ret (own (con Int))))) + (params x y) + (body x))) + "#, + ) + .unwrap(); + let ty = match &m.defs[0] { + Def::Fn(fd) => &fd.ty, + _ => panic!("expected fn"), + }; + match ty { + Type::Fn { param_modes, ret_mode, .. } => { + assert_eq!( + param_modes, + &vec![ParamMode::Borrow, ParamMode::Implicit], + "first param parsed as `(borrow ...)`, second as bare" + ); + assert_eq!(*ret_mode, ParamMode::Own, "ret parsed as `(own ...)`"); + } + other => panic!("expected Type::Fn, got {other:?}"), + } + } + + /// Iter 18a: `(borrow T)` outside an `fn-type` param/ret slot is + /// a parse error. Specifically, a `const` whose declared type is + /// `(borrow ...)` must be rejected with a clear message — modes + /// are *not* a top-level type production. + #[test] + fn rejects_borrow_at_top_level_type_position() { + let err = parse( + r#" + (module m + (const c + (type (borrow (con Int))) + (body 0))) + "#, + ) + .err() + .expect("parse should fail"); + let msg = format!("{err}"); + assert!( + msg.contains("borrow"), + "diagnostic should mention `borrow`, got: {msg}" + ); + } + /// Iter 16b.1: minimal `(let-rec ...)` round-trips through the /// parser into a `Term::LetRec` whose `name`, `params`, `body` and /// `in_term` line up with the source. diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index c3bfb3d..7369fc3 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -8,7 +8,8 @@ //! per level. Comments are NOT emitted. use ailang_core::ast::{ - Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, Pattern, Term, Type, TypeDef, + Arm, ConstDef, Ctor, Def, FnDef, Import, Literal, Module, ParamMode, Pattern, Term, Type, + TypeDef, }; /// Print a module in form (A). @@ -169,6 +170,26 @@ fn write_const_def(out: &mut String, cd: &ConstDef, level: usize) { // ---- types ---------------------------------------------------------------- +/// Iter 18a: print one fn-type param/ret slot, wrapping with +/// `(borrow ...)` or `(own ...)` when the slot has an explicit +/// mode. `Implicit` is printed bare so pre-18a fixtures round-trip +/// unchanged. +fn write_fn_type_slot(out: &mut String, t: &Type, mode: ParamMode) { + match mode { + ParamMode::Implicit => write_type(out, t), + ParamMode::Own => { + out.push_str("(own "); + write_type(out, t); + out.push(')'); + } + ParamMode::Borrow => { + out.push_str("(borrow "); + write_type(out, t); + out.push(')'); + } + } +} + fn write_type(out: &mut String, t: &Type) { match t { Type::Var { name } => out.push_str(name), @@ -183,16 +204,19 @@ fn write_type(out: &mut String, t: &Type) { } Type::Fn { params, + param_modes, ret, + ret_mode, effects, } => { out.push_str("(fn-type (params"); - for p in params { + for (i, p) in params.iter().enumerate() { out.push(' '); - write_type(out, p); + let mode = param_modes.get(i).copied().unwrap_or(ParamMode::Implicit); + write_fn_type_slot(out, p, mode); } out.push_str(") (ret "); - write_type(out, ret); + write_fn_type_slot(out, ret, *ret_mode); out.push(')'); if !effects.is_empty() { out.push_str(" (effects"); diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 8f049a9..00f76fd 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -972,9 +972,9 @@ Memory layout (Iter 18b): types, the recursion is replaced by a worklist loop (Iter 18e). Codegen for `Term::Ctor` / `Term::Lam` env / closure pair under -`--memory=rc` calls `ailang_rc_alloc(SIZE)`. Iter 18b stops there +`--alloc=rc` calls `ailang_rc_alloc(SIZE)`. Iter 18b stops there — inc/dec instrumentation is added in Iter 18c, once the -inference is wired up. Until then, `--memory=rc` deliberately +inference is wired up. Until then, `--alloc=rc` deliberately leaks like the pre-Boehm era; this is purely about plumbing. ### Migration plan @@ -984,8 +984,8 @@ leaks like the pre-Boehm era; this is purely about plumbing. codegen change. `(con T)` ≡ `(own T)`. Existing fixtures unchanged. 2. **Iter 18b:** RC runtime. `runtime/rc.c` with header layout + - alloc/inc/dec. Codegen `--memory=rc` routes allocation through - `rc_alloc`; **no inc/dec yet**. `--memory=gc` remains default. + alloc/inc/dec. Codegen `--alloc=rc` routes allocation through + `rc_alloc`; **no inc/dec yet**. `--alloc=gc` remains default. 3. **Iter 18c:** uniqueness inference + codegen inc/dec instrumentation. Combines 18a's annotations with intra-fn dataflow. Linear-by-default enforcement turns on. `(clone X)` diff --git a/examples/borrow_own_demo.ail.json b/examples/borrow_own_demo.ail.json new file mode 100644 index 0000000..afbd1b2 --- /dev/null +++ b/examples/borrow_own_demo.ail.json @@ -0,0 +1 @@ +{"defs":[{"ctors":[{"fields":[],"name":"Nil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"List"}],"name":"Cons"}],"doc":"Monomorphic singly-linked Int list — boxed, recursive.","kind":"type","name":"List"},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"list_length","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Borrow xs, count its elements. Iter 18a: `(borrow (con List))`.","kind":"fn","name":"list_length","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["borrow"],"params":[{"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"Nil","fields":[],"p":"ctor"}},{"body":{"args":[{"name":"h","t":"var"},{"args":[{"name":"t","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"},"pat":{"ctor":"Cons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Consume xs, sum its elements. Iter 18a: `(own (con List))`.","kind":"fn","name":"sum_list","params":["xs"],"type":{"effects":[],"k":"fn","param_modes":["own"],"params":[{"k":"con","name":"List"}],"ret":{"k":"con","name":"Int"}}},{"body":{"body":{"lhs":{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"list_length","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"name":"xs","t":"var"}],"fn":{"name":"sum_list","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"name":"xs","t":"let","value":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[{"lit":{"kind":"int","value":3},"t":"lit"},{"args":[],"ctor":"Nil","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}],"ctor":"Cons","t":"ctor","type":"List"}},"doc":"Build [1,2,3]; print list_length (3) then sum_list (6).","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"borrow_own_demo","schema":"ailang/v0"} \ No newline at end of file diff --git a/examples/borrow_own_demo.ailx b/examples/borrow_own_demo.ailx new file mode 100644 index 0000000..4fed93d --- /dev/null +++ b/examples/borrow_own_demo.ailx @@ -0,0 +1,64 @@ +; Iter 18a — `(borrow T)` and `(own T)` mode annotations on +; fn-type params and ret slots. +; +; This fixture exercises the schema/parser/printer/typechecker path +; that Iter 18a adds. It does *not* exercise enforcement: under the +; semantics shipping with 18a, every mode is treated as +; `Implicit ≡ Own` and `(borrow ...)` / `(own ...)` are accepted but +; have no codegen consequence. Linearity / borrow checking comes in +; Iter 18c. +; +; What round-trips through the form-A printer: +; (borrow (con List)) — list_length's xs parameter +; (own (con List)) — sum_list's xs parameter +; +; Expected stdout (one int per line, via io/print_int + a newline): +; 3 +; 6 + +(module borrow_own_demo + + (data List + (doc "Monomorphic singly-linked Int list — boxed, recursive.") + (ctor Nil) + (ctor Cons (con Int) (con List))) + + (fn list_length + (doc "Borrow xs, count its elements. Iter 18a: `(borrow (con List))`.") + (type + (fn-type + (params (borrow (con List))) + (ret (con Int)))) + (params xs) + (body + (match xs + (case (pat-ctor Nil) 0) + (case (pat-ctor Cons h t) + (app + 1 (app list_length t)))))) + + (fn sum_list + (doc "Consume xs, sum its elements. Iter 18a: `(own (con List))`.") + (type + (fn-type + (params (own (con List))) + (ret (con Int)))) + (params xs) + (body + (match xs + (case (pat-ctor Nil) 0) + (case (pat-ctor Cons h t) + (app + h (app sum_list t)))))) + + (fn main + (doc "Build [1,2,3]; print list_length (3) then sum_list (6).") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body + (let xs + (term-ctor List Cons 1 + (term-ctor List Cons 2 + (term-ctor List Cons 3 + (term-ctor List Nil)))) + (seq + (do io/print_int (app list_length xs)) + (do io/print_int (app sum_list xs)))))))