Skip to content

Commit 7f27aa8

Browse files
feat(codegen): surface &T / &mut T as SharedBorrow / ExclBorrow (closes #221) (#255)
## Summary - Extends `Ty::Borrow` and `ExprKind::Borrow` with `mutable: bool` (mirrored on the `Surface*` enums); parser recognises `&mut` via a `mut_marker` atomic rule + `mut` keyword. - Adds `ty_to_ownership_kind` helper that maps `&T → SharedBorrow`, `&mut T → ExclBorrow`, linear `Ref`/`String → Linear`, else `Unrestricted`. Replaces `UserFnInfo.param_linear: Vec<bool>` with `param_kinds: Vec<OwnershipKind>`. - `emit_ownership_section` broadens its filter from "any linear" to "any non-Unrestricted" so shared-borrow-only signatures still emit an entry. - s-expr IR encoder uses `borrow` vs `borrow-mut` discriminator atoms to preserve mutability through the IR round-trip. Closes the producer-side gap flagged in both 2026-05-30 reviews of typed-wasm proposal 0001 — `SharedBorrow` + `ExclBorrow` were defined in `ownership.rs` but never emitted, so the verifier's L7 alias-exclusion check fired zero times against ephapax-emitted modules. ## Test plan - [x] `cargo test --workspace` green (all suites) - [x] `test_parse_mut_borrow` — `&mut x` parses with `mutable: true` - [x] `ownership_section_emits_shared_borrow_for_borrow_param` - [x] `ownership_section_emits_excl_borrow_for_mut_borrow_param` - [x] Existing ownership round-trip + linear-param tests unchanged and green - [x] Example `examples/linear/16-shared-vs-exclusive-borrow.eph` demonstrates both polarities ## Out of scope - Mutability is currently passed unchecked from parser → AST → codegen; the linear/affine checkers walk borrow children but don't yet enforce alias-exclusion at compile time. The verifier-side L7 check (typed-wasm) now has signal to enforce; ephapax-side static enforcement is a follow-up. Closes #221. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ff1ab29 commit 7f27aa8

18 files changed

Lines changed: 316 additions & 108 deletions

File tree

ephapax-linear/src/affine.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ impl AffineChecker {
239239
self.ctx.exit_region();
240240
}
241241

242-
ExprKind::Borrow(inner) | ExprKind::Deref(inner) => {
242+
ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) => {
243243
self.walk_expr(inner);
244244
}
245245

ephapax-linear/src/linear.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ impl LinearChecker {
288288
}
289289

290290
// --- Borrow/Deref: walk inner ---
291-
ExprKind::Borrow(inner) | ExprKind::Deref(inner) => {
291+
ExprKind::Borrow { inner, .. } | ExprKind::Deref(inner) => {
292292
self.walk_expr(inner);
293293
}
294294

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
//
4+
// Example: shared (`&`) vs exclusive (`&mut`) borrow parameters.
5+
//
6+
// At codegen these surface in `typedwasm.ownership` as `SharedBorrow`
7+
// and `ExclBorrow` respectively. The L7 alias-exclusion verifier pass
8+
// in typed-wasm fires on `ExclBorrow` parameters used more than once.
9+
10+
fn observe(buf: &String@r) -> I32 {
11+
// `buf` is a shared borrow — multiple shared borrows of the same
12+
// backing object may coexist. Emitted as SharedBorrow.
13+
0
14+
}
15+
16+
fn mutate(buf: &mut String@r) -> I32 {
17+
// `buf` is an exclusive borrow — no other live borrow of the same
18+
// backing object is permitted. Emitted as ExclBorrow.
19+
0
20+
}

src/ephapax-analysis/src/escape.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ impl EscapeAnalysis {
114114
Self::analyze_expr(inner, escaping, in_escaping_context);
115115
}
116116

117-
ExprKind::Borrow(inner) => {
117+
ExprKind::Borrow { inner, .. } => {
118118
Self::analyze_expr(inner, escaping, in_escaping_context);
119119
}
120120

src/ephapax-analysis/src/free_vars.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ impl FreeVarAnalysis {
123123

124124
ExprKind::Drop(inner)
125125
| ExprKind::Copy(inner)
126-
| ExprKind::Borrow(inner)
126+
| ExprKind::Borrow { inner, .. }
127127
| ExprKind::Deref(inner)
128128
| ExprKind::Fst(inner)
129129
| ExprKind::Snd(inner)

src/ephapax-analysis/src/liveness.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ impl LivenessAnalysis {
128128

129129
ExprKind::Drop(inner)
130130
| ExprKind::Copy(inner)
131-
| ExprKind::Borrow(inner)
131+
| ExprKind::Borrow { inner, .. }
132132
| ExprKind::Deref(inner)
133133
| ExprKind::Fst(inner)
134134
| ExprKind::Snd(inner)

src/ephapax-desugar/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,10 @@ impl Desugarer {
454454
body: Box::new(self.desugar_expr(body)?),
455455
},
456456

457-
SurfaceExprKind::Borrow(inner) => ExprKind::Borrow(Box::new(self.desugar_expr(inner)?)),
457+
SurfaceExprKind::Borrow { inner, mutable } => ExprKind::Borrow {
458+
inner: Box::new(self.desugar_expr(inner)?),
459+
mutable: *mutable,
460+
},
458461
SurfaceExprKind::Deref(inner) => ExprKind::Deref(Box::new(self.desugar_expr(inner)?)),
459462
SurfaceExprKind::Drop(inner) => ExprKind::Drop(Box::new(self.desugar_expr(inner)?)),
460463
SurfaceExprKind::Copy(inner) => ExprKind::Copy(Box::new(self.desugar_expr(inner)?)),
@@ -553,7 +556,10 @@ impl Desugarer {
553556
name: name.clone(),
554557
inner: Box::new(self.desugar_ty(inner)?),
555558
}),
556-
SurfaceTy::Borrow(inner) => Ok(Ty::Borrow(Box::new(self.desugar_ty(inner)?))),
559+
SurfaceTy::Borrow { inner, mutable } => Ok(Ty::Borrow {
560+
inner: Box::new(self.desugar_ty(inner)?),
561+
mutable: *mutable,
562+
}),
557563
SurfaceTy::Var(v) => Ok(Ty::Var(v.clone())),
558564
SurfaceTy::List(inner) => Ok(Ty::List(Box::new(self.desugar_ty(inner)?))),
559565
SurfaceTy::Tuple(elements) => {

src/ephapax-interp/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ impl Value {
213213
param: Box::new(param_ty.clone()),
214214
ret: Box::new(Ty::Base(BaseTy::Unit)), // Unknown without evaluation
215215
},
216-
Value::Borrow(inner) => Ty::Borrow(Box::new(inner.to_type())),
216+
Value::Borrow(inner) => Ty::Borrow { inner: Box::new(inner.to_type()), mutable: false },
217217
}
218218
}
219219
}
@@ -436,7 +436,7 @@ impl Interpreter {
436436
else_branch,
437437
} => self.eval_if(cond, then_branch, else_branch),
438438
ExprKind::Region { name, body } => self.eval_region(name, body),
439-
ExprKind::Borrow(inner) => self.eval_borrow(inner),
439+
ExprKind::Borrow { inner, .. } => self.eval_borrow(inner),
440440
ExprKind::Deref(inner) => self.eval_deref(inner),
441441
ExprKind::Drop(inner) => self.eval_drop(inner),
442442
ExprKind::Copy(inner) => self.eval_copy(inner),

src/ephapax-ir/src/lib.rs

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,9 +503,10 @@ fn expr_to_sexpr(expr: &Expr) -> SExpr {
503503
SExpr::Atom(escape_atom(name)),
504504
expr_to_sexpr(body),
505505
]),
506-
ExprKind::Borrow(inner) => {
507-
SExpr::List(vec![SExpr::Atom("borrow".into()), expr_to_sexpr(inner)])
508-
}
506+
ExprKind::Borrow { inner, mutable } => SExpr::List(vec![
507+
SExpr::Atom(if *mutable { "borrow-mut" } else { "borrow" }.into()),
508+
expr_to_sexpr(inner),
509+
]),
509510
ExprKind::Deref(inner) => {
510511
SExpr::List(vec![SExpr::Atom("deref".into()), expr_to_sexpr(inner)])
511512
}
@@ -735,7 +736,14 @@ fn decode_expr(expr: &SExpr) -> Result<Expr, SExprError> {
735736
name: SmolStr::new(atom_string(&list[1])?),
736737
body: Box::new(decode_expr(&list[2])?),
737738
},
738-
"borrow" => ExprKind::Borrow(Box::new(decode_expr(&list[1])?)),
739+
"borrow" => ExprKind::Borrow {
740+
inner: Box::new(decode_expr(&list[1])?),
741+
mutable: false,
742+
},
743+
"borrow-mut" => ExprKind::Borrow {
744+
inner: Box::new(decode_expr(&list[1])?),
745+
mutable: true,
746+
},
739747
"deref" => ExprKind::Deref(Box::new(decode_expr(&list[1])?)),
740748
"drop" => ExprKind::Drop(Box::new(decode_expr(&list[1])?)),
741749
"copy" => ExprKind::Copy(Box::new(decode_expr(&list[1])?)),
@@ -851,7 +859,10 @@ fn ty_to_sexpr(ty: &Ty) -> SExpr {
851859
SExpr::Atom(escape_atom(name)),
852860
ty_to_sexpr(inner),
853861
]),
854-
Ty::Borrow(inner) => SExpr::List(vec![SExpr::Atom("borrow".into()), ty_to_sexpr(inner)]),
862+
Ty::Borrow { inner, mutable } => SExpr::List(vec![
863+
SExpr::Atom(if *mutable { "borrow-mut" } else { "borrow" }.into()),
864+
ty_to_sexpr(inner),
865+
]),
855866
Ty::Var(v) => SExpr::List(vec![SExpr::Atom("var".into()), SExpr::Atom(escape_atom(v))]),
856867
Ty::List(inner) => SExpr::List(vec![SExpr::Atom("list".into()), ty_to_sexpr(inner)]),
857868
Ty::Tuple(elem_types) => {
@@ -926,7 +937,14 @@ fn decode_ty(expr: &SExpr) -> Result<Ty, SExprError> {
926937
name: SmolStr::new(atom_string(&list[1])?),
927938
inner: Box::new(decode_ty(&list[2])?),
928939
}),
929-
"borrow" => Ok(Ty::Borrow(Box::new(decode_ty(&list[1])?))),
940+
"borrow" => Ok(Ty::Borrow {
941+
inner: Box::new(decode_ty(&list[1])?),
942+
mutable: false,
943+
}),
944+
"borrow-mut" => Ok(Ty::Borrow {
945+
inner: Box::new(decode_ty(&list[1])?),
946+
mutable: true,
947+
}),
930948
"var" => Ok(Ty::Var(SmolStr::new(atom_string(&list[1])?))),
931949
_ => Err(SExprError::Invalid("unknown type tag".into())),
932950
}

