plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
01f9be3
— Peter 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
|
-
|
|
945
|
+
let ty = plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt);
|
|
944
|
-
|
|
946
|
+
self.env.insert(name.clone(), TypeScheme::mono(ty));
|
|
945
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1592
|
+
let ty = if matches!(value, ast::Expr::Closure(_)) {
|
|
1577
|
-
|
|
1593
|
+
// The checker's own closure inference (`infer_expr` on
|
|
1578
|
-
|
|
1594
|
+
// `Expr::Closure`) infers the return type by recursively
|
|
1579
|
-
|
|
1595
|
+
// inferring the body's tail expression with each param bound
|
|
1580
|
-
|
|
1596
|
+
// to a fresh, unconstrained `TVar` — e.g. a captured/param
|
|
1581
|
-
|
|
1597
|
+
// attribute access (`c.age`) on a `TVar`-typed object isn't a
|
|
1582
|
-
|
|
1598
|
+
// known class, so it errors out entirely, and this call site
|
|
1583
|
-
|
|
1599
|
+
// then silently defaults to `TInt` — the *wrong* wasm local
|
|
1584
|
-
|
|
1600
|
+
// width for what's actually always an `i32` pointer. All that
|
|
1585
|
-
|
|
1601
|
+
// actually matters here is the local's wasm width, and every
|
|
1586
|
-
|
|
1602
|
+
// closure value is an i32 pointer regardless of its
|
|
1587
|
-
|
|
1603
|
+
// parameter/return types, so skip inference entirely.
|
|
1588
|
-
|
|
1604
|
+
PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
|
|
1589
|
-
|
|
1605
|
+
} else {
|
|
1590
|
-
|
|
1606
|
+
plum_checker::infer_expr(value, &self.env, &self.cctx).unwrap_or(PlumType::TInt)
|
|
1591
|
-
|
|
1607
|
+
};
|
|
1592
|
-
|
|
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
|
-
|
|
2007
|
+
// See the matching comment in `Collector::walk_stmt`: the checker's
|
|
1985
|
-
|
|
2008
|
+
// closure inference is unreliable (can error out entirely depending
|
|
1986
|
-
|
|
2009
|
+
// on the body), but every closure value is an i32 pointer regardless
|
|
1987
|
-
|
|
2010
|
+
// of its real signature, so don't bother inferring it at all here.
|
|
1988
|
-
|
|
2011
|
+
let vty = if matches!(value, ast::Expr::Closure(_)) {
|
|
1989
|
-
|
|
2012
|
+
PlumType::TFun(Vec::new(), Box::new(PlumType::TUnit))
|
|
1990
|
-
|
|
2013
|
+
} else {
|
|
1991
|
-
|
|
2014
|
+
infer_local_type(value, ctx)
|
|
1992
|
-
|
|
2015
|
+
};
|
|
1993
|
-
|
|
2016
|
+
compile_expr(value, body, ctx, state)?;
|
|
1994
|
-
|
|
2017
|
+
let idx = ctx
|
|
1995
|
-
|
|
2018
|
+
.locals
|
|
1996
|
-
|
|
2019
|
+
.get(name)
|
|
1997
|
-
|
|
2020
|
+
.copied()
|
|
1998
|
-
|
|
2021
|
+
.ok_or_else(|| format!("undeclared local '{}'", name))?;
|
|
1999
|
-
|
|
2022
|
+
Instruction::LocalSet(idx).encode(body);
|
|
2000
|
-
|
|
2023
|
+
ctx.type_env.borrow_mut().insert(name.clone(), TypeScheme::mono(vty));
|
|
2001
|
-
|
|
2024
|
+
// If this assigns a closure *literal*, remember its exact, already-
|
|
2002
|
-
|
|
2025
|
+
// correct signature (computed by the discovery pass) so a later call
|
|
2003
|
-
|
|
2026
|
+
// to it doesn't have to re-derive one — see `closure_local_sigs`.
|
|
2004
|
-
|
|
2027
|
+
if let ast::Expr::Closure(cl) = value {
|
|
2005
|
-
|
|
2028
|
+
let key = cl.as_ref() as *const ast::Closure as usize;
|
|
2006
|
-
|
|
2029
|
+
if let Some(info) = ctx.closures.get(&key) {
|
|
2007
|
-
|
|
2030
|
+
let mut sig_params = vec![ValType::I32];
|
|
2008
|
-
|
|
2031
|
+
sig_params.extend(info.param_vts.iter().copied());
|
|
2009
|
-
|
|
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
|
+
}
|