plum

#treesitter#compiler#wasm

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

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


380a51cPeter John 2026-07-20T11:43:17+05:30
feat(plum-wasm-codegen): compile general enum variant construction
plum-wasm-codegen/src/lib.rs CHANGED
@@ -3,7 +3,7 @@ use std::cell::RefCell;
3
3
  use std::collections::HashMap;
4
4
  use plum_core::ast;
5
5
  use plum_checker::types::{PlumType, TypeEnv, TypeScheme};
6
- use plum_checker::{ClassEnv, MethodEnv, EnumVariants};
6
+ use plum_checker::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo};
7
7
 
8
8
  /// Bump-allocated heap for class instances starts at the second 64KiB page so it can
9
9
  /// never collide with the (small, compile-time-sized) string literal data area below it.
@@ -478,6 +478,14 @@ impl<'a> Collector<'a> {
478
478
  self.walk_expr(&t.else_);
479
479
  }
480
480
  ast::Expr::FnCall(call) => {
481
+ let carries_payload = self.cctx.enum_variants.get(&call.name)
482
+ .map(|info| !info.field_types.is_empty())
483
+ .unwrap_or(false);
484
+ if carries_payload {
485
+ let idx = self.next_classcall_slot;
486
+ self.next_classcall_slot += 1;
487
+ self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx);
488
+ }
481
489
  for arg in &call.args {
482
490
  self.walk_arg(arg);
483
491
  }
@@ -1038,19 +1046,23 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
1038
1046
  Instruction::End.encode(body);
1039
1047
  }
