plum

#treesitter#compiler#wasm

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

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


5d8ada1 — Peter John 2026-07-19T21:40:57+05:30
feat(plum-wasm-codegen): class/method codegen, strings, match lowering

Generated files not shown: Cargo.lock

plum-wasm-codegen/Cargo.toml CHANGED
@@ -6,8 +6,10 @@ edition = "2021"
6
6
  [dependencies]
7
7
  wasm-encoder = "0.220"
8
8
  plum-core = { path = "../plum-core" }
9
+ plum-checker = { path = "../plum-checker" }
9
10
 
10
11
  [dev-dependencies]
11
12
  wasmparser = "0.220"
12
13
  tree-sitter = "0.26"
13
14
  tree-sitter-plum = { path = "../tooling/tree-sitter-plum" }
15
+ wasmtime = "28"
plum-wasm-codegen/src/lib.rs CHANGED
@@ -1,6 +1,15 @@
1
1
  use wasm_encoder::*;
2
+ use std::cell::RefCell;
2
3
  use std::collections::HashMap;
3
4
  use plum_core::ast;
5
+ use plum_checker::types::{PlumType, TypeEnv, TypeScheme};
6
+ use plum_checker::{ClassEnv, MethodEnv, EnumVariants};
7
+
8
+ /// Bump-allocated heap for class instances starts at the second 64KiB page so it can
9
+ /// never collide with the (small, compile-time-sized) string literal data area below it.
10
+ const HEAP_BASE: u32 = 65536;
11
+ /// String literal data (length-prefixed UTF-8 blobs) is laid out from here upward.
12
+ const STRING_BASE: u32 = 8;
4
13
 
5
14
  pub struct WasmModule {
6
15
  types: Vec<FuncType>,
@@ -172,31 +181,57 @@ pub struct FuncSig {
172
181
  pub ret: Option<ValType>,
173
182
  }
174
183
 
184
+ /// Global, read-only lookup tables shared by every function body being compiled.
175
185
  pub struct CompileCtx {
176
- pub module: WasmModule,
177
186
  pub func_ids: HashMap<String, u32>,
178
187
  pub func_sigs: HashMap<String, FuncSig>,
188
+ pub classes: ClassEnv,
189
+ pub methods: MethodEnv,
179
- pub current_locals: HashMap<String, u32>,
190
+ pub enum_variants: EnumVariants,
180
- pub label_count: u32,
191
+ pub global_env: TypeEnv,
181
- pub bump_offset: u32,
192
+ pub bump_global: u32,
182
193
  }
183
194
 
195
+ /// Per-module state that accumulates as function bodies are compiled: the running
196
+ /// offset for placing string literal bytes, and the resulting data segments.
184
- impl CompileCtx {
197
+ struct ModuleState {
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,
198
+ next_string_offset: u32,
199
+ data_segments: Vec<(u32, Vec<u8>)>,
193
- }
200
+ }
201
+
202
+ struct LocalCtx<'a> {
203
+ locals: HashMap<String, u32>,
204
+ /// First local index reserved for `ClassCall` scratch temporaries.
205
+ classcall_scratch_base: u32,
206
+ /// `ClassCall` expr identity (pointer address) -> scratch slot offset.
207
+ classcall_scratch: HashMap<usize, u32>,
208
+ /// First local index reserved for `match` subject scratch temporaries.
209
+ match_scratch_base: u32,
210
+ /// `Match` stmt identity (pointer address) -> scratch slot offset.
211
+ match_scratch_index: HashMap<usize, u32>,
212
+ func_ids: &'a HashMap<String, u32>,
213
+ func_sigs: &'a HashMap<String, FuncSig>,
214
+ classes: &'a ClassEnv,
215
+ methods: &'a MethodEnv,
216
+ enum_variants: &'a EnumVariants,
217
+ /// Tracks each binding's inferred type as compilation proceeds through
218
+ /// statements in order, mirroring `plum-checker`'s own env evolution — needed
219
+ /// to resolve `Attribute`/`ClassCall` targets and pick the right load/store width.
220
+ type_env: RefCell<TypeEnv>,
221
+ bump_global: u32,
222
+ }
223
+
224
+ fn fn_key(f: &ast::Fn) -> String {
225
+ match &f.type_param {
226
+ Some(recv) => format!("{}::{}", recv, f.name),
227
+ None => f.name.clone(),
194
228
  }
195
229
  }
196
230
 
231
+ fn param_type_name(pt: &ast::ParamType) -> &str {
197
- impl Default for CompileCtx {
232
+ match pt {
198
- fn default() -> Self {
233
+ ast::ParamType::Type(t) => t.name.as_str(),
199
- Self::new()
234
+ ast::ParamType::Variadic(t) => t.name.as_str(),
200
235
  }
201
236
  }
202
237
 
@@ -207,7 +242,16 @@ fn ast_type_to_wasm(name: &str) -> Option<ValType> {
207
242
  "Bool" => Some(ValType::I32),
208
243
  "Str" => Some(ValType::I32),
209
244
  "Unit" => None,
245
+ _ => Some(ValType::I32), // class instance / unmodeled type: pointer
246
+ }
247
+ }
248
+
249
+ fn plum_type_to_valtype(t: &PlumType) -> ValType {
250
+ match t {
210
- _ => Some(ValType::I64),
251
+ PlumType::TInt => ValType::I64,
252
+ PlumType::TFloat => ValType::F64,
253
+ PlumType::TBool | PlumType::TStr | PlumType::TNamed(_) => ValType::I32,
254
+ PlumType::TVar(_) | PlumType::TUnit | PlumType::TFun(_, _) => ValType::I64,
211
255
  }
212
256
  }
213
257
 
@@ -231,156 +275,335 @@ fn encode_leb128_u32(mut val: u32) -> Vec<u8> {
231
275
  bytes
232
276
  }
233
277
 
278
+ fn check_ctx_of<'a>(ctx_classes: &'a ClassEnv, ctx_methods: &'a MethodEnv, ctx_enum_variants: &'a EnumVariants) -> plum_checker::CheckCtx<'a> {
279
+ plum_checker::CheckCtx { classes: ctx_classes, methods: ctx_methods, enum_variants: ctx_enum_variants }
280
+ }
281
+
282
+ /// Infers an expression's type using the function's current (mutable, evolving) type
283
+ /// environment. Defaults to `TInt` if inference fails — codegen assumes the source was
284
+ /// already accepted by `plum_checker::check_source`, so a failure here would indicate
285
+ /// codegen is being driven directly on unchecked input (as some tests do).
286
+ fn infer_local_type(expr: &ast::Expr, ctx: &LocalCtx) -> PlumType {
287
+ let env = ctx.type_env.borrow();
288
+ let cctx = check_ctx_of(ctx.classes, ctx.methods, ctx.enum_variants);
289
+ plum_checker::infer_expr(expr, &env, &cctx).unwrap_or(PlumType::TInt)
290
+ }
291
+
234
292
  pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {
293
+ let (global_env, classes, methods, enum_variants) = plum_checker::build_global_tables(source);
294
+
235
- let mut ctx = CompileCtx::new();
295
+ let mut module = WasmModule::new();
296
+ module.add_memory(2, None);
297
+ let mut bump_init = Vec::new();
298
+ Instruction::I32Const(HEAP_BASE as i32).encode(&mut bump_init);
299
+ let bump_global = module.add_global(ValType::I32, true, &bump_init);
236
300
 
301
+ let mut func_ids: HashMap<String, u32> = HashMap::new();
302
+ let mut func_sigs: HashMap<String, FuncSig> = HashMap::new();
303
+
237
- // First pass: register all function type signatures and allocate function slots
304
+ // Register every function AND method signature up front (methods get an implicit
305
+ // leading `self: pointer` param and are keyed as "Receiver::method").
238
306
  for item in &source.items {
239
307
  if let ast::Item::Fn(f) = item {
308
+ let mut param_types: Vec<ValType> = Vec::new();
240
309
  if f.type_param.is_some() {
241
- continue; // skip method impls in v1
310
+ param_types.push(ValType::I32);
242
- }
311
+ }
243
- let param_types: Vec<ValType> = f
244
- .params
245
- .iter()
246
- .map(|p| {
247
- let name = match &p.ty {
312
+ for p in &f.params {
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)
313
+ param_types.push(ast_type_to_wasm(param_type_name(&p.ty)).unwrap_or(ValType::I32));
252
- })
314
+ }
253
- .collect();
254
315
  let ret = ret_type_to_wasm(f.returns.as_ref());
255
316
  let results_vec: Vec<ValType> = ret.into_iter().collect();
256
- let type_idx = ctx.module.add_type(&param_types, &results_vec);
317
+ let type_idx = module.add_type(&param_types, &results_vec);
257
- let func_idx = ctx.module.add_function(type_idx, &[]);
318
+ let func_idx = module.add_function(type_idx, &[]);
319
+ let key = fn_key(f);
258
- ctx.func_ids.insert(f.name.clone(), func_idx);
320
+ func_ids.insert(key.clone(), func_idx);
259
- ctx.func_sigs.insert(f.name.clone(), FuncSig { params: param_types, ret });
321
+ func_sigs.insert(key, FuncSig { params: param_types, ret });
260
322
  }
261
323
  }
