plum

#treesitter#compiler#wasm

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

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


13507faPeter John 2026-09-07T15:28:19+05:30
feat(class/enum): mutable fields + Gleam-style spread update
plum-checker/src/lib.rs CHANGED
@@ -721,15 +721,13 @@ fn checkStmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_na
721
721
  let label = format!("{}.{}", describeTargetObject(object), field_name);
722
722
  match (inferExpr(object, env, ctx), inferExpr(value, env, ctx)) {
723
723
  (Ok(PlumType::TNamed(class_name)), Ok(value_ty)) => {
724
- match ctx.classes.get(&class_name).and_then(|fields| {
724
+ match lookupFieldType(&class_name, field_name, ctx) {
725
- fields.iter().find(|(n, _)| n == field_name).map(|(_, ty)| ty.clone())
726
- }) {
727
- Some(field_ty) => {
725
+ Ok(field_ty) => {
728
726
  if let Err(msg) = unifyArg(&field_ty, &value_ty, value, ctx) {
729
727
  errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) });
730
728
  }
731
729
  }
732
- None => errors.push(CheckError { message: format!("fn '{}': assign '{}': no field '{}' on class '{}'", fn_name, label, field_name, class_name) }),
730
+ Err(msg) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) }),
733
731
  }
734
732
  }
735
733
  (Ok(other), Ok(_)) => errors.push(CheckError { message: format!("fn '{}': assign '{}': cannot access field on non-class type {}", fn_name, label, other) }),
@@ -899,6 +897,28 @@ fn initCallableWithNoArgs(params: &[PlumType]) -> bool {
899
897
  params.is_empty() || matches!(params, [PlumType::TVariadic(_)])
900
898
  }
901
899
 
900
+ /// Validates a `Type(..source, ...)` spread entry: `source` must be a bare
901
+ /// variable/`self` (codegen recomputes it once per missing field via a plain
902
+ /// `LocalGet`, rather than allocating a scratch local — an arbitrary expression
903
+ /// would either re-evaluate side effects or require one) and its type must be
904
+ /// `expected_type_name` — for a class construction, that's the class itself;
905
+ /// for a named-payload enum variant, it's the OWNING ENUM (not the variant),
906
+ /// deliberately a plain `unify` rather than `unifyArg`'s syntactic exact-variant
907
+ /// check: a variable's inferred type is always the widened enum, never a
908
+ /// specific variant (see `unifyArg`'s own doc comment), so requiring exact-variant
909
+ /// proof here would reject passing any ordinary variable — the missing-field
910
+ /// reads codegen emits for a spread are already a checked downcast (same
911
+ /// runtime-trapping idiom plain `.field` access on an enum value already uses),
912
+ /// so no static proof beyond "same enum" is needed.
913
+ fn checkSpreadSource(spread: &ast::Expr, label: &str, expected_type_name: &str, env: &TypeEnv, ctx: &CheckCtx) -> Result<(), String> {
914
+ if !matches!(spread, ast::Expr::Var(_) | ast::Expr::Self_) {
915
+ return Err(format!("spread source for '{}(..)' must be a variable", label));
916
+ }
917
+ let actual = inferExpr(spread, env, ctx)?;
918
+ unify(&PlumType::TNamed(expected_type_name.to_string()), &actual, ctx)
919
+ .map_err(|e| format!("spread source for '{}': {}", label, e))
920
+ }
921
+
902
922
  fn inferClassCallRaw(call: &ast::ClassCall, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> {
903
923
  // A zero-arg `Type()` construction with a nested, no-param `fun init() ->
904
924
  // Type = ...` method declared is sugar for calling that method — the
@@ -925,9 +945,14 @@ fn inferClassCallRaw(call: &ast::ClassCall, env: &TypeEnv, ctx: &CheckCtx) -> Re
925
945
  None => return Err(format!("unknown field '{}' on class '{}'", fa.name, call.type_name)),
926
946
  }
927
947
  }
