plum
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 +10 -8
- plum-checker/src/lib.rs +7 -1
- plum-examples/generics.plum +13 -10
- plum-wasm-codegen/src/lib.rs +48 -1
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
|
-
|
|
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),
|
|
294
|
+
sumWith(List(1, 2, 3), intNum) # 6
|
|
293
|
-
sumWith(List(
|
|
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).
|
|
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
|
|
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
|
|
72
|
+
# explicitly as a record of functions (a "Num dictionary"). `num.add(a, b)` calls
|
|
73
|
-
#
|
|
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],
|
|
77
|
+
fun sumWith(xs: List[T], num: Num[T]) -> T =
|
|
75
|
-
acc := zero
|
|
76
|
-
for i := range xs.length()
|
|
77
|
-
|
|
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),
|
|
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"),
|
|
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
|
-
.
|
|
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)
|