src/ephapax-lsp/src/main.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -701,7 +701,7 @@ fn format_ty(ty: &Ty) -> String {
701701
Ty::Ref { inner, .. } => format!("Ref({})", format_ty(inner)),
702702
Ty::String(region) => format!("String@{}", region),
703703
Ty::Region { name, inner } => format!("Region({}, {})", name, format_ty(inner)),
704-
Ty::Borrow(inner) => format!("&{}", format_ty(inner)),
704+
Ty::Borrow { inner, mutable } => format!("&{}{}", if *mutable { "mut " } else { "" }, format_ty(inner)),
705705
Ty::Var(name) => name.to_string(),
706706
Ty::ForAll { var, body } => format!("forall {}. {}", var, format_ty(body)),
707707
Ty::Unif(id) => format!("?{}", id),
@@ -847,7 +847,7 @@ fn find_let_binding_span(expr: &Expr, target: &str) -> Option<Span> {
847847
| ExprKind::Inr { value: inner, .. }
848848
| ExprKind::Drop(inner)
849849
| ExprKind::Copy(inner)
850-
| ExprKind::Borrow(inner)
850+
| ExprKind::Borrow { inner, .. }
851851
| ExprKind::Deref(inner)
852852
| ExprKind::UnaryOp { operand: inner, .. }
853853
| ExprKind::StringLen(inner) => find_let_binding_span(inner, target),

0 commit comments

Comments
 (0)