262
324
 
263
- // Second pass: compile each function body
325
+ let ctx = CompileCtx { func_ids, func_sigs, classes, methods, enum_variants, global_env, bump_global };
326
+
264
- let fns: Vec<ast::Fn> = source
327
+ let fns: Vec<&ast::Fn> = source.items.iter().filter_map(|item| match item {
265
- .items
266
- .iter()
267
- .filter_map(|item| {
268
- if let ast::Item::Fn(f) = item {
328
+ ast::Item::Fn(f) => Some(f),
269
- if f.type_param.is_none() {
270
- Some(f.clone())
329
+ _ => None,
271
- } else {
272
- None
273
- }
274
- } else {
275
- None
276
- }
277
- })
278
- .collect();
330
+ }).collect();
331
+
332
+ let mut state = ModuleState { next_string_offset: STRING_BASE, data_segments: Vec::new() };
279
333
 
280
334
  let mut compiled_bodies: Vec<(String, Vec<u8>)> = Vec::new();
281
335
  for f in &fns {
282
- let body = compile_fn_body(f, &ctx)?;
336
+ let body = compile_fn_body(f, &ctx, &mut state)?;
283
- compiled_bodies.push((f.name.clone(), body));
337
+ compiled_bodies.push((fn_key(f), body));
338
+ }
339
+
340
+ // Patch compiled bodies back into their pre-registered function slots.
341
+ for (name, body) in &compiled_bodies {
342
+ let idx = *ctx.func_ids.get(name).expect("function was registered in the first pass");
343
+ let slot = (idx - module.func_import_count) as usize;
344
+ module.functions[slot].1 = body.clone();
284
345
  }
285
346
 
286
- // Patch bodies into the module
287
- for (i, (_, body)) in compiled_bodies.iter().enumerate() {
347
+ for (offset, bytes) in &state.data_segments {
288
- ctx.module.functions[i].1 = body.clone();
348
+ module.add_data_segment(*offset, bytes);
289
349
  }
290
350
 
291
- // Export only main
292
351
  if let Some(&main_idx) = ctx.func_ids.get("main") {
293
- ctx.module.add_export("main", ExportKind::Func, main_idx);
352
+ module.add_export("main", ExportKind::Func, main_idx);
294
353
  }
295
354
 
296
- Ok(ctx.module.finish())
355
+ Ok(module.finish())
297
356
  }
298
357
 
358
+ /// Walks a function body once to determine: (1) every locally-assigned/bound name and
359
+ /// its inferred type, (2) how many `ClassCall` scratch temporaries it needs, and (3)
360
+ /// the subject type for every `match` statement (for its own scratch temporary).
361
+ struct Collector<'a> {
362
+ env: TypeEnv,
363
+ cctx: plum_checker::CheckCtx<'a>,
364
+ named: Vec<(String, PlumType)>,
299
- fn collect_local_names(body: &ast::FnBody) -> Vec<String> {
365
+ named_set: std::collections::HashSet<String>,
300
- let mut names = Vec::new();
301
- if let ast::FnBody::Block(block) = body {
302
- collect_block_locals(block, &mut names);
366
+ classcall_scratch: HashMap<usize, u32>,
303
- }
367
+ match_scratch: HashMap<usize, PlumType>,
304
- names
368
+ next_classcall_slot: u32,
305
369
  }
306
370
 
