plum

#treesitter#compiler#wasm

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

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


47abc49Peter 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
- match infer_expr(value, env, ctx) {
265
+ match infer_expr(value, env, ctx) {
249
- Ok(t) => { env.insert(target.clone(), TypeScheme::mono(t)); }
266
+ Ok(t) => { env.insert(name.clone(), TypeScheme::mono(t)); }
250
- Err(msg) => errors.push(CheckError { message: format!("fn '{}': assign '{}': {}", fn_name, target, msg) }),
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.iter().zip(a.values.iter_mut()) {
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
- env.insert(target.clone(), TypeScheme::mono(ty));
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 = "\