948
+ match &call.spread {
949
+ Some(spread) => checkSpreadSource(spread, &call.type_name, &call.type_name, env, ctx)?,
950
+ None => {
928
- for (field_name, _) in fields {
951
+ for (field_name, _) in fields {
929
- if !call.fields.iter().any(|fa| &fa.name == field_name) {
952
+ if !call.fields.iter().any(|fa| &fa.name == field_name) {
930
- return Err(format!("class '{}' missing field '{}'", call.type_name, field_name));
953
+ return Err(format!("class '{}' missing field '{}'", call.type_name, field_name));
954
+ }
955
+ }
931
956
  }
932
957
  }
933
958
  Ok(PlumType::TNamed(call.type_name.clone()))
@@ -950,9 +975,14 @@ fn inferClassCallRaw(call: &ast::ClassCall, env: &TypeEnv, ctx: &CheckCtx) -> Re
950
975
  None => return Err(format!("unknown field '{}' on variant '{}'", fa.name, call.type_name)),
951
976
  }
952
977
  }
978
+ match &call.spread {
979
+ Some(spread) => checkSpreadSource(spread, &call.type_name, &info.enum_name, env, ctx)?,
980
+ None => {
953
- for field_name in &info.field_names {
981
+ for field_name in &info.field_names {
954
- if !call.fields.iter().any(|fa| &fa.name == field_name) {
982
+ if !call.fields.iter().any(|fa| &fa.name == field_name) {
955
- return Err(format!("variant '{}' missing field '{}'", call.type_name, field_name));
983
+ return Err(format!("variant '{}' missing field '{}'", call.type_name, field_name));
984
+ }
985
+ }
956
986
  }
957
987
  }
958
988
  Ok(PlumType::TNamed(info.enum_name.clone()))
@@ -1100,6 +1130,45 @@ fn resolveClosureParamFromFieldUsage(
1100
1130
  }
1101
1131
  }
1102
1132
 
1133
+ /// Resolves `class_name`'s field named `field_name` to its type — shared by
1134
+ /// `.field` reads (`inferExpr`'s `AttrKind::Field` arm) and `.field = value`
1135
+ /// writes (`checkStmt`'s `AssignTarget::Field` arm), so mutation gets the same
1136
+ /// class / discriminant-enum-param / single-owning-variant fallback chain
1137
+ /// reads already have. See the call sites' own comments for why each fallback
1138
+ /// is safe (in particular: exactly one variant may own a given field name;
1139
+ /// more than one is ambiguous and a compile error).
1140
+ fn lookupFieldType(class_name: &str, field_name: &str, ctx: &CheckCtx) -> Result<PlumType, String> {
1141
+ match ctx.classes.get(class_name) {
1142
+ Some(fields) => fields.iter()
1143
+ .find(|(n, _)| n == field_name)
1144
+ .map(|(_, t)| t.clone())
1145
+ .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1146
+ None => match ctx.enum_params.get(class_name) {
1147
+ Some(params) => params.iter()
1148
+ .find(|(n, _)| n == field_name)
1149
+ .map(|(_, t)| t.clone())
1150
+ .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1151
+ None => {
1152
+ let owners: Vec<&EnumVariantInfo> = ctx.enum_variants.values()
1153
+ .filter(|info| info.enum_name == class_name
1154
+ && info.field_names.iter().any(|n| n == field_name))
1155
+ .collect();
1156
+ match owners.as_slice() {
1157
+ [info] => {
1158
+ let idx = info.field_names.iter().position(|n| n == field_name).expect("just filtered on this");
1159
+ Ok(info.field_types[idx].clone())
1160
+ }
1161
+ [] => Ok(PlumType::TVar("_".to_string())),
1162
+ _ => Err(format!(
1163
+ "field '{}' is ambiguous across multiple variants of enum '{}' — use a match",
1164
+ field_name, class_name
1165
+ )),
1166
+ }
1167
+ }
1168
+ },
1169
+ }
1170
+ }
1171
+
1103
1172
  pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> {
1104
1173
  match expr {
1105
1174
  ast::Expr::Int(_) => Ok(PlumType::TInt),
@@ -1362,51 +1431,7 @@ pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<Plum
1362
1431
  PlumType::TNamed(n) => n.as_str(),
1363
1432
  _ => "Str",
1364
1433
  };
1365
- match ctx.classes.get(class_name) {
1366
- Some(fields) => fields.iter()
1367
- .find(|(n, _)| n == field_name)
1434
+ lookupFieldType(class_name, field_name, ctx)
1368
- .map(|(_, t)| t.clone())
1369
- .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1370
- None => match ctx.enum_params.get(class_name) {
1371
- Some(params) => params.iter()
1372
- .find(|(n, _)| n == field_name)
1373
- .map(|(_, t)| t.clone())
1374
- .ok_or_else(|| format!("no field '{}' on type '{}'", field_name, class_name)),
1375
- // Not a discriminant enum either: `class_name` might
1376
- // still be an ordinary enum whose value provably came
1377
- // from a named-payload variant (`Circle(radius: 5)`,
1378
- // widened to `TNamed("Shape")` by construction — see
1379
- // `inferClassCallRaw`). If exactly ONE of the enum's
1380
- // variants declares a field with this name, `.field`
1381
- // is safe to allow here: codegen compiles it as a
1382
- // checked downcast to that one variant (traps at
1383
- // runtime if the value turns out to be a different
1384
- // variant), so no static proof the value IS that
1385
- // variant is required. If more than one variant
1386
- // shares the field name, which one's value would win
1387
- // is genuinely ambiguous — require a real `match`
1388
- // instead. Zero owners means an unmodeled type
1389
- // (unresolved generic, etc.) — allow, codegen will
1390
- // catch a genuine mismatch.
1391
- None => {
1392
- let owners: Vec<&EnumVariantInfo> = ctx.enum_variants.values()
1393
- .filter(|info| info.enum_name == class_name
1394
- && info.field_names.iter().any(|n| n == field_name))
1395
- .collect();
1396
- match owners.as_slice() {
1397
- [info] => {
1398
- let idx = info.field_names.iter().position(|n| n == field_name).expect("just filtered on this");
1399
- Ok(info.field_types[idx].clone())
1400
- }
1401
- [] => Ok(PlumType::TVar("_".to_string())),
1402
- _ => Err(format!(
1403
- "field '{}' is ambiguous across multiple variants of enum '{}' — use a match",
1404
- field_name, class_name
1405
- )),
1406
- }
1407
- }
1408
- },
1409
- }
1410
1435
  }
1411
1436
  // An unresolved generic method return (e.g. `Result[T, E].unwrap()`'s
1412
1437
  // bare `T`, which this checker's `PlumType` erases entirely — see
plum-core/src/ast.rs CHANGED
@@ -450,6 +450,11 @@ pub struct ClassCall {
450
450
  /// class's own field VALUES can't pin down its type params on their own
451
451
  /// (e.g. constructing an empty `List` with nothing to infer `T` from).
452
452
  pub generics: Vec<Type>,
453
+ /// Gleam-style `Type(..source, field: value, ...)` update — fields not
454
+ /// listed in `fields` are read from `source` instead of being required.
455
+ /// Restricted to a bare variable/`self` (see `plum-checker`'s `inferClassCallRaw`)
456
+ /// so codegen can recompute it once per missing field with no scratch local.
457
+ pub spread: Option<Box<Expr>>,
453
458
  }
454
459
 
455
460
  #[derive(Debug, Clone, PartialEq)]
plum-core/src/parser.rs CHANGED
@@ -892,24 +892,29 @@ impl<'a> AstParser<'a> {
892
892
  .map(|n| self.parseType(n))
893
893
  .collect()
894
894
  };
895
+ let mut spread = None;
895
896
  let fields = node.child_by_field_name("arguments")
896
897
  .map(|args_node| {
897
- // class_argument_list: "(" (var_identifier ":" expression),* ")"
898
+ // class_argument_list: "(" (spread_argument | field_argument),* ")"
898
- // Named children alternate: var_identifier, expression, ...
899
+ // each named child is one of those two wrapper nodes.
899
900
  let mut cursor = args_node.walk();
900
- let named: Vec<Node> = args_node.named_children(&mut cursor).collect();
901
+ args_node.named_children(&mut cursor).filter_map(|n| match n.kind() {
901
- named.chunks(2).filter_map(|chunk| {
902
- if chunk.len() == 2 {
903
- let name = self.text(chunk[0]);
902
+ "spread_argument" => {
903
+ let vn = n.child_by_field_name("value").expect("spread_argument has a value");
904
- let u = self.unwrapExprNode(chunk[1]);
904
+ let u = self.unwrapExprNode(vn);
905
- Some(FieldArg { name, value: self.parseExpression(u) })
905
+ spread = Some(Box::new(self.parseExpression(u)));
906
- } else {
907
906
  None
908
907
  }
908
+ _ => {
909
+ let name = n.child_by_field_name("name").map(|nn| self.text(nn)).unwrap_or_default();
910
+ let vn = n.child_by_field_name("value").expect("field_argument has a value");
911
+ let u = self.unwrapExprNode(vn);
912
+ Some(FieldArg { name, value: self.parseExpression(u) })
913
+ }
909
914
  }).collect()
910
915
  })
911
916
  .unwrap_or_default();
912
- ClassCall { type_name, fields, generics }
917
+ ClassCall { type_name, fields, generics, spread }
913
918
  }
914
919
 
915
920
  // ---- string literals --------------------------------------------------
plum-examples/types.plum CHANGED
@@ -202,6 +202,39 @@ test "enum class field construct and destructure runs correctly"
202
202
  b := OptionBox(value: Some(42))
203
203
  assert b.unwrap(0) == 42
204
204
 
205
+ test "class field mutation and spread update run correctly"
206
+ p := Point(x: 1, y: 2)
207
+ p.x = 10
208
+ assert p.x == 10
209
+ assert p.y == 2
210
+ p2 := Point(..p, x: 100)
211
+ assert p2.x == 100
212
+ assert p2.y == 2
213
+ # the spread source is untouched by the update it fed
214
+ assert p.x == 10
215
+
216
+ test "single-variant named-payload enum field mutation and spread update run correctly"
217
+ v := Vec2(x: 1, y: 2)
218
+ v.x = 10
219
+ assert v.x == 10
220
+ assert v.y == 2
221
+ v2 := Vec2(..v, x: 100)
222
+ assert v2.x == 100
223
+ assert v2.y == 2
224
+ assert v.x == 10
225
+
226
+ test "multi-variant enum field mutation and spread update run correctly"
227
+ # Same checked-downcast idiom `.field` reads already use on a
228
+ # multi-variant enum's uniquely-owned field name (see the test above
229
+ # about `ShapeWithFields`) — traps at runtime if the value is ever the
230
+ # OTHER variant, no static proof required.
231
+ c := CircleField(radius: 5)
232
+ c.radius = 9
233
+ assert c.radius == 9
234
+ c2 := CircleField(..c, radius: 50)
235
+ assert c2.radius == 50
236
+ assert c.radius == 9
237
+
205
238
  test "gc type registry produces a well formed type section alongside bump allocator codegen"
206
239
  # Task 1 Step 5 of the wasm-gc migration plan: the wasm-gc type registry
207
240
  # emits a well-formed type section — a struct type per class, a
plum-tooling/tree-sitter-plum/grammar.js CHANGED
@@ -562,13 +562,22 @@ module.exports = grammar({
562
562
  class_argument_list: ($) =>
563
563
  seq(
564
564
  "(",
565
- optional(
565
+ optional(seq(
566
- commaSep1(seq(field("name", $.var_identifier), ":", field("value", $.expression)),),
566
+ commaSep1(choice($.spread_argument, $.field_argument)),
567
- ),
568
- optional(","),
567
+ optional(","),
568
+ )),
569
569
  ")",
570
570
  ),
571
571
 
572
+ field_argument: ($) =>
573
+ seq(field("name", $.var_identifier), ":", field("value", $.expression)),
574
+
575
+ // Gleam-style functional-update entry: `Point(..p, x: 10)` — fields not
576
+ // named after it are read from `p` instead of being required. Only ever
577
+ // valid as the FIRST argument — enforced by `plum-core`'s parser, not here.
578
+ spread_argument: ($) =>
579
+ seq("..", field("value", $.expression)),
580
+
572
581
  ternary_expression: ($) =>
573
582
  prec.right(
574
583
  PREC.conditional,
plum-tooling/tree-sitter-plum/src/grammar.json CHANGED
Binary file
plum-tooling/tree-sitter-plum/src/node-types.json CHANGED
Binary file
plum-tooling/tree-sitter-plum/src/parser.c CHANGED
Binary file
plum-wasm-codegen/src/lib.rs CHANGED
@@ -812,7 +812,7 @@ fn buildGcTypeRegistry(
812
812
  let params = enum_params.get(enum_name).cloned().unwrap_or_default();
813
813
  let field_types: Vec<FieldType> = params.iter().map(|(_, ty)| FieldType {
814
814
  element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
815
- mutable: false,
815
+ mutable: true,
816
816
  }).collect();
817
817
  SubType {
818
818
  is_final: false,
@@ -831,7 +831,7 @@ fn buildGcTypeRegistry(
831
831
  // subtyping, so no special-casing is needed here.
832
832
  let field_types: Vec<FieldType> = info.field_types.iter().map(|ty| FieldType {
833
833
  element_type: StorageType::Val(plumTypeToGcValtype(ty, &registry)),
834
- mutable: false,
834
+ mutable: true,
835
835
  }).collect();
836
836
  SubType {
837
837
  is_final: true,
@@ -3304,22 +3304,51 @@ fn compileStmt(stmt: &ast::Stmt, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
3304
3304
  PlumType::TNamed(n) => n.clone(),
3305
3305
  other => return Err(format!("codegen: cannot assign field '{}' on non-class type {}", field_name, other)),
3306
3306
  };
3307
- let fields = ctx
3308
- .classes
3309
- .get(&class_name)
3310
- .ok_or_else(|| format!("codegen: unknown class '{}'", class_name))?;
3311
- let field_idx = fields
3312
- .iter()
3313
- .position(|(n, _)| n == field_name)
3314
- .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
3315
- let class_type_idx = *ctx.gc_types.class_type_idx.get(&class_name)
3316
- .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", class_name))?;
3317
3307
  // struct.set expects [(ref null $t) value] on the stack (ref
3318
3308
  // pushed first/deeper, value second/on top) — same push order
3319
- // this already used for the old memory store.
3309
+ // this already used for the old memory store. Mirrors the read
3310
+ // side's class / discriminant-enum-param / single-owning-variant
3311
+ // fallback chain (`AttrKind::Field` below) so a named-payload
3312
+ // enum variant's own field can be mutated too, not just a class's.
3313
+ match ctx.classes.get(&class_name) {
3314
+ Some(fields) => {
3315
+ let field_idx = fields
3316
+ .iter()
3317
+ .position(|(n, _)| n == field_name)
3318
+ .ok_or_else(|| format!("codegen: no field '{}' on class '{}'", field_name, class_name))?;
3319
+ let class_type_idx = *ctx.gc_types.class_type_idx.get(&class_name)
3320
+ .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", class_name))?;
3320
- compileExpr(object, body, ctx, state)?;
3321
+ compileExpr(object, body, ctx, state)?;
3321
- compileExpr(value, body, ctx, state)?;
3322
+ compileExpr(value, body, ctx, state)?;
3322
- Instruction::StructSet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
3323
+ Instruction::StructSet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
3324
+ }
3325
+ None => match ctx.enum_params.get(&class_name) {
3326
+ Some(params) => {
3327
+ let field_idx = params
3328
+ .iter()
3329
+ .position(|(n, _)| n == field_name)
3330
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
3331
+ let super_type_idx = *ctx.gc_types.enum_super_type_idx.get(&class_name)
3332
+ .ok_or_else(|| format!("codegen: enum '{}' missing from the GC type registry", class_name))?;
3333
+ compileExpr(object, body, ctx, state)?;
3334
+ compileExpr(value, body, ctx, state)?;
3335
+ Instruction::StructSet { struct_type_index: super_type_idx, field_index: field_idx as u32 }.encode(body);
3336
+ }
3337
+ None => {
3338
+ let (variant_name, info) = ctx.enum_variants.iter()
3339
+ .find(|(_, info)| info.enum_name == class_name && info.field_names.iter().any(|n| n == field_name))
3340
+ .ok_or_else(|| format!("codegen: no field '{}' on enum '{}'", field_name, class_name))?;
3341
+ let field_idx = info.field_names.iter().position(|n| n == field_name)
3342
+ .expect("just found by this field name");
3343
+ let variant_type_idx = *ctx.gc_types.variant_type_idx.get(variant_name)
3344
+ .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", variant_name))?;
3345
+ compileExpr(object, body, ctx, state)?;
3346
+ Instruction::RefCastNonNull(HeapType::Concrete(variant_type_idx)).encode(body);
3347
+ compileExpr(value, body, ctx, state)?;
3348
+ Instruction::StructSet { struct_type_index: variant_type_idx, field_index: field_idx as u32 }.encode(body);
3349
+ }
3350
+ },
3351
+ }
3323
3352
  }
3324
3353
  }
3325
3354
  }
@@ -4382,7 +4411,7 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4382
4411
  // the equivalent static method-call expression and let the existing
4383
4412
  // `AttrKind::Method` codegen (below) handle it, rather than duplicating that
4384
4413
  // logic here.
4385
- ast::Expr::ClassCall(call) if call.fields.is_empty()
4414
+ ast::Expr::ClassCall(call) if call.fields.is_empty() && call.spread.is_none()
4386
4415
  && matches!(ctx.methods.get(&(call.type_name.clone(), "init".to_string())), Some(PlumType::TFun(p, _)) if initCallableWithNoArgs(p)) =>
4387
4416
  {
4388
4417
  let synthetic = ast::Expr::Attribute(Box::new(ast::AttributeExpr {
@@ -4406,10 +4435,26 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4406
4435
  let variant_idx = *ctx.gc_types.variant_type_idx.get(&call.type_name)
4407
4436
  .ok_or_else(|| format!("codegen: variant '{}' missing from the GC type registry", call.type_name))?;
4408
4437
  let field_names = info.field_names.clone();
4409
- for field_name in &field_names {
4438
+ for (field_idx, field_name) in field_names.iter().enumerate() {
4410
- let fa = call.fields.iter().find(|fa| &fa.name == field_name)
4439
+ match call.fields.iter().find(|fa| &fa.name == field_name) {
4440
+ Some(fa) => compileExpr(&fa.value, body, ctx, state)?,
4441
+ // `Circle(..c, radius: 10)` — a field not overridden is read
4442
+ // straight off the spread source instead (checker only proved
4443
+ // `c` is SOME value of the owning enum, restricted to a bare
4444
+ // variable/`self` — see `checkSpreadSource` — not statically
4445
+ // that it's exactly this variant, so a checked `ref.cast` down
4446
+ // to this variant's own struct type is needed here, same as
4447
+ // plain `.field` access on a multi-variant enum value already
4448
+ // does; recompiling the source is just a cheap `LocalGet`, not
4449
+ // a re-evaluation of anything with side effects).
4450
+ None => {
4451
+ let spread = call.spread.as_ref()
4411
- .ok_or_else(|| format!("codegen: variant '{}' missing field '{}'", call.type_name, field_name))?;
4452
+ .ok_or_else(|| format!("codegen: variant '{}' missing field '{}'", call.type_name, field_name))?;
4412
- compileExpr(&fa.value, body, ctx, state)?;
4453
+ compileExpr(spread, body, ctx, state)?;
4454
+ Instruction::RefCastNonNull(HeapType::Concrete(variant_idx)).encode(body);
4455
+ Instruction::StructGet { struct_type_index: variant_idx, field_index: field_idx as u32 }.encode(body);
4456
+ }
4457
+ }
4413
4458
  }
4414
4459
  Instruction::StructNew(variant_idx).encode(body);
4415
4460
  }
@@ -4426,10 +4471,17 @@ fn compileExpr(expr: &ast::Expr, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut
4426
4471
  let class_type_idx = *ctx.gc_types.class_type_idx.get(&call.type_name)
4427
4472
  .ok_or_else(|| format!("codegen: class '{}' missing from the GC type registry", call.type_name))?;
4428
4473
 
4429
- for (field_name, _) in &fields {
4474
+ for (field_idx, (field_name, _)) in fields.iter().enumerate() {
4430
- let fa = call.fields.iter().find(|fa| &fa.name == field_name)
4475
+ match call.fields.iter().find(|fa| &fa.name == field_name) {
4476
+ Some(fa) => compileExpr(&fa.value, body, ctx, state)?,
4477
+ // See the matching comment in the variant-construction arm above.
4478
+ None => {
4479
+ let spread = call.spread.as_ref()
4431
- .ok_or_else(|| format!("codegen: class '{}' missing field '{}'", call.type_name, field_name))?;
4480
+ .ok_or_else(|| format!("codegen: class '{}' missing field '{}'", call.type_name, field_name))?;
4432
- compileExpr(&fa.value, body, ctx, state)?;
4481
+ compileExpr(spread, body, ctx, state)?;
4482
+ Instruction::StructGet { struct_type_index: class_type_idx, field_index: field_idx as u32 }.encode(body);
4483
+ }
4484
+ }
4433
4485
  }
4434
4486
  Instruction::StructNew(class_type_idx).encode(body);
4435
4487
  }