plum

#treesitter#compiler#wasm

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

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


bb8ca38Peter John 2026-07-19T19:25:17+05:30
feat(plum-wasm-codegen): implement WasmModule, CompileCtx and compile_source
plum-wasm-codegen/Cargo.toml CHANGED
@@ -10,3 +10,5 @@ plum-checker = { path = "../plum-checker" }
10
10
 
11
11
  [dev-dependencies]
12
12
  wasmparser = "0.220"
13
+ tree-sitter = "0.26"
14
+ tree-sitter-plum = { path = "../tooling/tree-sitter-plum" }
plum-wasm-codegen/src/lib.rs CHANGED
@@ -1 +1,666 @@
1
+ use wasm_encoder::*;
2
+ use std::collections::HashMap;
3
+ use plum_core::ast;
4
+
5
+ pub struct WasmModule {
6
+ types: Vec<FuncType>,
7
+ imports: Vec<(String, String, u32)>,
8
+ functions: Vec<(u32, Vec<u8>)>,
9
+ exports: Vec<(String, ExportKind, u32)>,
10
+ memories: Vec<MemoryType>,
11
+ globals: Vec<(ValType, bool, Vec<u8>)>,
12
+ data_segments: Vec<(u32, Vec<u8>)>,
13
+ pub func_import_count: u32,
14
+ pub func_count: u32,
15
+ global_count: u32,
16
+ }
17
+
18
+ impl WasmModule {
19
+ pub fn new() -> Self {
20
+ Self {
21
+ types: Vec::new(),
22
+ imports: Vec::new(),
23
+ functions: Vec::new(),
24
+ exports: Vec::new(),
25
+ memories: Vec::new(),
26
+ globals: Vec::new(),
27
+ data_segments: Vec::new(),
28
+ func_import_count: 0,
29
+ func_count: 0,
30
+ global_count: 0,
31
+ }
32
+ }
33
+
34
+ pub fn add_type(&mut self, params: &[ValType], results: &[ValType]) -> u32 {
35
+ let idx = self.types.len() as u32;
36
+ self.types.push(FuncType::new(params.iter().copied(), results.iter().copied()));
37
+ idx
38
+ }
39
+
40
+ pub fn add_import(&mut self, module: &str, name: &str, type_idx: u32) -> u32 {
41
+ let idx = self.func_import_count;
42
+ self.imports.push((module.to_string(), name.to_string(), type_idx));
43
+ self.func_import_count += 1;
44
+ idx
45
+ }
46
+
47
+ pub fn add_function(&mut self, type_idx: u32, body: &[u8]) -> u32 {
48
+ let idx = self.func_import_count + self.func_count;
49
+ self.functions.push((type_idx, body.to_vec()));
50
+ self.func_count += 1;
51
+ idx
52
+ }
53
+
54
+ pub fn add_export(&mut self, name: &str, kind: ExportKind, idx: u32) {
55
+ self.exports.push((name.to_string(), kind, idx));
56
+ }
57
+
58
+ pub fn add_memory(&mut self, min: u64, max: Option<u64>) -> u32 {
59
+ let idx = self.memories.len() as u32;
60
+ self.memories.push(MemoryType {
61
+ minimum: min,
62
+ maximum: max,
63
+ memory64: false,
64
+ shared: false,
65
+ page_size_log2: None,
66
+ });
67
+ idx
68
+ }
69
+
70
+ pub fn add_global(&mut self, val_type: ValType, mutable: bool, init: &[u8]) -> u32 {
71
+ let idx = self.global_count;
72
+ self.globals.push((val_type, mutable, init.to_vec()));
73
+ self.global_count += 1;
74
+ idx
75
+ }
76
+
77
+ pub fn add_data_segment(&mut self, offset: u32, data: &[u8]) {
78
+ self.data_segments.push((offset, data.to_vec()));
79
+ }
80
+
81
+ pub fn finish(&mut self) -> Vec<u8> {
82
+ let mut module = wasm_encoder::Module::new();
83
+
1
- // placeholder
84
+ // Type section
85
+ let mut types = TypeSection::new();
86
+ for ft in &self.types {
87
+ types.ty().function(ft.params().iter().copied(), ft.results().iter().copied());
88
+ }
89
+ module.section(&types);
90
+
91
+ // Import section
92
+ if !self.imports.is_empty() {
93
+ let mut imports = ImportSection::new();
94
+ for (module_name, name, type_idx) in &self.imports {
95
+ imports.import(module_name, name, EntityType::Function(*type_idx));
96
+ }
97
+ module.section(&imports);
98
+ }
99
+
100
+ // Function section
101
+ if !self.functions.is_empty() {
102
+ let mut funcs = FunctionSection::new();
103
+ for (type_idx, _) in &self.functions {
104
+ funcs.function(*type_idx);
105
+ }
106
+ module.section(&funcs);
107
+ }
108
+
109
+ // Memory section
110
+ if !self.memories.is_empty() {
111
+ let mut mem = MemorySection::new();
112
+ for mt in &self.memories {
113
+ mem.memory(*mt);
114
+ }
115
+ module.section(&mem);
116
+ }
117
+
118
+ // Global section
119
+ if !self.globals.is_empty() {
120
+ let mut globals = GlobalSection::new();
121
+ for (val_type, mutable, init_expr) in &self.globals {
122
+ let expr = ConstExpr::raw(init_expr.iter().copied());
123
+ globals.global(
124
+ GlobalType { val_type: *val_type, mutable: *mutable, shared: false },
125
+ &expr,
126
+ );
127
+ }
128
+ module.section(&globals);
129
+ }
130
+
131
+ // Export section
132
+ if !self.exports.is_empty() {
133
+ let mut exports = ExportSection::new();
134
+ for (name, kind, idx) in &self.exports {
135
+ exports.export(name, *kind, *idx);
136
+ }
137
+ module.section(&exports);
138
+ }
139
+
140
+ // Code section
141
+ if !self.functions.is_empty() {
142
+ let mut code = CodeSection::new();
143
+ for (_, body_bytes) in &self.functions {
144
+ code.raw(body_bytes);
145
+ }
146
+ module.section(&code);
147
+ }
148
+
149
+ // Data section
150
+ if !self.data_segments.is_empty() {
151
+ let mut data = DataSection::new();
152
+ for (offset, bytes) in &self.data_segments {
153
+ let offset_expr = ConstExpr::i32_const(*offset as i32);
154
+ data.active(0, &offset_expr, bytes.iter().copied());
155
+ }
156
+ module.section(&data);
157
+ }
158
+
159
+ module.finish()
160
+ }
161
+ }
162
+
163
+ impl Default for WasmModule {
164
+ fn default() -> Self {
165
+ Self::new()
166
+ }
167
+ }
168
+
169
+ #[derive(Clone)]
170
+ pub struct FuncSig {
171
+ pub params: Vec<ValType>,
172
+ pub ret: Option<ValType>,
173
+ }
174
+
175
+ pub struct CompileCtx {
176
+ pub module: WasmModule,
177
+ pub func_ids: HashMap<String, u32>,
178
+ pub func_sigs: HashMap<String, FuncSig>,
179
+ pub current_locals: HashMap<String, u32>,
180
+ pub label_count: u32,
181
+ pub bump_offset: u32,
182
+ }
183
+
184
+ impl CompileCtx {
185
+ pub fn new() -> Self {
186
+ Self {
187
+ module: WasmModule::new(),
188
+ func_ids: HashMap::new(),
189
+ func_sigs: HashMap::new(),
190
+ current_locals: HashMap::new(),
191
+ label_count: 0,
192
+ bump_offset: 0,
193
+ }
194
+ }
195
+ }
196
+
197
+ impl Default for CompileCtx {
198
+ fn default() -> Self {
199
+ Self::new()
200
+ }
201
+ }
202
+
203
+ fn ast_type_to_wasm(name: &str) -> Option<ValType> {
204
+ match name {
205
+ "Int" => Some(ValType::I64),
206
+ "Float" => Some(ValType::F64),
207
+ "Bool" => Some(ValType::I32),
208
+ "Str" => Some(ValType::I32),
209
+ "Unit" => None,
210
+ _ => Some(ValType::I64),
211
+ }
212
+ }
213
+
214
+ fn ret_type_to_wasm(ret: Option<&ast::ReturnType>) -> Option<ValType> {
215
+ ret.and_then(|r| ast_type_to_wasm(&r.name))
216
+ }
217
+
218
+ fn encode_leb128_u32(mut val: u32) -> Vec<u8> {
219
+ let mut bytes = Vec::new();
220
+ loop {
221
+ let mut byte = (val & 0x7f) as u8;
222
+ val >>= 7;
223
+ if val != 0 {
224
+ byte |= 0x80;
225
+ }
226
+ bytes.push(byte);
227
+ if val == 0 {
228
+ break;
229
+ }
230
+ }
231
+ bytes
232
+ }
233
+
234
+ pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
235
+ let mut ctx = CompileCtx::new();
236
+
237
+ // First pass: register all function type signatures and allocate function slots
238
+ for item in &source.items {
239
+ if let ast::Item::Fn(f) = item {
240
+ if f.type_param.is_some() {
241
+ continue; // skip method impls in v1
242
+ }
243
+ let param_types: Vec<ValType> = f
244
+ .params
245
+ .iter()
246
+ .map(|p| {
247
+ let name = match &p.ty {
248
+ ast::ParamType::Type(t) => t.name.as_str(),
249
+ ast::ParamType::Variadic(t) => t.name.as_str(),
250
+ };
251
+ ast_type_to_wasm(name).unwrap_or(ValType::I64)
252
+ })
253
+ .collect();
254
+ let ret = ret_type_to_wasm(f.returns.as_ref());
255
+ let results_vec: Vec<ValType> = ret.into_iter().collect();
256
+ let type_idx = ctx.module.add_type(&param_types, &results_vec);
257
+ let func_idx = ctx.module.add_function(type_idx, &[]);
258
+ ctx.func_ids.insert(f.name.clone(), func_idx);
259
+ ctx.func_sigs.insert(f.name.clone(), FuncSig { params: param_types, ret });
260
+ }
261
+ }
262
+
263
+ // Second pass: compile each function body
264
+ let fns: Vec<ast::Fn> = source
265
+ .items
266
+ .iter()
267
+ .filter_map(|item| {
268
+ if let ast::Item::Fn(f) = item {
269
+ if f.type_param.is_none() {
270
+ Some(f.clone())
271
+ } else {
272
+ None
273
+ }
274
+ } else {
275
+ None
276
+ }
277
+ })
278
+ .collect();
279
+
280
+ let mut compiled_bodies: Vec<(String, Vec<u8>)> = Vec::new();
281
+ for f in &fns {
282
+ let body = compile_fn_body(f, &ctx)?;
283
+ compiled_bodies.push((f.name.clone(), body));
284
+ }
285
+
286
+ // Patch bodies into the module
287
+ for (i, (_, body)) in compiled_bodies.iter().enumerate() {
288
+ ctx.module.functions[i].1 = body.clone();
289
+ }
290
+
291
+ // Export all top-level functions
292
+ let func_ids_snapshot: Vec<(String, u32)> = ctx.func_ids.iter().map(|(k, v)| (k.clone(), *v)).collect();
293
+ for (name, idx) in func_ids_snapshot {
294
+ ctx.module.add_export(&name, ExportKind::Func, idx);
295
+ }
296
+
297
+ Ok(ctx.module.finish())
298
+ }
299
+
300
+ fn collect_local_names(body: &ast::FnBody) -> Vec<String> {
301
+ let mut names = Vec::new();
302
+ if let ast::FnBody::Block(block) = body {
303
+ collect_block_locals(block, &mut names);
304
+ }
305
+ names
306
+ }
307
+
308
+ fn collect_block_locals(block: &ast::Block, names: &mut Vec<String>) {
309
+ for stmt in &block.stmts {
310
+ collect_stmt_locals(stmt, names);
311
+ }
312
+ }
313
+
314
+ fn collect_stmt_locals(stmt: &ast::Stmt, names: &mut Vec<String>) {
315
+ match stmt {
316
+ ast::Stmt::Assign(a) => {
317
+ for t in &a.targets {
318
+ if !names.contains(t) {
319
+ names.push(t.clone());
320
+ }
321
+ }
322
+ }
323
+ ast::Stmt::If(i) => {
324
+ collect_block_locals(&i.body, names);
325
+ for ei in &i.else_ifs {
326
+ collect_block_locals(&ei.body, names);
327
+ }
328
+ if let Some(e) = &i.else_ {
329
+ collect_block_locals(e, names);
330
+ }
331
+ }
332
+ ast::Stmt::While(w) => collect_block_locals(&w.body, names),
333
+ ast::Stmt::For(f) => {
334
+ for v in &f.vars {
335
+ if !names.contains(v) {
336
+ names.push(v.clone());
337
+ }
338
+ }
339
+ collect_block_locals(&f.body, names);
340
+ }
341
+ _ => {}
342
+ }
343
+ }
344
+
345
+ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx) -> Result<Vec<u8>, String> {
346
+ let mut body = Vec::new();
347
+ let param_count = f.params.len() as u32;
348
+
349
+ // Collect extra (non-param) local variable names declared in the body
350
+ let extra_locals = collect_local_names(&f.body);
351
+ let extra_count = extra_locals.len() as u32;
352
+
353
+ // Encode local declarations: group count, then each group as (count, type)
354
+ if extra_count > 0 {
355
+ body.extend(encode_leb128_u32(1)); // 1 group
356
+ body.extend(encode_leb128_u32(extra_count));
357
+ ValType::I64.encode(&mut body);
358
+ } else {
359
+ body.push(0); // 0 groups
360
+ }
361
+
362
+ // Build locals map: params first, then extra locals
363
+ let mut locals: HashMap<String, u32> = HashMap::new();
364
+ for (i, p) in f.params.iter().enumerate() {
365
+ locals.insert(p.name.clone(), i as u32);
366
+ }
367
+ for (i, name) in extra_locals.iter().enumerate() {
368
+ locals.insert(name.clone(), param_count + i as u32);
369
+ }
370
+
371
+ let local_ctx = LocalCtx {
372
+ locals,
373
+ func_ids: &ctx.func_ids,
374
+ func_sigs: &ctx.func_sigs,
375
+ };
376
+
377
+ let has_return_value = f.returns.as_ref().map(|r| r.name != "Unit").unwrap_or(false);
378
+
379
+ match &f.body {
380
+ ast::FnBody::Expr(e) => {
381
+ compile_expr(e, &mut body, &local_ctx)?;
382
+ }
383
+ ast::FnBody::Block(block) => {
384
+ compile_block_as_fn_body(block, &mut body, &local_ctx, has_return_value)?;
385
+ }
386
+ }
387
+
388
+ Instruction::End.encode(&mut body);
389
+ Ok(body)
390
+ }
391
+
392
+ struct LocalCtx<'a> {
393
+ locals: HashMap<String, u32>,
394
+ func_ids: &'a HashMap<String, u32>,
395
+ func_sigs: &'a HashMap<String, FuncSig>,
396
+ }
397
+
398
+ fn compile_block(block: &ast::Block, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<(), String> {
399
+ for stmt in &block.stmts {
400
+ compile_stmt(stmt, body, ctx)?;
401
+ }
402
+ Ok(())
403
+ }
404
+
405
+ /// Compile a block that is the body of a function. If the function returns a value
406
+ /// and the last statement is an expression, that expression's value is left on the
407
+ /// stack instead of being dropped.
408
+ fn compile_block_as_fn_body(
409
+ block: &ast::Block,
410
+ body: &mut Vec<u8>,
411
+ ctx: &LocalCtx,
412
+ has_return_value: bool,
413
+ ) -> Result<(), String> {
414
+ let stmts = &block.stmts;
415
+ if has_return_value {
416
+ if let Some((last, rest)) = stmts.split_last() {
417
+ for stmt in rest {
418
+ compile_stmt(stmt, body, ctx)?;
419
+ }
420
+ // Last statement: if it's an Expr, leave its value on stack
421
+ match last {
422
+ ast::Stmt::Expr(e) => {
423
+ compile_expr(e, body, ctx)?;
424
+ // do NOT drop — this is the return value
425
+ }
426
+ _ => {
427
+ compile_stmt(last, body, ctx)?;
428
+ }
429
+ }
430
+ return Ok(());
431
+ }
432
+ }
433
+ for stmt in stmts {
434
+ compile_stmt(stmt, body, ctx)?;
435
+ }
436
+ Ok(())
437
+ }
438
+
439
+ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<(), String> {
440
+ match stmt {
441
+ ast::Stmt::Assign(a) => {
442
+ for (target, value) in a.targets.iter().zip(a.values.iter()) {
443
+ compile_expr(value, body, ctx)?;
444
+ let idx = *ctx
445
+ .locals
446
+ .get(target)
447
+ .ok_or_else(|| format!("undeclared local '{}'", target))?;
448
+ Instruction::LocalSet(idx).encode(body);
449
+ }
450
+ }
451
+ ast::Stmt::Return(Some(e)) => {
452
+ compile_expr(e, body, ctx)?;
453
+ Instruction::Return.encode(body);
454
+ }
455
+ ast::Stmt::Return(None) => {
456
+ Instruction::Return.encode(body);
457
+ }
458
+ ast::Stmt::If(if_) => {
459
+ compile_expr(&if_.condition, body, ctx)?;
460
+ // Condition is i64 (Bool is I32 but comparison results in I32)
461
+ // Convert to i32 for if: if condition came from compare ops it's already i32
462
+ // Use I32WrapI64 if needed — but compare ops return i32 already
463
+ Instruction::If(BlockType::Empty).encode(body);
464
+ compile_block(&if_.body, body, ctx)?;
465
+ if !if_.else_ifs.is_empty() || if_.else_.is_some() {
466
+ Instruction::Else.encode(body);
467
+ for ei in &if_.else_ifs {
468
+ compile_expr(&ei.condition, body, ctx)?;
469
+ Instruction::If(BlockType::Empty).encode(body);
470
+ compile_block(&ei.body, body, ctx)?;
471
+ Instruction::Else.encode(body);
472
+ }
473
+ if let Some(else_block) = &if_.else_ {
474
+ compile_block(else_block, body, ctx)?;
475
+ }
476
+ for _ in &if_.else_ifs {
477
+ Instruction::End.encode(body);
478
+ }
479
+ }
480
+ Instruction::End.encode(body);
481
+ }
482
+ ast::Stmt::While(w) => {
483
+ // block { loop { cond; i32.eqz; br_if 1; body; br 0 } }
484
+ Instruction::Block(BlockType::Empty).encode(body);
485
+ Instruction::Loop(BlockType::Empty).encode(body);
486
+ compile_expr(&w.condition, body, ctx)?;
487
+ Instruction::I32Eqz.encode(body);
488
+ Instruction::BrIf(1).encode(body);
489
+ compile_block(&w.body, body, ctx)?;
490
+ Instruction::Br(0).encode(body);
491
+ Instruction::End.encode(body);
492
+ Instruction::End.encode(body);
493
+ }
494
+ ast::Stmt::For(f) => {
495
+ if let ast::Expr::Binary(b) = &f.iter {
496
+ if matches!(b.op, ast::BinOp::Range) && f.vars.len() == 1 {
497
+ let var_name = &f.vars[0];
498
+ let var_idx = *ctx
499
+ .locals
500
+ .get(var_name)
501
+ .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
502
+ compile_expr(&b.left, body, ctx)?;
503
+ Instruction::LocalSet(var_idx).encode(body);
504
+ Instruction::Block(BlockType::Empty).encode(body);
505
+ Instruction::Loop(BlockType::Empty).encode(body);
506
+ Instruction::LocalGet(var_idx).encode(body);
507
+ compile_expr(&b.right, body, ctx)?;
508
+ Instruction::I64GeS.encode(body);
509
+ Instruction::BrIf(1).encode(body);
510
+ compile_block(&f.body, body, ctx)?;
511
+ Instruction::LocalGet(var_idx).encode(body);
512
+ Instruction::I64Const(1).encode(body);
513
+ Instruction::I64Add.encode(body);
514
+ Instruction::LocalSet(var_idx).encode(body);
515
+ Instruction::Br(0).encode(body);
516
+ Instruction::End.encode(body);
517
+ Instruction::End.encode(body);
518
+ return Ok(());
519
+ }
520
+ }
521
+ compile_expr(&f.iter, body, ctx)?;
522
+ Instruction::Drop.encode(body);
523
+ }
524
+ ast::Stmt::Expr(e) => {
525
+ let has_result = expr_has_result(e, ctx);
526
+ compile_expr(e, body, ctx)?;
527
+ if has_result {
528
+ Instruction::Drop.encode(body);
529
+ }
530
+ }
531
+ ast::Stmt::Break => {
532
+ Instruction::Br(1).encode(body);
533
+ }
534
+ ast::Stmt::Continue => {
535
+ Instruction::Br(0).encode(body);
536
+ }
537
+ ast::Stmt::Assert(_) | ast::Stmt::Match(_) | ast::Stmt::Todo => {}
538
+ }
539
+ Ok(())
540
+ }
541
+
542
+ /// Returns true if the expression leaves a value on the wasm stack.
543
+ fn expr_has_result(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
544
+ match expr {
545
+ ast::Expr::FnCall(call) => {
546
+ ctx.func_sigs.get(&call.name).map(|s| s.ret.is_some()).unwrap_or(true)
547
+ }
548
+ _ => true,
549
+ }
550
+ }
551
+
552
+ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<(), String> {
553
+ match expr {
554
+ ast::Expr::Int(n) => {
555
+ Instruction::I64Const(*n).encode(body);
556
+ }
557
+ ast::Expr::Float(f) => {
558
+ Instruction::F64Const(*f).encode(body);
559
+ }
560
+ ast::Expr::Var(name) => {
561
+ if name == "true" {
562
+ Instruction::I32Const(1).encode(body);
563
+ } else if name == "false" {
564
+ Instruction::I32Const(0).encode(body);
565
+ } else {
566
+ let idx = *ctx
567
+ .locals
568
+ .get(name.as_str())
569
+ .ok_or_else(|| format!("undeclared variable '{}'", name))?;
570
+ Instruction::LocalGet(idx).encode(body);
571
+ }
572
+ }
573
+ ast::Expr::Paren(inner) => {
574
+ compile_expr(inner, body, ctx)?;
575
+ }
576
+ ast::Expr::Unary(u) => {
577
+ match u.op {
578
+ ast::UnOp::Neg => {
579
+ // WASM has no i64.neg; use i64.const(0) - operand
580
+ Instruction::I64Const(0).encode(body);
581
+ compile_expr(&u.operand, body, ctx)?;
582
+ Instruction::I64Sub.encode(body);
583
+ }
584
+ ast::UnOp::Pos => {
585
+ compile_expr(&u.operand, body, ctx)?;
586
+ }
587
+ }
588
+ }
589
+ ast::Expr::Binary(b) => {
590
+ compile_expr(&b.left, body, ctx)?;
591
+ compile_expr(&b.right, body, ctx)?;
592
+ match b.op {
593
+ ast::BinOp::Add => Instruction::I64Add.encode(body),
594
+ ast::BinOp::Sub => Instruction::I64Sub.encode(body),
595
+ ast::BinOp::Mul => Instruction::I64Mul.encode(body),
596
+ ast::BinOp::Div => Instruction::I64DivS.encode(body),
597
+ ast::BinOp::Mod => Instruction::I64RemS.encode(body),
598
+ ast::BinOp::BitOr => Instruction::I64Or.encode(body),
599
+ ast::BinOp::BitAnd => Instruction::I64And.encode(body),
600
+ ast::BinOp::Xor => Instruction::I64Xor.encode(body),
601
+ ast::BinOp::Shl => Instruction::I64Shl.encode(body),
602
+ ast::BinOp::Shr => Instruction::I64ShrS.encode(body),
603
+ ast::BinOp::Range => {
604
+ // Range used outside of For: leave end on stack (start consumed)
605
+ }
606
+ }
607
+ }
608
+ ast::Expr::Bool(b) => {
609
+ compile_expr(&b.left, body, ctx)?;
610
+ compile_expr(&b.right, body, ctx)?;
611
+ match b.op {
612
+ ast::BoolOp::And => Instruction::I32And.encode(body),
613
+ ast::BoolOp::Or => Instruction::I32Or.encode(body),
614
+ }
615
+ }
616
+ ast::Expr::Not(inner) => {
617
+ compile_expr(inner, body, ctx)?;
618
+ Instruction::I32Eqz.encode(body);
619
+ }
620
+ ast::Expr::Compare(c) => {
621
+ compile_expr(&c.left, body, ctx)?;
622
+ compile_expr(&c.right, body, ctx)?;
623
+ match c.op {
624
+ ast::CmpOp::Lt => Instruction::I64LtS.encode(body),
625
+ ast::CmpOp::Lte => Instruction::I64LeS.encode(body),
626
+ ast::CmpOp::Eq => Instruction::I64Eq.encode(body),
627
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I64Ne.encode(body),
628
+ ast::CmpOp::Gte => Instruction::I64GeS.encode(body),
629
+ ast::CmpOp::Gt => Instruction::I64GtS.encode(body),
630
+ }
631
+ }
632
+ ast::Expr::Ternary(t) => {
633
+ compile_expr(&t.condition, body, ctx)?;
634
+ Instruction::If(BlockType::Result(ValType::I64)).encode(body);
635
+ compile_expr(&t.then, body, ctx)?;
636
+ Instruction::Else.encode(body);
637
+ compile_expr(&t.else_, body, ctx)?;
638
+ Instruction::End.encode(body);
639
+ }
640
+ ast::Expr::FnCall(call) => {
641
+ for arg in &call.args {
642
+ let arg_expr = match arg {
643
+ ast::Arg::Positional(e) => e,
644
+ ast::Arg::Keyword { value, .. } => value,
645
+ ast::Arg::Pair { value, .. } => value,
646
+ };
647
+ compile_expr(arg_expr, body, ctx)?;
648
+ }
649
+ let func_idx = ctx
650
+ .func_ids
651
+ .get(&call.name)
652
+ .ok_or_else(|| format!("unknown function '{}'", call.name))?;
653
+ Instruction::Call(*func_idx).encode(body);
654
+ }
655
+ ast::Expr::Self_ => {
656
+ return Err("'self' not supported in v1 codegen".to_string());
657
+ }
658
+ ast::Expr::TypeName(_) | ast::Expr::ClassCall(_) | ast::Expr::Attribute(_) => {
659
+ Instruction::I64Const(0).encode(body);
660
+ }
661
+ ast::Expr::String(_) => {
662
+ Instruction::I32Const(0).encode(body);
663
+ }
664
+ }
665
+ Ok(())
666
+ }
plum-wasm-codegen/tests/codegen_tests.rs ADDED
@@ -0,0 +1,30 @@
1
+ use plum_wasm_codegen::compile_source;
2
+ use plum_core::AstParser;
3
+
4
+ fn parse(src: &str) -> plum_core::ast::Source {
5
+ let mut parser = tree_sitter::Parser::new();
6
+ parser.set_language(&tree_sitter_plum::LANGUAGE.into()).unwrap();
7
+ let tree = parser.parse(src, None).unwrap();
8
+ let ap = AstParser::new(src);
9
+ ap.parse_source(tree.root_node())
10
+ }
11
+
12
+ #[test]
13
+ fn compiles_to_valid_wasm() {
14
+ let src = "add(a: Int, b: Int) -> Int =\n a + b\n";
15
+ let source = parse(src);
16
+ let bytes = compile_source(&source).expect("compile failed");
17
+ // Valid WASM starts with the magic number
18
+ assert_eq!(&bytes[0..4], b"\0asm");
19
+ assert_eq!(&bytes[4..8], &[1, 0, 0, 0]); // version 1
20
+ }
21
+
22
+ #[test]
23
+ fn output_validates() {
24
+ let src = "add(a: Int, b: Int) -> Int =\n a + b\n";
25
+ let source = parse(src);
26
+ let bytes = compile_source(&source).expect("compile failed");
27
+ // wasmparser should accept the output
28
+ let result = wasmparser::validate(&bytes);
29
+ assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
30
+ }