diff --git a/ephapax-linear/src/affine.rs b/ephapax-linear/src/affine.rs index 1965d17..eeec126 100644 --- a/ephapax-linear/src/affine.rs +++ b/ephapax-linear/src/affine.rs @@ -239,7 +239,7 @@ impl AffineChecker { self.ctx.exit_region(); } - ExprKind::Borrow(inner) | ExprKind::Deref(inner) => { + ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) => { self.walk_expr(inner); } diff --git a/ephapax-linear/src/linear.rs b/ephapax-linear/src/linear.rs index 38ffa14..47fc0a2 100644 --- a/ephapax-linear/src/linear.rs +++ b/ephapax-linear/src/linear.rs @@ -288,7 +288,7 @@ impl LinearChecker { } // --- Borrow/Deref: walk inner --- - ExprKind::Borrow(inner) | ExprKind::Deref(inner) => { + ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) => { self.walk_expr(inner); } diff --git a/examples/linear/16-shared-vs-exclusive-borrow.eph b/examples/linear/16-shared-vs-exclusive-borrow.eph new file mode 100644 index 0000000..a56beac --- /dev/null +++ b/examples/linear/16-shared-vs-exclusive-borrow.eph @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: PMPL-1.0-or-later +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +// +// Example: shared (`&`) vs exclusive (`&mut`) borrow parameters. +// +// At codegen these surface in `typedwasm.ownership` as `SharedBorrow` +// and `ExclBorrow` respectively. The L7 alias-exclusion verifier pass +// in typed-wasm fires on `ExclBorrow` parameters used more than once. + +fn observe(buf: &String@r) -> I32 { + // `buf` is a shared borrow — multiple shared borrows of the same + // backing object may coexist. Emitted as SharedBorrow. + 0 +} + +fn mutate(buf: &mut String@r) -> I32 { + // `buf` is an exclusive borrow — no other live borrow of the same + // backing object is permitted. Emitted as ExclBorrow. + 0 +} diff --git a/src/ephapax-analysis/src/escape.rs b/src/ephapax-analysis/src/escape.rs index 779f005..ecc9633 100644 --- a/src/ephapax-analysis/src/escape.rs +++ b/src/ephapax-analysis/src/escape.rs @@ -114,7 +114,7 @@ impl EscapeAnalysis { Self::analyze_expr(inner, escaping, in_escaping_context); } - ExprKind::Borrow(inner) => { + ExprKind::Borrow { inner, .. } => { Self::analyze_expr(inner, escaping, in_escaping_context); } diff --git a/src/ephapax-analysis/src/free_vars.rs b/src/ephapax-analysis/src/free_vars.rs index dcd6e50..da6a140 100644 --- a/src/ephapax-analysis/src/free_vars.rs +++ b/src/ephapax-analysis/src/free_vars.rs @@ -123,7 +123,7 @@ impl FreeVarAnalysis { ExprKind::Drop(inner) | ExprKind::Copy(inner) - | ExprKind::Borrow(inner) + | ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) | ExprKind::Fst(inner) | ExprKind::Snd(inner) diff --git a/src/ephapax-analysis/src/liveness.rs b/src/ephapax-analysis/src/liveness.rs index 5451067..1534794 100644 --- a/src/ephapax-analysis/src/liveness.rs +++ b/src/ephapax-analysis/src/liveness.rs @@ -128,7 +128,7 @@ impl LivenessAnalysis { ExprKind::Drop(inner) | ExprKind::Copy(inner) - | ExprKind::Borrow(inner) + | ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) | ExprKind::Fst(inner) | ExprKind::Snd(inner) diff --git a/src/ephapax-desugar/src/lib.rs b/src/ephapax-desugar/src/lib.rs index fd60edb..aac539c 100644 --- a/src/ephapax-desugar/src/lib.rs +++ b/src/ephapax-desugar/src/lib.rs @@ -454,7 +454,10 @@ impl Desugarer { body: Box::new(self.desugar_expr(body)?), }, - SurfaceExprKind::Borrow(inner) => ExprKind::Borrow(Box::new(self.desugar_expr(inner)?)), + SurfaceExprKind::Borrow { inner, mutable } => ExprKind::Borrow { + inner: Box::new(self.desugar_expr(inner)?), + mutable: *mutable, + }, SurfaceExprKind::Deref(inner) => ExprKind::Deref(Box::new(self.desugar_expr(inner)?)), SurfaceExprKind::Drop(inner) => ExprKind::Drop(Box::new(self.desugar_expr(inner)?)), SurfaceExprKind::Copy(inner) => ExprKind::Copy(Box::new(self.desugar_expr(inner)?)), @@ -553,7 +556,10 @@ impl Desugarer { name: name.clone(), inner: Box::new(self.desugar_ty(inner)?), }), - SurfaceTy::Borrow(inner) => Ok(Ty::Borrow(Box::new(self.desugar_ty(inner)?))), + SurfaceTy::Borrow { inner, mutable } => Ok(Ty::Borrow { + inner: Box::new(self.desugar_ty(inner)?), + mutable: *mutable, + }), SurfaceTy::Var(v) => Ok(Ty::Var(v.clone())), SurfaceTy::List(inner) => Ok(Ty::List(Box::new(self.desugar_ty(inner)?))), SurfaceTy::Tuple(elements) => { diff --git a/src/ephapax-interp/src/lib.rs b/src/ephapax-interp/src/lib.rs index 1f182fa..3f5bef2 100644 --- a/src/ephapax-interp/src/lib.rs +++ b/src/ephapax-interp/src/lib.rs @@ -213,7 +213,7 @@ impl Value { param: Box::new(param_ty.clone()), ret: Box::new(Ty::Base(BaseTy::Unit)), // Unknown without evaluation }, - Value::Borrow(inner) => Ty::Borrow(Box::new(inner.to_type())), + Value::Borrow(inner) => Ty::Borrow { inner: Box::new(inner.to_type()), mutable: false }, } } } @@ -436,7 +436,7 @@ impl Interpreter { else_branch, } => self.eval_if(cond, then_branch, else_branch), ExprKind::Region { name, body } => self.eval_region(name, body), - ExprKind::Borrow(inner) => self.eval_borrow(inner), + ExprKind::Borrow { inner, .. } => self.eval_borrow(inner), ExprKind::Deref(inner) => self.eval_deref(inner), ExprKind::Drop(inner) => self.eval_drop(inner), ExprKind::Copy(inner) => self.eval_copy(inner), diff --git a/src/ephapax-ir/src/lib.rs b/src/ephapax-ir/src/lib.rs index bf91a8d..e748d3c 100644 --- a/src/ephapax-ir/src/lib.rs +++ b/src/ephapax-ir/src/lib.rs @@ -503,9 +503,10 @@ fn expr_to_sexpr(expr: &Expr) -> SExpr { SExpr::Atom(escape_atom(name)), expr_to_sexpr(body), ]), - ExprKind::Borrow(inner) => { - SExpr::List(vec![SExpr::Atom("borrow".into()), expr_to_sexpr(inner)]) - } + ExprKind::Borrow { inner, mutable } => SExpr::List(vec![ + SExpr::Atom(if *mutable { "borrow-mut" } else { "borrow" }.into()), + expr_to_sexpr(inner), + ]), ExprKind::Deref(inner) => { SExpr::List(vec![SExpr::Atom("deref".into()), expr_to_sexpr(inner)]) } @@ -735,7 +736,14 @@ fn decode_expr(expr: &SExpr) -> Result { name: SmolStr::new(atom_string(&list[1])?), body: Box::new(decode_expr(&list[2])?), }, - "borrow" => ExprKind::Borrow(Box::new(decode_expr(&list[1])?)), + "borrow" => ExprKind::Borrow { + inner: Box::new(decode_expr(&list[1])?), + mutable: false, + }, + "borrow-mut" => ExprKind::Borrow { + inner: Box::new(decode_expr(&list[1])?), + mutable: true, + }, "deref" => ExprKind::Deref(Box::new(decode_expr(&list[1])?)), "drop" => ExprKind::Drop(Box::new(decode_expr(&list[1])?)), "copy" => ExprKind::Copy(Box::new(decode_expr(&list[1])?)), @@ -851,7 +859,10 @@ fn ty_to_sexpr(ty: &Ty) -> SExpr { SExpr::Atom(escape_atom(name)), ty_to_sexpr(inner), ]), - Ty::Borrow(inner) => SExpr::List(vec![SExpr::Atom("borrow".into()), ty_to_sexpr(inner)]), + Ty::Borrow { inner, mutable } => SExpr::List(vec![ + SExpr::Atom(if *mutable { "borrow-mut" } else { "borrow" }.into()), + ty_to_sexpr(inner), + ]), Ty::Var(v) => SExpr::List(vec![SExpr::Atom("var".into()), SExpr::Atom(escape_atom(v))]), Ty::List(inner) => SExpr::List(vec![SExpr::Atom("list".into()), ty_to_sexpr(inner)]), Ty::Tuple(elem_types) => { @@ -926,7 +937,14 @@ fn decode_ty(expr: &SExpr) -> Result { name: SmolStr::new(atom_string(&list[1])?), inner: Box::new(decode_ty(&list[2])?), }), - "borrow" => Ok(Ty::Borrow(Box::new(decode_ty(&list[1])?))), + "borrow" => Ok(Ty::Borrow { + inner: Box::new(decode_ty(&list[1])?), + mutable: false, + }), + "borrow-mut" => Ok(Ty::Borrow { + inner: Box::new(decode_ty(&list[1])?), + mutable: true, + }), "var" => Ok(Ty::Var(SmolStr::new(atom_string(&list[1])?))), _ => Err(SExprError::Invalid("unknown type tag".into())), } diff --git a/src/ephapax-lsp/src/main.rs b/src/ephapax-lsp/src/main.rs index 8ae8ebf..f9b054a 100644 --- a/src/ephapax-lsp/src/main.rs +++ b/src/ephapax-lsp/src/main.rs @@ -701,7 +701,7 @@ fn format_ty(ty: &Ty) -> String { Ty::Ref { inner, .. } => format!("Ref({})", format_ty(inner)), Ty::String(region) => format!("String@{}", region), Ty::Region { name, inner } => format!("Region({}, {})", name, format_ty(inner)), - Ty::Borrow(inner) => format!("&{}", format_ty(inner)), + Ty::Borrow { inner, mutable } => format!("&{}{}", if *mutable { "mut " } else { "" }, format_ty(inner)), Ty::Var(name) => name.to_string(), Ty::ForAll { var, body } => format!("forall {}. {}", var, format_ty(body)), Ty::Unif(id) => format!("?{}", id), @@ -847,7 +847,7 @@ fn find_let_binding_span(expr: &Expr, target: &str) -> Option { | ExprKind::Inr { value: inner, .. } | ExprKind::Drop(inner) | ExprKind::Copy(inner) - | ExprKind::Borrow(inner) + | ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) | ExprKind::UnaryOp { operand: inner, .. } | ExprKind::StringLen(inner) => find_let_binding_span(inner, target), diff --git a/src/ephapax-parser/src/ephapax.pest b/src/ephapax-parser/src/ephapax.pest index 568b6a9..5710e6a 100644 --- a/src/ephapax-parser/src/ephapax.pest +++ b/src/ephapax-parser/src/ephapax.pest @@ -145,7 +145,9 @@ unit_ty = { "()" } string_ty = { "String" ~ ("@" ~ identifier)? } -borrow_ty = { "&" ~ type_atom } +borrow_ty = { "&" ~ mut_marker? ~ type_atom } + +mut_marker = @{ "mut" ~ !(ASCII_ALPHANUMERIC | "_") } list_ty = { "[" ~ ty ~ "]" } @@ -402,7 +404,7 @@ expr_list = { expression ~ ("," ~ expression)* } inl_expr = { "inl" ~ "[" ~ ty ~ "]" ~ "(" ~ expression ~ ")" } inr_expr = { "inr" ~ "[" ~ ty ~ "]" ~ "(" ~ expression ~ ")" } -borrow_expr = { "&" ~ unary_expr } +borrow_expr = { "&" ~ mut_marker? ~ unary_expr } fst_expr = { "fst" ~ "(" ~ expression ~ ")" } snd_expr = { "snd" ~ "(" ~ expression ~ ")" } @@ -451,7 +453,7 @@ keyword_boundary = { | "region" | "case" | "of" | "inl" | "inr" | "end" | "fst" | "snd" | "drop" | "copy" | "type" | "data" | "match" | "extern" - | "true" | "false" | "pub" | "import" | "module" | "linear" + | "true" | "false" | "pub" | "import" | "module" | "linear" | "mut" | "perform" | "handle" | "with" | "return" | "resume" | "once" | "multi" | "Bool" | "I32" | "I64" | "F32" | "F64" | "String" ) ~ !(ASCII_ALPHANUMERIC | "_") @@ -462,7 +464,7 @@ keyword = { | "region" | "case" | "of" | "inl" | "inr" | "end" | "fst" | "snd" | "drop" | "copy" | "type" | "data" | "match" | "extern" - | "true" | "false" | "pub" | "import" | "module" | "linear" + | "true" | "false" | "pub" | "import" | "module" | "linear" | "mut" | "perform" | "handle" | "with" | "return" | "resume" | "once" | "multi" | "Bool" | "I32" | "I64" | "F32" | "F64" | "String" } diff --git a/src/ephapax-parser/src/lib.rs b/src/ephapax-parser/src/lib.rs index 441f011..2add696 100644 --- a/src/ephapax-parser/src/lib.rs +++ b/src/ephapax-parser/src/lib.rs @@ -583,13 +583,22 @@ fn parse_type_atom(pair: pest::iterators::Pair) -> Result Ok(Ty::String(region)) } Rule::borrow_ty => { - let inner_ty = parse_type_atom( - inner - .into_inner() - .next() - .ok_or_else(|| ParseError::missing("borrowed type"))?, - )?; - Ok(Ty::Borrow(Box::new(inner_ty))) + let mut children = inner.into_inner(); + let first = children + .next() + .ok_or_else(|| ParseError::missing("borrowed type"))?; + let (mutable, ty_pair) = if first.as_rule() == Rule::mut_marker { + ( + true, + children + .next() + .ok_or_else(|| ParseError::missing("borrowed type"))?, + ) + } else { + (false, first) + }; + let inner_ty = parse_type_atom(ty_pair)?; + Ok(Ty::Borrow { inner: Box::new(inner_ty), mutable }) } Rule::list_ty => { let elem_ty = parse_type( @@ -1748,13 +1757,25 @@ fn parse_atom_expr(pair: pest::iterators::Pair) -> Result { - let inner_expr = parse_unary_expr( - inner - .into_inner() - .next() - .ok_or_else(|| ParseError::missing("borrow operand"))?, - )?; - Ok(Expr::new(ExprKind::Borrow(Box::new(inner_expr)), span)) + let mut children = inner.into_inner(); + let first = children + .next() + .ok_or_else(|| ParseError::missing("borrow operand"))?; + let (mutable, operand_pair) = if first.as_rule() == Rule::mut_marker { + ( + true, + children + .next() + .ok_or_else(|| ParseError::missing("borrow operand"))?, + ) + } else { + (false, first) + }; + let inner_expr = parse_unary_expr(operand_pair)?; + Ok(Expr::new( + ExprKind::Borrow { inner: Box::new(inner_expr), mutable }, + span, + )) } Rule::fst_expr => { let inner_expr = parse_expression( @@ -2106,13 +2127,25 @@ mod tests { #[test] fn test_parse_borrow() { let expr = parse_ok("&x"); - if let ExprKind::Borrow(inner) = expr.kind { + if let ExprKind::Borrow { inner, mutable } = expr.kind { assert!(matches!(inner.kind, ExprKind::Var(_))); + assert!(!mutable, "`&x` must parse as shared (mutable=false)"); } else { panic!("Expected borrow"); } } + #[test] + fn test_parse_mut_borrow() { + let expr = parse_ok("&mut x"); + if let ExprKind::Borrow { inner, mutable } = expr.kind { + assert!(matches!(inner.kind, ExprKind::Var(_))); + assert!(mutable, "`&mut x` must parse as exclusive (mutable=true)"); + } else { + panic!("Expected mut borrow"); + } + } + #[test] fn test_parse_drop() { let expr = parse_ok("drop(x)"); diff --git a/src/ephapax-parser/src/surface.rs b/src/ephapax-parser/src/surface.rs index 353d7e4..9ca5d0c 100644 --- a/src/ephapax-parser/src/surface.rs +++ b/src/ephapax-parser/src/surface.rs @@ -1490,14 +1490,23 @@ fn parse_atom_expr(pair: pest::iterators::Pair) -> Result { - let inner_expr = parse_unary_expr( - inner - .into_inner() - .next() - .ok_or_else(|| ParseError::missing("borrow"))?, - )?; + let mut children = inner.into_inner(); + let first = children + .next() + .ok_or_else(|| ParseError::missing("borrow"))?; + let (mutable, operand_pair) = if first.as_rule() == Rule::mut_marker { + ( + true, + children + .next() + .ok_or_else(|| ParseError::missing("borrow"))?, + ) + } else { + (false, first) + }; + let inner_expr = parse_unary_expr(operand_pair)?; Ok(SurfaceExpr::new( - SurfaceExprKind::Borrow(Box::new(inner_expr)), + SurfaceExprKind::Borrow { inner: Box::new(inner_expr), mutable }, span, )) } @@ -1787,13 +1796,22 @@ fn parse_type_atom(pair: pest::iterators::Pair) -> Result { - let inner_ty = parse_type_atom( - inner - .into_inner() - .next() - .ok_or_else(|| ParseError::missing("borrow inner"))?, - )?; - Ok(SurfaceTy::Borrow(Box::new(inner_ty))) + let mut children = inner.into_inner(); + let first = children + .next() + .ok_or_else(|| ParseError::missing("borrow inner"))?; + let (mutable, ty_pair) = if first.as_rule() == Rule::mut_marker { + ( + true, + children + .next() + .ok_or_else(|| ParseError::missing("borrow inner"))?, + ) + } else { + (false, first) + }; + let inner_ty = parse_type_atom(ty_pair)?; + Ok(SurfaceTy::Borrow { inner: Box::new(inner_ty), mutable }) } Rule::list_ty => { let elem_ty = parse_type( diff --git a/src/ephapax-repl/src/lib.rs b/src/ephapax-repl/src/lib.rs index aacf921..ab58489 100644 --- a/src/ephapax-repl/src/lib.rs +++ b/src/ephapax-repl/src/lib.rs @@ -413,7 +413,7 @@ fn format_type(ty: &Ty) -> String { Ty::Fun { param, ret } => format!("{} -> {}", format_type(param), format_type(ret)), Ty::Prod { left, right } => format!("({}, {})", format_type(left), format_type(right)), Ty::Sum { left, right } => format!("{} + {}", format_type(left), format_type(right)), - Ty::Borrow(inner) => format!("&{}", format_type(inner)), + Ty::Borrow { inner, mutable } => format!("&{}{}", if *mutable { "mut " } else { "" }, format_type(inner)), Ty::List(elem_ty) => format!("[{}]", format_type(elem_ty)), Ty::Tuple(elem_types) => { let types_str = elem_types diff --git a/src/ephapax-surface/src/lib.rs b/src/ephapax-surface/src/lib.rs index d1fca6b..29a925a 100644 --- a/src/ephapax-surface/src/lib.rs +++ b/src/ephapax-surface/src/lib.rs @@ -91,8 +91,8 @@ pub enum SurfaceTy { inner: Box, }, - /// Second-class borrow &T - Borrow(Box), + /// Second-class borrow `&T` (shared) or `&mut T` (exclusive). + Borrow { inner: Box, mutable: bool }, /// Type variable (bound by data declaration) Var(TyVar), @@ -324,7 +324,7 @@ pub enum SurfaceExprKind { name: RegionName, body: Box, }, - Borrow(Box), + Borrow { inner: Box, mutable: bool }, Deref(Box), Drop(Box), Copy(Box), diff --git a/src/ephapax-syntax/src/lib.rs b/src/ephapax-syntax/src/lib.rs index 173eed0..f39eac7 100644 --- a/src/ephapax-syntax/src/lib.rs +++ b/src/ephapax-syntax/src/lib.rs @@ -83,8 +83,10 @@ pub enum Ty { /// Region-scoped type Region { name: RegionName, inner: Box }, - /// Second-class borrow &T - Borrow(Box), + /// Second-class borrow `&T` (shared) or `&mut T` (exclusive). + /// `mutable: true` corresponds to `&mut T`; emitted as `ExclBorrow` + /// in `typedwasm.ownership` (L7 aliasing enforcement). + Borrow { inner: Box, mutable: bool }, /// Type variable (for polymorphism) Var(SmolStr), @@ -135,7 +137,7 @@ impl Ty { Ty::Prod { left, right } | Ty::Sum { left, right } => { left.references_region(region) || right.references_region(region) } - Ty::Borrow(inner) => inner.references_region(region), + Ty::Borrow { inner, .. } => inner.references_region(region), Ty::List(inner) => inner.references_region(region), Ty::Tuple(elements) => elements.iter().any(|t| t.references_region(region)), Ty::ForAll { body, .. } => body.references_region(region), @@ -194,7 +196,10 @@ impl Ty { name: name.clone(), inner: Box::new(inner.subst_var(var, replacement)), }, - Ty::Borrow(inner) => Ty::Borrow(Box::new(inner.subst_var(var, replacement))), + Ty::Borrow { inner, mutable } => Ty::Borrow { + inner: Box::new(inner.subst_var(var, replacement)), + mutable: *mutable, + }, Ty::Effectful { param, ret, effects } => Ty::Effectful { param: Box::new(param.subst_var(var, replacement)), ret: Box::new(ret.subst_var(var, replacement)), @@ -219,7 +224,7 @@ impl Ty { } Ty::Ref { inner, .. } | Ty::Region { inner, .. } - | Ty::Borrow(inner) + | Ty::Borrow { inner, .. } | Ty::List(inner) | Ty::ForAll { body: inner, .. } => inner.contains_unif(id), Ty::Effectful { param, ret, .. } => { @@ -260,7 +265,10 @@ impl Ty { name: name.clone(), inner: Box::new(inner.resolve(solutions)), }, - Ty::Borrow(inner) => Ty::Borrow(Box::new(inner.resolve(solutions))), + Ty::Borrow { inner, mutable } => Ty::Borrow { + inner: Box::new(inner.resolve(solutions)), + mutable: *mutable, + }, Ty::ForAll { var, body } => Ty::ForAll { var: var.clone(), body: Box::new(body.resolve(solutions)), @@ -505,8 +513,10 @@ pub enum ExprKind { Region { name: RegionName, body: Box }, // ===== Borrowing ===== - /// Create borrow: &e - Borrow(Box), + /// Create borrow: `&e` (shared) or `&mut e` (exclusive). + /// `mutable: true` requires an `&mut T` parameter type and produces + /// an `ExclBorrow` ownership classification at codegen. + Borrow { inner: Box, mutable: bool }, /// Dereference: *e Deref(Box), diff --git a/src/ephapax-typing/src/lib.rs b/src/ephapax-typing/src/lib.rs index f64b824..806fed7 100644 --- a/src/ephapax-typing/src/lib.rs +++ b/src/ephapax-typing/src/lib.rs @@ -410,7 +410,10 @@ fn subst_tys(ty: &Ty, subst: &HashMap) -> Ty { name: name.clone(), inner: Box::new(subst_tys(inner, subst)), }, - Ty::Borrow(inner) => Ty::Borrow(Box::new(subst_tys(inner, subst))), + Ty::Borrow { inner, mutable } => Ty::Borrow { + inner: Box::new(subst_tys(inner, subst)), + mutable: *mutable, + }, Ty::ForAll { var, body } => { if subst.contains_key(var) { ty.clone() // shadowed by binder @@ -869,7 +872,15 @@ impl TypeChecker { { self.unify(s, i1, i2) } - (Ty::Borrow(i1), Ty::Borrow(i2)) => self.unify(s, i1, i2), + (Ty::Borrow { inner: i1, mutable: m1 }, Ty::Borrow { inner: i2, mutable: m2 }) => { + if m1 != m2 { + return Err(self.at(s, TypeError::TypeMismatch { + expected: a.clone(), + found: b.clone(), + })); + } + self.unify(s, i1, i2) + } (Ty::List(i1), Ty::List(i2)) => self.unify(s, i1, i2), (Ty::Tuple(e1), Ty::Tuple(e2)) if e1.len() == e2.len() => { for (t1, t2) in e1.iter().zip(e2.iter()) { @@ -913,7 +924,7 @@ impl TypeChecker { else_branch, } => self.check_if(s, cond, then_branch, else_branch), ExprKind::Region { name, body } => self.check_region(s, name, body), - ExprKind::Borrow(inner) => self.check_borrow(s, inner), + ExprKind::Borrow { inner, mutable } => self.check_borrow(s, inner, *mutable), ExprKind::Drop(inner) => self.check_drop(s, inner), ExprKind::Copy(inner) => self.check_copy(s, inner), ExprKind::StringLen(inner) => self.check_string_len(s, inner), @@ -1189,7 +1200,7 @@ impl TypeChecker { Ok(body_ty) } - fn check_borrow(&mut self, s: Span, inner: &Expr) -> Result { + fn check_borrow(&mut self, s: Span, inner: &Expr, mutable: bool) -> Result { match &inner.kind { ExprKind::Var(name) => { let ty = self @@ -1197,11 +1208,11 @@ impl TypeChecker { .lookup(name) .ok_or_else(|| self.at(s, TypeError::UnboundVariable(name.clone())))? .clone(); - Ok(Ty::Borrow(Box::new(ty))) + Ok(Ty::Borrow { inner: Box::new(ty), mutable }) } _ => { let inner_ty = self.check(inner)?; - Ok(Ty::Borrow(Box::new(inner_ty))) + Ok(Ty::Borrow { inner: Box::new(inner_ty), mutable }) } } } @@ -1234,7 +1245,7 @@ impl TypeChecker { match inner_ty { Ty::String(_) => Ok(Ty::Base(BaseTy::I32)), - Ty::Borrow(ref boxed) => match boxed.as_ref() { + Ty::Borrow { inner: ref boxed, .. } => match boxed.as_ref() { Ty::String(_) => Ok(Ty::Base(BaseTy::I32)), _ => Err(self.at( s, @@ -1680,12 +1691,12 @@ impl TypeChecker { let inner_ty = self.check(inner)?; match inner_ty { - Ty::Borrow(boxed) => Ok(*boxed), + Ty::Borrow { inner: boxed, .. } => Ok(*boxed), Ty::Ref { inner, .. } => Ok(*inner), _ => Err(self.at( s, TypeError::TypeMismatch { - expected: Ty::Borrow(Box::new(Ty::Base(BaseTy::Unit))), + expected: Ty::Borrow { inner: Box::new(Ty::Base(BaseTy::Unit)), mutable: false }, found: inner_ty, }, )), @@ -2532,9 +2543,10 @@ mod tests { tc.ctx .extend("s".into(), Ty::String("r".into()), BindingForm::LetBang); - let borrow_expr = dummy_expr(ExprKind::Borrow(Box::new(dummy_expr(ExprKind::Var( - "s".into(), - ))))); + let borrow_expr = dummy_expr(ExprKind::Borrow { + inner: Box::new(dummy_expr(ExprKind::Var("s".into()))), + mutable: false, + }); let result = tc.check(&borrow_expr); assert!(result.is_ok()); @@ -2550,9 +2562,10 @@ mod tests { tc.ctx .extend("s".into(), Ty::String("r".into()), BindingForm::LetBang); - let borrow_expr = dummy_expr(ExprKind::Borrow(Box::new(dummy_expr(ExprKind::Var( - "s".into(), - ))))); + let borrow_expr = dummy_expr(ExprKind::Borrow { + inner: Box::new(dummy_expr(ExprKind::Var("s".into()))), + mutable: false, + }); assert!(tc.check(&borrow_expr).is_ok()); let drop_expr = dummy_expr(ExprKind::Drop(Box::new(dummy_expr(ExprKind::Var( diff --git a/src/ephapax-wasm/src/lib.rs b/src/ephapax-wasm/src/lib.rs index 6590a85..a949945 100644 --- a/src/ephapax-wasm/src/lib.rs +++ b/src/ephapax-wasm/src/lib.rs @@ -55,8 +55,8 @@ pub use ownership::{ use ephapax_ir::module_from_sexpr; use ephapax_syntax::{ - BaseTy, BinOp, Decl, Expr, ExprKind, ExternItem, Literal, MatchArm, Module as AstModule, - Pattern, Ty, UnaryOp, + BaseTy, BinOp, Decl, Expr, ExprKind, ExternItem, Linearity, Literal, MatchArm, + Module as AstModule, Pattern, Ty, UnaryOp, }; use smol_str::SmolStr; use std::collections::HashMap; @@ -165,6 +165,32 @@ impl std::fmt::Display for CodegenError { impl std::error::Error for CodegenError {} +// --------------------------------------------------------------------------- +// Ty → OwnershipKind mapping +// --------------------------------------------------------------------------- + +/// Classify a parameter `Ty` into its `OwnershipKind` for the +/// `typedwasm.ownership` custom section. +/// +/// Mapping: +/// - `Ty::Borrow { mutable: true, .. }` → `ExclBorrow` (typed-wasm L7 +/// alias-exclusion fires on these) +/// - `Ty::Borrow { mutable: false, .. }` → `SharedBorrow` +/// - `Ty::Ref { linearity: Linear, .. }`, `Ty::String(_)` → `Linear` +/// - `Ty::Region { inner, .. }`, `Ty::ForAll { body, .. }` → recurse +/// - everything else → `Unrestricted` +fn ty_to_ownership_kind(ty: &Ty) -> ownership::OwnershipKind { + match ty { + Ty::Borrow { mutable: true, .. } => ownership::OwnershipKind::ExclBorrow, + Ty::Borrow { mutable: false, .. } => ownership::OwnershipKind::SharedBorrow, + Ty::Ref { linearity: Linearity::Linear, .. } => ownership::OwnershipKind::Linear, + Ty::String(_) => ownership::OwnershipKind::Linear, + Ty::Region { inner, .. } => ty_to_ownership_kind(inner), + Ty::ForAll { body, .. } => ty_to_ownership_kind(body), + _ => ownership::OwnershipKind::Unrestricted, + } +} + // --------------------------------------------------------------------------- // Metadata for a user-defined function // --------------------------------------------------------------------------- @@ -179,10 +205,12 @@ struct UserFnInfo { /// Parameter names (in order) for local binding #[allow(dead_code)] param_names: Vec, - /// Whether each parameter is linear (must-use-once). Drives the + /// Ownership classification for each parameter. Drives the /// `typedwasm.ownership` custom section emitted by - /// [`Codegen::emit_ownership_section`]. - param_linear: Vec, + /// [`Codegen::emit_ownership_section`]. Computed from the + /// parameter's `Ty` at function-collection time via + /// [`ty_to_ownership_kind`]. + param_kinds: Vec, } // --------------------------------------------------------------------------- @@ -579,7 +607,7 @@ impl Codegen { wasm_fn_idx: self.first_user_fn(), wasm_type_idx: TYPE_VOID_VOID, param_names: Vec::new(), - param_linear: Vec::new(), + param_kinds: Vec::new(), }, ); @@ -741,8 +769,8 @@ impl Codegen { let param_names: Vec = params.iter().map(|(n, _)| n.to_string()).collect(); - let param_linear: Vec = - params.iter().map(|(_, ty)| ty.is_linear()).collect(); + let param_kinds: Vec = + params.iter().map(|(_, ty)| ty_to_ownership_kind(ty)).collect(); self.user_fns.insert( name.to_string(), @@ -750,7 +778,7 @@ impl Codegen { wasm_fn_idx: idx, wasm_type_idx: type_idx, param_names, - param_linear, + param_kinds, }, ); idx += 1; @@ -1267,24 +1295,15 @@ impl Codegen { let mut entries: Vec = self .user_fns .values() - .filter(|info| info.param_linear.iter().any(|&l| l)) - .map(|info| { - let param_kinds = info - .param_linear + .filter(|info| { + info.param_kinds .iter() - .map(|&is_linear| { - if is_linear { - ownership::OwnershipKind::Linear - } else { - ownership::OwnershipKind::Unrestricted - } - }) - .collect(); - ownership::OwnershipEntry { - func_idx: info.wasm_fn_idx, - param_kinds, - ret_kind: ownership::OwnershipKind::Unrestricted, - } + .any(|k| !matches!(k, ownership::OwnershipKind::Unrestricted)) + }) + .map(|info| ownership::OwnershipEntry { + func_idx: info.wasm_fn_idx, + param_kinds: info.param_kinds.clone(), + ret_kind: ownership::OwnershipKind::Unrestricted, }) .collect(); if entries.is_empty() { @@ -1728,7 +1747,7 @@ impl Codegen { else_branch, } => self.compile_if(func, cond, then_branch, else_branch), ExprKind::Region { name: _, body } => self.compile_region(func, body), - ExprKind::Borrow(inner) | ExprKind::Deref(inner) => { + ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) => { // Borrow/deref are identity at the WASM level self.compile_expr(func, inner); } @@ -1960,7 +1979,7 @@ impl Codegen { ExprKind::Region { body, .. } => { collect(body, bound, free, seen); } - ExprKind::Borrow(inner) | ExprKind::Deref(inner) | ExprKind::Drop(inner) => { + ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) | ExprKind::Drop(inner) => { collect(inner, bound, free, seen); } ExprKind::StringConcat { left, right } => { @@ -3524,9 +3543,10 @@ mod tests { fn compile_borrow_deref() { let mut codegen = Codegen::new(); // *(&42) - let expr = e(ExprKind::Deref(Box::new(e(ExprKind::Borrow(Box::new(e( - ExprKind::Lit(Literal::I32(42)), - ))))))); + let expr = e(ExprKind::Deref(Box::new(e(ExprKind::Borrow { + inner: Box::new(e(ExprKind::Lit(Literal::I32(42)))), + mutable: false, + })))); let wasm = codegen.compile_program(&expr); assert_wasm_header(&wasm); validate_wasm(&wasm); @@ -4715,6 +4735,74 @@ mod tests { ); } + #[test] + fn ownership_section_emits_shared_borrow_for_borrow_param() { + // fn obs(b: &String) -> I32 = 0 + // &T at the parameter position must surface as SharedBorrow + // (typed-wasm L7 alias-shared check fires on these). + let module = AstModule { + name: "test".into(), + imports: vec![], + decls: vec![Decl::Fn { + name: "obs".into(), + visibility: Visibility::Private, + type_params: vec![], + params: vec![( + "b".into(), + Ty::Borrow { + inner: Box::new(Ty::String("r".into())), + mutable: false, + }, + )], + ret_ty: Ty::Base(BaseTy::I32), + body: e(ExprKind::Lit(Literal::I32(0))), + }], + }; + let wasm = compile_module(&module).expect("compilation failed"); + let payload = ownership_payload(&wasm).expect("ownership section missing"); + let entries = ownership::parse_ownership_section_payload(&payload); + assert_eq!(entries.len(), 1); + assert_eq!( + entries[0].param_kinds, + vec![ownership::OwnershipKind::SharedBorrow], + "&T must classify as SharedBorrow, not Unrestricted (#221)" + ); + } + + #[test] + fn ownership_section_emits_excl_borrow_for_mut_borrow_param() { + // fn write(b: &mut String) -> I32 = 0 + // &mut T must classify as ExclBorrow — closes the L7 + // alias-exclusion enforcement gap on the ephapax producer side. + let module = AstModule { + name: "test".into(), + imports: vec![], + decls: vec![Decl::Fn { + name: "write".into(), + visibility: Visibility::Private, + type_params: vec![], + params: vec![( + "b".into(), + Ty::Borrow { + inner: Box::new(Ty::String("r".into())), + mutable: true, + }, + )], + ret_ty: Ty::Base(BaseTy::I32), + body: e(ExprKind::Lit(Literal::I32(0))), + }], + }; + let wasm = compile_module(&module).expect("compilation failed"); + let payload = ownership_payload(&wasm).expect("ownership section missing"); + let entries = ownership::parse_ownership_section_payload(&payload); + assert_eq!(entries.len(), 1); + assert_eq!( + entries[0].param_kinds, + vec![ownership::OwnershipKind::ExclBorrow], + "&mut T must classify as ExclBorrow (#221)" + ); + } + // ----- Nested constructor patterns in arm args (#68) ----- /// `Some(Some(v))` — nested constructor in an arm arg. Outer