1040
1048
  ast::Expr::FnCall(call) => {
1049
+ if let Some(info) = ctx.enum_variants.get(&call.name) {
1050
+ compile_variant_construction(info, call, expr, body, ctx, state)?;
1051
+ } else {
1041
- for arg in &call.args {
1052
+ for arg in &call.args {
1042
- let arg_expr = match arg {
1053
+ let arg_expr = match arg {
1043
- ast::Arg::Positional(e) => e,
1054
+ ast::Arg::Positional(e) => e,
1044
- ast::Arg::Keyword { value, .. } => value,
1055
+ ast::Arg::Keyword { value, .. } => value,
1045
- ast::Arg::Pair { value, .. } => value,
1056
+ ast::Arg::Pair { value, .. } => value,
1046
- };
1057
+ };
1047
- compile_expr(arg_expr, body, ctx, state)?;
1058
+ compile_expr(arg_expr, body, ctx, state)?;
1059
+ }
1060
+ let func_idx = ctx
1061
+ .func_ids
1062
+ .get(&call.name)
1063
+ .ok_or_else(|| format!("unknown function '{}'", call.name))?;
1064
+ Instruction::Call(*func_idx).encode(body);
1048
1065
  }
1049
- let func_idx = ctx
1050
- .func_ids
1051
- .get(&call.name)
1052
- .ok_or_else(|| format!("unknown function '{}'", call.name))?;
1053
- Instruction::Call(*func_idx).encode(body);
1054
1066
  }
1055
1067
  ast::Expr::Self_ => {
1056
1068
  let idx = ctx
@@ -1060,10 +1072,12 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
1060
1072
  .ok_or_else(|| "codegen: 'self' used outside a method".to_string())?;
1061
1073
  Instruction::LocalGet(idx).encode(body);
1062
1074
  }
1063
- ast::Expr::TypeName(n) => match n.as_str() {
1075
+ ast::Expr::TypeName(n) => match ctx.enum_variants.get(n) {
1076
+ Some(info) if info.field_types.is_empty() => {
1064
- "True" => Instruction::I32Const(1).encode(body),
1077
+ Instruction::I32Const(info.tag).encode(body);
1078
+ }
1065
- "False" => Instruction::I32Const(0).encode(body),
1079
+ Some(_) => return Err(format!("codegen: '{}' carries a payload — construct it with '{}(...)'", n, n)),
1066
- other => return Err(format!("codegen: type name '{}' is not yet supported as a value", other)),
1080
+ None => return Err(format!("codegen: type name '{}' is not yet supported as a value", n)),
1067
1081
  },
1068
1082
  ast::Expr::ClassCall(call) => {
1069
1083
  let fields = ctx
@@ -1176,3 +1190,64 @@ fn compile_expr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
1176
1190
  }
1177
1191
  Ok(())
1178
1192
  }
1193
+
1194
+ /// Compiles a variant-construction call. A payload-free variant (`None`, called as
1195
+ /// `None()` rather than used bare) is just its tag. A payload variant bump-allocates
1196
+ /// `[tag: i32][field0][field1]...` (8-byte stride per slot, matching class field
1197
+ /// layout) and leaves the base pointer on the stack.
1198
+ fn compile_variant_construction(
1199
+ info: &EnumVariantInfo,
1200
+ call: &ast::FnCall,
1201
+ expr: &ast::Expr,
1202
+ body: &mut Vec<u8>,
1203
+ ctx: &LocalCtx,
1204
+ state: &mut ModuleState,
1205
+ ) -> Result<(), String> {
1206
+ if call.args.len() != info.field_types.len() {
1207
+ return Err(format!(
1208
+ "codegen: variant '{}' expects {} arg(s), got {}",
1209
+ call.name, info.field_types.len(), call.args.len()
1210
+ ));
1211
+ }
1212
+ if info.field_types.is_empty() {
1213
+ Instruction::I32Const(info.tag).encode(body);
1214
+ return Ok(());
1215
+ }
1216
+
1217
+ let size = (1 + info.field_types.len() as i32) * 8;
1218
+ let scratch_key = expr as *const ast::Expr as usize;
1219
+ let scratch_idx = *ctx
1220
+ .classcall_scratch
1221
+ .get(&scratch_key)
1222
+ .ok_or_else(|| "internal codegen error: missing variant-call scratch slot".to_string())?;
1223
+ let scratch_local = ctx.classcall_scratch_base + scratch_idx;
1224
+
1225
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
1226
+ Instruction::LocalSet(scratch_local).encode(body);
1227
+ Instruction::GlobalGet(ctx.bump_global).encode(body);
1228
+ Instruction::I32Const(size).encode(body);
1229
+ Instruction::I32Add.encode(body);
1230
+ Instruction::GlobalSet(ctx.bump_global).encode(body);
1231
+
1232
+ Instruction::LocalGet(scratch_local).encode(body);
1233
+ Instruction::I32Const(info.tag).encode(body);
1234
+ Instruction::I32Store(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body);
1235
+
1236
+ for (i, (arg, field_ty)) in call.args.iter().zip(info.field_types.iter()).enumerate() {
1237
+ let arg_expr = match arg {
1238
+ ast::Arg::Positional(e) => e,
1239
+ ast::Arg::Keyword { value, .. } => value,
1240
+ ast::Arg::Pair { value, .. } => value,
1241
+ };
1242
+ Instruction::LocalGet(scratch_local).encode(body);
1243
+ compile_expr(arg_expr, body, ctx, state)?;
1244
+ let offset = ((i + 1) as u64) * 8;
1245
+ match plum_type_to_valtype(field_ty) {
1246
+ ValType::I64 => Instruction::I64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
1247
+ ValType::F64 => Instruction::F64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
1248
+ _ => Instruction::I32Store(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
1249
+ };
1250
+ }
1251
+ Instruction::LocalGet(scratch_local).encode(body);
1252
+ Ok(())
1253
+ }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -366,3 +366,59 @@ main() -> Int =
366
366
  let err = main.call(&mut store, ()).expect_err("todo should trap");
367
367
  assert_eq!(err.downcast_ref::<wasmtime::Trap>(), Some(&wasmtime::Trap::UnreachableCodeReached), "got: {}", err);
368
368
  }
369
+
370
+ #[test]
371
+ fn payload_free_variant_construction_compiles() {
372
+ let src = "\
373
+ enum Color =
374
+ | Red
375
+ | Green
376
+ | Blue
377
+
378
+ main() -> Int =\n x = Green\n 0\n";
379
+ assert_valid(src);
380
+ }
381
+
382
+ #[test]
383
+ fn payload_variant_construction_compiles_and_runs() {
384
+ let src = "\
385
+ enum Option =
386
+ | Some(Int)
387
+ | None
388
+
389
+ unwrapOr(o: Option, default: Int) -> Int =
390
+ match o
391
+ Some(v) =>
392
+ return v
393
+ None =>
394
+ return default
395
+
396
+ main() -> Int =
397
+ unwrapOr(Some(7), 0)
398
+ ";
399
+ let source = parse(src);
400
+ let bytes = compile_source(&source).expect("compile failed");
401
+ assert_eq!(run_main(&bytes), 7);
402
+ }
403
+
404
+ #[test]
405
+ fn multi_field_variant_construction_compiles_and_runs() {
406
+ let src = "\
407
+ enum Shape =
408
+ | Rect(Int, Int)
409
+ | Circle(Int)
410
+
411
+ area(s: Shape) -> Int =
412
+ match s
413
+ Rect(w, h) =>
414
+ return w * h
415
+ Circle(r) =>
416
+ return r * r
417
+
418
+ main() -> Int =
419
+ area(Rect(3, 4))
420
+ ";
421
+ let source = parse(src);
422
+ let bytes = compile_source(&source).expect("compile failed");
423
+ assert_eq!(run_main(&bytes), 12);
424
+ }