plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
47abc49
— Peter John
2026-07-23T18:50:33+05:30
feat(plum-checker): type-check obj.field assignment targets
plum-checker/src/lib.rs
CHANGED
|
@@ -240,14 +240,53 @@ fn check_block(block: &ast::Block, env: &mut TypeEnv, declared_ret: &PlumType, f
|
|
|
240
240
|
errors
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
fn describe_target_object(expr: &ast::Expr) -> String {
|
|
244
|
+
match expr {
|
|
245
|
+
ast::Expr::Self_ => "self".to_string(),
|
|
246
|
+
ast::Expr::Var(n) => n.clone(),
|
|
247
|
+
ast::Expr::Attribute(a) => {
|
|
248
|
+
if let ast::AttrKind::Field(f) = &a.attr {
|
|
249
|
+
format!("{}.{}", describe_target_object(&a.object), f)
|
|
250
|
+
} else {
|
|
251
|
+
"<expr>".to_string()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
_ => "<expr>".to_string(),
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
243
258
|
fn check_stmt(stmt: &ast::Stmt, env: &mut TypeEnv, declared_ret: &PlumType, fn_name: &str, ctx: &CheckCtx) -> Vec<CheckError> {
|
|
244
259
|
let mut errors = Vec::new();
|
|
245
260
|
match stmt {
|
|
246
261
|
ast::Stmt::Assign(a) => {
|
|
247
262
|
for (target, value) in a.targets.iter().zip(a.values.iter()) {
|
|
263
|
+
match target {
|
|
264
|
+
ast::AssignTarget::Var(name) => {
|
|
248
|
-
|
|
265
|
+
match infer_expr(value, env, ctx) {
|
|
249
|
-
|
|
266
|
+
Ok(t) => { env.insert(name.clone(), TypeScheme::mono(t)); }
|
|
250
|
-
|
|
267
|
+
Err(msg) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, name, msg) }),
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
ast::AssignTarget::Field(object, field_name) => {
|
|
271
|
+
let label = format!("{}.{}", describe_target_object(object), field_name);
|
|
272
|
+
match (infer_expr(object, env, ctx), infer_expr(value, env, ctx)) {
|
|
273
|
+
(Ok(PlumType::TNamed(class_name)), Ok(value_ty)) => {
|
|
274
|
+
match ctx.classes.get(&class_name).and_then(|fields| {
|
|
275
|
+
fields.iter().find(|(n, _)| n == field_name).map(|(_, ty)| ty.clone())
|
|
276
|
+
}) {
|
|
277
|
+
Some(field_ty) => {
|
|
278
|
+
if let Err(msg) = unify(&field_ty, &value_ty) {
|
|
279
|
+
errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
None => errors.push(CheckError { message: format!("fn '{}': assign '{}': no field '{}' on class '{}'", fn_name, label, field_name, class_name) }),
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
(Ok(other), Ok(_)) => errors.push(CheckError { message: format!("fn '{}': assign '{}': cannot access field on non-class type {}", fn_name, label, other) }),
|
|
286
|
+
(Err(msg), _) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) }),
|
|
287
|
+
(_, Err(msg)) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, label, msg) }),
|
|
288
|
+
}
|
|
289
|
+
}
|
|
251
290
|
}
|
|
252
291
|
}
|
|
253
292
|
}
|
plum-checker/src/monomorphize.rs
CHANGED
|
@@ -378,10 +378,17 @@ impl<'a> Monomorphizer<'a> {
|
|
|
378
378
|
fn rewrite_stmt(&mut self, stmt: &mut ast::Stmt, env: &mut TypeEnv) -> Result<(), String> {
|
|
379
379
|
match stmt {
|
|
380
380
|
ast::Stmt::Assign(a) => {
|
|
381
|
-
for (target, value) in a.targets.
|
|
381
|
+
for (target, value) in a.targets.iter_mut().zip(a.values.iter_mut()) {
|
|
382
382
|
self.rewrite_expr(value, env)?;
|
|
383
383
|
let ty = self.infer(value, env);
|
|
384
|
+
match target {
|
|
385
|
+
ast::AssignTarget::Var(name) => {
|
|
384
|
-
|
|
386
|
+
env.insert(name.clone(), TypeScheme::mono(ty));
|
|
387
|
+
}
|
|
388
|
+
ast::AssignTarget::Field(object, _) => {
|
|
389
|
+
self.rewrite_expr(object, env)?;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
385
392
|
}
|
|
386
393
|
}
|
|
387
394
|
ast::Stmt::Return(Some(e)) => self.rewrite_expr(e, env)?,
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -597,6 +597,50 @@ each(cb: fn(Int) -> Bool) -> Bool =
|
|
|
597
597
|
assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
|
|
598
598
|
}
|
|
599
599
|
|
|
600
|
+
#[test]
|
|
601
|
+
fn field_assignment_target_with_matching_type_passes() {
|
|
602
|
+
let src = "\
|
|
603
|
+
type Cat =
|
|
604
|
+
name: Str
|
|
605
|
+
age: Int
|
|
606
|
+
|
|
607
|
+
haveBirthday<Cat>() =
|
|
608
|
+
self.age = self.age + 1
|
|
609
|
+
";
|
|
610
|
+
let source = parse(src);
|
|
611
|
+
assert!(check_source(&source).is_ok(), "expected Ok");
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#[test]
|
|
615
|
+
fn field_assignment_target_with_mismatched_type_is_error() {
|
|
616
|
+
let src = "\
|
|
617
|
+
type Cat =
|
|
618
|
+
name: Str
|
|
619
|
+
age: Int
|
|
620
|
+
|
|
621
|
+
breakCat<Cat>() =
|
|
622
|
+
self.age = \"oops\"
|
|
623
|
+
";
|
|
624
|
+
let source = parse(src);
|
|
625
|
+
let result = check_source(&source);
|
|
626
|
+
assert!(result.is_err());
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
#[test]
|
|
630
|
+
fn field_assignment_target_unknown_field_is_error() {
|
|
631
|
+
let src = "\
|
|
632
|
+
type Cat =
|
|
633
|
+
name: Str
|
|
634
|
+
age: Int
|
|
635
|
+
|
|
636
|
+
breakCat<Cat>() =
|
|
637
|
+
self.nope = 1
|
|
638
|
+
";
|
|
639
|
+
let source = parse(src);
|
|
640
|
+
let result = check_source(&source);
|
|
641
|
+
assert!(result.is_err());
|
|
642
|
+
}
|
|
643
|
+
|
|
600
644
|
#[test]
|
|
601
645
|
fn closure_passed_to_fn_value_typed_param_type_checks() {
|
|
602
646
|
let src = "\
|