plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
13507fa
— Peter John
2026-09-07T15:28:19+05:30
feat(class/enum): mutable fields + Gleam-style spread update
- plum-checker/src/lib.rs +81 -56
- plum-core/src/ast.rs +5 -0
- plum-core/src/parser.rs +15 -10
- plum-examples/types.plum +33 -0
- plum-tooling/tree-sitter-plum/grammar.js +13 -4
- plum-tooling/tree-sitter-plum/src/grammar.json +0 -0
- plum-tooling/tree-sitter-plum/src/node-types.json +0 -0
- plum-tooling/tree-sitter-plum/src/parser.c +0 -0
- plum-wasm-codegen/src/lib.rs +77 -25
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
|
|
724
|
+
match lookupFieldType(&class_name, field_name, ctx) {
|
|
725
|
-
fields.iter().find(|(n, _)| n == field_name).map(|(_, ty)| ty.clone())
|
|
726
|
-
}) {
|
|
727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
951
|
+
for (field_name, _) in fields {
|
|
929
|
-
|
|
952
|
+
if !call.fields.iter().any(|fa| &fa.name == field_name) {
|
|
930
|
-
|
|
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
|
-
|
|
981
|
+
for field_name in &info.field_names {
|
|
954
|
-
|
|
982
|
+
if !call.fields.iter().any(|fa| &fa.name == field_name) {
|
|
955
|
-
|
|
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
|
-
|
|
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: "(" (
|
|
898
|
+
// class_argument_list: "(" (spread_argument | field_argument),* ")"
|
|
898
|
-
//
|
|
899
|
+
// — each named child is one of those two wrapper nodes.
|
|
899
900
|
let mut cursor = args_node.walk();
|
|
900
|
-
|
|
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
|
-
|
|
902
|
+
"spread_argument" => {
|
|
903
|
+
let vn = n.child_by_field_name("value").expect("spread_argument has a value");
|
|
904
|
-
let u = self.unwrapExprNode(
|
|
904
|
+
let u = self.unwrapExprNode(vn);
|
|
905
|
-
|
|
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(
|
|
566
|
+
commaSep1(choice($.spread_argument, $.field_argument)),
|
|
567
|
-
),
|
|
568
|
-
|
|
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, ®istry)),
|
|
815
|
-
mutable:
|
|
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, ®istry)),
|
|
834
|
-
mutable:
|
|
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
|
-
|
|
3321
|
+
compileExpr(object, body, ctx, state)?;
|
|
3321
|
-
|
|
3322
|
+
compileExpr(value, body, ctx, state)?;
|
|
3322
|
-
|
|
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
|
|
4438
|
+
for (field_idx, field_name) in field_names.iter().enumerate() {
|
|
4410
|
-
|
|
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
|
-
|
|
4452
|
+
.ok_or_else(|| format!("codegen: variant '{}' missing field '{}'", call.type_name, field_name))?;
|
|
4412
|
-
|
|
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
|
|
4474
|
+
for (field_idx, (field_name, _)) in fields.iter().enumerate() {
|
|
4430
|
-
|
|
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
|
-
|
|
4480
|
+
.ok_or_else(|| format!("codegen: class '{}' missing field '{}'", call.type_name, field_name))?;
|
|
4432
|
-
|
|
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
|
}
|