plum
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 +6 -1
- plum-core/src/ast.rs +15 -0
- plum-core/src/lib.rs +2 -0
- plum-core/src/tail.rs +265 -0
- plum-core/src/trmc.rs +446 -0
- plum-core/tests/tail_test.rs +134 -0
- plum-core/tests/trmc_test.rs +287 -0
- plum-examples/tail_calls.plum +90 -0
- plum-game/src/host.rs +3 -1
- plum-game/web-test/fixtures/stress_state_view_deep.plum +202 -0
- plum-runtime/src/main.rs +3 -1
- plum-wasm-codegen/src/lib.rs +186 -158
- plum-wasm-codegen/tests/tail_calls_e2e_test.rs +283 -0
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
|
-
///
|
|
500
|
+
/// `tail_depth` as it stood when the NEAREST ENCLOSING LOOP's body began.
|
|
500
|
-
///
|
|
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
|
-
///
|
|
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
|
-
///
|
|
503
|
+
/// pattern tests and `when` guards, catch blocks) before reaching the loop's
|
|
505
|
-
/// `
|
|
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
|
-
|
|
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::
|
|
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
|
-
|
|
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
|
|
4826
|
+
let looped = local_ctx.self_func.as_ref().is_some_and(|s| s.looped);
|
|
4845
|
-
|
|
4827
|
+
if looped {
|
|
4846
|
-
|
|
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
|
-
//
|
|
4879
|
+
// The enclosing wasm `if`s are counted by the callers' `TailDepth` guards;
|
|
4909
|
-
//
|
|
4880
|
+
// see `loop_base_depth`.
|
|
4910
|
-
ctx.loop_extra_depth.set(ctx.loop_extra_depth.get() + 1);
|
|
4911
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
7064
|
+
let _tg = TailDepth::enter(&ctx.tail_depth);
|
|
7082
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
8578
|
+
let ret_vts = key.1.clone();
|
|
8554
|
-
if
|
|
8579
|
+
if tailMode(tail, None, &ret_vts, r, ctx) == TailMode::Plain {
|
|
8555
|
-
Instruction::
|
|
8580
|
+
Instruction::CallIndirect {
|
|
8556
8581
|
type_index,
|
|
8557
8582
|
table_index: 0,
|
|
8558
8583
|
}
|
|
8559
8584
|
.encode(body);
|
|
8560
8585
|
} else {
|
|
8561
|
-
Instruction::
|
|
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
|
-
|
|
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
|
+
}
|