plum

#treesitter#compiler#wasm

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

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


b952f81 — Peter John 2026-10-26T13:41:57+05:30
feat: fn-typed enum fields (record of functions); closures capture called fn params
README.md CHANGED
@@ -283,17 +283,19 @@ in outer[Plain] (called at main.plum:9:3):
283
283
  - **Explicit "trait values".** When a generic needs behavior that is not a method on `T`, pass it explicitly, like a Zig comptime parameter or a Haskell dictionary:
284
284
 
285
285
  ```plum
286
+ enum Num[T] =
286
- fun sumWith(xs: List[T], zero: T, add: fn(T, T) -> T) -> T =
287
+ | Num(add: fn(T, T) -> T, zero: T)
287
- acc := zero
288
- for i := range xs.length()
289
- acc = add(acc, xs.get(i) ?? zero)
290
- acc
291
288
 
289
+ fun sumWith(xs: List[T], num: Num[T]) -> T =
290
+ xs.reduce(num.zero, |acc, x| num.add(acc, x))
291
+
292
+ intNum := Num(add: |a, b| a + b, zero: 0)
293
+ floatNum := Num(add: |a, b| a + b, zero: 0.0)
292
- sumWith(List(1, 2, 3), 0, |a, b| a + b) # 6
294
+ sumWith(List(1, 2, 3), intNum) # 6
293
- sumWith(List("a", "b"), "", |a, b| a + b) # "ab"
295
+ sumWith(List(1.5, 2.5), floatNum) # 4.0
294
296
  ```
295
297
 
296
- See `num_record_passed_explicitly` in [`plum-examples/generics.plum`](plum-examples/generics.plum). (Enum fields cannot hold `fn` types yet, so the "record of functions" is passed as separate arguments.)
298
+ `num.add(a, b)` calls the closure stored in the fn-typed field `add`; if the type also declares a method named `add`, the method wins. See `num_record_passed_explicitly` in [`plum-examples/generics.plum`](plum-examples/generics.plum).
297
299
 
298
300
  - **Mixed types are enums.** To hold several kinds of value in one list, declare an enum and `match self`:
299
301
 