371
+ impl<'a> Collector<'a> {
307
- fn collect_block_locals(block: &ast::Block, names: &mut Vec<String>) {
372
+ fn bind(&mut self, name: &str, ty: PlumType) {
308
- for stmt in &block.stmts {
309
- collect_stmt_locals(stmt, names);
373
+ if self.named_set.insert(name.to_string()) {
374
+ self.named.push((name.to_string(), ty.clone()));
375
+ }
376
+ self.env.insert(name.to_string(), TypeScheme::mono(ty));
310
377
  }
311
- }
312
378
 
379
+ fn walk_block(&mut self, block: &ast::Block) {
380
+ for s in &block.stmts {
381
+ self.walk_stmt(s);
382
+ }
383
+ }
384
+
313
- fn collect_stmt_locals(stmt: &ast::Stmt, names: &mut Vec<String>) {
385
+ fn walk_stmt(&mut self, stmt: &ast::Stmt) {
314
- match stmt {
386
+ match stmt {
315
- ast::Stmt::Assign(a) => {
387
+ ast::Stmt::Assign(a) => {
388
+ for (target, value) in a.targets.iter().zip(a.values.iter()) {
389
+ self.walk_expr(value);
390
+ let ty = plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
316
- for t in &a.targets {
391
+ self.bind(target, ty);
317
- if !names.contains(t) {
318
- names.push(t.clone());
319
392
  }
320
393
  }
394
+ ast::Stmt::Return(Some(e)) => self.walk_expr(e),
395
+ ast::Stmt::Return(None) => {}
396
+ ast::Stmt::If(i) => {
397
+ self.walk_expr(&i.condition);
398
+ self.walk_block(&i.body);
399
+ for ei in &i.else_ifs {
400
+ self.walk_expr(&ei.condition);
401
+ self.walk_block(&ei.body);
321
- }
402
+ }
322
- ast::Stmt::If(i) => {
323
- collect_block_locals(&i.body, names);
324
- for ei in &i.else_ifs {
403
+ if let Some(e) = &i.else_ {
325
- collect_block_locals(&ei.body, names);
404
+ self.walk_block(e);
405
+ }
326
406
  }
407
+ ast::Stmt::While(w) => {
408
+ self.walk_expr(&w.condition);
409
+ self.walk_block(&w.body);
410
+ }
411
+ ast::Stmt::For(f) => {
327
- if let Some(e) = &i.else_ {
412
+ self.walk_expr(&f.iter);
413
+ for v in &f.vars {
414
+ self.bind(v, PlumType::TInt);
415
+ }
328
- collect_block_locals(e, names);
416
+ self.walk_block(&f.body);
329
417
  }
418
+ ast::Stmt::Expr(e) => self.walk_expr(e),
419
+ ast::Stmt::Assert(e) => self.walk_expr(e),
420
+ ast::Stmt::Match(m) => {
421
+ let subject_ty = if m.subjects.len() == 1 {
422
+ self.walk_expr(&m.subjects[0]);
423
+ plum_checker::infer_expr(&m.subjects[0], &self.env, &self.cctx).unwrap_or(PlumType::TInt)
424
+ } else {
425
+ for s in &m.subjects {
426
+ self.walk_expr(s);
427
+ }
428
+ PlumType::TInt
429
+ };
430
+ self.match_scratch.insert(m as *const ast::Match as usize, subject_ty.clone());
431
+ for case in &m.cases {
432
+ let saved = self.env.clone();
433
+ if m.subjects.len() == 1 {
434
+ if let Some(ast::CasePattern::Name(n)) = case.patterns.first() {
435
+ let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
436
+ && self.cctx.enum_variants.contains_key(n);
437
+ if !is_variant {
438
+ self.bind(n, subject_ty.clone());
439
+ }
440
+ }
441
+ }
442
+ self.walk_block(&case.body);
443
+ self.env = saved;
444
+ }
445
+ }
446
+ ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
330
447
  }
448
+ }
449
+
450
+ fn walk_expr(&mut self, expr: &ast::Expr) {
451
+ match expr {
452
+ ast::Expr::ClassCall(call) => {
453
+ let idx = self.next_classcall_slot;
454
+ self.next_classcall_slot += 1;
331
- ast::Stmt::While(w) => collect_block_locals(&w.body, names),
455
+ self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
456
+ for fa in &call.fields {
457
+ self.walk_expr(&fa.value);
458
+ }
459
+ }
460
+ ast::Expr::Binary(b) => {
461
+ self.walk_expr(&b.left);
462
+ self.walk_expr(&b.right);
463
+ }
332
- ast::Stmt::For(f) => {
464
+ ast::Expr::Bool(b) => {
465
+ self.walk_expr(&b.left);
466
+ self.walk_expr(&b.right);
467
+ }
468
+ ast::Expr::Compare(c) => {
469
+ self.walk_expr(&c.left);
470
+ self.walk_expr(&c.right);
471
+ }
472
+ ast::Expr::Not(inner) => self.walk_expr(inner),
473
+ ast::Expr::Unary(u) => self.walk_expr(&u.operand),
474
+ ast::Expr::Paren(inner) => self.walk_expr(inner),
475
+ ast::Expr::Ternary(t) => {
476
+ self.walk_expr(&t.condition);
477
+ self.walk_expr(&t.then);
478
+ self.walk_expr(&t.else_);
479
+ }
480
+ ast::Expr::FnCall(call) => {
333
- for v in &f.vars {
481
+ for arg in &call.args {
482
+ self.walk_arg(arg);
483
+ }
484
+ }
485
+ ast::Expr::Attribute(a) => {
486
+ self.walk_expr(&a.object);
487
+ if let ast::AttrKind::Method(call) = &a.attr {
334
- if !names.contains(v) {
488
+ for arg in &call.args {
335
- names.push(v.clone());
489
+ self.walk_arg(arg);
490
+ }
336
491
  }
337
492
  }
493
+ ast::Expr::Int(_)
494
+ | ast::Expr::Float(_)
495
+ | ast::Expr::String(_)
496
+ | ast::Expr::Self_
497
+ | ast::Expr::Var(_)
498
+ | ast::Expr::TypeName(_) => {}
499
+ }
500
+ }
501
+
502
+ fn walk_arg(&mut self, arg: &ast::Arg) {
503
+ match arg {
338
- collect_block_locals(&f.body, names);
504
+ ast::Arg::Positional(e) => self.walk_expr(e),
505
+ ast::Arg::Keyword { value, .. } => self.walk_expr(value),
506
+ ast::Arg::Pair { value, .. } => self.walk_expr(value),
339
507
  }
340
- _ => {}
341
508
  }
342
509
  }
343
510
 
344
- fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx) -> Result<Vec<u8>, String> {
511
+ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx, state: &mut ModuleState) -> Result<Vec<u8>, String> {
345
512
  let mut body = Vec::new();
346
- let param_count = f.params.len() as u32;
347
513
 
348
- // Collect extra (non-param) local variable names declared in the body
349
- let extra_locals = collect_local_names(&f.body);
350
- let extra_count = extra_locals.len() as u32;
514
+ let mut base_env = ctx.global_env.clone();
515
+ if let Some(recv) = &f.type_param {
516
+ base_env.insert("self".to_string(), TypeScheme::mono(PlumType::TNamed(recv.clone())));
517
+ }
518
+ for p in &f.params {
519
+ let ty = match &p.ty {
520
+ ast::ParamType::Type(t) => plum_checker::plum_type_from_ast(t),
521
+ ast::ParamType::Variadic(t) => plum_checker::plum_type_from_ast(t),
522
+ };
523
+ base_env.insert(p.name.clone(), TypeScheme::mono(ty));
524
+ }
351
525
 
526
+ let mut collector = Collector {
527
+ env: base_env.clone(),
352
- // Encode local declarations: group count, then each group as (count, type)
528
+ cctx: check_ctx_of(&ctx.classes, &ctx.methods, &ctx.enum_variants),
529
+ named: Vec::new(),
530
+ named_set: Default::default(),
531
+ classcall_scratch: HashMap::new(),
532
+ match_scratch: HashMap::new(),
353
- if extra_count > 0 {
533
+ next_classcall_slot: 0,
354
- body.extend(encode_leb128_u32(1)); // 1 group
355
- body.extend(encode_leb128_u32(extra_count));
534
+ };
356
- ValType::I64.encode(&mut body);
535
+ if let ast::FnBody::Block(block) = &f.body {
357
- } else {
358
- body.push(0); // 0 groups
536
+ collector.walk_block(block);
359
537
  }
360
538
 
361
- // Build locals map: params first, then extra locals
539
+ // ---- assign local indices: [self?][params][named...][classcall scratch...][match scratch...] ----
362
540
  let mut locals: HashMap<String, u32> = HashMap::new();
541
+ let mut groups: Vec<ValType> = Vec::new();
542
+ let mut idx = 0u32;
543
+
363
- for (i, p) in f.params.iter().enumerate() {
544
+ if f.type_param.is_some() {
545
+ locals.insert("self".to_string(), idx);
546
+ idx += 1;
547
+ }
548
+ for p in &f.params {
364
- locals.insert(p.name.clone(), i as u32);
549
+ locals.insert(p.name.clone(), idx);
550
+ idx += 1;
551
+ }
552
+ for (name, ty) in &collector.named {
553
+ let vt = plum_type_to_valtype(ty);
554
+ locals.insert(name.clone(), idx);
555
+ groups.push(vt);
556
+ idx += 1;
557
+ }
558
+
559
+ let classcall_scratch_base = idx;
560
+ let classcall_count = collector.classcall_scratch.values().copied().max().map(|m| m + 1).unwrap_or(0);
561
+ for _ in 0..classcall_count {
562
+ groups.push(ValType::I32);
563
+ idx += 1;
564
+ }
565
+
566
+ let match_scratch_base = idx;
567
+ let mut match_scratch_index: HashMap<usize, u32> = HashMap::new();
568
+ for (ptr, ty) in collector.match_scratch.iter() {
569
+ match_scratch_index.insert(*ptr, idx - match_scratch_base);
570
+ groups.push(plum_type_to_valtype(ty));
571
+ idx += 1;
365
572
  }
573
+
366
- for (i, name) in extra_locals.iter().enumerate() {
574
+ if groups.is_empty() {
575
+ body.push(0);
576
+ } else {
367
- locals.insert(name.clone(), param_count + i as u32);
577
+ body.extend(encode_leb128_u32(groups.len() as u32));
578
+ for g in &groups {
579
+ body.extend(encode_leb128_u32(1));
580
+ g.encode(&mut body);
581
+ }
368
582
  }
369
583
 
370
584
  let local_ctx = LocalCtx {
371
585
  locals,
586
+ classcall_scratch_base,
587
+ classcall_scratch: collector.classcall_scratch,
588
+ match_scratch_base,
589
+ match_scratch_index,
372
590
  func_ids: &ctx.func_ids,
373
591
  func_sigs: &ctx.func_sigs,
592
+ classes: &ctx.classes,
593
+ methods: &ctx.methods,
594
+ enum_variants: &ctx.enum_variants,
595
+ type_env: RefCell::new(base_env),
596
+ bump_global: ctx.bump_global,
374
597
  };
375
598
 
376
599
  let has_return_value = f.returns.as_ref().map(|r| r.name != "Unit").unwrap_or(false);
377
600
 
378
601
  match &f.body {
379
602
  ast::FnBody::Expr(e) => {
380
- compile_expr(e, &mut body, &local_ctx)?;
603
+ compile_expr(e, &mut body, &local_ctx, state)?;
381
604
  }
382
605
  ast::FnBody::Block(block) => {
383
- compile_block_as_fn_body(block, &mut body, &local_ctx, has_return_value)?;
606
+ compile_block_as_fn_body(block, &mut body, &local_ctx, state, has_return_value)?;
384
607
  }
385
608
  }
386
609
 
@@ -388,89 +611,93 @@ fn compile_fn_body(f: &ast::Fn, ctx: &CompileCtx) -> Result<Vec<u8>, String> {
388
611
  Ok(body)
389
612
  }
390
613
 
391
- struct LocalCtx<'a> {
392
- locals: HashMap<String, u32>,
393
- func_ids: &'a HashMap<String, u32>,
394
- func_sigs: &'a HashMap<String, FuncSig>,
395
- }
396
-
397
- fn compile_block(block: &ast::Block, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<(), String> {
614
+ fn compile_block(block: &ast::Block, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
398
615
  for stmt in &block.stmts {
399
- compile_stmt(stmt, body, ctx)?;
616
+ compile_stmt(stmt, body, ctx, state)?;
400
617
  }
401
618
  Ok(())
402
619
  }
403
620
 
404
- /// Compile a block that is the body of a function. If the function returns a value
621
+ /// Compiles a block that is the body of a function. If the function returns a value
405
622
  /// and the last statement is an expression, that expression's value is left on the
406
623
  /// stack instead of being dropped.
407
624
  fn compile_block_as_fn_body(
408
625
  block: &ast::Block,
409
626
  body: &mut Vec<u8>,
410
627
  ctx: &LocalCtx,
628
+ state: &mut ModuleState,
411
629
  has_return_value: bool,
412
630
  ) -> Result<(), String> {
413
631
  let stmts = &block.stmts;
414
632
  if has_return_value {
415
633
  if let Some((last, rest)) = stmts.split_last() {
416
634
  for stmt in rest {
417
- compile_stmt(stmt, body, ctx)?;
635
+ compile_stmt(stmt, body, ctx, state)?;
418
636
  }
419
- // Last statement: if it's an Expr, leave its value on stack
420
637
  match last {
421
638
  ast::Stmt::Expr(e) => {
422
- compile_expr(e, body, ctx)?;
639
+ compile_expr(e, body, ctx, state)?;
423
640
  // do NOT drop — this is the return value
424
641
  }
642
+ ast::Stmt::Match(m) => {
643
+ compile_match(m, body, ctx, state)?;
644
+ // Every arm of a tail-position match must itself `return`; if a
645
+ // non-exhaustive match falls through, trap rather than continue
646
+ // with the required-but-never-produced result value missing.
647
+ // This also satisfies wasm validation, which requires a value at
648
+ // the function's `end` regardless of whether every branch inside
649
+ // the match's (BlockType::Empty) if/else chain already returned.
650
+ Instruction::Unreachable.encode(body);
651
+ }
425
652
  _ => {
426
- compile_stmt(last, body, ctx)?;
653
+ compile_stmt(last, body, ctx, state)?;
427
654
  }
428
655
  }
429
656
  return Ok(());
430
657
  }
431
658
  }
432
659
  for stmt in stmts {
433
- compile_stmt(stmt, body, ctx)?;
660
+ compile_stmt(stmt, body, ctx, state)?;
434
661
  }
435
662
  Ok(())
436
663
  }
437
664
 
438
- fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<(), String> {
665
+ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
439
666
  match stmt {
440
667
  ast::Stmt::Assign(a) => {
441
668
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
669
+ let vty = infer_local_type(value, ctx);
442
- compile_expr(value, body, ctx)?;
670
+ compile_expr(value, body, ctx, state)?;
443
- let idx = *ctx
671
+ let idx = ctx
444
672
  .locals
445
673
  .get(target)
674
+ .copied()
446
675
  .ok_or_else(|| format!("undeclared local '{}'", target))?;
447
676
  Instruction::LocalSet(idx).encode(body);
677
+ ctx.type_env.borrow_mut().insert(target.clone(), TypeScheme::mono(vty));
448
678
  }
449
679
  }
450
680
  ast::Stmt::Return(Some(e)) => {
451
- compile_expr(e, body, ctx)?;
681
+ compile_expr(e, body, ctx, state)?;
452
682
  Instruction::Return.encode(body);
453
683
  }
454
684
  ast::Stmt::Return(None) => {
455
685
  Instruction::Return.encode(body);
456
686
  }
457
687
  ast::Stmt::If(if_) => {
458
- compile_expr(&if_.condition, body, ctx)?;
688
+ compile_expr(&if_.condition, body, ctx, state)?;
459
- // Condition is i64 (Bool is I32 but comparison results in I32)
460
- // Convert to i32 for if: if condition came from compare ops it's already i32
461
- // Use I32WrapI64 if needed — but compare ops return i32 already
462
689
  Instruction::If(BlockType::Empty).encode(body);
463
- compile_block(&if_.body, body, ctx)?;
690
+ compile_block(&if_.body, body, ctx, state)?;
464
691
  if !if_.else_ifs.is_empty() || if_.else_.is_some() {
465
692
  Instruction::Else.encode(body);
466
693
  for ei in &if_.else_ifs {
467
- compile_expr(&ei.condition, body, ctx)?;
694
+ compile_expr(&ei.condition, body, ctx, state)?;
468
695
  Instruction::If(BlockType::Empty).encode(body);
469
- compile_block(&ei.body, body, ctx)?;
696
+ compile_block(&ei.body, body, ctx, state)?;
470
697
  Instruction::Else.encode(body);
471
698
  }
472
699
  if let Some(else_block) = &if_.else_ {
473
- compile_block(else_block, body, ctx)?;
700
+ compile_block(else_block, body, ctx, state)?;
474
701
  }
475
702
  for _ in &if_.else_ifs {
476
703
  Instruction::End.encode(body);
@@ -479,13 +706,12 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
479
706
  Instruction::End.encode(body);
480
707
  }
481
708
  ast::Stmt::While(w) => {
482
- // block { loop { cond; i32.eqz; br_if 1; body; br 0 } }
483
709
  Instruction::Block(BlockType::Empty).encode(body);
484
710
  Instruction::Loop(BlockType::Empty).encode(body);
485
- compile_expr(&w.condition, body, ctx)?;
711
+ compile_expr(&w.condition, body, ctx, state)?;
486
712
  Instruction::I32Eqz.encode(body);
487
713
  Instruction::BrIf(1).encode(body);
488
- compile_block(&w.body, body, ctx)?;
714
+ compile_block(&w.body, body, ctx, state)?;
489
715
  Instruction::Br(0).encode(body);
490
716
  Instruction::End.encode(body);
491
717
  Instruction::End.encode(body);
@@ -494,19 +720,21 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
494
720
  if let ast::Expr::Binary(b) = &f.iter {
495
721
  if matches!(b.op, ast::BinOp::Range) && f.vars.len() == 1 {
496
722
  let var_name = &f.vars[0];
497
- let var_idx = *ctx
723
+ let var_idx = ctx
498
724
  .locals
499
725
  .get(var_name)
726
+ .copied()
500
727
  .ok_or_else(|| format!("undeclared loop var '{}'", var_name))?;
728
+ ctx.type_env.borrow_mut().insert(var_name.clone(), TypeScheme::mono(PlumType::TInt));
501
- compile_expr(&b.left, body, ctx)?;
729
+ compile_expr(&b.left, body, ctx, state)?;
502
730
  Instruction::LocalSet(var_idx).encode(body);
503
731
  Instruction::Block(BlockType::Empty).encode(body);
504
732
  Instruction::Loop(BlockType::Empty).encode(body);
505
733
  Instruction::LocalGet(var_idx).encode(body);
506
- compile_expr(&b.right, body, ctx)?;
734
+ compile_expr(&b.right, body, ctx, state)?;
507
735
  Instruction::I64GeS.encode(body);
508
736
  Instruction::BrIf(1).encode(body);
509
- compile_block(&f.body, body, ctx)?;
737
+ compile_block(&f.body, body, ctx, state)?;
510
738
  Instruction::LocalGet(var_idx).encode(body);
511
739
  Instruction::I64Const(1).encode(body);
512
740
  Instruction::I64Add.encode(body);
@@ -517,12 +745,12 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
517
745
  return Ok(());
518
746
  }
519
747
  }
520
- compile_expr(&f.iter, body, ctx)?;
748
+ compile_expr(&f.iter, body, ctx, state)?;
521
749
  Instruction::Drop.encode(body);
522
750
  }
523
751
  ast::Stmt::Expr(e) => {
524
752
  let has_result = expr_has_result(e, ctx);
525
- compile_expr(e, body, ctx)?;
753
+ compile_expr(e, body, ctx, state)?;
526
754
  if has_result {
527
755
  Instruction::Drop.encode(body);
528
756
  }
@@ -533,7 +761,10 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
533
761
  ast::Stmt::Continue => {
534
762
  Instruction::Br(0).encode(body);
535
763
  }
764
+ ast::Stmt::Match(m) => {
765
+ compile_match(m, body, ctx, state)?;
766
+ }
536
- ast::Stmt::Assert(_) | ast::Stmt::Match(_) | ast::Stmt::Todo => {}
767
+ ast::Stmt::Assert(_) | ast::Stmt::Todo => {}
537
768
  }
538
769
  Ok(())
539
770
  }
@@ -541,14 +772,139 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
541
772
  /// Returns true if the expression leaves a value on the wasm stack.
542
773
  fn expr_has_result(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
543
774
  match expr {
775
+ ast::Expr::FnCall(call) => ctx.func_sigs.get(&call.name).map(|s| s.ret.is_some()).unwrap_or(true),
776
+ ast::Expr::Attribute(attr) => match &attr.attr {
544
- ast::Expr::FnCall(call) => {
777
+ ast::AttrKind::Method(call) => {
778
+ if let PlumType::TNamed(class_name) = infer_local_type(&attr.object, ctx) {
779
+ let key = format!("{}::{}", class_name, call.name);
545
- ctx.func_sigs.get(&call.name).map(|s| s.ret.is_some()).unwrap_or(true)
780
+ ctx.func_sigs.get(&key).map(|s| s.ret.is_some()).unwrap_or(true)
781
+ } else {
782
+ true
546
- }
783
+ }
784
+ }
785
+ ast::AttrKind::Field(_) => true,
786
+ },
547
787
  _ => true,
548
788
  }
549
789
  }
550
790
 
791
+ fn compile_match(m: &ast::Match, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
792
+ if m.subjects.len() != 1 {
793
+ return Err("codegen: multi-subject match is not yet supported".to_string());
794
+ }
795
+ let subject = &m.subjects[0];
796
+ let subject_ty = infer_local_type(subject, ctx);
797
+ let subject_vt = plum_type_to_valtype(&subject_ty);
798
+
799
+ let key = m as *const ast::Match as usize;
800
+ let slot = *ctx
801
+ .match_scratch_index
802
+ .get(&key)
803
+ .ok_or_else(|| "internal codegen error: missing match scratch slot".to_string())?;
804
+ let scratch_local = ctx.match_scratch_base + slot;
805
+
806
+ compile_expr(subject, body, ctx, state)?;
807
+ Instruction::LocalSet(scratch_local).encode(body);
808
+
809
+ compile_match_arms(&m.cases, subject_vt, scratch_local, body, ctx, state)
810
+ }
811
+
812
+ fn compile_match_arms(
813
+ cases: &[ast::Case],
814
+ subject_vt: ValType,
815
+ scratch_local: u32,
816
+ body: &mut Vec<u8>,
817
+ ctx: &LocalCtx,
818
+ state: &mut ModuleState,
819
+ ) -> Result<(), String> {
820
+ let (case, rest) = match cases.split_first() {
821
+ None => return Ok(()),
822
+ Some(pair) => pair,
823
+ };
824
+ let pat = case.patterns.first().ok_or_else(|| "codegen: match case has no pattern".to_string())?;
825
+ match pat {
826
+ ast::CasePattern::Wildcard => {
827
+ // Any cases after a wildcard are unreachable, matching real match semantics.
828
+ compile_block(&case.body, body, ctx, state)
829
+ }
830
+ ast::CasePattern::Name(n) => {
831
+ let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
832
+ && ctx.enum_variants.contains_key(n);
833
+ if is_variant {
834
+ compile_variant_eq_arm(n, subject_vt, scratch_local, case, rest, body, ctx, state)
835
+ } else {
836
+ let idx = ctx
837
+ .locals
838
+ .get(n)
839
+ .copied()
840
+ .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?;
841
+ Instruction::LocalGet(scratch_local).encode(body);
842
+ Instruction::LocalSet(idx).encode(body);
843
+ ctx.type_env.borrow_mut().insert(n.clone(), TypeScheme::mono(plum_type_from_valtype_hint(subject_vt)));
844
+ compile_block(&case.body, body, ctx, state)
845
+ // A binding arm always matches — any following cases are unreachable.
846
+ }
847
+ }
848
+ ast::CasePattern::Int(n) => {
849
+ if subject_vt != ValType::I64 {
850
+ return Err("codegen: integer match pattern against a non-Int subject".to_string());
851
+ }
852
+ Instruction::LocalGet(scratch_local).encode(body);
853
+ Instruction::I64Const(*n).encode(body);
854
+ Instruction::I64Eq.encode(body);
855
+ Instruction::If(BlockType::Empty).encode(body);
856
+ compile_block(&case.body, body, ctx, state)?;
857
+ Instruction::Else.encode(body);
858
+ compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
859
+ Instruction::End.encode(body);
860
+ Ok(())
861
+ }
862
+ ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()),
863
+ ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()),
864
+ ast::CasePattern::Class { .. } => Err("codegen: constructor match patterns are not yet supported".to_string()),
865
+ }
866
+ }
867
+
868
+ #[allow(clippy::too_many_arguments)]
869
+ fn compile_variant_eq_arm(
870
+ name: &str,
871
+ subject_vt: ValType,
872
+ scratch_local: u32,
873
+ case: &ast::Case,
874
+ rest: &[ast::Case],
875
+ body: &mut Vec<u8>,
876
+ ctx: &LocalCtx,
877
+ state: &mut ModuleState,
878
+ ) -> Result<(), String> {
879
+ // Only Bool's own variants have a concrete runtime representation in v1.5.
880
+ let tag = match name {
881
+ "True" => 1i32,
882
+ "False" => 0i32,
883
+ other => return Err(format!("codegen: enum variant pattern '{}' is not yet supported (only True/False)", other)),
884
+ };
885
+ if subject_vt != ValType::I32 {
886
+ return Err("codegen: Bool match pattern against a non-Bool subject".to_string());
887
+ }
888
+ Instruction::LocalGet(scratch_local).encode(body);
889
+ Instruction::I32Const(tag).encode(body);
890
+ Instruction::I32Eq.encode(body);
891
+ Instruction::If(BlockType::Empty).encode(body);
892
+ compile_block(&case.body, body, ctx, state)?;
893
+ Instruction::Else.encode(body);
894
+ compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?;
895
+ Instruction::End.encode(body);
896
+ Ok(())
897
+ }
898
+
899
+ fn plum_type_from_valtype_hint(vt: ValType) -> PlumType {
900
+ match vt {
901
+ ValType::I64 => PlumType::TInt,
902
+ ValType::F64 => PlumType::TFloat,
903
+ _ => PlumType::TVar("_".to_string()),
904
+ }
905
+ }
906
+
551
- fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<(), String> {
907
+ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> {
552
908
  match expr {
553
909
  ast::Expr::Int(n) => {
554
910
  Instruction::I64Const(*n).encode(body);
@@ -557,42 +913,40 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
557
913
  Instruction::F64Const(*f).encode(body);
558
914
  }
559
915
  ast::Expr::Var(name) => {
560
- if name == "true" {
561
- Instruction::I32Const(1).encode(body);
562
- } else if name == "false" {
563
- Instruction::I32Const(0).encode(body);
564
- } else {
565
- let idx = *ctx
916
+ let idx = *ctx
566
- .locals
917
+ .locals
567
- .get(name.as_str())
918
+ .get(name.as_str())
568
- .ok_or_else(|| format!("undeclared variable '{}'", name))?;
919
+ .ok_or_else(|| format!("undeclared variable '{}'", name))?;
569
- Instruction::LocalGet(idx).encode(body);
920
+ Instruction::LocalGet(idx).encode(body);
570
- }
571
921
  }
572
922
  ast::Expr::Paren(inner) => {
573
- compile_expr(inner, body, ctx)?;
923
+ compile_expr(inner, body, ctx, state)?;
574
924
  }
575
- ast::Expr::Unary(u) => {
925
+ ast::Expr::Unary(u) => match u.op {
576
- match u.op {
577
- ast::UnOp::Neg => {
926
+ ast::UnOp::Neg => {
927
+ if matches!(infer_local_type(&u.operand, ctx), PlumType::TFloat) {
928
+ compile_expr(&u.operand, body, ctx, state)?;
929
+ Instruction::F64Neg.encode(body);
930
+ } else {
578
- // WASM has no i64.neg; use i64.const(0) - operand
931
+ // WASM has no i64.neg; use 0 - operand.
579
932
  Instruction::I64Const(0).encode(body);
580
- compile_expr(&u.operand, body, ctx)?;
933
+ compile_expr(&u.operand, body, ctx, state)?;
581
934
  Instruction::I64Sub.encode(body);
582
935
  }
583
- ast::UnOp::Pos => {
584
- compile_expr(&u.operand, body, ctx)?;
585
- }
586
936
  }
937
+ ast::UnOp::Pos => {
938
+ compile_expr(&u.operand, body, ctx, state)?;
587
- }
939
+ }
940
+ },
588
941
  ast::Expr::Binary(b) => {
942
+ let is_float = matches!(infer_local_type(&b.left, ctx), PlumType::TFloat);
589
- compile_expr(&b.left, body, ctx)?;
943
+ compile_expr(&b.left, body, ctx, state)?;
590
- compile_expr(&b.right, body, ctx)?;
944
+ compile_expr(&b.right, body, ctx, state)?;
591
945
  match b.op {
592
- ast::BinOp::Add => Instruction::I64Add.encode(body),
946
+ ast::BinOp::Add => if is_float { Instruction::F64Add } else { Instruction::I64Add }.encode(body),
593
- ast::BinOp::Sub => Instruction::I64Sub.encode(body),
947
+ ast::BinOp::Sub => if is_float { Instruction::F64Sub } else { Instruction::I64Sub }.encode(body),
594
- ast::BinOp::Mul => Instruction::I64Mul.encode(body),
948
+ ast::BinOp::Mul => if is_float { Instruction::F64Mul } else { Instruction::I64Mul }.encode(body),
595
- ast::BinOp::Div => Instruction::I64DivS.encode(body),
949
+ ast::BinOp::Div => if is_float { Instruction::F64Div } else { Instruction::I64DivS }.encode(body),
596
950
  ast::BinOp::Mod => Instruction::I64RemS.encode(body),
597
951
  ast::BinOp::BitOr => Instruction::I64Or.encode(body),
598
952
  ast::BinOp::BitAnd => Instruction::I64And.encode(body),
@@ -600,40 +954,55 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
600
954
  ast::BinOp::Shl => Instruction::I64Shl.encode(body),
601
955
  ast::BinOp::Shr => Instruction::I64ShrS.encode(body),
602
956
  ast::BinOp::Range => {
603
- // Range used outside of For: leave end on stack (start consumed)
957
+ // Range used outside of `for`: leave end on stack (start consumed).
604
958
  }
605
959
  }
606
960
  }
607
961
  ast::Expr::Bool(b) => {
608
- compile_expr(&b.left, body, ctx)?;
962
+ compile_expr(&b.left, body, ctx, state)?;
609
- compile_expr(&b.right, body, ctx)?;
963
+ compile_expr(&b.right, body, ctx, state)?;
610
964
  match b.op {
611
965
  ast::BoolOp::And => Instruction::I32And.encode(body),
612
966
  ast::BoolOp::Or => Instruction::I32Or.encode(body),
613
967
  }
614
968
  }
615
969
  ast::Expr::Not(inner) => {
616
- compile_expr(inner, body, ctx)?;
970
+ compile_expr(inner, body, ctx, state)?;
617
971
  Instruction::I32Eqz.encode(body);
618
972
  }
619
973
  ast::Expr::Compare(c) => {
974
+ let is_float = matches!(infer_local_type(&c.left, ctx), PlumType::TFloat);
620
- compile_expr(&c.left, body, ctx)?;
975
+ compile_expr(&c.left, body, ctx, state)?;
621
- compile_expr(&c.right, body, ctx)?;
976
+ compile_expr(&c.right, body, ctx, state)?;
977
+ if is_float {
622
- match c.op {
978
+ match c.op {
979
+ ast::CmpOp::Lt => Instruction::F64Lt,
980
+ ast::CmpOp::Lte => Instruction::F64Le,
981
+ ast::CmpOp::Eq => Instruction::F64Eq,
982
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::F64Ne,
983
+ ast::CmpOp::Gte => Instruction::F64Ge,
984
+ ast::CmpOp::Gt => Instruction::F64Gt,
985
+ }
986
+ .encode(body);
987
+ } else {
988
+ match c.op {
623
- ast::CmpOp::Lt => Instruction::I64LtS.encode(body),
989
+ ast::CmpOp::Lt => Instruction::I64LtS,
624
- ast::CmpOp::Lte => Instruction::I64LeS.encode(body),
990
+ ast::CmpOp::Lte => Instruction::I64LeS,
625
- ast::CmpOp::Eq => Instruction::I64Eq.encode(body),
991
+ ast::CmpOp::Eq => Instruction::I64Eq,
626
- ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I64Ne.encode(body),
992
+ ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Instruction::I64Ne,
627
- ast::CmpOp::Gte => Instruction::I64GeS.encode(body),
993
+ ast::CmpOp::Gte => Instruction::I64GeS,
628
- ast::CmpOp::Gt => Instruction::I64GtS.encode(body),
994
+ ast::CmpOp::Gt => Instruction::I64GtS,
995
+ }
996
+ .encode(body);
629
997
  }
630
998
  }
631
999
  ast::Expr::Ternary(t) => {
1000
+ let result_vt = plum_type_to_valtype(&infer_local_type(&t.then, ctx));
632
- compile_expr(&t.condition, body, ctx)?;
1001
+ compile_expr(&t.condition, body, ctx, state)?;
633
- Instruction::If(BlockType::Result(ValType::I64)).encode(body);
1002
+ Instruction::If(BlockType::Result(result_vt)).encode(body);
634
- compile_expr(&t.then, body, ctx)?;
1003
+ compile_expr(&t.then, body, ctx, state)?;
635
1004
  Instruction::Else.encode(body);
636
- compile_expr(&t.else_, body, ctx)?;
1005
+ compile_expr(&t.else_, body, ctx, state)?;
637
1006
  Instruction::End.encode(body);
638
1007
  }
639
1008
  ast::Expr::FnCall(call) => {
@@ -643,7 +1012,7 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
643
1012
  ast::Arg::Keyword { value, .. } => value,
644
1013
  ast::Arg::Pair { value, .. } => value,
645
1014
  };
646
- compile_expr(arg_expr, body, ctx)?;
1015
+ compile_expr(arg_expr, body, ctx, state)?;
647
1016
  }
648
1017
  let func_idx = ctx
649
1018
  .func_ids
@@ -652,13 +1021,125 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx) -> Result<
652
1021
  Instruction::Call(*func_idx).encode(body);
653
1022
  }
654
1023
  ast::Expr::Self_ => {
1024
+ let idx = ctx
1025
+ .locals
1026
+ .get("self")
1027
+ .copied()
655
- return Err("'self' not supported in v1 codegen".to_string());
1028
+ .ok_or_else(|| "codegen: 'self' used outside a method".to_string())?;
1029
+ Instruction::LocalGet(idx).encode(body);
656
1030
  }
657
- ast::Expr::TypeName(_) | ast::Expr::ClassCall(_) | ast::Expr::Attribute(_) => {
1031
+ ast::Expr::TypeName(n) => match n.as_str() {
1032
+ "True" => Instruction::I32Const(1).encode(body),
1033
+ "False" => Instruction::I32Const(0).encode(body),
1034
+ other => return Err(format!("codegen: type name '{}' is not yet supported as a value", other)),
1035
+ },
1036
+ ast::Expr::ClassCall(call) => {
1037
+ let fields = ctx
1038
+ .classes
1039
+ .get(&call.type_name)
1040
+ .ok_or_else(|| format!("codegen: unknown class '{}'", call.type_name))?
1041
+ .clone();
1042
+ let size = (fields.len() as i32) * 8;
1043
+
1044
+ let scratch_key = expr as *const ast::Expr as usize;
1045
+ let scratch_idx = *ctx
1046
+ .classcall_scratch
1047
+ .get(&scratch_key)
1048
+ .ok_or_else(|| "internal codegen error: missing classcall scratch slot".to_string())?;
1049
+ let scratch_local = ctx.classcall_scratch_base + scratch_idx;
1050
+
1051
+ // base = bump_global; bump_global += size (runtime bump allocation, so every
1052
+ // call — even inside a loop — gets a fresh, non-overlapping region).
1053
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
1054
+ Instruction::LocalSet(scratch_local).encode(body);
1055
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
658
- Instruction::I64Const(0).encode(body);
1056
+ Instruction::I32Const(size).encode(body);
1057
+ Instruction::I32Add.encode(body);
1058
+ Instruction::GlobalSet(ctx.bump_global).encode(body);
1059
+
1060
+ for fa in &call.fields {
1061
+ let (field_idx, field_ty) = fields
1062
+ .iter()
1063
+ .position(|(n, _)| n == &fa.name)
1064
+ .map(|i| (i, fields[i].1.clone()))
1065
+ .ok_or_else(|| format!("codegen: unknown field '{}' on class '{}'", fa.name, call.type_name))?;
1066
+ Instruction::LocalGet(scratch_local).encode(body);
1067
+ compile_expr(&fa.value, body, ctx, state)?;
1068
+ let offset = (field_idx as u64) * 8;
1069
+ match plum_type_to_valtype(&field_ty) {
1070
+ ValType::I64 => Instruction::I64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
1071
+ ValType::F64 => Instruction::F64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
1072
+ _ => Instruction::I32Store(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
1073
+ };
1074
+ }
1075
+ Instruction::LocalGet(scratch_local).encode(body);
659
1076
  }
1077
+ ast::Expr::Attribute(attr) => {
1078
+ let obj_ty = infer_local_type(&attr.object, ctx);
1079
+ match &attr.attr {
1080
+ ast::AttrKind::Field(field_name) => {
1081
+ let class_name = match &obj_ty {
1082
+ PlumType::TNamed(n) => n.clone(),
1083
+ other => return Err(format!("codegen: cannot access field '{}' on non-class type {}", field_name, other)),
1084
+ };
1085
+ let fields = ctx
1086
+ .classes
1087
+ .get(&class_name)
1088
+ .ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
1089
+ let (field_idx, field_ty) = fields
1090
+ .iter()
1091
+ .position(|(n, _)| n == field_name)
1092
+ .map(|i| (i, fields[i].1.clone()))
1093
+ .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
1094
+ compile_expr(&attr.object, body, ctx, state)?;
1095
+ let offset = (field_idx as u64) * 8;
1096
+ match plum_type_to_valtype(&field_ty) {
1097
+ ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
1098
+ ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
1099
+ _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
1100
+ };
1101
+ }
1102
+ ast::AttrKind::Method(call) => {
1103
+ let class_name = match &obj_ty {
1104
+ PlumType::TNamed(n) => n.clone(),
1105
+ other => return Err(format!("codegen: cannot call method '{}' on non-class type {}", call.name, other)),
1106
+ };
1107
+ let key = format!("{}::{}", class_name, call.name);
1108
+ let func_idx = *ctx
1109
+ .func_ids
1110
+ .get(&key)
1111
+ .ok_or_else(|| format!("codegen: unknown method '{}.{}'", class_name, call.name))?;
1112
+ compile_expr(&attr.object, body, ctx, state)?; // push self
1113
+ for arg in &call.args {
1114
+ let arg_expr = match arg {
1115
+ ast::Arg::Positional(e) => e,
1116
+ ast::Arg::Keyword { value, .. } => value,
1117
+ ast::Arg::Pair { value, .. } => value,
1118
+ };
1119
+ compile_expr(arg_expr, body, ctx, state)?;
1120
+ }
1121
+ Instruction::Call(func_idx).encode(body);
1122
+ }
1123
+ }
1124
+ }
660
- ast::Expr::String(_) => {
1125
+ ast::Expr::String(s) => {
1126
+ let mut text = String::new();
1127
+ for part in &s.parts {
1128
+ match part {
1129
+ ast::StringPart::Text(t) => text.push_str(t),
1130
+ ast::StringPart::Interp(_) => {
1131
+ return Err("codegen: string interpolation is not yet supported".to_string());
1132
+ }
1133
+ }
1134
+ }
1135
+ let bytes = text.as_bytes();
1136
+ let offset = state.next_string_offset;
1137
+ let mut data = Vec::with_capacity(4 + bytes.len());
1138
+ data.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
1139
+ data.extend_from_slice(bytes);
1140
+ state.next_string_offset += data.len() as u32;
1141
+ state.data_segments.push((offset, data));
661
- Instruction::I32Const(0).encode(body);
1142
+ Instruction::I32Const(offset as i32).encode(body);
662
1143
  }
663
1144
  }
664
1145
  Ok(())
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -31,8 +31,12 @@ fn output_validates() {
31
31
 
32
32
  #[test]
33
33
  fn factorial_compiles() {
34
- // Simple recursive function that the v1 codegen can handle
34
+ let src = "\
35
- let src = "factorial(x: Int) -> Int =\n x\n";
35
+ factorial(x: Int) -> Int =
36
+ if x < 2
37
+ return 1
38
+ return x * factorial(x - 1)
39
+ ";
36
40
  let source = parse(src);
37
41
  let bytes = compile_source(&source).expect("factorial should compile");
38
42
  assert_eq!(&bytes[0..4], b"\0asm");
@@ -49,3 +53,255 @@ fn give42_compiles_and_exports() {
49
53
  let result = wasmparser::validate(&bytes);
50
54
  assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
51
55
  }
56
+
57
+ fn assert_valid(src: &str) -> Vec<u8> {
58
+ let source = parse(src);
59
+ let bytes = compile_source(&source).unwrap_or_else(|e| panic!("compile failed for {:?}: {}", src, e));
60
+ let result = wasmparser::validate(&bytes);
61
+ assert!(result.is_ok(), "wasm validation failed: {:?}", result.err());
62
+ bytes
63
+ }
64
+
65
+ #[test]
66
+ fn bool_literals_compile() {
67
+ assert_valid("main() -> Bool =\n True\n");
68
+ assert_valid("main() -> Bool =\n False\n");
69
+ }
70
+
71
+ #[test]
72
+ fn string_literal_compiles() {
73
+ assert_valid("main() -> Str =\n \"hello\"\n");
74
+ }
75
+
76
+ #[test]
77
+ fn empty_string_literal_compiles() {
78
+ assert_valid("main() -> Str =\n \"\"\n");
79
+ }
80
+
81
+ #[test]
82
+ fn string_interpolation_is_a_clear_error() {
83
+ let src = "main() -> Str =\n x = 1\n \"{x}\"\n";
84
+ let source = parse(src);
85
+ let err = compile_source(&source).expect_err("interpolation should not silently compile");
86
+ assert!(err.contains("interpolation"), "got: {}", err);
87
+ }
88
+
89
+ #[test]
90
+ fn float_arithmetic_and_negation_compile() {
91
+ assert_valid("main(x: Float) -> Float =\n y = -x\n y + 1.5\n");
92
+ }
93
+
94
+ #[test]
95
+ fn class_field_and_method_compile() {
96
+ let src = "\
97
+ type Cat =
98
+ name: Str
99
+ age: Int
100
+
101
+ getAge<Cat>() -> Int =
102
+ self.age
103
+
104
+ makeCat() -> Int =
105
+ c = Cat(name: \"x\", age: 3)
106
+ c.getAge()
107
+ ";
108
+ assert_valid(src);
109
+ }
110
+
111
+ #[test]
112
+ fn nested_class_call_compiles() {
113
+ let src = "\
114
+ type Pair =
115
+ a: Int
116
+ b: Int
117
+
118
+ type Wrapper =
119
+ inner: Pair
120
+ tag: Int
121
+
122
+ make() -> Int =
123
+ w = Wrapper(inner: Pair(a: 1, b: 2), tag: 9)
124
+ w.tag
125
+ ";
126
+ assert_valid(src);
127
+ }
128
+
129
+ #[test]
130
+ fn match_with_int_and_wildcard_compiles() {
131
+ let src = "main(a: Int) -> Int =\n match a\n 1 =>\n return 10\n _ =>\n return 0\n";
132
+ assert_valid(src);
133
+ }
134
+
135
+ #[test]
136
+ fn match_binding_pattern_compiles() {
137
+ let src = "main(a: Int) -> Int =\n match a\n x =>\n return x\n";
138
+ assert_valid(src);
139
+ }
140
+
141
+ #[test]
142
+ fn match_bool_variant_pattern_compiles() {
143
+ let src = "main(a: Bool) -> Int =\n match a\n True =>\n return 1\n False =>\n return 0\n";
144
+ assert_valid(src);
145
+ }
146
+
147
+ #[test]
148
+ fn match_string_pattern_is_a_clear_error() {
149
+ let src = "main(a: Str) -> Int =\n match a\n \"x\" =>\n 1\n _ =>\n 0\n";
150
+ let source = parse(src);
151
+ let err = compile_source(&source).expect_err("string match patterns are not yet supported");
152
+ assert!(err.contains("string match"), "got: {}", err);
153
+ }
154
+
155
+ /// Runs `main`'s wasm bytes and returns its i64 result. wasm's own validator
156
+ /// (via wasmparser, above) only proves the module is well-formed — it can't catch
157
+ /// wrong *values*, so these tests actually execute the compiled output.
158
+ fn run_main(bytes: &[u8]) -> i64 {
159
+ let engine = wasmtime::Engine::default();
160
+ let module = wasmtime::Module::new(&engine, bytes).expect("module should be loadable");
161
+ let mut store = wasmtime::Store::new(&engine, ());
162
+ let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate");
163
+ let main = instance
164
+ .get_typed_func::<(), i64>(&mut store, "main")
165
+ .expect("main should have signature () -> i64");
166
+ main.call(&mut store, ()).expect("main should not trap")
167
+ }
168
+
169
+ #[test]
170
+ fn factorial_runs_correctly() {
171
+ let src = "\
172
+ factorial(x: Int) -> Int =
173
+ if x < 2
174
+ return 1
175
+ return x * factorial(x - 1)
176
+
177
+ main() -> Int =
178
+ factorial(5)
179
+ ";
180
+ let source = parse(src);
181
+ let bytes = compile_source(&source).expect("compile failed");
182
+ assert_eq!(run_main(&bytes), 120);
183
+ }
184
+
185
+ #[test]
186
+ fn class_field_and_method_run_correctly() {
187
+ let src = "\
188
+ type Cat =
189
+ name: Str
190
+ age: Int
191
+
192
+ getAge<Cat>() -> Int =
193
+ self.age
194
+
195
+ main() -> Int =
196
+ c = Cat(name: \"x\", age: 7)
197
+ c.getAge()
198
+ ";
199
+ let source = parse(src);
200
+ let bytes = compile_source(&source).expect("compile failed");
201
+ assert_eq!(run_main(&bytes), 7);
202
+ }
203
+
204
+ #[test]
205
+ fn nested_class_call_runs_correctly() {
206
+ let src = "\
207
+ type Pair =
208
+ a: Int
209
+ b: Int
210
+
211
+ type Wrapper =
212
+ inner: Pair
213
+ tag: Int
214
+
215
+ main() -> Int =
216
+ w = Wrapper(inner: Pair(a: 11, b: 22), tag: 99)
217
+ w.inner.b
218
+ ";
219
+ let source = parse(src);
220
+ let bytes = compile_source(&source).expect("compile failed");
221
+ assert_eq!(run_main(&bytes), 22);
222
+ }
223
+
224
+ #[test]
225
+ fn repeated_class_call_in_a_loop_does_not_alias() {
226
+ // Regression test: class instances are bump-allocated at *runtime* (via a
227
+ // mutable wasm global), not at a compile-time-fixed address — otherwise every
228
+ // iteration's `Box(...)` would alias the same memory and this would sum to 5*4=20
229
+ // instead of 0+1+2+3+4=10.
230
+ let src = "\
231
+ type Box =
232
+ v: Int
233
+
234
+ sumBoxes() -> Int =
235
+ total = 0
236
+ for i in 0..5
237
+ b = Box(v: i)
238
+ total = total + b.v
239
+ return total
240
+
241
+ main() -> Int =
242
+ sumBoxes()
243
+ ";
244
+ let source = parse(src);
245
+ let bytes = compile_source(&source).expect("compile failed");
246
+ assert_eq!(run_main(&bytes), 10);
247
+ }
248
+
249
+ #[test]
250
+ fn match_int_and_wildcard_run_correctly() {
251
+ let src = "\
252
+ classify(a: Int) -> Int =
253
+ match a
254
+ 1 =>
255
+ return 100
256
+ 2 =>
257
+ return 200
258
+ _ =>
259
+ return 0
260
+
261
+ main() -> Int =
262
+ classify(2)
263
+ ";
264
+ let source = parse(src);
265
+ let bytes = compile_source(&source).expect("compile failed");
266
+ assert_eq!(run_main(&bytes), 200);
267
+ }
268
+
269
+ #[test]
270
+ fn match_bool_variant_pattern_runs_correctly() {
271
+ // Regression test: True/False are built-in Bool variants and must be treated as
272
+ // tag comparisons, not bindings, even without an explicit `enum Bool` in this
273
+ // source file — otherwise the first arm always "matches" (as a rebinding) and
274
+ // `pick(False)` would wrongly return 1.
275
+ let src = "\
276
+ pick(a: Bool) -> Int =
277
+ match a
278
+ True =>
279
+ return 1
280
+ False =>
281
+ return 0
282
+
283
+ main() -> Int =
284
+ pick(False)
285
+ ";
286
+ let source = parse(src);
287
+ let bytes = compile_source(&source).expect("compile failed");
288
+ assert_eq!(run_main(&bytes), 0);
289
+ }
290
+
291
+ #[test]
292
+ fn lowercase_single_word_function_call_runs_correctly() {
293
+ // Regression test: `factorial(...)` (an all-lowercase, no-uppercase, no-underscore
294
+ // callee) used to fail to parse at all — `var_identifier` and `fn_identifier` both
295
+ // matched its text and the grammar's lexer would nondeterministically commit to
296
+ // `var_identifier`, breaking every such call site.
297
+ let src = "\
298
+ double(n: Int) -> Int =
299
+ n * 2
300
+
301
+ main() -> Int =
302
+ double(21)
303
+ ";
304
+ let source = parse(src);
305
+ let bytes = compile_source(&source).expect("compile failed");
306
+ assert_eq!(run_main(&bytes), 42);
307
+ }