plum

#treesitter#compiler#wasm

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

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


01f9be3Peter John 2026-07-23T18:56:23+05:30
feat(plum-wasm-codegen): compile obj.field = value assignment targets
plum-wasm-codegen/src/lib.rs CHANGED
@@ -940,9 +940,16 @@ impl<'a, 'c> ClosureWalker<'a, 'c> {
940
940
  ast::Stmt::Assign(a) => {
941
941
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
942
942
  self.walk_expr(value, None);
943
+ match target {
944
+ ast::AssignTarget::Var(name) => {
943
- let ty = plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
945
+ let ty = plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
944
- self.env.insert(target.clone(), TypeScheme::mono(ty));
946
+ self.env.insert(name.clone(), TypeScheme::mono(ty));
945
- self.locals.insert(target.clone());
947
+ self.locals.insert(name.clone());
948
+ }
949
+ ast::AssignTarget::Field(object, _) => {
950
+ self.walk_expr(object, None);
951
+ }
952
+ }
946
953
  }
947
954
  }
948
955
  ast::Stmt::Return(Some(e)) => self.walk_expr(e, None),
@@ -1335,7 +1342,9 @@ fn fv_collect_bound_block(block: &ast::Block, bound: &mut std::collections::Hash
1335
1342
  match s {
1336
1343
  ast::Stmt::Assign(a) => {
1337
1344
  for t in &a.targets {
1345
+ if let ast::AssignTarget::Var(name) = t {
1338
- bound.insert(t.clone());
1346
+ bound.insert(name.clone());
1347
+ }
1339
1348
  }
1340
1349
  }
1341
1350
  ast::Stmt::For(f) => {
@@ -1393,6 +1402,11 @@ fn fv_collect_refs_block(
1393
1402
  for v in &a.values {
1394
1403
  fv_collect_refs_expr(v, bound, seen, free, env, fn_decls);
1395
1404
  }
1405
+ for t in &a.targets {
1406
+ if let ast::AssignTarget::Field(object, _) = t {
1407
+ fv_collect_refs_expr(object, bound, seen, free, env, fn_decls);
1408
+ }
1409
+ }
1396
1410
  }
1397
1411
  ast::Stmt::Return(Some(e)) | ast::Stmt::Expr(e) | ast::Stmt::Assert(e) => {
1398
1412
  fv_collect_refs_expr(e, bound, seen, free, env, fn_decls);
@@ -1573,23 +1587,30 @@ impl<'a> Collector<'a> {
1573
1587
  ast::Stmt::Assign(a) => {
1574
1588
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
1575
1589
  self.walk_expr(value);
1590
+ match target {
1591
+ ast::AssignTarget::Var(name) => {
1576
- let ty = if matches!(value, ast::Expr::Closure(_)) {
1592
+ let ty = if matches!(value, ast::Expr::Closure(_)) {
1577
- // The checker's own closure inference (`infer_expr` on
1593
+ // The checker's own closure inference (`infer_expr` on
1578
- // `Expr::Closure`) infers the return type by recursively
1594
+ // `Expr::Closure`) infers the return type by recursively
1579
- // inferring the body's tail expression with each param bound
1595
+ // inferring the body's tail expression with each param bound
1580
- // to a fresh, unconstrained `TVar` — e.g. a captured/param
1596
+ // to a fresh, unconstrained `TVar` — e.g. a captured/param
1581
- // attribute access (`c.age`) on a `TVar`-typed object isn't a
1597
+ // attribute access (`c.age`) on a `TVar`-typed object isn't a
1582
- // known class, so it errors out entirely, and this call site
1598
+ // known class, so it errors out entirely, and this call site
1583
- // then silently defaults to `TInt` — the *wrong* wasm local
1599
+ // then silently defaults to `TInt` — the *wrong* wasm local
1584
- // width for what's actually always an `i32` pointer. All that
1600
+ // width for what's actually always an `i32` pointer. All that
1585
- // actually matters here is the local's wasm width, and every
1601
+ // actually matters here is the local's wasm width, and every
1586
- // closure value is an i32 pointer regardless of its
1602
+ // closure value is an i32 pointer regardless of its
1587
- // parameter/return types, so skip inference entirely.
1603
+ // parameter/return types, so skip inference entirely.
1588
- PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
1604
+ PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
1589
- } else {
1605
+ } else {
1590
- plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
1606
+ plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
1591
- };
1607
+ };
1592
- self.bind(target, ty);
1608
+ self.bind(name, ty);
1609
+ }
1610
+ ast::AssignTarget::Field(object, _) => {
1611
+ self.walk_expr(object);
1612
+ }
1613
+ }
1593
1614
  }
1594
1615
  }
1595
1616
  ast::Stmt::Return(Some(e)) => self.walk_expr(e),
@@ -1981,32 +2002,60 @@ fn compile_stmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mu
1981
2002
  match stmt {
1982
2003
  ast::Stmt::Assign(a) => {
1983
2004
  for (target, value) in a.targets.iter().zip(a.values.iter()) {
2005
+ match target {
2006
+ ast::AssignTarget::Var(name) => {
1984
- // See the matching comment in `Collector::walk_stmt`: the checker's
2007
+ // See the matching comment in `Collector::walk_stmt`: the checker's
1985
- // closure inference is unreliable (can error out entirely depending
2008
+ // closure inference is unreliable (can error out entirely depending
1986
- // on the body), but every closure value is an i32 pointer regardless
2009
+ // on the body), but every closure value is an i32 pointer regardless
1987
- // of its real signature, so don't bother inferring it at all here.
2010
+ // of its real signature, so don't bother inferring it at all here.
1988
- let vty = if matches!(value, ast::Expr::Closure(_)) {
2011
+ let vty = if matches!(value, ast::Expr::Closure(_)) {
1989
- PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
2012
+ PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
1990
- } else {
2013
+ } else {
1991
- infer_local_type(value, ctx)
2014
+ infer_local_type(value, ctx)
1992
- };
2015
+ };
1993
- compile_expr(value, body, ctx, state)?;
2016
+ compile_expr(value, body, ctx, state)?;
1994
- let idx = ctx
2017
+ let idx = ctx
1995
- .locals
2018
+ .locals
1996
- .get(target)
2019
+ .get(name)
1997
- .copied()
2020
+ .copied()
1998
- .ok_or_else(|| format!("undeclared local '{}'", target))?;
2021
+ .ok_or_else(|| format!("undeclared local '{}'", name))?;
1999
- Instruction::LocalSet(idx).encode(body);
2022
+ Instruction::LocalSet(idx).encode(body);
2000
- ctx.type_env.borrow_mut().insert(target.clone(), TypeScheme::mono(vty));
2023
+ ctx.type_env.borrow_mut().insert(name.clone(), TypeScheme::mono(vty));
2001
- // If this assigns a closure *literal*, remember its exact, already-
2024
+ // If this assigns a closure *literal*, remember its exact, already-
2002
- // correct signature (computed by the discovery pass) so a later call
2025
+ // correct signature (computed by the discovery pass) so a later call
2003
- // to it doesn't have to re-derive one — see `closure_local_sigs`.
2026
+ // to it doesn't have to re-derive one — see `closure_local_sigs`.
2004
- if let ast::Expr::Closure(cl) = value {
2027
+ if let ast::Expr::Closure(cl) = value {
2005
- let key = cl.as_ref() as *const ast::Closure as usize;
2028
+ let key = cl.as_ref() as *const ast::Closure as usize;
2006
- if let Some(info) = ctx.closures.get(&key) {
2029
+ if let Some(info) = ctx.closures.get(&key) {
2007
- let mut sig_params = vec![ValType::I32];
2030
+ let mut sig_params = vec![ValType::I32];
2008
- sig_params.extend(info.param_vts.iter().copied());
2031
+ sig_params.extend(info.param_vts.iter().copied());
2009
- ctx.closure_local_sigs.borrow_mut().insert(target.clone(), (sig_params, info.ret_vt));
2032
+ ctx.closure_local_sigs.borrow_mut().insert(name.clone(), (sig_params, info.ret_vt));
2033
+ }
2034
+ }
2035
+ }
2036
+ ast::AssignTarget::Field(object, field_name) => {
2037
+ let obj_ty = infer_local_type(object, ctx);
2038
+ let class_name = match &obj_ty {
2039
+ PlumType::TNamed(n) => n.clone(),
2040
+ other => return Err(format!("codegen: cannot assign field '{}' on non-class type {}", field_name, other)),
2041
+ };
2042
+ let fields = ctx
2043
+ .classes
2044
+ .get(&class_name)
2045
+ .ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
2046
+ let (field_idx, field_ty) = fields
2047
+ .iter()
2048
+ .position(|(n, _)| n == field_name)
2049
+ .map(|i| (i, fields[i].1.clone()))
2050
+ .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
2051
+ compile_expr(object, body, ctx, state)?;
2052
+ compile_expr(value, body, ctx, state)?;
2053
+ let offset = (field_idx as u64) * 8;
2054
+ match plum_type_to_valtype(&field_ty) {
2055
+ ValType::I64 => Instruction::I64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
2056
+ ValType::F64 => Instruction::F64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body),
2057
+ _ => Instruction::I32Store(MemArg { offset, align: 2, memory_index: 0 }).encode(body),
2058
+ };
2010
2059
  }
2011
2060
  }
2012
2061
  }
plum-wasm-codegen/tests/codegen_tests.rs CHANGED
@@ -1369,3 +1369,60 @@ main() -> Int =
1369
1369
  let bytes = compile_source(&source).expect("compile failed");
1370
1370
  assert_eq!(run_main(&bytes), 7);
1371
1371
  }
1372
+
1373
+ #[test]
1374
+ fn field_assignment_target_runs_correctly() {
1375
+ let src = "\
1376
+ type Counter =
1377
+ value: Int
1378
+
1379
+ bump<Counter>() =
1380
+ self.value = self.value + 1
1381
+
1382
+ main() -> Int =
1383
+ c = Counter(value: 41)
1384
+ c.bump()
1385
+ c.value
1386
+ ";
1387
+ let source = parse(src);
1388
+ let bytes = compile_source(&source).expect("compile failed");
1389
+ assert_eq!(run_main(&bytes), 42);
1390
+ }
1391
+
1392
+ #[test]
1393
+ fn chained_field_assignment_target_runs_correctly() {
1394
+ let src = "\
1395
+ type Inner =
1396
+ value: Int
1397
+
1398
+ type Outer =
1399
+ inner: Inner
1400
+
1401
+ bump<Outer>() =
1402
+ self.inner.value = self.inner.value + 1
1403
+
1404
+ main() -> Int =
1405
+ o = Outer(inner: Inner(value: 9))
1406
+ o.bump()
1407
+ o.inner.value
1408
+ ";
1409
+ let source = parse(src);
1410
+ let bytes = compile_source(&source).expect("compile failed");
1411
+ assert_eq!(run_main(&bytes), 10);
1412
+ }
1413
+
1414
+ #[test]
1415
+ fn mixed_multi_assign_with_field_target_runs_correctly() {
1416
+ let src = "\
1417
+ type Counter =
1418
+ value: Int
1419
+
1420
+ main() -> Int =
1421
+ c = Counter(value: 5)
1422
+ a, c.value = 100, 7
1423
+ a + c.value
1424
+ ";
1425
+ let source = parse(src);
1426
+ let bytes = compile_source(&source).expect("compile failed");
1427
+ assert_eq!(run_main(&bytes), 107);
1428
+ }