plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


184c054 — Peter John 2026-11-03T16:59:10+05:30
Tail calls: self tail calls as loops, return_call, tail modulo cons; break depth fix
plum-cli/src/main.rs CHANGED
@@ -794,9 +794,11 @@ fn cmdRun(file: std::path::PathBuf, lib_path: std::path::PathBuf) -> Result<()>
794
794
  // GC + typed function references are required to load modules containing
795
795
  // Plum's structs/enums/strings/closures (see docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
796
796
  let mut config = wasmtime::Config::new();
797
- config.wasm_tail_call(true);
798
797
  config.wasm_gc(true);
799
798
  config.wasm_function_references(true);
799
+ config.wasm_tail_call(true);
800
+ config.max_wasm_stack(4 << 20);
801
+ config.async_stack_size(8 << 20);
800
802
  let engine = wasmtime::Engine::new(&config)
801
803
  .map_err(|e| anyhow::anyhow!("failed to construct wasmtime engine: {e}"))?;
802
804
 
@@ -947,6 +949,9 @@ fn runTestFile(file: &std::path::Path, lib_path: &std::path::Path) -> Result<(us
947
949
  let mut config = wasmtime::Config::new();
948
950
  config.wasm_gc(true);
949
951
  config.wasm_function_references(true);
952
+ config.wasm_tail_call(true);
953
+ config.max_wasm_stack(4 << 20);
954
+ config.async_stack_size(8 << 20);
950
955
  let engine = wasmtime::Engine::new(&config)
951
956
  .map_err(|e| anyhow::anyhow!("failed to construct wasmtime engine: {e}"))?;
952
957
 
plum-core/src/ast.rs CHANGED
@@ -724,6 +724,21 @@ pub struct FnCall {
724
724
  /// `obj.name(...)`), for diagnostics. `(0, 0)` for a synthetic call.
725
725
  pub line: usize,
726
726
  pub column: usize,
727
+ /// Set by `tail::markTailCalls`: whether (and how) this call is in tail position.
728
+ pub tail: TailKind,
729
+ }
730
+
731
+ /// How a call in tail position is made (see `tail.rs`). `NotTail` is the default.
732
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
733
+ pub enum TailKind {
734
+ #[default]
735
+ NotTail,
736
+ /// A call to the enclosing fn itself (`self.f(..)` for a method).
737
+ SelfCall,
738
+ /// A tail call to another fn or method.
739
+ Other,
740
+ /// A tail call of a closure value held in a local or parameter.
741
+ Closure,
727
742
  }
728
743
 
729
744
  #[derive(Debug, Clone, PartialEq)]
plum-core/src/lib.rs CHANGED
@@ -10,7 +10,9 @@ pub mod formatter;
10
10
  pub mod forms;
11
11
  pub mod loader;
12
12
  pub mod parser;
13
+ pub mod tail;
13
14
  pub mod traits;
15
+ pub mod trmc;
14
16
  pub mod union_lower;
15
17
  pub mod unions;
16
18
 
plum-core/src/tail.rs ADDED
@@ -0,0 +1,265 @@
1
+ //! Marks calls in tail position (`FnCall::tail`). Runs on the fully lowered
2
+ //! program, just before codegen.
3
+ //!
4
+ //! Tail position: the value of a fn body, the last expression of a block in tail
5
+ //! position, each `if`/`match`/inline-`if` arm in tail position, and `return e`.
6
+ //! Not tail: under `try`/`!`/`catch`, as an argument or operand, under a loop,
7
+ //! inside a `test` (desugared `__test_N` fns), or in an `export fn`. A call inside
8
+ //! a closure body is tail relative to the closure only.
9
+ use crate::ast::*;
10
+ use std::collections::HashSet;
11
+
12
+ #[derive(Debug, Clone, PartialEq)]
13
+ pub struct TailSite {
14
+ pub callee: String,
15
+ pub kind: TailKind,
16
+ pub line: usize,
17
+ }
18
+
19
+ /// Marks every tail call of every fn in `src`.
20
+ pub fn markTailCalls(src: &mut Source) {
21
+ for item in &mut src.items {
22
+ if let Item::Fn(f) = item {
23
+ markFn(f);
24
+ }
25
+ }
26
+ }
27
+
28
+ /// The tail calls of `f` (as `markTailCalls` would mark them), in source order.
29
+ pub fn tailCalls(f: &Fn) -> Vec<TailSite> {
30
+ markFn(&mut f.clone())
31
+ }
32
+
33
+ struct Ctx {
34
+ name: String,
35
+ isMethod: bool,
36
+ hasSelf: bool,
37
+ locals: HashSet<String>,
38
+ sites: Vec<TailSite>,
39
+ }
40
+
41
+ fn markFn(f: &mut Fn) -> Vec<TailSite> {
42
+ let skip = f.is_export || f.name.starts_with("__test_");
43
+ let mut cx = Ctx {
44
+ name: f.name.clone(),
45
+ isMethod: f.type_param.is_some(),
46
+ hasSelf: f.has_self,
47
+ locals: f.params.iter().map(|p| p.name.clone()).collect(),
48
+ sites: Vec::new(),
49
+ };
50
+ for p in &mut f.params {
51
+ if let Some(d) = &mut p.default {
52
+ expr(&mut cx, d, false);
53
+ }
54
+ }
55
+ match &mut f.body {
56
+ FnBody::Expr(e) => expr(&mut cx, e, !skip),
57
+ FnBody::Block(b) => block(&mut cx, b, !skip, !skip),
58
+ FnBody::Extern => {}
59
+ }
60
+ cx.sites
61
+ }
62
+
63
+ fn block(cx: &mut Ctx, b: &mut Block, tail: bool, ret: bool) {
64
+ let n = b.stmts.len();
65
+ for (i, s) in b.stmts.iter_mut().enumerate() {
66
+ stmt(cx, s, tail && i + 1 == n, ret);
67
+ }
68
+ }
69
+
70
+ fn stmt(cx: &mut Ctx, s: &mut Stmt, tail: bool, ret: bool) {
71
+ match s {
72
+ Stmt::Assign(a) => {
73
+ for t in &mut a.targets {
74
+ match t {
75
+ AssignTarget::Var(n) => {
76
+ cx.locals.insert(n.clone());
77
+ }
78
+ AssignTarget::Field(o, _) => expr(cx, o, false),
79
+ }
80
+ }
81
+ for v in &mut a.values {
82
+ expr(cx, v, false);
83
+ }
84
+ }
85
+ Stmt::Break | Stmt::Continue | Stmt::Todo => {}
86
+ Stmt::Assert(c) => expr(cx, &mut c.cond, false),
87
+ Stmt::ForLoop(l) => {
88
+ cx.locals.extend(l.binders.iter().cloned());
89
+ expr(cx, &mut l.source, false);
90
+ for c in &mut l.clauses {
91
+ match c {
92
+ ForClause::If(e) => expr(cx, e, false),
93
+ ForClause::Bind(n, e) => {
94
+ cx.locals.insert(n.clone());
95
+ expr(cx, e, false)
96
+ }
97
+ }
98
+ }
99
+ block(cx, &mut l.body, false, false);
100
+ }
101
+ Stmt::While(w) => {
102
+ expr(cx, &mut w.condition, false);
103
+ block(cx, &mut w.body, false, false);
104
+ }
105
+ Stmt::If(i) => {
106
+ expr(cx, &mut i.condition, false);
107
+ block(cx, &mut i.body, tail, ret);
108
+ for ei in &mut i.else_ifs {
109
+ expr(cx, &mut ei.condition, false);
110
+ block(cx, &mut ei.body, tail, ret);
111
+ }
112
+ if let Some(e) = &mut i.else_ {
113
+ block(cx, e, tail, ret);
114
+ }
115
+ }
116
+ Stmt::Match(m) => {
117
+ for s in &mut m.subjects {
118
+ expr(cx, s, false);
119
+ }
120
+ for c in &mut m.cases {
121
+ case(cx, c, tail, ret);
122
+ }
123
+ }
124
+ Stmt::Return(e) => {
125
+ if let Some(e) = e {
126
+ expr(cx, e, ret);
127
+ }
128
+ }
129
+ Stmt::Crash(e) => expr(cx, e, false),
130
+ Stmt::Expr(e, _, _) => expr(cx, e, tail),
131
+ }
132
+ }
133
+
134
+ fn case(cx: &mut Ctx, c: &mut Case, tail: bool, ret: bool) {
135
+ for p in &c.patterns {
136
+ pattern(cx, p);
137
+ }
138
+ if let Some(g) = &mut c.guard {
139
+ expr(cx, g, false);
140
+ }
141
+ block(cx, &mut c.body, tail, ret);
142
+ }
143
+
144
+ fn pattern(cx: &mut Ctx, p: &CasePattern) {
145
+ match p {
146
+ CasePattern::Name(n) => {
147
+ cx.locals.insert(n.clone());
148
+ }
149
+ CasePattern::Class { fields, .. } => fields.iter().for_each(|f| pattern(cx, f)),
150
+ _ => {}
151
+ }
152
+ }
153
+
154
+ fn args(cx: &mut Ctx, a: &mut [Arg]) {
155
+ for x in a {
156
+ match x {
157
+ Arg::Positional(e) | Arg::Keyword { value: e, .. } => expr(cx, e, false),
158
+ }
159
+ }
160
+ }
161
+
162
+ fn expr(cx: &mut Ctx, e: &mut Expr, tail: bool) {
163
+ match e {
164
+ Expr::FnCall(c) => {
165
+ args(cx, &mut c.args);
166
+ c.tail = if !tail {
167
+ TailKind::NotTail
168
+ } else if cx.locals.contains(&c.name) {
169
+ TailKind::Closure
170
+ } else if c.name == cx.name && !cx.isMethod {
171
+ TailKind::SelfCall
172
+ } else {
173
+ TailKind::Other
174
+ };
175
+ site(cx, c);
176
+ }
177
+ Expr::Attribute(a) => {
178
+ expr(cx, &mut a.object, false);
179
+ if let AttrKind::Method(c) = &mut a.attr {
180
+ args(cx, &mut c.args);
181
+ let onSelf = matches!(a.object, Expr::Self_);
182
+ c.tail = if !tail {
183
+ TailKind::NotTail
184
+ } else if onSelf && cx.hasSelf && c.name == cx.name && cx.isMethod {
185
+ TailKind::SelfCall
186
+ } else {
187
+ TailKind::Other
188
+ };
189
+ site(cx, c);
190
+ }
191
+ }
192
+ Expr::Paren(x) => expr(cx, x, tail),
193
+ Expr::InlineIf(i) => {
194
+ expr(cx, &mut i.condition, false);
195
+ expr(cx, &mut i.then, tail);
196
+ expr(cx, &mut i.else_, tail);
197
+ }
198
+ Expr::Binary(b) => {
199
+ expr(cx, &mut b.left, false);
200
+ expr(cx, &mut b.right, false);
201
+ }
202
+ Expr::Unary(u) => expr(cx, &mut u.operand, false),
203
+ Expr::Bool(b) => {
204
+ expr(cx, &mut b.left, false);
205
+ expr(cx, &mut b.right, false);
206
+ }
207
+ Expr::Compare(c) => {
208
+ expr(cx, &mut c.left, false);
209
+ expr(cx, &mut c.right, false);
210
+ }
211
+ Expr::Not(x) | Expr::Try(x) => expr(cx, x, false),
212
+ Expr::Bang(b) => expr(cx, &mut b.value, false),
213
+ Expr::Catch(c) => {
214
+ expr(cx, &mut c.value, false);
215
+ match &mut c.handler {
216
+ CatchHandler::Default(d) => expr(cx, d, false),
217
+ CatchHandler::Block { binder, body } => {
218
+ cx.locals.insert(binder.clone());
219
+ block(cx, body, false, false);
220
+ }
221
+ }
222
+ }
223
+ Expr::Bind(b) => {
224
+ pattern(cx, &b.case.patterns[0]);
225
+ expr(cx, &mut b.value, false);
226
+ if let Some(g) = &mut b.case.guard {
227
+ expr(cx, g, false);
228
+ }
229
+ }
230
+ Expr::Yield(v) => v.iter_mut().for_each(|x| expr(cx, x, false)),
231
+ Expr::ClassCall(c) => {
232
+ for f in &mut c.fields {
233
+ expr(cx, &mut f.value, false);
234
+ }
235
+ if let Some(s) = &mut c.spread {
236
+ expr(cx, s, false);
237
+ }
238
+ }
239
+ Expr::String(s) => {
240
+ for p in &mut s.parts {
241
+ if let StringPart::Interp(x) = p {
242
+ expr(cx, x, false);
243
+ }
244
+ }
245
+ }
246
+ Expr::Closure(c) => {
247
+ // Tail relative to the closure only; its params are locals.
248
+ cx.locals.extend(c.params.iter().cloned());
249
+ block(cx, &mut c.body, true, true);
250
+ }
251
+ Expr::ErrorValue(_)
252
+ | Expr::Int(_)
253
+ | Expr::Float(_)
254
+ | Expr::BoolLit(_)
255
+ | Expr::Self_
256
+ | Expr::Var(_)
257
+ | Expr::TypeName(_) => {}
258
+ }
259
+ }
260
+
261
+ fn site(cx: &mut Ctx, c: &FnCall) {
262
+ if c.tail != TailKind::NotTail {
263
+ cx.sites.push(TailSite { callee: c.name.clone(), kind: c.tail, line: c.line });
264
+ }
265
+ }
plum-core/src/trmc.rs ADDED
@@ -0,0 +1,446 @@
1
+ //! Tail recursion modulo cons. Rewrites a free fn whose tail position builds an
2
+ //! inline-variant cell around exactly one direct self call,
3
+ //!
4
+ //! ```text
5
+ //! fun map(xs, f) -> Link[B] = match xs
6
+ //! Nil => Nil
7
+ //! Cons(h, t) => Cons(head: f(h), tail: map(t, f))
8
+ //! ```
9
+ //!
10
+ //! into plain loops (no new AST node kinds, no codegen support needed). The first
11
+ //! loop is the original body with each recursive cell pushed onto an accumulator
12
+ //! chain (`__acc = Cons(head: .., tail: __acc)`) and the parameters reassigned
13
+ //! for the next iteration; a base case stores its value in `__r` and clears `__go` (not a `break`:
14
+ //! a `break` inside an `if` inside a `match` arm inside a `while` hangs today). The second loop rebuilds the cells in order around `__r`, last cell
15
+ //! first, so the result is the list the recursion would have produced.
16
+ //!
17
+ //! The classic form (allocate each cell with a hole and write it next iteration)
18
+ //! needs field assignment on the cell, which the checker does not allow on the
19
+ //! hidden records behind inline variants; this form needs none, at the price of
20
+ //! allocating each cell twice. Evaluation order is unchanged: fields before the
21
+ //! recursive one, then the recursive call's arguments, in source order.
22
+ //!
23
+ //! Only rewrites free, non-export fns whose return type is an enum with an inline
24
+ //! variant and a nullary variant, with exactly one recursive field; fields after
25
+ //! the recursive one must be plain variables or literals. Nothing under
26
+ //! `try`/`!`/`catch`, closures or `yield`, no other `return`s outside tail
27
+ //! position, no variadic params.
28
+ use crate::ast::*;
29
+
30
+ /// Rewrites every eligible fn of `src`; returns their names.
31
+ pub fn rewrite(src: &mut Source) -> Vec<String> {
32
+ let enums: Vec<Enum> = src
33
+ .items
34
+ .iter()
35
+ .filter_map(|i| if let Item::Enum(e) = i { Some(e.clone()) } else { None })
36
+ .collect();
37
+ let mut done = Vec::new();
38
+ for item in &mut src.items {
39
+ if let Item::Fn(f) = item {
40
+ if let Some(nf) = rewriteFn(f, &enums) {
41
+ *f = nf;
42
+ done.push(f.name.clone());
43
+ }
44
+ }
45
+ }
46
+ done
47
+ }
48
+
49
+ struct Cx<'a> {
50
+ f: &'a Fn,
51
+ enums: &'a [Enum],
52
+ ret: Type,
53
+ /// The cons variant (record enum) and its declared field names.
54
+ cons: Option<(String, Vec<String>, usize)>,
55
+ nullary: String,
56
+ /// The variant of the return enum that wraps the cons record (`Link__Cons`).
57
+ wrapper: Option<String>,
58
+ consGenerics: Vec<Type>,
59
+ counter: usize,
60
+ found: bool,
61
+ }
62
+
63
+ fn rewriteFn(f: &Fn, enums: &[Enum]) -> Option<Fn> {
64
+ if f.is_export
65
+ || f.is_extern
66
+ || f.is_star
67
+ || f.type_param.is_some()
68
+ || f.has_self
69
+ || f.yields.is_some()
70
+ || f.name.starts_with("__test_")
71
+ {
72
+ return None;
73
+ }
74
+ if f.params.iter().any(|p| {
75
+ p.isOwn || p.default.is_some() || !matches!(p.ty, ParamType::Type(_) | ParamType::Fn(..))
76
+ }) {
77
+ return None;
78
+ }
79
+ let ret = f.returns.clone()?;
80
+ let owner = enums.iter().find(|e| e.name == ret.name && e.inlineOf.is_none())?;
81
+ let nullary = owner
82
+ .variants
83
+ .iter()
84
+ .find(|v| v.member.is_none() && v.fields.is_empty())?
85
+ .name
86
+ .clone();
87
+ let FnBody::Block(body) = &f.body else {
88
+ // `= if .. then .. else ..` bodies are expressions; handled below.
89
+ return rewriteExprBody(f, enums, ret, nullary);
90
+ };
91
+ let dbg = format!("{:?}", body);
92
+ if bannedText(&dbg) {
93
+ return None;
94
+ }
95
+ let mut cx = Cx { f, enums, ret, cons: None, nullary, wrapper: None, consGenerics: vec![], counter: 0, found: false };
96
+ let loopBody = cx.tailBlock(body.clone())?;
97
+ cx.finish(loopBody)
98
+ }
99
+
100
+ fn rewriteExprBody(f: &Fn, enums: &[Enum], ret: Type, nullary: String) -> Option<Fn> {
101
+ let FnBody::Expr(e) = &f.body else { return None };
102
+ if bannedText(&format!("{:?}", e)) {
103
+ return None;
104
+ }
105
+ let mut cx = Cx { f, enums, ret, cons: None, nullary, wrapper: None, consGenerics: vec![], counter: 0, found: false };
106
+ let stmts = cx.tailExpr(e.clone())?;
107
+ cx.finish(Block { stmts })
108
+ }
109
+
110
+ fn bannedText(dbg: &str) -> bool {
111
+ ["Try(", "Bang(", "Catch(", "Yield(", "Closure(", "Return(", "Todo", "Crash("]
112
+ .iter()
113
+ .any(|b| dbg.contains(b))
114
+ }
115
+
116
+ fn wrap(w: &Option<String>, e: Expr) -> Expr {
117
+ match w {
118
+ Some(w) => Expr::FnCall(FnCall {
119
+ name: w.clone(),
120
+ args: vec![Arg::Positional(e)],
121
+ line: 0,
122
+ column: 0,
123
+ tail: TailKind::NotTail,
124
+ }),
125
+ None => e,
126
+ }
127
+ }
128
+
129
+ fn var(n: &str) -> Expr {
130
+ Expr::Var(n.to_string())
131
+ }
132
+
133
+ fn declare(n: &str, ty: Option<Type>, e: Expr) -> Stmt {
134
+ Stmt::Assign(Assign {
135
+ targets: vec![AssignTarget::Var(n.to_string())],
136
+ values: vec![e],
137
+ declare: true,
138
+ ty,
139
+ })
140
+ }
141
+
142
+ fn set(n: &str, e: Expr) -> Stmt {
143
+ Stmt::Assign(Assign {
144
+ targets: vec![AssignTarget::Var(n.to_string())],
145
+ values: vec![e],
146
+ declare: false,
147
+ ty: None,
148
+ })
149
+ }
150
+
151
+ fn pure(e: &Expr) -> bool {
152
+ match e {
153
+ Expr::Var(_) | Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_) => true,
154
+ Expr::Paren(x) => pure(x),
155
+ _ => false,
156
+ }
157
+ }
158
+
159
+ fn mentions(e: &Expr, name: &str) -> bool {
160
+ format!("{:?}", e).contains(&format!("name: {:?}", name))
161
+ }
162
+
163
+ impl<'a> Cx<'a> {
164
+ fn fresh(&mut self, tag: &str) -> String {
165
+ self.counter += 1;
166
+ format!("__tc_{}{}", tag, self.counter)
167
+ }
168
+
169
+ /// Assembles the rewritten fn around the transformed body `loopBody`.
170
+ fn finish(self, loopBody: Block) -> Option<Fn> {
171
+ if !self.found {
172
+ return None;
173
+ }
174
+ let (cons, fields, rec) = self.cons.clone()?;
175
+ let mut stmts = vec![
176
+ declare("__tc_acc", Some(self.ret.clone()), Expr::TypeName(self.nullary.clone())),
177
+ declare("__tc_r", Some(self.ret.clone()), Expr::TypeName(self.nullary.clone())),
178
+ declare("__tc_go", None, Expr::Int(1)),
179
+ Stmt::While(While {
180
+ condition: Expr::Compare(Box::new(CompareExpr {
181
+ op: CmpOp::Gt,
182
+ left: var("__tc_go"),
183
+ right: Expr::Int(0),
184
+ })),
185
+ body: loopBody,
186
+ }),
187
+ ];
188
+ // Rebuild: for every accumulated cell, last first, wrap `__tc_r` in a fresh one.
189
+ let binders: Vec<String> = (0..fields.len()).map(|i| format!("__tc_p{i}")).collect();
190
+ let args: Vec<FieldArg> = fields
191
+ .iter()
192
+ .zip(&binders)
193
+ .enumerate()
194
+ .map(|(i, (n, b))| FieldArg {
195
+ name: n.clone(),
196
+ value: if i == rec { var("__tc_r") } else { var(b) },
197
+ })
198
+ .collect();
199
+ let rebuild = Case {
200
+ patterns: vec![{
201
+ let inner = CasePattern::Class {
202
+ name: cons.clone(),
203
+ fields: binders.iter().map(|b| CasePattern::Name(b.clone())).collect(),
204
+ };
205
+ match &self.wrapper {
206
+ Some(w) => CasePattern::Class { name: w.clone(), fields: vec![inner] },
207
+ None => inner,
208
+ }
209
+ }],
210
+ guard: None,
211
+ body: Block {
212
+ stmts: vec![
213
+ set(
214
+ "__tc_r",
215
+ wrap(
216
+ &self.wrapper,
217
+ Expr::ClassCall(ClassCall {
218
+ type_name: cons,
219
+ fields: args,
220
+ generics: self.consGenerics.clone(),
221
+ spread: None,
222
+ }),
223
+ ),
224
+ ),
225
+ set("__tc_acc", var(&binders[rec])),
226
+ ],
227
+ },
228
+ };
229
+ let stop = Case {
230
+ patterns: vec![CasePattern::Wildcard],
231
+ guard: None,
232
+ body: Block { stmts: vec![Stmt::Break] },
233
+ };
234
+ stmts.push(Stmt::While(While {
235
+ condition: Expr::BoolLit(true),
236
+ body: Block {
237
+ stmts: vec![Stmt::Match(Match {
238
+ subjects: vec![var("__tc_acc")],
239
+ cases: vec![rebuild, stop],
240
+ })],
241
+ },
242
+ }));
243
+ stmts.push(Stmt::Return(Some(var("__tc_r"))));
244
+ let mut nf = self.f.clone();
245
+ nf.body = FnBody::Block(Block { stmts });
246
+ Some(nf)
247
+ }
248
+
249
+ // ---- tail-position walk ----
250
+
251
+ fn tailBlock(&mut self, b: Block) -> Option<Block> {
252
+ let n = b.stmts.len();
253
+ let mut out = Vec::new();
254
+ for (i, s) in b.stmts.into_iter().enumerate() {
255
+ if i + 1 == n {
256
+ out.extend(self.tailStmt(s)?);
257
+ } else {
258
+ out.push(s);
259
+ }
260
+ }
261
+ Some(Block { stmts: out })
262
+ }
263
+
264
+ fn tailStmt(&mut self, s: Stmt) -> Option<Vec<Stmt>> {
265
+ match s {
266
+ Stmt::Expr(e, ..) => self.tailExpr(e),
267
+ Stmt::If(mut i) => {
268
+ i.else_.as_ref()?;
269
+ i.body = self.tailBlock(i.body)?;
270
+ for ei in &mut i.else_ifs {
271
+ ei.body = self.tailBlock(std::mem::replace(&mut ei.body, Block { stmts: vec![] }))?;
272
+ }
273
+ i.else_ = Some(self.tailBlock(i.else_.take()?)?);
274
+ Some(vec![Stmt::If(i)])
275
+ }
276
+ Stmt::Match(mut m) => {
277
+ for c in &mut m.cases {
278
+ c.body = self.tailBlock(std::mem::replace(&mut c.body, Block { stmts: vec![] }))?;
279
+ }
280
+ Some(vec![Stmt::Match(m)])
281
+ }
282
+ _ => None,
283
+ }
284
+ }
285
+
286
+ fn tailExpr(&mut self, e: Expr) -> Option<Vec<Stmt>> {
287
+ match e {
288
+ Expr::Paren(x) => self.tailExpr(*x),
289
+ Expr::InlineIf(i) => {
290
+ let i = *i;
291
+ let then = Block { stmts: self.tailExpr(i.then)? };
292
+ let else_ = Block { stmts: self.tailExpr(i.else_)? };
293
+ Some(vec![Stmt::If(If {
294
+ condition: i.condition,
295
+ body: then,
296
+ else_ifs: vec![],
297
+ else_: Some(else_),
298
+ })])
299
+ }
300
+ Expr::ClassCall(c) => match self.consLeaf(&c, None)? {
301
+ Some(stmts) => Some(stmts),
302
+ None => Some(self.baseLeaf(Expr::ClassCall(c))),
303
+ },
304
+ Expr::FnCall(fc) => {
305
+ let wrapped = match (&fc.args[..], self.enums.iter().find(|e| e.name == self.ret.name)) {
306
+ ([Arg::Positional(Expr::ClassCall(inner))], Some(owner)) => owner
307
+ .variants
308
+ .iter()
309
+ .any(|v| {
310
+ v.name == fc.name
311
+ && v.member.as_ref().map(|m| m.name.as_str())
312
+ == Some(inner.type_name.as_str())
313
+ })
314
+ .then(|| inner.clone()),
315
+ _ => None,
316
+ };
317
+ if let Some(inner) = wrapped {
318
+ match self.consLeaf(&inner, Some(fc.name.clone()))? {
319
+ Some(stmts) => return Some(stmts),
320
+ None => {}
321
+ }
322
+ }
323
+ Some(self.baseLeaf(Expr::FnCall(fc)))
324
+ }
325
+ e => Some(self.baseLeaf(e)),
326
+ }
327
+ }
328
+
329
+ fn baseLeaf(&mut self, e: Expr) -> Vec<Stmt> {
330
+ vec![set("__tc_r", e), set("__tc_go", Expr::Int(0))]
331
+ }
332
+
333
+ /// `Ok(None)`-style: outer `None` aborts the whole rewrite; `Some(None)` means
334
+ /// this call is not a cons-with-self-call and is an ordinary base value.
335
+ fn consLeaf(&mut self, c: &ClassCall, wrapper: Option<String>) -> Option<Option<Vec<Stmt>>> {
336
+ let Some(rec) = self.enums.iter().find(|e| {
337
+ e.name == c.type_name
338
+ && e.isRecord
339
+ && e.inlineOf.as_deref() == Some(self.ret.name.as_str())
340
+ }) else {
341
+ return Some(None);
342
+ };
343
+ let selfName = self.f.name.clone();
344
+ let selfFields: Vec<usize> = c
345
+ .fields
346
+ .iter()
347
+ .enumerate()
348
+ .filter(|(_, f)| mentions(&f.value, &selfName))
349
+ .map(|(i, _)| i)
350
+ .collect();
351
+ if selfFields.is_empty() {
352
+ return Some(None);
353
+ }
354
+ let [k] = selfFields[..] else { return Some(None) };
355
+ let Expr::FnCall(call) = &c.fields[k].value else { return Some(None) };
356
+ if call.name != selfName || c.spread.is_some() {
357
+ return Some(None);
358
+ }
359
+ let declared = rec.variants.first()?.field_names.clone();
360
+ let mut given: Vec<&str> = c.fields.iter().map(|f| f.name.as_str()).collect();
361
+ given.sort();
362
+ let mut want: Vec<&str> = declared.iter().map(|s| s.as_str()).collect();
363
+ want.sort();
364
+ if given != want {
365
+ return Some(None);
366
+ }
367
+ // Fields after the recursive one run after the recursion in the original.
368
+ if !c.fields[k + 1..].iter().all(|f| pure(&f.value)) {
369
+ return Some(None);
370
+ }
371
+ let recName = c.fields[k].name.clone();
372
+ let recIdx = declared.iter().position(|n| *n == recName)?;
373
+ match &self.cons {
374
+ Some((n, _, i)) if *n != c.type_name || *i != recIdx || self.wrapper != wrapper => {
375
+ return None
376
+ }
377
+ _ => {
378
+ self.cons = Some((c.type_name.clone(), declared.clone(), recIdx));
379
+ self.wrapper = wrapper.clone();
380
+ self.consGenerics = c.generics.clone();
381
+ }
382
+ }
383
+ // New arguments, by parameter position.
384
+ let params = &self.f.params;
385
+ if call.args.len() != params.len() {
386
+ return None;
387
+ }
388
+ let mut newArgs: Vec<Option<Expr>> = vec![None; params.len()];
389
+ for (i, a) in call.args.iter().enumerate() {
390
+ match a {
391
+ Arg::Positional(x) => newArgs[i] = Some(x.clone()),
392
+ Arg::Keyword { name, value } => {
393
+ let j = params.iter().position(|p| p.name == *name)?;
394
+ newArgs[j] = Some(value.clone());
395
+ }
396
+ }
397
+ }
398
+ let newArgs: Vec<Expr> = newArgs.into_iter().collect::<Option<_>>()?;
399
+ if newArgs.iter().any(|a| matches!(a, Expr::TypeName(_))) {
400
+ return None;
401
+ }
402
+ self.found = true;
403
+ let mut stmts = Vec::new();
404
+ // Fields before the recursive one, in source order.
405
+ let mut fields = Vec::new();
406
+ for (i, f) in c.fields.iter().enumerate() {
407
+ if i == k {
408
+ fields.push(FieldArg { name: f.name.clone(), value: var("__tc_acc") });
409
+ } else if i < k && !pure(&f.value) {
410
+ let t = self.fresh("f");
411
+ stmts.push(declare(&t, None, f.value.clone()));
412
+ fields.push(FieldArg { name: f.name.clone(), value: var(&t) });
413
+ } else {
414
+ fields.push(f.clone());
415
+ }
416
+ }
417
+ // Arguments of the recursive call.
418
+ let mut moves = Vec::new();
419
+ for (p, a) in params.iter().zip(newArgs) {
420
+ if matches!(&a, Expr::Var(n) if *n == p.name) {
421
+ continue;
422
+ }
423
+ if matches!(a, Expr::Int(_) | Expr::Float(_) | Expr::BoolLit(_)) {
424
+ moves.push(set(&p.name, a));
425
+ } else {
426
+ let t = self.fresh("a");
427
+ stmts.push(declare(&t, None, a));
428
+ moves.push(set(&p.name, var(&t)));
429
+ }
430
+ }
431
+ stmts.push(set(
432
+ "__tc_acc",
433
+ wrap(
434
+ &wrapper,
435
+ Expr::ClassCall(ClassCall {
436
+ type_name: c.type_name.clone(),
437
+ fields,
438
+ generics: c.generics.clone(),
439
+ spread: None,
440
+ }),
441
+ ),
442
+ ));
443
+ stmts.extend(moves);
444
+ Some(Some(stmts))
445
+ }
446
+ }
plum-core/tests/tail_test.rs ADDED
@@ -0,0 +1,134 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_core::ast::*;
3
+ use plum_core::tail::{markTailCalls, tailCalls, TailSite};
4
+ use plum_core::AstParser;
5
+
6
+ fn parse(src: &str) -> Source {
7
+ let mut parser = tree_sitter::Parser::new();
8
+ parser
9
+ .set_language(&tree_sitter_plum::LANGUAGE.into())
10
+ .unwrap();
11
+ let tree = parser.parse(src, None).unwrap();
12
+ assert!(!tree.root_node().has_error(), "{}", tree.root_node().to_sexp());
13
+ AstParser::new(src).parseSource(tree.root_node())
14
+ }
15
+
16
+ /// Tail calls of the first fn named `name` (a method: the one with that name).
17
+ fn sites(src: &str, name: &str) -> Vec<(String, TailKind)> {
18
+ let s = parse(src);
19
+ let mut all = Vec::new();
20
+ for i in &s.items {
21
+ if let Item::Fn(f) = i {
22
+ if f.name == name {
23
+ all = tailCalls(f);
24
+ }
25
+ }
26
+ }
27
+ all.into_iter().map(|t| (t.callee, t.kind)).collect()
28
+ }
29
+
30
+ fn one(callee: &str, kind: TailKind) -> Vec<(String, TailKind)> {
31
+ vec![(callee.to_string(), kind)]
32
+ }
33
+
34
+ #[test]
35
+ fn self_tail_in_match_arm() {
36
+ let src = "fun sumAcc(xs: Int, acc: Int) -> Int = match xs\n 0 => acc\n n => sumAcc(n - 1, acc + n)\n";
37
+ let f = match &parse(src).items[0] {
38
+ Item::Fn(f) => f.clone(),
39
+ _ => panic!(),
40
+ };
41
+ assert_eq!(
42
+ tailCalls(&f),
43
+ vec![TailSite { callee: "sumAcc".into(), kind: TailKind::SelfCall, line: 3 }]
44
+ );
45
+ }
46
+
47
+ #[test]
48
+ fn self_tail_in_if_else() {
49
+ let src = "fun f(n: Int) -> Int =\n if n == 0\n return 0\n else\n f(n - 1)\n";
50
+ assert_eq!(sites(src, "f"), one("f", TailKind::SelfCall));
51
+ }
52
+
53
+ #[test]
54
+ fn not_tail_under_binary_op() {
55
+ let src = "fun length(xs: Int) -> Int = match xs\n 0 => 0\n t => 1 + length(t)\n";
56
+ assert_eq!(sites(src, "length"), vec![]);
57
+ }
58
+
59
+ #[test]
60
+ fn not_tail_as_argument() {
61
+ let src = "fun f(n: Int) -> Int = g(f(n))\n";
62
+ assert_eq!(sites(src, "f"), one("g", TailKind::Other));
63
+ }
64
+
65
+ #[test]
66
+ fn not_tail_inside_try() {
67
+ let src = "fun f(n: Int) -> !Int =\n try g(n)\n";
68
+ assert_eq!(sites(src, "f"), vec![]);
69
+ let src = "fun f(n: Int) -> Int =\n g(n)!\n";
70
+ assert_eq!(sites(src, "f"), vec![]);
71
+ }
72
+
73
+ #[test]
74
+ fn other_fn_tail() {
75
+ let src = "fun f(n: Int) -> Int = g(n)\n";
76
+ assert_eq!(sites(src, "f"), one("g", TailKind::Other));
77
+ }
78
+
79
+ #[test]
80
+ fn mutual_recursion_both_tail() {
81
+ let src = "fun isEven(n: Int) -> Bool = if n == 0 then true else isOdd(n - 1)\nfun isOdd(n: Int) -> Bool = if n == 0 then false else isEven(n - 1)\n";
82
+ assert_eq!(sites(src, "isEven"), one("isOdd", TailKind::Other));
83
+ assert_eq!(sites(src, "isOdd"), one("isEven", TailKind::Other));
84
+ }
85
+
86
+ #[test]
87
+ fn method_self_tail() {
88
+ let src = "record Counter(n: Int) =\n fun count(self, k: Int) -> Int =\n if k == 0 then self.n else self.count(k - 1)\n";
89
+ assert_eq!(sites(src, "count"), one("count", TailKind::SelfCall));
90
+ }
91
+
92
+ #[test]
93
+ fn closure_value_call_tail() {
94
+ let src = "fun f(g: fn(Int) -> Int, n: Int) -> Int = g(n)\n";
95
+ assert_eq!(sites(src, "f"), one("g", TailKind::Closure));
96
+ }
97
+
98
+ #[test]
99
+ fn return_expr_tail() {
100
+ let src = "fun f(n: Int) -> Int =\n if n > 3\n return f(n - 1)\n n\n";
101
+ assert_eq!(sites(src, "f"), one("f", TailKind::SelfCall));
102
+ }
103
+
104
+ #[test]
105
+ fn block_last_expr_tail() {
106
+ let src = "fun f(n: Int) -> Int =\n x := n + 1\n g(x)\n";
107
+ assert_eq!(sites(src, "f"), one("g", TailKind::Other));
108
+ let src = "fun f(n: Int) -> Int =\n g(n)\n n\n";
109
+ assert_eq!(sites(src, "f"), vec![]);
110
+ }
111
+
112
+ #[test]
113
+ fn closure_body_is_tail_relative_to_closure() {
114
+ let src = "fun f(n: Int) -> Int =\n h(|x| g(x))\n";
115
+ assert_eq!(
116
+ sites(src, "f"),
117
+ vec![("g".to_string(), TailKind::Other), ("h".to_string(), TailKind::Other)]
118
+ );
119
+ let src = "fun f(n: Int) -> Int =\n k := h(|x| g(x))\n n\n";
120
+ assert_eq!(sites(src, "f"), one("g", TailKind::Other));
121
+ }
122
+
123
+ #[test]
124
+ fn mark_sets_field() {
125
+ let mut s = parse("fun f(n: Int) -> Int = f(n)\n");
126
+ markTailCalls(&mut s);
127
+ match &s.items[0] {
128
+ Item::Fn(f) => match &f.body {
129
+ FnBody::Expr(Expr::FnCall(c)) => assert_eq!(c.tail, TailKind::SelfCall),
130
+ b => panic!("{b:?}"),
131
+ },
132
+ _ => panic!(),
133
+ }
134
+ }
plum-core/tests/trmc_test.rs ADDED
@@ -0,0 +1,287 @@
1
+ #![allow(non_snake_case)]
2
+ //! Tail recursion modulo cons (`plum_core::trmc`): which fns are rewritten, and
3
+ //! that the rewrite computes what the recursion would (compiled and run through
4
+ //! the real pipeline, which is where `trmc::rewrite` is wired in).
5
+ use plum_wasm_codegen::compileSource;
6
+
7
+ fn gcEngine() -> wasmtime::Engine {
8
+ let mut config = wasmtime::Config::new();
9
+ config.wasm_gc(true);
10
+ config.wasm_function_references(true);
11
+ config.wasm_tail_call(true);
12
+ wasmtime::Engine::new(&config).expect("engine with GC config should construct")
13
+ }
14
+
15
+ fn repoRoot() -> std::path::PathBuf {
16
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
17
+ }
18
+
19
+ fn load(name: &str, src: &str) -> plum_core::ast::Source {
20
+ let dir = std::env::temp_dir().join(format!("plum_trmc_{}_{}", name, std::process::id()));
21
+ std::fs::create_dir_all(&dir).unwrap();
22
+ let path = dir.join("main.plum");
23
+ std::fs::write(&path, src).unwrap();
24
+ let r = plum_core::loadAndMerge(&path, &repoRoot());
25
+ let _ = std::fs::remove_dir_all(&dir);
26
+ r.unwrap_or_else(|e| panic!("load: {e}"))
27
+ }
28
+
29
+ /// Compiles `src` through the real pipeline and runs its `main() -> Int`.
30
+ fn runInt(name: &str, src: &str) -> i64 {
31
+ let source = load(name, src);
32
+ let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
33
+ wasmparser::validate(&bytes).expect("valid wasm");
34
+ let engine = gcEngine();
35
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
36
+ let mut store = wasmtime::Store::new(&engine, ());
37
+ let mut linker = wasmtime::Linker::new(&engine);
38
+ linker
39
+ .define_unknown_imports_as_default_values(&mut store, &module)
40
+ .unwrap();
41
+ let instance = linker.instantiate(&mut store, &module).unwrap();
42
+ let main = instance
43
+ .get_typed_func::<(), i64>(&mut store, "main")
44
+ .unwrap();
45
+ main.call(&mut store, ()).unwrap()
46
+ }
47
+
48
+ /// Whether the lowered program the checker hands codegen has `name` rewritten.
49
+ fn isRewritten(name: &str, src: &str, fnName: &str) -> bool {
50
+ let source = load(name, src);
51
+ let info = plum_checker::typedSource(&source).unwrap_or_else(|e| panic!("check: {e:?}"));
52
+ let lowered = info.lowered.expect("lowered program");
53
+ lowered.items.iter().any(|i| match i {
54
+ plum_core::ast::Item::Fn(f) if f.name == fnName => format!("{:?}", f.body).contains("__tc_acc"),
55
+ _ => false,
56
+ })
57
+ }
58
+
59
+ /// Direct unit check of the pass on a lowered program.
60
+ fn rewrittenNames(name: &str, src: &str) -> Vec<String> {
61
+ let source = load(name, src);
62
+ let info = plum_checker::typedSource(&source).unwrap();
63
+ // `typedSource` already rewrote; rewriting again must find nothing new to do
64
+ // only if the fns are already loops, so rewrite the pre-rewrite program.
65
+ let _ = info;
66
+ let mut s = source.clone();
67
+ plum_core::forms::begin(&s);
68
+ let lowered = {
69
+ let _ = plum_checker::checkSource(&s);
70
+ plum_core::forms::finish(&s)
71
+ };
72
+ s = lowered;
73
+ plum_core::trmc::rewrite(&mut s)
74
+ }
75
+
76
+ const HEAD: &str = "\
77
+ import std/Str
78
+ import std/Int
79
+ import std/Bool
80
+
81
+ enum Link[T] = Nil | Cons(head: T, tail: Link[T])
82
+
83
+ fun fromTo(a: Int, b: Int) -> Link[Int] =
84
+ if a >= b then Nil else Cons(head: a, tail: fromTo(a + 1, b))
85
+
86
+ fun map(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
87
+ Nil => Nil
88
+ Cons(h, t) => Cons(head: f(h), tail: map(t, f))
89
+
90
+ fun append(xs: Link[T], ys: Link[T]) -> Link[T] = match xs
91
+ Nil => ys
92
+ Cons(h, t) => Cons(head: h, tail: append(t, ys))
93
+
94
+ fun filter(xs: Link[T], keep: fn(T) -> Bool) -> Link[T] = match xs
95
+ Nil => Nil
96
+ Cons(h, t) =>
97
+ rest := filter(t, keep)
98
+ if keep(h) then Cons(head: h, tail: rest) else rest
99
+
100
+ fun sum(xs: Link[Int]) -> Int =
101
+ cur := xs
102
+ total := 0
103
+ while true
104
+ match cur
105
+ Nil =>
106
+ break
107
+ Cons(h, t) =>
108
+ total = total + h
109
+ cur = t
110
+ return total
111
+
112
+ fun count(xs: Link[T]) -> Int =
113
+ cur := xs
114
+ n := 0
115
+ while true
116
+ match cur
117
+ Nil =>
118
+ break
119
+ Cons(_, t) =>
120
+ n = n + 1
121
+ cur = t
122
+ return n
123
+
124
+ fun toInt(xs: Link[Int]) -> Int =
125
+ cur := xs
126
+ r := 0
127
+ while true
128
+ match cur
129
+ Nil =>
130
+ break
131
+ Cons(h, t) =>
132
+ r = r * 10 + h
133
+ cur = t
134
+ return r
135
+
136
+ ";
137
+
138
+ fn prog(body: &str) -> String {
139
+ format!("{HEAD}{body}")
140
+ }
141
+
142
+ #[test]
143
+ fn map_rewritten_same_result() {
144
+ let p = prog(
145
+ "\
146
+ fun mapRef(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
147
+ Nil => Nil
148
+ Cons(h, t) =>
149
+ rest := mapRef(t, f)
150
+ Cons(head: f(h), tail: rest)
151
+
152
+ fun main() -> Int =
153
+ none: Link[Int] := Nil
154
+ a := map(none, |x| x + 1)
155
+ b := map(fromTo(0, 1), |x| x + 1)
156
+ c := map(fromTo(0, 50), |x| x * 3)
157
+ d := mapRef(fromTo(0, 50), |x| x * 3)
158
+ big := map(fromTo(0, 1000000), |x| x + 1)
159
+ if count(a) == 0 && sum(b) == 1 && sum(c) == sum(d) && toInt(map(fromTo(1, 6), |x| x * 2)) == 24690 && count(big) == 1000000 && sum(big) == 500000500000 then 1 else 0
160
+ ",
161
+ );
162
+ assert!(isRewritten("map1", &p, "map"));
163
+ assert!(!isRewritten("map2", &p, "mapRef"));
164
+ assert_eq!(runInt("map3", &p), 1);
165
+ }
166
+
167
+ #[test]
168
+ fn filter_rewritten_same_result() {
169
+ let p = prog(
170
+ "\
171
+ fun main() -> Int =
172
+ xs := fromTo(0, 20)
173
+ none: Link[Int] := Nil
174
+ evens := filter(xs, |x| x % 2 == 0)
175
+ if count(filter(none, |x| x > 0)) == 0 && sum(evens) == 90 && count(evens) == 10 then 1 else 0
176
+ ",
177
+ );
178
+ assert!(!isRewritten("filter1", &p, "filter"));
179
+ assert_eq!(runInt("filter2", &p), 1);
180
+ }
181
+
182
+ #[test]
183
+ fn fromTo_rewritten() {
184
+ let p = prog(
185
+ "\
186
+ fun main() -> Int =
187
+ big := fromTo(0, 1000000)
188
+ if count(fromTo(3, 3)) == 0 && count(fromTo(5, 2)) == 0 && toInt(fromTo(1, 5)) == 1234 && count(big) == 1000000 && sum(big) == 499999500000 then 1 else 0
189
+ ",
190
+ );
191
+ assert!(isRewritten("fromTo1", &p, "fromTo"));
192
+ assert_eq!(runInt("fromTo2", &p), 1);
193
+ }
194
+
195
+ #[test]
196
+ fn append_rewritten() {
197
+ let p = prog(
198
+ "\
199
+ fun main() -> Int =
200
+ none: Link[Int] := Nil
201
+ a := fromTo(0, 500000)
202
+ b := fromTo(500000, 1000000)
203
+ big := append(a, b)
204
+ if count(append(none, none)) == 0 && toInt(append(fromTo(1, 3), fromTo(3, 5))) == 1234 && toInt(append(none, fromTo(1, 4))) == 123 && toInt(append(fromTo(1, 4), none)) == 123 && count(big) == 1000000 && sum(big) == 499999500000 then 1 else 0
205
+ ",
206
+ );
207
+ assert!(isRewritten("append1", &p, "append"));
208
+ assert_eq!(runInt("append2", &p), 1);
209
+ }
210
+
211
+ #[test]
212
+ fn two_recursive_fields_not_rewritten() {
213
+ let p = "\
214
+ import std/Str
215
+ import std/Int
216
+ import std/Bool
217
+
218
+ enum Tree = Empty | Node(left: Tree, value: Int, right: Tree)
219
+
220
+ fun mapTree(t: Tree, f: fn(Int) -> Int) -> Tree = match t
221
+ Empty => Empty
222
+ Node(l, v, r) => Node(left: mapTree(l, f), value: f(v), right: mapTree(r, f))
223
+
224
+ fun total(t: Tree) -> Int = match t
225
+ Empty => 0
226
+ Node(l, v, r) => total(l) + v + total(r)
227
+
228
+ fun main() -> Int =
229
+ t := Node(left: Node(left: Empty, value: 1, right: Empty), value: 2, right: Node(left: Empty, value: 3, right: Empty))
230
+ total(mapTree(t, |x| x * 10))
231
+ ";
232
+ assert!(!isRewritten("tree1", p, "mapTree"));
233
+ assert_eq!(runInt("tree2", p), 60);
234
+ }
235
+
236
+ #[test]
237
+ fn side_effect_order_kept() {
238
+ let p = prog(
239
+ "\
240
+ record Log(n: Int)
241
+
242
+ fun note(l: Log, x: Int) -> Int =
243
+ l.n = l.n * 10 + x
244
+ return x
245
+
246
+ fun walk(xs: Link[Int], log: Log, tag: Int) -> Link[Int] = match xs
247
+ Nil => Nil
248
+ Cons(h, t) => Cons(head: note(log, h), tail: walk(t, log, note(log, 9)))
249
+
250
+ fun main() -> Int =
251
+ log := Log(n: 0)
252
+ ys := walk(fromTo(1, 4), log, 0)
253
+ if toInt(ys) == 123 then log.n else 0
254
+ ",
255
+ );
256
+ assert!(isRewritten("order1", &p, "walk"));
257
+ // head 1, arg 9, head 2, arg 9, head 3, arg 9
258
+ assert_eq!(runInt("order2", &p), 192939);
259
+ }
260
+
261
+ #[test]
262
+ fn pass_reports_names() {
263
+ let p = prog("fun main() -> Int = 0\n");
264
+ let names = rewrittenNames("names", &p);
265
+ assert!(names.contains(&"map".to_string()), "{names:?}");
266
+ assert!(names.contains(&"fromTo".to_string()), "{names:?}");
267
+ assert!(names.contains(&"append".to_string()), "{names:?}");
268
+ assert!(!names.contains(&"filter".to_string()), "{names:?}");
269
+ }
270
+
271
+ #[test]
272
+ fn nested_if_in_arm_rewritten_same_result() {
273
+ let p = prog(
274
+ "\
275
+ fun replaceNth(xs: Link[T], i: Int, x: T) -> Link[T] = match xs
276
+ Nil => Nil
277
+ Cons(h, t) => if i <= 0 then Cons(head: x, tail: t) else Cons(head: h, tail: replaceNth(t, i - 1, x))
278
+
279
+ fun main() -> Int =
280
+ xs := fromTo(1, 6)
281
+ none: Link[Int] := Nil
282
+ if toInt(replaceNth(xs, 2, 9)) == 12945 && toInt(replaceNth(xs, 0, 7)) == 72345 && toInt(replaceNth(xs, 9, 7)) == 12345 && count(replaceNth(none, 1, 5)) == 0 then 1 else 0
283
+ ",
284
+ );
285
+ assert!(isRewritten("nth1", &p, "replaceNth"));
286
+ assert_eq!(runInt("nth2", &p), 1);
287
+ }
plum-examples/tail_calls.plum ADDED
@@ -0,0 +1,90 @@
1
+ # Tail calls: deep recursion that must not overflow the stack.
2
+ #
3
+ # Self tail calls compile to loops, other tail calls use `return_call`, and a
4
+ # constructor wrapped self call (`Cons(head: f(h), tail: map(t, f))`) is rewritten
5
+ # to a loop (tail recursion modulo cons). Building a long list is slow under
6
+ # wasmtime GC, so list building tests use 200,000 cells; allocation free loops
7
+ # use 1,000,000.
8
+ module tail_calls
9
+ import std/Int
10
+ import std/Float
11
+
12
+ enum Link[T] = Nil | Cons(head: T, tail: Link[T])
13
+
14
+ fun sumTo(n: Int, acc: Int) -> Int =
15
+ if n == 0 then acc else sumTo(n - 1, acc + n)
16
+
17
+ fun build(n: Int, acc: Link[Int]) -> Link[Int] =
18
+ if n == 0 then acc else build(n - 1, Cons(head: n, tail: acc))
19
+
20
+ fun fromTo(a: Int, b: Int) -> Link[Int] =
21
+ if a >= b then Nil else Cons(head: a, tail: fromTo(a + 1, b))
22
+
23
+ fun map(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
24
+ Nil => Nil
25
+ Cons(h, t) => Cons(head: f(h), tail: map(t, f))
26
+
27
+ fun foldl(xs: Link[T], acc: A, f: fn(A, T) -> A) -> A = match xs
28
+ Nil => acc
29
+ Cons(h, t) => foldl(t, f(acc, h), f)
30
+
31
+ fun length(xs: Link[T], acc: Int) -> Int = match xs
32
+ Nil => acc
33
+ Cons(_, t) => length(t, acc + 1)
34
+
35
+ fun isEven(n: Int) -> Bool =
36
+ if n == 0 then true else isOdd(n - 1)
37
+
38
+ fun isOdd(n: Int) -> Bool =
39
+ if n == 0 then false else isEven(n - 1)
40
+
41
+ # `call` tail calls a closure value; `dec` tail calls `call`.
42
+ fun call(f: fn(Int) -> Int, n: Int) -> Int = f(n)
43
+
44
+ fun dec(n: Int) -> Int =
45
+ if n == 0 then 0 else call(dec, n - 1)
46
+
47
+ own record Counter(n: Int, total: Int)
48
+
49
+ fun spin(own c: Counter) -> Counter =
50
+ if c.n == 0 then c else spin(Counter(..c, n: c.n - 1, total: c.total + 2))
51
+
52
+ fun depth(n: Int) -> Int =
53
+ if n == 0 then 0 else 1 + depth(n - 1)
54
+
55
+ test "tail_sum_million"
56
+ assert sumTo(1000000, 0) == 500000500000
57
+
58
+ test "tail_build_200k"
59
+ assert length(build(200000, Nil), 0) == 200000
60
+
61
+ test "fold_200k"
62
+ xs := fromTo(0, 200000)
63
+ assert foldl(xs, 0, |a, x| a + x) == 19999900000
64
+
65
+ test "mutual_even_odd_million"
66
+ assert isEven(1000000)
67
+ assert !isOdd(1000000)
68
+ assert isOdd(999999)
69
+
70
+ test "trmc_map_200k"
71
+ ys := map(fromTo(0, 200000), |x| x + 1)
72
+ assert length(ys, 0) == 200000
73
+ assert foldl(ys, 0, |a, x| a + x) == 20000100000
74
+
75
+ test "trmc_fromTo_200k"
76
+ xs := fromTo(0, 200000)
77
+ assert length(xs, 0) == 200000
78
+
79
+ test "closure_tail_million"
80
+ assert dec(1000000) == 0
81
+
82
+ test "own_record_loop_100k"
83
+ c := spin(Counter(n: 100000, total: 0))
84
+ assert c.n == 0
85
+ assert c.total == 200000
86
+
87
+ # Non-tail recursion still uses the wasm stack. Measured limit under the 4 MiB
88
+ # host stack: about 265,000 frames (260,000 passes, 270,000 traps) of `1 + depth(n - 1)`; tested well below it.
89
+ test "tree_recursion_depth_recorded"
90
+ assert depth(100000) == 100000
plum-game/src/host.rs CHANGED
@@ -160,9 +160,11 @@ impl<B: Backend> PlumGame<B> {
160
160
  /// Instantiates `wasm`, requiring `init`, `update` and `draw` exports.
161
161
  pub fn load(wasm: &[u8], backend: B) -> Result<Self> {
162
162
  let mut config = wasmtime::Config::new();
163
- config.wasm_tail_call(true);
164
163
  config.wasm_gc(true);
165
164
  config.wasm_function_references(true);
165
+ config.wasm_tail_call(true);
166
+ config.max_wasm_stack(4 << 20);
167
+ config.async_stack_size(8 << 20);
166
168
  let engine = Engine::new(&config)?;
167
169
  let module = Module::new(&engine, wasm)?;
168
170
  for name in ["init", "update", "draw"] {
plum-game/web-test/fixtures/stress_state_view_deep.plum ADDED
@@ -0,0 +1,202 @@
1
+ # Variant of stress_state_view.plum: all 5000 dancers live in ONE Link (each records its
2
+ # spinner index), so step recurses 5000 deep. Original header follows.
3
+ # The stress scene of stress_pure_scene.plum written as state and view, now recursive:
4
+ # 5000 sprites in 50 slowly rotating groups, 500 rects and 200 circles (sprites of a
5
+ # 1x1 white texture), 50 texts with a changing number, drawn through a 0.8 camera.
6
+ # `State` is recursive `Link` lists (each spinner owns the `Link` of its 100 dancers);
7
+ # `step` is pure, a structural recursion that returns the next state; `view` is pure
8
+ # and rebuilds the whole SceneNode tree every frame with recursive functions, which
9
+ # `Picture.draw` interprets. Nothing is retained between frames but `State`. The
10
+ # deepest recursion is the 700 drifters; no loops run in step or view.
11
+ module stress_state_view
12
+ import std/Str
13
+ import std/Int
14
+ import std/Float
15
+ import std/List
16
+ import std/game/Vec2
17
+ import std/game/Color
18
+ import std/game/Game
19
+ import std/game/Picture
20
+
21
+ primitive Counts =
22
+ Sprites = 5000
23
+ Rects = 500
24
+ Circles = 200
25
+ Texts = 50
26
+ Groups = 50
27
+ WorldW = 1600
28
+ WorldH = 900
29
+
30
+ enum Link[T] = Nil | Cons(head: T, tail: Link[T])
31
+
32
+ fun mapLink(xs: Link[A], f: fn(A) -> B) -> Link[B] = match xs
33
+ Nil => Nil
34
+ Cons(h, t) => Cons(head: f(h), tail: mapLink(t, f))
35
+
36
+ # The list f(i), f(i + 1) ... f(n - 1).
37
+ fun rangeLink(i: Int, n: Int, f: fn(Int) -> T) -> Link[T] =
38
+ if i >= n then Nil else Cons(head: f(i), tail: rangeLink(i + 1, n, f))
39
+
40
+ fun appendLink(xs: Link[T], ys: Link[T]) -> Link[T] = match xs
41
+ Nil => ys
42
+ Cons(h, t) => Cons(head: h, tail: appendLink(t, ys))
43
+
44
+ # The element at `i`, or `dflt` past the end.
45
+ fun nthOr(xs: Link[T], i: Int, dflt: T) -> T = match xs
46
+ Nil => dflt
47
+ Cons(h, t) => if i <= 0 then h else nthOr(t, i - 1, dflt)
48
+
49
+ # The nodes f(x) for every x of `xs`, followed by `rest`.
50
+ fun toNodes(xs: Link[T], f: fn(T) -> SceneNode, rest: Nodes) -> Nodes = match xs
51
+ Nil => rest
52
+ Cons(h, t) => More(head: f(h), tail: toNodes(t, f, rest))
53
+
54
+ fun countNodes(ns: Nodes) -> Int = match ns
55
+ End => 0
56
+ More(_, t) => 1 + countNodes(t)
57
+
58
+ # A sprite of the sheet inside its group, spinning about its centre.
59
+ struct Dancer(gi: Int, id: Int, x: Float, y: Float, rotation: Float, srcX: Float)
60
+
61
+ # A rotating group of dancers: top-left (x, y), pivot (75, 85).
62
+ record Spinner(id: Int, x: Float, y: Float, rotation: Float)
63
+
64
+ # A square of the white texture that moves by (dx, dy) per frame and wraps.
65
+ struct Drifter(id: Int, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: Float, color: Color)
66
+
67
+ # Shows "n <count>" and counts up each frame.
68
+ struct Counter(id: Int, x: Float, y: Float, n: Int)
69
+
70
+ record State(sheet: Int, white: Int, spinners: Link[Spinner], dancers: Link[Dancer], drifters: Link[Drifter], counters: Link[Counter])
71
+
72
+ fun hue(i: Int) -> Color =
73
+ Color(r: Float(i % 7) / 7.0, g: Float(i % 5) / 5.0, b: 0.4 + Float(i % 3) / 5.0, a: 0.8)
74
+
75
+ fun newDrifter(id: Int, c: Color, x: Float, y: Float, w: Float, h: Float, dx: Float, dy: Float) -> Drifter =
76
+ Drifter(id: id, x: x, y: y, w: w, h: h, dx: dx, dy: dy, color: c)
77
+
78
+ fun newDancer(gi: Int, j: Int) -> Dancer =
79
+ i := gi * {Counts.Sprites / Counts.Groups} + j
80
+ Dancer(gi: gi, id: 1000 + i, x: Float({j * j * 13 + j * 37} % 150), y: Float({j * j * 29 + j * 53} % 170), rotation: Float(i), srcX: Float(i % 4 * 16))
81
+
82
+ fun newSpinner(gi: Int) -> Spinner =
83
+ Spinner(id: 100 + gi, x: Float(gi % 10 * 160), y: Float(gi / 10 * 180), rotation: 0.0)
84
+
85
+ # All dancers, group after group, built by recursion over the spinner index.
86
+ fun allDancers(gi: Int) -> Link[Dancer] =
87
+ if gi >= Counts.Groups then Nil else appendLink(rangeLink(0, Counts.Sprites / Counts.Groups, |j| newDancer(gi, j)), allDancers(gi + 1))
88
+
89
+ fun newState(sheet: Int, white: Int) -> State =
90
+ rects := rangeLink(0, Counts.Rects, |i| newDrifter(10000 + i, hue(i), Float(i * 61 % Counts.WorldW), Float(i * 29 % Counts.WorldH), 12.0, 8.0, 2.0, 0.0))
91
+ circles := rangeLink(0, Counts.Circles, |i| newDrifter(20000 + i, hue(i + 3), Float(i * 83 % Counts.WorldW), Float(i * 47 % Counts.WorldH), 12.0, 12.0, 3.0, 1.0))
92
+ State(
93
+ sheet: sheet, white: white, spinners: rangeLink(0, Counts.Groups, |gi| newSpinner(gi)), dancers: allDancers(0),
94
+ drifters: appendLink(rects, circles),
95
+ counters: rangeLink(0, Counts.Texts, |i| Counter(id: 30000 + i, x: Float(i * 30 % Counts.WorldW), y: Float(i * 17 % Counts.WorldH), n: i)))
96
+
97
+ # --- step: pure ---
98
+
99
+ fun stepDrifter(d: Drifter) -> Drifter =
100
+ nx := d.x + d.dx
101
+ ny := d.y + d.dy
102
+ if nx > Float(Counts.WorldW)
103
+ nx = nx - Float(Counts.WorldW)
104
+ if ny > Float(Counts.WorldH)
105
+ ny = ny - Float(Counts.WorldH)
106
+ Drifter(id: d.id, x: nx, y: ny, w: d.w, h: d.h, dx: d.dx, dy: d.dy, color: d.color)
107
+
108
+ fun stepDancer(d: Dancer, dt: Float) -> Dancer =
109
+ Dancer(gi: d.gi, id: d.id, x: d.x, y: d.y, rotation: d.rotation + 1.2 * dt, srcX: d.srcX)
110
+
111
+ fun stepSpinner(p: Spinner, dt: Float) -> Spinner =
112
+ Spinner(id: p.id, x: p.x, y: p.y, rotation: p.rotation + 0.24 * dt)
113
+
114
+ fun stepCounter(c: Counter) -> Counter =
115
+ Counter(id: c.id, x: c.x, y: c.y, n: c.n + 1)
116
+
117
+ fun step(s: State, dt: Float) -> State =
118
+ State(
119
+ sheet: s.sheet, white: s.white, spinners: mapLink(s.spinners, |p| stepSpinner(p, dt)), dancers: mapLink(s.dancers, |d| stepDancer(d, dt)),
120
+ drifters: mapLink(s.drifters, |d| stepDrifter(d)), counters: mapLink(s.counters, |c| stepCounter(c)))
121
+
122
+ # --- view: pure, the whole tree every frame ---
123
+
124
+ fun dancerView(sheet: Int, d: Dancer) -> SceneNode =
125
+ Group(
126
+ transform: Compose(first: Translate(dx: d.x + 8.0, dy: d.y + 8.0), then: Compose(first: Rotate(radians: d.rotation), then: Translate(dx: 0.0 - 8.0, dy: 0.0 - 8.0))),
127
+ children: More(head: Sprite(
128
+ tex: sheet, x: 0.0, y: 0.0, w: 16.0, h: 16.0, srcX: d.srcX, srcY: 0.0, srcW: 16.0, srcH: 16.0,
129
+ tint: Color.White), tail: End))
130
+
131
+ # The leading dancers of `ds` that belong to group `gi`, as nodes.
132
+ fun groupNodes(sheet: Int, ds: Link[Dancer], gi: Int) -> Nodes = match ds
133
+ Nil => End
134
+ Cons(h, t) => if h.gi == gi then More(head: dancerView(sheet, h), tail: groupNodes(sheet, t, gi)) else End
135
+
136
+ # `ds` without its leading dancers of group `gi`.
137
+ fun dropGroup(ds: Link[Dancer], gi: Int) -> Link[Dancer] = match ds
138
+ Nil => Nil
139
+ Cons(h, t) => if h.gi == gi then dropGroup(t, gi) else ds
140
+
141
+ fun spinnerView(sheet: Int, p: Spinner, ds: Link[Dancer]) -> SceneNode =
142
+ Group(
143
+ transform: Compose(first: Translate(dx: p.x + 75.0, dy: p.y + 85.0), then: Compose(first: Rotate(radians: p.rotation), then: Translate(dx: 0.0 - 75.0, dy: 0.0 - 85.0))),
144
+ children: groupNodes(sheet, ds, p.id - 100))
145
+
146
+ # Spinner groups in order, each taking its dancers from the front of the one list.
147
+ fun spinnerNodes(sheet: Int, ps: Link[Spinner], ds: Link[Dancer], rest: Nodes) -> Nodes = match ps
148
+ Nil => rest
149
+ Cons(p, t) => More(head: spinnerView(sheet, p, ds), tail: spinnerNodes(sheet, t, dropGroup(ds, p.id - 100), rest))
150
+
151
+ fun drifterView(white: Int, d: Drifter) -> SceneNode =
152
+ Sprite(
153
+ tex: white, x: d.x, y: d.y, w: d.w, h: d.h, srcX: 0.0, srcY: 0.0, srcW: 0.0, srcH: 0.0,
154
+ tint: d.color)
155
+
156
+ fun counterView(c: Counter) -> SceneNode =
157
+ Text(text: "n {c.n}", x: c.x, y: c.y, size: 14.0, color: Color.White)
158
+
159
+ fun view(s: State) -> SceneNode =
160
+ texts := toNodes(s.counters, |c| counterView(c), End)
161
+ drifts := toNodes(s.drifters, |d| drifterView(s.white, d), texts)
162
+ Group(transform: Scale(sx: 0.8, sy: 0.8), children: spinnerNodes(s.sheet, s.spinners, s.dancers, drifts))
163
+
164
+ fun whiteTexture() -> Int =
165
+ t := textureCreate(1, 1)
166
+ renderTarget(t)
167
+ clear(Color.White)
168
+ renderTarget(-1)
169
+ t
170
+
171
+ record World(state: State)
172
+
173
+ export fun init() -> World =
174
+ World(state: newState(textureLoad("sprites.png"), whiteTexture()))
175
+
176
+ export fun update(w: World, dt: Float) =
177
+ w.state = step(w.state, dt)
178
+
179
+ export fun draw(w: World) =
180
+ clear(Color.rgb(0.05, 0.06, 0.1))
181
+ Picture.draw(view(w.state))
182
+
183
+ test "ten frames of step run"
184
+ s := newState(0, 0)
185
+ for i := range 10
186
+ s = step(s, 0.016)
187
+ assert nthOr(s.counters, 3, Counter(id: 0, x: 0.0, y: 0.0, n: 0)).n == 13
188
+ assert {nthOr(s.drifters, 0, newDrifter(0, Color.White, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0)).x - 20.0}.abs() < 0.000001
189
+ assert {nthOr(s.dancers, 0, newDancer(0, 0)).rotation - 0.192}.abs() < 0.000001
190
+
191
+ test "view has one node per actor"
192
+ s := newState(0, 0)
193
+ v := view(s)
194
+ match v
195
+ Group(_, kids) =>
196
+ assert countNodes(kids) == 50 + 500 + 200 + 50
197
+ _ =>
198
+ assert false
199
+
200
+ test "dancers in one list 5000"
201
+ s := newState(0, 0)
202
+ assert countNodes(toNodes(s.dancers, |d| dancerView(0, d), End)) == 5000
plum-runtime/src/main.rs CHANGED
@@ -359,9 +359,11 @@ fn main() {
359
359
  // GC + typed function references are required to load modules containing
360
360
  // Plum's structs/enums/strings/closures (see docs/superpowers/plans/2026-07-25-wasm-gc-migration.md).
361
361
  let mut config = wasmtime::Config::new();
362
- config.wasm_tail_call(true);
363
362
  config.wasm_gc(true);
364
363
  config.wasm_function_references(true);
364
+ config.wasm_tail_call(true);
365
+ config.max_wasm_stack(4 << 20);
366
+ config.async_stack_size(8 << 20);
365
367
  let engine = wasmtime::Engine::new(&config).expect("engine with GC config should construct");
366
368
 
367
369
  let module = wasmtime::Module::new(&engine, WASM_BYTES).expect("embedded wasm should be valid");
plum-wasm-codegen/src/lib.rs CHANGED
@@ -398,6 +398,8 @@ type ClosureSigKey = (Vec<ValType>, Vec<ValType>);
398
398
 
399
399
  /// Global, read-only lookup tables shared by every function body being compiled.
400
400
  pub struct CompileCtx<'a> {
401
+ /// Number of imported functions (their indices come first).
402
+ pub import_count: u32,
401
403
  pub func_ids: HashMap<String, u32>,
402
404
  pub func_sigs: HashMap<String, FuncSig>,
403
405
  /// Instantiation id -> its wasm function and signature.
@@ -495,18 +497,12 @@ struct LocalCtx<'a> {
495
497
  /// statements in order, mirroring `plum-checker`'s own env evolution — needed
496
498
  /// to resolve `Attribute`/`ClassCall` targets and pick the right load/store width.
497
499
  type_env: RefCell<TypeEnv>,
498
- /// How many wasm structured blocks (`if`/`else`) currently lie between the
499
- /// point being compiled and its NEAREST ENCLOSING LOOP's own `block`/`loop`
500
+ /// `tail_depth` as it stood when the NEAREST ENCLOSING LOOP's body began.
500
- /// pair — used by `Stmt::Break`/`Stmt::Continue` to compute the right `br`
501
+ /// `Stmt::Break`/`Stmt::Continue` branch over `tail_depth - loop_base_depth`
501
- /// target depth. Reset to 0 on entering a loop's own body (a nested loop
502
- /// starts counting fresh, relative to itself); incremented by 1 around each
502
+ /// wasm `if`s (every one is `TailDepth`-guarded: if/else-if chains, match
503
- /// `compileCaseBody` call (`if`/`match` arm bodies), the only places that
504
- /// add a structured block without being a loop themselves. Without this,
503
+ /// pattern tests and `when` guards, catch blocks) before reaching the loop's
505
- /// `break`/`continue` inside an `if` nested in a loop silently computed the
504
+ /// own `block`/`loop` pair. A nested loop re-bases it for its own body.
506
- /// WRONG branch depth (hardcoded as if directly in the loop body) and fell
507
- /// through to the next statement instead of skipping/exiting — see the
508
- /// `plum_compiler_bugs_found_2026_08` memory.
509
- loop_extra_depth: Cell<u32>,
505
+ loop_base_depth: Cell<u32>,
510
506
  /// The declared return type of the function being compiled (`None` inside a
511
507
  /// closure body) — `?` builds the OUTER function's error result from it.
512
508
  ret_type: Option<PlumType>,
@@ -516,6 +512,40 @@ struct LocalCtx<'a> {
516
512
  /// Match-subject scratch local -> the subject's full plum type (type arguments
517
513
  /// included), so constructor patterns can instantiate their payload types.
518
514
  subject_ptypes: RefCell<HashMap<u32, PlumType>>,
515
+ /// The fn being compiled, when its calls in tail position may be tail calls
516
+ /// (`None` in a closure body or a const initializer).
517
+ self_func: Option<SelfFunc>,
518
+ /// Wasm blocks (`if` arms) between the point being compiled and the fn body's
519
+ /// self-tail-call `loop` (see `TailDepth`).
520
+ tail_depth: Cell<u32>,
521
+ /// Imported function count: calls to imports are never tail calls.
522
+ import_count: u32,
523
+ }
524
+
525
+ /// What tail-call emission needs to know about the fn being compiled.
526
+ struct SelfFunc {
527
+ idx: u32,
528
+ /// Wasm result types of this fn.
529
+ ret: Vec<ValType>,
530
+ /// Whether the body is wrapped in a `loop` that self tail calls branch back to.
531
+ looped: bool,
532
+ }
533
+
534
+ /// RAII marker for one enclosing wasm `if` between a self tail call and the fn
535
+ /// body's loop; held from the `if` emission to the end of the emitting scope.
536
+ struct TailDepth<'a>(&'a Cell<u32>);
537
+
538
+ impl<'a> TailDepth<'a> {
539
+ fn enter(c: &'a Cell<u32>) -> Self {
540
+ c.set(c.get() + 1);
541
+ TailDepth(c)
542
+ }
543
+ }
544
+
545
+ impl Drop for TailDepth<'_> {
546
+ fn drop(&mut self) {
547
+ self.0.set(self.0.get() - 1);
548
+ }
519
549
  }
520
550
 
521
551
  fn fnKey(f: &ast::Fn) -> String {
@@ -2859,7 +2889,7 @@ fn compileSourceInner(
2859
2889
  for i in 0..sig.params.len() {
2860
2890
  Instruction::LocalGet((i + 1) as u32).encode(&mut tbody);
2861
2891
  }
2862
- Instruction::Call(real_func_idx).encode(&mut tbody);
2892
+ Instruction::ReturnCall(real_func_idx).encode(&mut tbody);
2863
2893
  Instruction::End.encode(&mut tbody);
2864
2894
  let func_idx = module.addFunction(type_idx, &tbody);
2865
2895
  module.nameFunction(func_idx, &format!("{}.trampoline", name));
@@ -2917,6 +2947,7 @@ fn compileSourceInner(
2917
2947
  module.functions[start_slot].1 = final_start_body;
2918
2948
 
2919
2949
  let ctx = CompileCtx {
2950
+ import_count: module.func_import_count,
2920
2951
  func_ids,
2921
2952
  func_sigs,
2922
2953
  inst_funcs,
@@ -4609,80 +4640,6 @@ fn isBuiltConstInit(e: &ast::Expr) -> bool {
4609
4640
  }
4610
4641
  }
4611
4642
 
4612
- thread_local! {
4613
- /// Addresses of the call expressions in tail position of the function body being
4614
- /// compiled (see `collectTailCalls`); a call here MAY become a `return_call`
4615
- /// (see `tailCallOk`). Reset per function.
4616
- static TAIL_EXPRS: RefCell<std::collections::HashSet<usize>> = RefCell::new(Default::default());
4617
- }
4618
-
4619
- fn collectTailBlock(block: &ast::Block, out: &mut std::collections::HashSet<usize>) {
4620
- if let Some(last) = block.stmts.last() {
4621
- collectTailStmt(last, out);
4622
- }
4623
- for stmt in &block.stmts {
4624
- if let ast::Stmt::Return(Some(e)) = stmt {
4625
- collectTailExpr(e, out);
4626
- }
4627
- }
4628
- }
4629
-
4630
- fn collectTailStmt(stmt: &ast::Stmt, out: &mut std::collections::HashSet<usize>) {
4631
- match stmt {
4632
- ast::Stmt::Expr(e, ..) | ast::Stmt::Return(Some(e)) => collectTailExpr(e, out),
4633
- ast::Stmt::If(i) => {
4634
- collectTailBlock(&i.body, out);
4635
- for ei in &i.else_ifs {
4636
- collectTailBlock(&ei.body, out);
4637
- }
4638
- if let Some(b) = &i.else_ {
4639
- collectTailBlock(b, out);
4640
- }
4641
- }
4642
- ast::Stmt::Match(m) => {
4643
- for c in &m.cases {
4644
- collectTailBlock(&c.body, out);
4645
- }
4646
- }
4647
- _ => {}
4648
- }
4649
- }
4650
-
4651
- fn collectTailExpr(e: &ast::Expr, out: &mut std::collections::HashSet<usize>) {
4652
- match e {
4653
- ast::Expr::FnCall(_) => {
4654
- out.insert(e as *const ast::Expr as usize);
4655
- }
4656
- ast::Expr::Attribute(a) if matches!(a.attr, ast::AttrKind::Method(_)) => {
4657
- out.insert(e as *const ast::Expr as usize);
4658
- }
4659
- ast::Expr::InlineIf(t) => {
4660
- collectTailExpr(&t.then, out);
4661
- collectTailExpr(&t.else_, out);
4662
- }
4663
- _ => {}
4664
- }
4665
- }
4666
-
4667
- /// Whether the call `expr` (callee wasm results `callee_ret`) may be emitted as a
4668
- /// tail call: it is in tail position and its value is returned as-is — same Plum
4669
- /// type as the function's own return type, no error-union wrap, same wasm results.
4670
- fn tailCallOk(expr: &ast::Expr, callee_ret: &[ValType], ctx: &LocalCtx) -> bool {
4671
- let key = expr as *const ast::Expr as usize;
4672
- if !TAIL_EXPRS.with(|t| t.borrow().contains(&key)) {
4673
- return false;
4674
- }
4675
- let Some(rt) = &ctx.ret_type else { return false };
4676
- if matches!(rt, PlumType::TUnit) || ctx.types.contains_key(&(key + plum_checker::ERR_WRAP)) {
4677
- return false;
4678
- }
4679
- if inferLocalType(expr, ctx) != *rt {
4680
- return false;
4681
- }
4682
- let own = withGcTypes(|g| flatValtypes(rt, g));
4683
- own.as_slice() == callee_ret
4684
- }
4685
-
4686
4643
  /// Compiles a function body. With `const_init`, `f` is a bodyless stand-in and the
4687
4644
  /// body is the lazily-run, cache-once initializer of that const, returning its value.
4688
4645
  fn compileFnBodyImpl(
@@ -4709,6 +4666,28 @@ fn compileFnBodyImpl(
4709
4666
  decl_types.insert(p.name.clone(), ty.clone());
4710
4667
  }
4711
4668
 
4669
+ // Tail calls: only real fns with a plain (non error-union) result.
4670
+ let self_func = if const_init.is_some() || matches!(ret_decl, PlumType::TErr(..)) {
4671
+ None
4672
+ } else {
4673
+ let own = match unit.inst {
4674
+ Some(id) => ctx.inst_funcs.get(&id).map(|(i, s)| (*i, s.ret.clone())),
4675
+ None => ctx
4676
+ .func_ids
4677
+ .get(&fnKey(f))
4678
+ .zip(ctx.func_sigs.get(&fnKey(f)))
4679
+ .map(|(i, s)| (*i, s.ret.clone())),
4680
+ };
4681
+ own.map(|(idx, ret)| SelfFunc {
4682
+ idx,
4683
+ ret,
4684
+ looped: !matches!(f.body, ast::FnBody::Extern)
4685
+ && plum_core::tail::tailCalls(f)
4686
+ .iter()
4687
+ .any(|t| t.kind == ast::TailKind::SelfCall),
4688
+ })
4689
+ };
4690
+
4712
4691
  let mut collector = Collector {
4713
4692
  types: unit.types,
4714
4693
  enum_variants: &ctx.enum_variants,
@@ -4818,10 +4797,13 @@ fn compileFnBodyImpl(
4818
4797
  gc_types: &ctx.gc_types,
4819
4798
  singleton_globals: &ctx.singleton_globals,
4820
4799
  type_env: RefCell::new(base_env),
4821
- loop_extra_depth: Cell::new(0),
4800
+ loop_base_depth: Cell::new(0),
4822
4801
  ret_type: f.returns.as_ref().map(|_| ret_decl.clone()),
4823
4802
  match_subject_types: RefCell::new(HashMap::new()),
4824
4803
  subject_ptypes: RefCell::new(HashMap::new()),
4804
+ self_func,
4805
+ tail_depth: Cell::new(0),
4806
+ import_count: ctx.import_count,
4825
4807
  };
4826
4808
 
4827
4809
  let result_vt = if f.returns.is_none() || matches!(ret_decl, PlumType::TUnit) {
@@ -4841,18 +4823,10 @@ fn compileFnBodyImpl(
4841
4823
  Instruction::End.encode(&mut body);
4842
4824
  return Ok(body);
4843
4825
  }
4844
- let mut tail = std::collections::HashSet::new();
4826
+ let looped = local_ctx.self_func.as_ref().is_some_and(|s| s.looped);
4845
- match &f.body {
4827
+ if looped {
4846
- ast::FnBody::Expr(e) => collectTailExpr(e, &mut tail),
4828
+ Instruction::Loop(blockTypeFor(result_vt)).encode(&mut body);
4847
- ast::FnBody::Block(block)
4848
- if !matches!(&ret_decl, PlumType::TErr(t, _) if matches!(**t, PlumType::TUnit)) =>
4849
- {
4850
- collectTailBlock(block, &mut tail)
4851
- }
4852
- _ => {}
4853
4829
  }
4854
- TAIL_EXPRS.with(|t| *t.borrow_mut() = tail);
4855
- let result = (|| -> Result<(), String> {
4856
4830
  match &f.body {
4857
4831
  ast::FnBody::Expr(e) => {
4858
4832
  compileExpr(e, &mut body, &local_ctx, state)?;
@@ -4872,11 +4846,10 @@ fn compileFnBodyImpl(
4872
4846
  // `fns` excludes every `extern fun` (no body to compile).
4873
4847
  ast::FnBody::Extern => unreachable!("extern fns are excluded from `fns`"),
4874
4848
  }
4875
- Ok(())
4876
- })();
4877
- TAIL_EXPRS.with(|t| t.borrow_mut().clear());
4878
- result?;
4879
4849
 
4850
+ if looped {
4851
+ Instruction::End.encode(&mut body);
4852
+ }
4880
4853
  Instruction::End.encode(&mut body);
4881
4854
  Ok(body)
4882
4855
  }
@@ -4903,17 +4876,12 @@ fn compileCaseBody(
4903
4876
  ctx: &LocalCtx,
4904
4877
  state: &mut ModuleState,
4905
4878
  ) -> Result<(), String> {
4906
- // Every caller (an `if`/`else if`/`else` body, or a `match` arm body) wraps
4907
- // this block in exactly one MORE wasm `if` construct than its own caller —
4908
- // tracked here (not at each call site) so it can't be missed on an error
4879
+ // The enclosing wasm `if`s are counted by the callers' `TailDepth` guards;
4909
- // path; see `loop_extra_depth`'s doc comment for why this matters.
4880
+ // see `loop_base_depth`.
4910
- ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() + 1);
4911
- let result = match result_vt {
4881
+ match result_vt {
4912
4882
  Some(vt) => compileBlockInValuePosition(block, vt, body, ctx, state),
4913
4883
  None => compileBlock(block, body, ctx, state),
4914
- };
4884
+ }
4915
- ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() - 1);
4916
- result
4917
4885
  }
4918
4886
 
4919
4887
  /// Compiles a block whose value must be produced when control reaches its end — every
@@ -5027,12 +4995,15 @@ fn compileIf(
5027
4995
  let bt = blockTypeFor(result_vt);
5028
4996
  compileBoolConditionAsI32(&if_.condition, body, ctx, state)?;
5029
4997
  Instruction::If(bt).encode(body);
4998
+ // One entry per open wasm `if` (the else-if chain nests), for tail-call depth.
4999
+ let mut depth_guards = vec![TailDepth::enter(&ctx.tail_depth)];
5030
5000
  compileCaseBody(&if_.body, result_vt, body, ctx, state)?;
5031
5001
  if !if_.else_ifs.is_empty() || if_.else_.is_some() {
5032
5002
  Instruction::Else.encode(body);
5033
5003
  for ei in &if_.else_ifs {
5034
5004
  compileBoolConditionAsI32(&ei.condition, body, ctx, state)?;
5035
5005
  Instruction::If(bt).encode(body);
5006
+ depth_guards.push(TailDepth::enter(&ctx.tail_depth));
5036
5007
  compileCaseBody(&ei.body, result_vt, body, ctx, state)?;
5037
5008
  Instruction::Else.encode(body);
5038
5009
  }
@@ -5299,9 +5270,9 @@ fn compileStmt(
5299
5270
  // nearest target — reset the depth counter to 0 for its body, then
5300
5271
  // restore whatever it was (an outer loop's own count, if this loop
5301
5272
  // is itself nested inside an `if` inside another loop) afterward.
5302
- let saved_depth = ctx.loop_extra_depth.replace(0);
5273
+ let saved_depth = ctx.loop_base_depth.replace(ctx.tail_depth.get());
5303
5274
  let result = compileBlock(&w.body, body, ctx, state);
5304
- ctx.loop_extra_depth.set(saved_depth);
5275
+ ctx.loop_base_depth.set(saved_depth);
5305
5276
  result?;
5306
5277
  Instruction::Br(0).encode(body);
5307
5278
  Instruction::End.encode(body);
@@ -5315,10 +5286,10 @@ fn compileStmt(
5315
5286
  }
5316
5287
  }
5317
5288
  ast::Stmt::Break => {
5318
- Instruction::Br(ctx.loop_extra_depth.get() + 1).encode(body);
5289
+ Instruction::Br(ctx.tail_depth.get() - ctx.loop_base_depth.get() + 1).encode(body);
5319
5290
  }
5320
5291
  ast::Stmt::Continue => {
5321
- Instruction::Br(ctx.loop_extra_depth.get()).encode(body);
5292
+ Instruction::Br(ctx.tail_depth.get() - ctx.loop_base_depth.get()).encode(body);
5322
5293
  }
5323
5294
  ast::Stmt::Match(m) => {
5324
5295
  compileMatch(m, body, ctx, state, None)?;
@@ -5551,6 +5522,7 @@ fn compileTagPattern(
5551
5522
  pat: &ast::CasePattern,
5552
5523
  load: &dyn Fn(&mut Vec<u8>),
5553
5524
  result_vt: Option<ValType>,
5525
+ depth: &Cell<u32>,
5554
5526
  body: &mut Vec<u8>,
5555
5527
  state: &mut ModuleState,
5556
5528
  on_match: &mut dyn FnMut(&mut Vec<u8>, &mut ModuleState) -> Result<(), String>,
@@ -5563,6 +5535,7 @@ fn compileTagPattern(
5563
5535
  Instruction::I32Const(errCode(name)?).encode(body);
5564
5536
  Instruction::I32Eq.encode(body);
5565
5537
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5538
+ let _tg = TailDepth::enter(depth);
5566
5539
  on_match(body, state)?;
5567
5540
  Instruction::Else.encode(body);
5568
5541
  on_fail(body, state)?;
@@ -5720,6 +5693,7 @@ fn compileCasePositions(
5720
5693
  Some(guard) => {
5721
5694
  compileBoolConditionAsI32(guard, body, ctx, state)?;
5722
5695
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5696
+ let _tg = TailDepth::enter(&ctx.tail_depth);
5723
5697
  compileCaseBody(&case.body, result_vt, body, ctx, state)?;
5724
5698
  Instruction::Else.encode(body);
5725
5699
  compileMatchArmsMulti(
@@ -5749,6 +5723,7 @@ fn compileCasePositions(
5749
5723
  pat,
5750
5724
  &|b: &mut Vec<u8>| Instruction::LocalGet(scratch_local).encode(b),
5751
5725
  result_vt,
5726
+ &ctx.tail_depth,
5752
5727
  body,
5753
5728
  state,
5754
5729
  &mut |body, state| {
@@ -5800,6 +5775,7 @@ fn compileCasePositions(
5800
5775
  Instruction::I32Eqz.encode(body);
5801
5776
  }
5802
5777
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5778
+ let _tg = TailDepth::enter(&ctx.tail_depth);
5803
5779
  compileCasePositions(
5804
5780
  case,
5805
5781
  pos + 1,
@@ -5883,6 +5859,7 @@ fn compileCasePositions(
5883
5859
  Instruction::I64Const(*n).encode(body);
5884
5860
  Instruction::I64Eq.encode(body);
5885
5861
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5862
+ let _tg = TailDepth::enter(&ctx.tail_depth);
5886
5863
  compileCasePositions(
5887
5864
  case,
5888
5865
  pos + 1,
@@ -5974,6 +5951,7 @@ fn compileVariantEqArm(
5974
5951
  Instruction::LocalGet(scratch_local).encode(body);
5975
5952
  Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
5976
5953
  Instruction::If(blockTypeFor(result_vt)).encode(body);
5954
+ let _tg = TailDepth::enter(&ctx.tail_depth);
5977
5955
  compileCasePositions(
5978
5956
  case,
5979
5957
  pos + 1,
@@ -6075,6 +6053,7 @@ fn compileVariantConstructorArm(
6075
6053
  Instruction::LocalGet(scratch_local).encode(body);
6076
6054
  Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
6077
6055
  Instruction::If(blockTypeFor(result_vt)).encode(body);
6056
+ let _tg = TailDepth::enter(&ctx.tail_depth);
6078
6057
  // Narrow the subject down to this variant's concrete type before destructuring
6079
6058
  // its fields — valid here specifically because the ref.test just above proved it.
6080
6059
  Instruction::LocalGet(scratch_local).encode(body);
@@ -6283,6 +6262,7 @@ fn compileFieldPatterns(
6283
6262
  Instruction::I32Eqz.encode(body);
6284
6263
  }
6285
6264
  Instruction::If(blockTypeFor(result_vt)).encode(body);
6265
+ let _tg = TailDepth::enter(&ctx.tail_depth);
6286
6266
  compileFieldPatterns(
6287
6267
  fields,
6288
6268
  field_types,
@@ -6399,6 +6379,7 @@ fn compileFieldPatterns(
6399
6379
  );
6400
6380
  Instruction::RefTestNonNull(HeapType::Concrete(variant_idx)).encode(body);
6401
6381
  Instruction::If(blockTypeFor(result_vt)).encode(body);
6382
+ let _tg = TailDepth::enter(&ctx.tail_depth);
6402
6383
  compileFieldPatterns(
6403
6384
  fields,
6404
6385
  field_types,
@@ -6444,6 +6425,7 @@ fn compileFieldPatterns(
6444
6425
  Instruction::I64Const(*n).encode(body);
6445
6426
  Instruction::I64Eq.encode(body);
6446
6427
  Instruction::If(blockTypeFor(result_vt)).encode(body);
6428
+ let _tg = TailDepth::enter(&ctx.tail_depth);
6447
6429
  compileFieldPatterns(
6448
6430
  fields,
6449
6431
  field_types,
@@ -6602,6 +6584,7 @@ fn compileFieldPatterns(
6602
6584
  );
6603
6585
  Instruction::RefTestNonNull(HeapType::Concrete(inner_variant_idx)).encode(body);
6604
6586
  Instruction::If(blockTypeFor(result_vt)).encode(body);
6587
+ let _tg = TailDepth::enter(&ctx.tail_depth);
6605
6588
  // Narrow into `nested_local` now that ref.test just above proved it's safe.
6606
6589
  emitContainerField(
6607
6590
  body,
@@ -7078,10 +7061,8 @@ fn compileErrCatch(
7078
7061
  Instruction::If(resultBlock(&op.success)).encode(body);
7079
7062
  match &c.handler {
7080
7063
  ast::CatchHandler::Default(d) => {
7081
- ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() + 1);
7064
+ let _tg = TailDepth::enter(&ctx.tail_depth);
7082
- let r = compileExprInto(d, &op.success, body, ctx, state);
7065
+ compileExprInto(d, &op.success, body, ctx, state)?;
7083
- ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() - 1);
7084
- r?;
7085
7066
  }
7086
7067
  ast::CatchHandler::Block { binder, body: blk } => {
7087
7068
  let names = errSite(expr).map(|s| s.names).unwrap_or_default();
@@ -7101,6 +7082,7 @@ fn compileErrCatch(
7101
7082
  } else {
7102
7083
  Some(plumTypeToValtype(&op.success))
7103
7084
  };
7085
+ let _tg = TailDepth::enter(&ctx.tail_depth);
7104
7086
  compileCaseBody(blk, rvt, body, ctx, state)?;
7105
7087
  }
7106
7088
  }
@@ -7147,7 +7129,7 @@ fn compileErrAndThen(
7147
7129
  .stmts
7148
7130
  .split_last()
7149
7131
  .ok_or_else(|| "codegen: empty '?.' body".to_string())?;
7150
- ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() + 1);
7132
+ let _tg = TailDepth::enter(&ctx.tail_depth);
7151
7133
  for st in rest {
7152
7134
  compileStmt(st, body, ctx, state)?;
7153
7135
  }
@@ -7156,7 +7138,6 @@ fn compileErrAndThen(
7156
7138
  };
7157
7139
  // The checker recorded the body's coercion into the result's layout.
7158
7140
  compileExpr(e, body, ctx, state)?;
7159
- ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() - 1);
7160
7141
  Instruction::End.encode(body);
7161
7142
  Ok(())
7162
7143
  }
@@ -7452,6 +7433,7 @@ fn compileExprCore(
7452
7433
  let result_vt = plumTypeToValtype(&inferLocalType(expr, ctx));
7453
7434
  compileBoolConditionAsI32(&t.condition, body, ctx, state)?;
7454
7435
  Instruction::If(blockTypeFor(Some(result_vt))).encode(body);
7436
+ let _tg = TailDepth::enter(&ctx.tail_depth);
7455
7437
  compileExpr(&t.then, body, ctx, state)?;
7456
7438
  Instruction::Else.encode(body);
7457
7439
  compileExpr(&t.else_, body, ctx, state)?;
@@ -7543,11 +7525,7 @@ fn compileExprCore(
7543
7525
  let (func_idx, sig) = resolveCallee(ctx, expr, false, &call.name)
7544
7526
  .ok_or_else(|| format!("unknown function '{}'", call.name))?;
7545
7527
  compileCallArgs(&call.name, &call.args, sig, body, ctx, state)?;
7546
- if tailCallOk(expr, &sig.ret, ctx) {
7528
+ emitCall(func_idx, sig, call.tail, expr, body, ctx);
7547
- Instruction::ReturnCall(func_idx).encode(body);
7548
- } else {
7549
- Instruction::Call(func_idx).encode(body);
7550
- }
7551
7529
  }
7552
7530
  }
7553
7531
  ast::Expr::Self_ => {
@@ -7814,6 +7792,73 @@ fn compileExprCore(
7814
7792
  /// built into one GC array, and the declared defaults for omitted trailing params.
7815
7793
  /// The wasm function (and signature) a call expression invokes: its checked
7816
7794
  /// instantiation if the callee is generic, else the plain function `plain_key`.
7795
+ /// How a call whose arguments are already on the wasm stack is emitted.
7796
+ #[derive(Clone, Copy, PartialEq)]
7797
+ enum TailMode {
7798
+ Plain,
7799
+ /// `return_call` to a real wasm function / `return_call_indirect`.
7800
+ Return,
7801
+ /// Branch back to the fn body's loop (a self call).
7802
+ Loop,
7803
+ }
7804
+
7805
+ /// Whether the call in tail position (resolved to wasm fn `func_idx`, or `None` for a
7806
+ /// closure call) may skip the stack. Needs the callee's wasm result to equal the
7807
+ /// enclosing fn's with no conversion in between: the call's plum type must be the
7808
+ /// fn's declared return type.
7809
+ fn tailMode(
7810
+ tail: ast::TailKind,
7811
+ func_idx: Option<u32>,
7812
+ callee_ret: &[ValType],
7813
+ result_ty: &PlumType,
7814
+ ctx: &LocalCtx,
7815
+ ) -> TailMode {
7816
+ if tail == ast::TailKind::NotTail {
7817
+ return TailMode::Plain;
7818
+ }
7819
+ let Some(sf) = &ctx.self_func else {
7820
+ return TailMode::Plain;
7821
+ };
7822
+ if sf.ret != callee_ret {
7823
+ return TailMode::Plain;
7824
+ }
7825
+ let want = ctx.ret_type.clone().unwrap_or(PlumType::TUnit);
7826
+ if matches!(want, PlumType::TErr(..)) || &want != result_ty {
7827
+ return TailMode::Plain;
7828
+ }
7829
+ match func_idx {
7830
+ Some(i) if i < ctx.import_count => TailMode::Plain,
7831
+ Some(i) if i == sf.idx && sf.looped && tail == ast::TailKind::SelfCall => TailMode::Loop,
7832
+ _ => TailMode::Return,
7833
+ }
7834
+ }
7835
+
7836
+ /// Emits the call to `func_idx` (arguments already pushed): a plain `call`, a
7837
+ /// `return_call`, or - for a self tail call - `local.set` of every param followed by
7838
+ /// a branch back to the fn body's loop.
7839
+ fn emitCall(
7840
+ func_idx: u32,
7841
+ sig: &FuncSig,
7842
+ tail: ast::TailKind,
7843
+ result_expr: &ast::Expr,
7844
+ body: &mut Vec<u8>,
7845
+ ctx: &LocalCtx,
7846
+ ) {
7847
+ let ty = inferLocalType(result_expr, ctx);
7848
+ match tailMode(tail, Some(func_idx), &sig.ret, &ty, ctx) {
7849
+ TailMode::Plain => Instruction::Call(func_idx).encode(body),
7850
+ TailMode::Return => Instruction::ReturnCall(func_idx).encode(body),
7851
+ TailMode::Loop => {
7852
+ // Every argument is already on the stack (so none reads a param after
7853
+ // it is overwritten); assign them to the param locals in reverse.
7854
+ for i in (0..sig.params.len() as u32).rev() {
7855
+ Instruction::LocalSet(i).encode(body);
7856
+ }
7857
+ Instruction::Br(ctx.tail_depth.get()).encode(body);
7858
+ }
7859
+ }
7860
+ }
7861
+
7817
7862
  fn resolveCallee<'c>(
7818
7863
  ctx: &'c LocalCtx,
7819
7864
  call_expr: &ast::Expr,
@@ -8102,11 +8147,7 @@ fn compileMethodCallAs(
8102
8147
  compileExpr(object, body, ctx, state)?;
8103
8148
  }
8104
8149
  compileCallArgs(&method, &call.args, sig, body, ctx, state)?;
8105
- if tailCallOk(result_expr, &sig.ret, ctx) {
8150
+ emitCall(func_idx, sig, call.tail, result_expr, body, ctx);
8106
- Instruction::ReturnCall(func_idx).encode(body);
8107
- } else {
8108
- Instruction::Call(func_idx).encode(body);
8109
- }
8110
8151
  return Ok(());
8111
8152
  }
8112
8153
  let class_name = match plum_checker::methodReceiverName(obj_ty) {
@@ -8125,7 +8166,7 @@ fn compileMethodCallAs(
8125
8166
  if !ctx.func_ids.contains_key(&key) && ctx.locals.contains_key(&field_local) {
8126
8167
  compileFieldRead(object, &method, obj_ty, result_expr, body, ctx, state)?;
8127
8168
  Instruction::LocalSet(ctx.locals[&field_local]).encode(body);
8128
- return compileClosureCallArgs(&field_local, &call.args, body, ctx, state);
8169
+ return compileClosureCallArgs(&field_local, &call.args, ast::TailKind::NotTail, body, ctx, state);
8129
8170
  }
8130
8171
  let func_idx = *ctx
8131
8172
  .func_ids
@@ -8156,11 +8197,7 @@ fn compileMethodCallAs(
8156
8197
  .get(&key)
8157
8198
  .ok_or_else(|| format!("codegen: unknown method '{}.{}'", class_name, method))?;
8158
8199
  compileCallArgs(&key, &call.args, sig, body, ctx, state)?;
8159
- if tailCallOk(result_expr, &sig.ret, ctx) {
8200
+ emitCall(func_idx, sig, call.tail, result_expr, body, ctx);
8160
- Instruction::ReturnCall(func_idx).encode(body);
8161
- } else {
8162
- Instruction::Call(func_idx).encode(body);
8163
- }
8164
8201
  Ok(())
8165
8202
  }
8166
8203
 
@@ -8483,12 +8520,12 @@ fn compileClosureLiteral(
8483
8520
  /// table index (the `call_indirect` operand).
8484
8521
  fn compileClosureCall(
8485
8522
  call: &ast::FnCall,
8486
- call_expr: &ast::Expr,
8523
+ _call_expr: &ast::Expr,
8487
8524
  body: &mut Vec<u8>,
8488
8525
  ctx: &LocalCtx,
8489
8526
  state: &mut ModuleState,
8490
8527
  ) -> Result<(), String> {
8491
- compileClosureCallArgsTail(&call.name, &call.args, Some(call_expr), body, ctx, state)
8528
+ compileClosureCallArgs(&call.name, &call.args, call.tail, body, ctx, state)
8492
8529
  }
8493
8530
 
8494
8531
  /// `compileClosureCall` over the local `name` with explicit `args` — the args must be
@@ -8497,19 +8534,7 @@ fn compileClosureCall(
8497
8534
  fn compileClosureCallArgs(
8498
8535
  name: &str,
8499
8536
  args: &[ast::Arg],
8500
- body: &mut Vec<u8>,
8501
- ctx: &LocalCtx,
8537
+ tail: ast::TailKind,
8502
- state: &mut ModuleState,
8503
- ) -> Result<(), String> {
8504
- compileClosureCallArgsTail(name, args, None, body, ctx, state)
8505
- }
8506
-
8507
- /// `compileClosureCallArgs`; with `tail_expr` (the call expression) it may emit
8508
- /// `return_call_indirect` when the call is a valid tail call (see `tailCallOk`).
8509
- fn compileClosureCallArgsTail(
8510
- name: &str,
8511
- args: &[ast::Arg],
8512
- tail_expr: Option<&ast::Expr>,
8513
8538
  body: &mut Vec<u8>,
8514
8539
  ctx: &LocalCtx,
8515
8540
  state: &mut ModuleState,
@@ -8550,15 +8575,15 @@ fn compileClosureCallArgsTail(
8550
8575
  field_index: 0,
8551
8576
  }
8552
8577
  .encode(body);
8553
- let callee_ret: Vec<ValType> = withGcTypes(|g| flatValtypes(r, g));
8578
+ let ret_vts = key.1.clone();
8554
- if tail_expr.is_some_and(|e| tailCallOk(e, &callee_ret, ctx)) {
8579
+ if tailMode(tail, None, &ret_vts, r, ctx) == TailMode::Plain {
8555
- Instruction::ReturnCallIndirect {
8580
+ Instruction::CallIndirect {
8556
8581
  type_index,
8557
8582
  table_index: 0,
8558
8583
  }
8559
8584
  .encode(body);
8560
8585
  } else {
8561
- Instruction::CallIndirect {
8586
+ Instruction::ReturnCallIndirect {
8562
8587
  type_index,
8563
8588
  table_index: 0,
8564
8589
  }
@@ -8711,10 +8736,13 @@ fn compileClosureBody(
8711
8736
  gc_types: &ctx.gc_types,
8712
8737
  singleton_globals: &ctx.singleton_globals,
8713
8738
  type_env: RefCell::new(base_env),
8714
- loop_extra_depth: Cell::new(0),
8739
+ loop_base_depth: Cell::new(0),
8715
8740
  ret_type: Some(info.ret_ptype.clone()),
8716
8741
  match_subject_types: RefCell::new(HashMap::new()),
8717
8742
  subject_ptypes: RefCell::new(HashMap::new()),
8743
+ self_func: None,
8744
+ tail_depth: Cell::new(0),
8745
+ import_count: ctx.import_count,
8718
8746
  };
8719
8747
 
8720
8748
  if matches!(info.ret_ptype, PlumType::TUnit) {
plum-wasm-codegen/tests/tail_calls_e2e_test.rs ADDED
@@ -0,0 +1,283 @@
1
+ #![allow(non_snake_case)]
2
+ use plum_wasm_codegen::compileSource;
3
+
4
+ fn tailEngine() -> wasmtime::Engine {
5
+ let mut config = wasmtime::Config::new();
6
+ config.wasm_gc(true);
7
+ config.wasm_function_references(true);
8
+ config.wasm_tail_call(true);
9
+ wasmtime::Engine::new(&config).expect("engine with GC and tail calls should construct")
10
+ }
11
+
12
+ fn repoRoot() -> std::path::PathBuf {
13
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("..")
14
+ }
15
+
16
+ fn compile(name: &str, src: &str) -> Vec<u8> {
17
+ let dir = std::env::temp_dir().join(format!("plum_tail_calls_{}_{}", name, std::process::id()));
18
+ std::fs::create_dir_all(&dir).unwrap();
19
+ let path = dir.join("main.plum");
20
+ std::fs::write(&path, src).unwrap();
21
+ let r = plum_core::loadAndMerge(&path, &repoRoot());
22
+ let _ = std::fs::remove_dir_all(&dir);
23
+ let source = r.unwrap_or_else(|e| panic!("load: {e}"));
24
+ let bytes = compileSource(&source).unwrap_or_else(|e| panic!("compile failed: {e}"));
25
+ let mut v = wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
26
+ v.validate_all(&bytes).expect("valid wasm");
27
+ bytes
28
+ }
29
+
30
+ /// Compiles `src` through the real pipeline and runs its `main() -> Int`.
31
+ fn runInt(name: &str, src: &str) -> i64 {
32
+ let bytes = compile(name, src);
33
+ let engine = tailEngine();
34
+ let module = wasmtime::Module::new(&engine, &bytes).unwrap();
35
+ let mut store = wasmtime::Store::new(&engine, ());
36
+ let mut linker = wasmtime::Linker::new(&engine);
37
+ linker
38
+ .define_unknown_imports_as_default_values(&mut store, &module)
39
+ .unwrap();
40
+ let instance = linker.instantiate(&mut store, &module).unwrap();
41
+ let main = instance
42
+ .get_typed_func::<(), i64>(&mut store, "main")
43
+ .unwrap();
44
+ main.call(&mut store, ()).unwrap()
45
+ }
46
+
47
+ const PRE: &str = "import std/Str\nimport std/Int\nimport std/Float\nimport std/List\n\n";
48
+
49
+ fn prog(body: &str) -> String {
50
+ format!("{PRE}{body}")
51
+ }
52
+
53
+ const LINK: &str = "
54
+ enum Link[T] = Nil | Cons(head: T, tail: Link[T])
55
+
56
+ fun build(n: Int, acc: Link[Int]) -> Link[Int] =
57
+ if n == 0 then acc else build(n - 1, Cons(head: n, tail: acc))
58
+
59
+ fun sumAcc(xs: Link[Int], acc: Int) -> Int = match xs
60
+ Nil => acc
61
+ Cons(h, t) => sumAcc(t, acc + h)
62
+ ";
63
+
64
+ #[test]
65
+ fn self_tail_million() {
66
+ let src = prog(&format!(
67
+ "{LINK}
68
+ fun main() -> Int = sumAcc(build(1000000, Nil), 0)
69
+ "
70
+ ));
71
+ assert_eq!(runInt("self_tail_million", &src), 500000500000);
72
+ }
73
+
74
+ #[test]
75
+ fn self_tail_args_swap() {
76
+ let src = prog(
77
+ "fun gcd(a: Int, b: Int) -> Int = if b == 0 then a else gcd(b, a % b)
78
+
79
+ fun swapN(a: Int, b: Int, n: Int) -> Int =
80
+ if n == 0 then a * 1000 + b else swapN(b, a, n - 1)
81
+
82
+ fun main() -> Int = gcd(1071, 462) * 1000000 + swapN(1, 2, 999999)
83
+ ",
84
+ );
85
+ // gcd = 21; odd swap count leaves (2, 1).
86
+ assert_eq!(runInt("self_tail_args_swap", &src), 21 * 1000000 + 2001);
87
+ }
88
+
89
+ #[test]
90
+ fn mutual_tail_million() {
91
+ let src = prog(
92
+ "fun isEven(n: Int) -> Int = if n == 0 then 1 else isOdd(n - 1)
93
+
94
+ fun isOdd(n: Int) -> Int = if n == 0 then 0 else isEven(n - 1)
95
+
96
+ fun main() -> Int = isEven(1000001) * 10 + isOdd(1000001)
97
+ ",
98
+ );
99
+ assert_eq!(runInt("mutual_tail_million", &src), 1);
100
+ }
101
+
102
+ #[test]
103
+ fn closure_tail_call_runs() {
104
+ let src = prog(
105
+ "fun run(n: Int, k: fn(Int) -> Int) -> Int = if n == 0 then 7 else k(n - 1)
106
+
107
+ fun go(n: Int) -> Int = run(n, go)
108
+
109
+ fun main() -> Int = go(1000000)
110
+ ",
111
+ );
112
+ assert_eq!(runInt("closure_tail_call_runs", &src), 7);
113
+ }
114
+
115
+ #[test]
116
+ fn method_self_tail() {
117
+ let src = prog(
118
+ "record Counter(step: Int) =
119
+ fun count(self, n: Int, acc: Int) -> Int =
120
+ if n == 0 then acc else self.count(n - 1, acc + self.step)
121
+
122
+ fun main() -> Int = Counter(step: 3).count(1000000, 0)
123
+ ",
124
+ );
125
+ assert_eq!(runInt("method_self_tail", &src), 3000000);
126
+ }
127
+
128
+ #[test]
129
+ fn own_param_self_tail() {
130
+ let src = prog(
131
+ "own record Cell(x: Int, y: Int)
132
+
133
+ fun bump(own c: Cell, n: Int) -> Cell =
134
+ if n == 0 then c else bump(Cell(..c, x: c.x + 1), n - 1)
135
+
136
+ fun main() -> Int =
137
+ c := bump(Cell(x: 0, y: 5), 100000)
138
+ c.x * 10 + c.y
139
+ ",
140
+ );
141
+ assert_eq!(runInt("own_param_self_tail", &src), 1000005);
142
+ }
143
+
144
+ /// R3: a closure made in one iteration keeps that iteration's parameter value.
145
+ #[test]
146
+ fn closure_captures_iteration_value() {
147
+ let src = prog(
148
+ "fun go(i: Int, f: fn(Int) -> Int) -> Int =
149
+ if i == 3 then f(100) else go(i + 1, |x| x + i)
150
+
151
+ fun main() -> Int = go(0, |x| x)
152
+ ",
153
+ );
154
+ assert_eq!(runInt("closure_captures_iteration_value", &src), 102);
155
+ }
156
+
157
+ /// Match arms and else-if chains nest wasm blocks between the call and the loop.
158
+ #[test]
159
+ fn self_tail_through_nested_branches() {
160
+ let src = prog(
161
+ "fun f(n: Int, acc: Int) -> Int =
162
+ if n == 0 then acc
163
+ else if n % 3 == 0 then f(n - 1, acc + 1)
164
+ else if n % 3 == 1 then f(n - 1, acc + 10)
165
+ else f(n - 1, acc + 100)
166
+
167
+ fun g(n: Int, acc: Int) -> Int = match n
168
+ 0 => acc
169
+ k => if k % 2 == 0 then g(k - 1, acc + 2) else g(k - 1, acc + 1)
170
+
171
+ fun main() -> Int = f(30, 0) * 1000 + g(10, 0)
172
+ ",
173
+ );
174
+ assert_eq!(runInt("self_tail_nested", &src), 1110 * 1000 + 15);
175
+ }
176
+
177
+ #[derive(Default)]
178
+ struct FnOps {
179
+ calls: Vec<u32>,
180
+ branches: usize,
181
+ returnCalls: usize,
182
+ returnCallIndirects: usize,
183
+ }
184
+
185
+ /// Operator summary of the `n`-th function-section body whose export-or-name matches `pick`.
186
+ fn opsOf(bytes: &[u8], pick: &str) -> FnOps {
187
+ // Find the function index by its name-section entry.
188
+ let mut importCount = 0u32;
189
+ let mut target: Option<u32> = None;
190
+ let mut bodies: Vec<wasmparser::FunctionBody> = Vec::new();
191
+ for payload in wasmparser::Parser::new(0).parse_all(bytes) {
192
+ match payload.unwrap() {
193
+ wasmparser::Payload::ImportSection(r) => {
194
+ for imp in r {
195
+ if matches!(imp.unwrap().ty, wasmparser::TypeRef::Func(_)) {
196
+ importCount += 1;
197
+ }
198
+ }
199
+ }
200
+ wasmparser::Payload::CodeSectionEntry(f) => bodies.push(f),
201
+ wasmparser::Payload::CustomSection(c) => {
202
+ if let wasmparser::KnownCustom::Name(names) = c.as_known() {
203
+ for n in names {
204
+ if let Ok(wasmparser::Name::Function(map)) = n {
205
+ for naming in map {
206
+ let naming = naming.unwrap();
207
+ if naming.name == pick {
208
+ target = Some(naming.index);
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ }
215
+ _ => {}
216
+ }
217
+ }
218
+ let idx = target.unwrap_or_else(|| panic!("no function named {pick}"));
219
+ let body = &bodies[(idx - importCount) as usize];
220
+ let mut ops = FnOps::default();
221
+ let mut r = body.get_operators_reader().unwrap();
222
+ while !r.eof() {
223
+ match r.read().unwrap() {
224
+ wasmparser::Operator::Call { function_index } => ops.calls.push(function_index),
225
+ wasmparser::Operator::Br { .. } => ops.branches += 1,
226
+ wasmparser::Operator::ReturnCall { .. } => ops.returnCalls += 1,
227
+ wasmparser::Operator::ReturnCallIndirect { .. } => ops.returnCallIndirects += 1,
228
+ _ => {}
229
+ }
230
+ }
231
+ ops
232
+ }
233
+
234
+ fn funcIndexOf(bytes: &[u8], name: &str) -> u32 {
235
+ for payload in wasmparser::Parser::new(0).parse_all(bytes) {
236
+ if let wasmparser::Payload::CustomSection(c) = payload.unwrap() {
237
+ if let wasmparser::KnownCustom::Name(names) = c.as_known() {
238
+ for n in names {
239
+ if let Ok(wasmparser::Name::Function(map)) = n {
240
+ for naming in map {
241
+ let naming = naming.unwrap();
242
+ if naming.name == name {
243
+ return naming.index;
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+ }
250
+ }
251
+ panic!("no function named {name}");
252
+ }
253
+
254
+ #[test]
255
+ fn wasm_has_no_call_in_loop() {
256
+ let src = prog(&format!(
257
+ "{LINK}
258
+ fun main() -> Int = sumAcc(build(10, Nil), 0)
259
+ "
260
+ ));
261
+ let bytes = compile("wasm_has_no_call_in_loop", &src);
262
+ let me = funcIndexOf(&bytes, "sumAcc");
263
+ let ops = opsOf(&bytes, "sumAcc");
264
+ assert!(ops.branches >= 1, "sumAcc should branch back to its loop");
265
+ assert!(!ops.calls.contains(&me), "sumAcc must not call itself");
266
+ assert_eq!(ops.returnCalls, 0);
267
+ }
268
+
269
+ #[test]
270
+ fn return_call_emitted() {
271
+ let src = prog(
272
+ "fun isEven(n: Int) -> Int = if n == 0 then 1 else isOdd(n - 1)
273
+
274
+ fun isOdd(n: Int) -> Int = if n == 0 then 0 else isEven(n - 1)
275
+
276
+ fun main() -> Int = isEven(10)
277
+ ",
278
+ );
279
+ let bytes = compile("return_call_emitted", &src);
280
+ let ops = opsOf(&bytes, "isEven");
281
+ assert!(ops.returnCalls >= 1, "isEven should return_call isOdd");
282
+ assert!(ops.calls.is_empty());
283
+ }