plum-checker/src/lib.rs CHANGED
@@ -2275,7 +2275,13 @@ fn inferClassCallRaw(
2275
2275
  // separate class-construction path needed.
2276
2276
  Some(info) if !info.field_names.is_empty() => {
2277
2277
  let (owner, result, mut binds) = startVariant(info, &explicit, expected);
2278
+ // Closure-valued fields last (same two passes as `checkCallArgs`), so
2279
+ // `Num(add: |a, b| a + b, zero: 0)` learns `T = Int` from `zero` first.
2280
+ let (closures, others): (Vec<&ast::FieldArg>, Vec<&ast::FieldArg>) = call
2281
+ .fields
2282
+ .iter()
2283
+ .partition(|fa| matches!(fa.value, ast::Expr::Closure(_)));
2278
- for fa in &call.fields {
2284
+ for fa in others.into_iter().chain(closures) {
2279
2285
  match info.field_names.iter().position(|n| n == &fa.name) {
2280
2286
  Some(i) => {
2281
2287
  let pat = types::substitute(&info.field_types[i], &owner);
plum-examples/generics.plum CHANGED
@@ -69,13 +69,13 @@ fun nameOf(s: T) -> Str =
69
69
  # --- numeric -------------------------------------------------------------
70
70
 
71
71
  # "Trait values": there are no traits, so the behavior a generic needs is passed
72
- # explicitly (here the "Num dictionary" `add` + `zero` as two arguments; enum
72
+ # explicitly as a record of functions (a "Num dictionary"). `num.add(a, b)` calls
73
- # fields cannot hold `fn` types yet, so it is not bundled into a record).
73
+ # the closure stored in the `add` field (a method named `add` would win).
74
+ enum Num[T] =
75
+ | Num(add: fn(T, T) -> T, zero: T)
76
+
74
- fun sumWith(xs: List[T], zero: T, add: fn(T, T) -> T) -> T =
77
+ fun sumWith(xs: List[T], num: Num[T]) -> T =
75
- acc := zero
76
- for i := range xs.length()
77
- acc = add(acc, xs.get(i) ?? zero)
78
+ xs.reduce(num.zero, |acc, x| num.add(acc, x))
78
- acc
79
79
 
80
80
  test "Int extremes survive a generic fn"
81
81
  big := 9223372036854775807
@@ -220,6 +220,9 @@ test "== on T works for Int and Str"
220
220
  assert !same(9007199254740993, 9007199254740992)
221
221
 
222
222
  test "num_record_passed_explicitly"
223
+ intNum := Num(add: |a, b| a + b, zero: 0)
224
+ floatNum := Num(add: |a, b| a + b, zero: 0.0)
225
+ strNum := Num(add: |a, b| a + b, zero: "")
223
- assert sumWith(List(1, 2, 3), 0, |a, b| a + b) == 6
226
+ assert sumWith(List(1, 2, 3), intNum) == 6
227
+ assert sumWith(List(1.5, 2.5), floatNum) == 4.0
224
- assert sumWith(List("a", "b"), "", |a, b| a + b) == "ab"
228
+ assert sumWith(List("a", "b"), strNum) == "ab"
225
- assert sumWith(List(1.5, 2.5), 0.0, |a, b| a + b) == 4.0
plum-wasm-codegen/src/lib.rs CHANGED
@@ -551,6 +551,27 @@ fn instFields(info: &EnumVariantInfo, subject_ty: &PlumType) -> Vec<PlumType> {
551
551
  .collect()
552
552
  }
553
553
 
554
+ /// The `fn(...)` type of named field `field` on a value of type `obj_ty`, if it has one
555
+ /// (`add` in `enum Num[T] = | Num(add: fn(T, T) -> T, ...)`).
556
+ fn fnFieldType(obj_ty: &PlumType, field: &str, enum_variants: &EnumVariants) -> Option<PlumType> {
557
+ let enum_name = plum_checker::methodReceiverName(obj_ty)?;
558
+ enum_variants
559
+ .values()
560
+ .filter(|i| i.enum_name == enum_name)
561
+ .find_map(|i| {
562
+ let idx = i.field_names.iter().position(|n| n == field)?;
563
+ instFields(i, obj_ty).into_iter().nth(idx)
564
+ })
565
+ .filter(|t| matches!(t, PlumType::TFun(_, _)))
566
+ }
567
+
568
+ /// The synthetic local (reserved by `Collector`) that holds the closure read from an
569
+ /// fn-typed field for the call `rec.add(a, b)` at `call_expr` — `#` keeps it
570
+ /// disjoint from every source identifier.
571
+ fn fnFieldLocal(call_expr: &ast::Expr) -> String {
572
+ format!("fnfield#{}", call_expr as *const ast::Expr as usize)
573
+ }
574
+
554
575
  /// Import key of the `plum::crash(Str)` host function in `func_ids`.
555
576
  const CRASH_FN_KEY: &str = "__crash";
556
577
 
@@ -3698,6 +3719,12 @@ impl<'a> Collector<'a> {
3698
3719
  ast::Expr::Attribute(a) => {
3699
3720
  self.walkExpr(&a.object);
3700
3721
  if let ast::AttrKind::Method(call) = &a.attr {
3722
+ // A call through an fn-typed field needs a local to hold the
3723
+ // closure (read twice: env, then table index). Reserved even if
3724
+ // a same-named method wins at compile time (then left unused).
3725
+ if let Some(t) = fnFieldType(&self.ty(&a.object), &call.name, self.enum_variants) {
3726
+ self.bind(&fnFieldLocal(expr), t);
3727
+ }
3701
3728
  for arg in &call.args {
3702
3729
  self.walkArg(arg);
3703
3730
  }
@@ -4413,7 +4440,13 @@ fn exprHasResult(expr: &ast::Expr, ctx: &LocalCtx) -> bool {
4413
4440
  ctx.func_sigs
4414
4441
  .get(&key)
4415
4442
  .map(|s| s.ret.is_some())
4416
- .unwrap_or(true)
4443
+ .unwrap_or_else(|| {
4444
+ // A call through an fn-typed field (see `fnFieldLocal`).
4445
+ !matches!(
4446
+ ctx.local_decl_types.get(&fnFieldLocal(expr)),
4447
+ Some(PlumType::TFun(_, r)) if matches!(**r, PlumType::TUnit)
4448
+ )
4449
+ })
4417
4450
  } else {
4418
4451
  true
4419
4452
  }
@@ -6490,6 +6523,20 @@ fn compileMethodCall(
6490
6523
  }
6491
6524
  };
6492
6525
  let (dispatch_name, key, box_variant) = resolveMethodKey(&class_name, &call.name, ctx);
6526
+ // `rec.add(a, b)` with no method `add` but an fn-typed field `add`: read the
6527
+ // field's closure into its reserved local, then call it. A method wins.
6528
+ let field_local = fnFieldLocal(result_expr);
6529
+ if !ctx.func_ids.contains_key(&key) && ctx.locals.contains_key(&field_local) {
6530
+ compileFieldRead(object, &call.name, obj_ty, result_expr, body, ctx, state)?;
6531
+ Instruction::LocalSet(ctx.locals[&field_local]).encode(body);
6532
+ let field_call = ast::FnCall {
6533
+ name: field_local,
6534
+ args: call.args.clone(),
6535
+ line: call.line,
6536
+ column: call.column,
6537
+ };
6538
+ return compileClosureCall(&field_call, result_expr, body, ctx, state);
6539
+ }
6493
6540
  let func_idx = *ctx
6494
6541
  .func_ids
6495
6542
  .get(&key)