plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
ad8b044
— Peter John
2026-10-26T12:13:42+05:30
feat: duck-typed operators; README for Zig-style generics; results
- README.md +54 -3
- docs/plans/2026-10-26-zig-generics-and-numbers.md +21 -0
- plum-checker/src/lib.rs +100 -1
- plum-checker/tests/checker_tests.rs +12 -0
- plum-examples/generics.plum +14 -0
- plum-examples/operators.plum +68 -0
- plum-wasm-codegen/src/lib.rs +33 -0
README.md
CHANGED
|
@@ -127,6 +127,32 @@ negated = -sum
|
|
|
127
127
|
inverted = !cmp
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### Operators on your own types
|
|
131
|
+
|
|
132
|
+
On `Int`, `Float`, `Byte` and `Str` operators are built in. On any other (declared) type, an operator calls a method on the left operand, resolved statically per instantiation:
|
|
133
|
+
|
|
134
|
+
| Operator | Method |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `a + b`, `a - b`, `a * b`, `a / b` | `a.add(b)`, `sub`, `mul`, `div` |
|
|
137
|
+
| `-a` | `a.neg()` |
|
|
138
|
+
| `a == b`, `a != b` | `a.eq(b)`, `!a.eq(b)` |
|
|
139
|
+
| `a < b`, `a <= b`, `a > b`, `a >= b` | `a.lt(b)`, `a.le(b)`, `a.gt(b)`, `a.ge(b)` |
|
|
140
|
+
|
|
141
|
+
Each operator needs its own method (no derivation from `lt`/`eq`). The right operand may have a different type: `v * 2.0` calls `mul(self, s: Float)`. Using an operator without the method is an error such as `Vec2 has no operator '+' (define add)`, with the instantiation chain when it happens inside a generic. `==`/`!=` on a type with no `eq` keep their built-in meaning (variant identity), and tag-union `==` is unchanged.
|
|
142
|
+
|
|
143
|
+
```plum
|
|
144
|
+
enum Vec2 =
|
|
145
|
+
| Vec2(x: Float, y: Float)
|
|
146
|
+
|
|
147
|
+
fun add(self, o: Vec2) -> Vec2 =
|
|
148
|
+
Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
149
|
+
|
|
150
|
+
fun mul(self, s: Float) -> Vec2 =
|
|
151
|
+
Vec2(x: self.x * s, y: self.y * s)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
See [`plum-examples/operators.plum`](plum-examples/operators.plum).
|
|
155
|
+
|
|
130
156
|
## Variables and assignment
|
|
131
157
|
|
|
132
158
|
```plum
|
|
@@ -241,9 +267,34 @@ Generic types, methods, and free functions are duck-typed and compiled once per
|
|
|
241
267
|
|
|
242
268
|
### How generics compile
|
|
243
269
|
|
|
244
|
-
Generics are duck-typed, as in Zig. A generic body is type-checked once per concrete instantiation, and each instantiation gets its own compiled copy with unboxed values
|
|
270
|
+
Generics are duck-typed and compiled per type, as in Zig. A generic body is type-checked once per concrete instantiation, and each instantiation gets its own compiled copy with unboxed values: `List[Int]` and `List[Str]` are different code, and a `List[Float]` stores raw `f64`s.
|
|
271
|
+
|
|
272
|
+
- **No bounds, no traits.** `[T]` carries no constraint. `fun show(x: T) -> Str = x.toStr()` works for every type that has a `toStr` method and fails at the use site otherwise. The old `trait`/`implements` syntax and `[T: X]` bounds are a parse error with a migration hint.
|
|
273
|
+
- **Errors point at the use site.** A missing method is reported against the concrete type, with the chain of instantiations that led there:
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
in outer[Plain] (called at main.plum:9:3):
|
|
277
|
+
in maxOf[Plain] (called at main.plum:5:3):
|
|
278
|
+
Plain has no method 'lt'
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
- **Unreachable generics are not emitted.** A generic that no root (a non-generic fn or a test) reaches is neither checked per type nor compiled.
|
|
282
|
+
- **Typed arrays are native.** Each instantiation stores its elements in an array of its own element type, with no boxing at generic edges.
|
|
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
|
+
|
|
285
|
+
```plum
|
|
286
|
+
fun sumWith(xs: List[T], zero: T, add: fn(T, T) -> T) -> T =
|
|
287
|
+
acc := zero
|
|
288
|
+
for i := range xs.length()
|
|
289
|
+
acc = add(acc, xs.get(i) ?? zero)
|
|
290
|
+
acc
|
|
291
|
+
|
|
292
|
+
sumWith(List(1, 2, 3), 0, |a, b| a + b) # 6
|
|
293
|
+
sumWith(List("a", "b"), "", |a, b| a + b) # "ab"
|
|
294
|
+
```
|
|
295
|
+
|
|
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.)
|
|
245
297
|
|
|
246
|
-
- **No bounds, no traits.** `[T]` carries no constraint. `fun show(x: T) -> Str = x.toStr()` works for every type that has a `toStr` method and fails at the call site otherwise. The old `trait` and `implements` syntax and `[T: X]` bounds were removed and now give a parse error with a migration hint.
|
|
247
298
|
- **Mixed types are enums.** To hold several kinds of value in one list, declare an enum and `match self`:
|
|
248
299
|
|
|
249
300
|
```plum
|
|
@@ -350,7 +401,7 @@ fun greet(name: Str) -> Str =
|
|
|
350
401
|
extern fun printLn(s: Str)
|
|
351
402
|
```
|
|
352
403
|
|
|
353
|
-
`extern fun` declares a function backed by a host-provided wasm import, with no Plum body. `plum-std/Os.plum` declares `printLn` for use via `import std/os`.
|
|
404
|
+
`extern fun` declares a function backed by a host-provided wasm import, with no Plum body. At the boundary `Int` is narrowed to a wasm `i32` (the rest of the language uses 64-bit `Int`), so extern parameters and results are 32-bit. `plum-std/Os.plum` declares `printLn` for use via `import std/os`.
|
|
354
405
|
|
|
355
406
|
```sh
|
|
356
407
|
$ plum run plum-examples/io.plum
|
docs/plans/2026-10-26-zig-generics-and-numbers.md
CHANGED
|
@@ -128,3 +128,24 @@ Migrate every example, test and README snippet off traits. Update the README sec
|
|
|
128
128
|
## Open questions
|
|
129
129
|
|
|
130
130
|
- Will the user's further ideas change scope or order? This is still a draft.
|
|
131
|
+
|
|
132
|
+
## Results
|
|
133
|
+
|
|
134
|
+
T5 (duck-typed operators) and T6 (docs) are done. `a + b`, `-a`, `==` and `<` on a non-primitive left operand now call `add`, `neg`, `eq` and `lt`. Each of `sub mul div le gt ge` is its own method with no derivation from `lt`/`eq`, and the right operand may have a different type. A missing method is `<Type> has no operator '+' (define add)`, with the instantiation chain inside generics. `==`/`!=` on a type with no `eq` keep their built-in meaning, because existing code compares enum variants that way. Tag-union `==` is unchanged.
|
|
135
|
+
|
|
136
|
+
Tests: `cargo test --workspace` has 0 failures and `plum test plum-std plum-examples` ran 2321 passed, 0 failed (baseline 2252). New tests are in `plum-examples/operators.plum` (`vec2_add_operator`, `mul_scalar_rhs`, `neg`, `eq_and_neq`, `lt_ordering`), the checker test `operator_missing_method_error`, and `num_record_passed_explicitly` in `generics.plum`. `sumWith` takes `zero` and `add` as separate arguments, because enum fields cannot hold `fn` types yet.
|
|
137
|
+
|
|
138
|
+
Compile time (release bench, best of 5, from the repo root, over `plum-examples/*.plum` and `plum-std/*.plum`; load + check + codegen):
|
|
139
|
+
|
|
140
|
+
| | typical | max | wasm size |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| per-type copies (now) | 31-37 ms | 53.0 ms (`dop_visitor`; `oop_visitor` 52.4, `Os` 50.8) | 28.8-42.1 KB (typical about 29-30 KB) |
|
|
143
|
+
| erased baseline | 20-25 ms | not recorded | Bool.wasm 19 KB |
|
|
144
|
+
|
|
145
|
+
So compile time is about 1.4x the erased baseline and the smallest wasm is about 1.5x larger. Part of that is the always-loaded std prelude: even a near-empty file costs 31 ms and 28.8 KB, and the examples add only 0-20 ms on top of that.
|
|
146
|
+
|
|
147
|
+
LOC since b7a5ee8 (excluding generated parser files): 62 files changed, 2509 insertions(+), 2721 deletions(-). That is 212 fewer lines, even with the per-instantiation machinery added.
|
|
148
|
+
|
|
149
|
+
Known gaps:
|
|
150
|
+
- A closure that calls an `fn`-typed parameter fails with `unknown function`; this was found while writing `sumWith`.
|
|
151
|
+
- An enum field cannot have an `fn` type.
|
plum-checker/src/lib.rs
CHANGED
|
@@ -31,6 +31,9 @@ thread_local! {
|
|
|
31
31
|
/// generic arguments are pinned like a directly-returned expression's.
|
|
32
32
|
static TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
|
|
33
33
|
static MATCH_TAIL: RefCell<Option<PlumType>> = const { RefCell::new(None) };
|
|
34
|
+
/// Operator expressions (address) on non-primitive operands -> the synthetic method call
|
|
35
|
+
/// (`a + b` -> `a.add(b)`) they resolve to. Kept so the cloned arguments have stable addresses.
|
|
36
|
+
static OP_CALLS: RefCell<HashMap<usize, std::rc::Rc<ast::FnCall>>> = RefCell::new(HashMap::new());
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/// A call to a generic fn/method seen in some body: callee identity plus the
|
|
@@ -100,6 +103,9 @@ pub struct TypedInfo {
|
|
|
100
103
|
/// the instantiation it calls. `implicit` marks calls codegen makes itself (the
|
|
101
104
|
/// `toStr` of a `"{x}"` interpolation, keyed by the interpolated expression).
|
|
102
105
|
pub call_inst: HashMap<(Option<usize>, usize, bool), usize>,
|
|
106
|
+
/// Operator expression address -> the method call it desugars to (`a + b` -> `a.add(b)`;
|
|
107
|
+
/// the receiver is the operator's own left operand, `args` holds the right one).
|
|
108
|
+
pub op_calls: HashMap<usize, std::rc::Rc<ast::FnCall>>,
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/// One reached instantiation of a generic fn or method of a generic type.
|
|
@@ -1003,7 +1009,9 @@ fn checkSourceFull(source: &ast::Source) -> (CheckResult<()>, TypedInfo) {
|
|
|
1003
1009
|
RECORD.with(|r| *r.borrow_mut() = Some(HashMap::new()));
|
|
1004
1010
|
RECORD_BINDS.with(|r| *r.borrow_mut() = Some(Binds::new()));
|
|
1005
1011
|
CALLS.with(|r| *r.borrow_mut() = Some(Vec::new()));
|
|
1012
|
+
OP_CALLS.with(|r| r.borrow_mut().clear());
|
|
1006
1013
|
let (result, mut info) = checkSourceInner(source);
|
|
1014
|
+
info.op_calls = OP_CALLS.with(|r| std::mem::take(&mut *r.borrow_mut()));
|
|
1007
1015
|
let mut recorded = RECORD.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1008
1016
|
let binds = RECORD_BINDS.with(|r| r.borrow_mut().take()).unwrap_or_default();
|
|
1009
1017
|
CALLS.with(|r| r.borrow_mut().take());
|
|
@@ -2866,6 +2874,46 @@ fn expectedTagLiteral(
|
|
|
2866
2874
|
}
|
|
2867
2875
|
}
|
|
2868
2876
|
|
|
2877
|
+
/// True for an operand whose operators are user methods (`add`, `eq`, ...): a
|
|
2878
|
+
/// declared class/enum, not a primitive, `Str`, `Bool` or tag union.
|
|
2879
|
+
pub fn isOperatorOperand(t: &PlumType) -> bool {
|
|
2880
|
+
matches!(t, PlumType::TNamed(n) | PlumType::TApp(n, _) if n != "Str" && n != "Int" && n != "Float" && n != "Bool" && n != "Byte")
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
/// Resolves operator `sym` on a non-primitive left operand to `left.method(right)`,
|
|
2884
|
+
/// recording the synthetic call (see `TypedInfo::op_calls`) and its instantiation.
|
|
2885
|
+
#[allow(clippy::too_many_arguments)]
|
|
2886
|
+
fn inferOperatorCall(
|
|
2887
|
+
expr: &ast::Expr,
|
|
2888
|
+
lt: &PlumType,
|
|
2889
|
+
sym: &str,
|
|
2890
|
+
method: &str,
|
|
2891
|
+
right: Option<&ast::Expr>,
|
|
2892
|
+
env: &TypeEnv,
|
|
2893
|
+
ctx: &CheckCtx,
|
|
2894
|
+
expected: Option<&PlumType>,
|
|
2895
|
+
) -> Result<PlumType, String> {
|
|
2896
|
+
let class = methodReceiverName(lt).unwrap_or_default();
|
|
2897
|
+
if !ctx.methods.contains_key(&(class, method.to_string())) {
|
|
2898
|
+
return Err(format!("{} has no operator '{}' (define {})", lt, sym, method));
|
|
2899
|
+
}
|
|
2900
|
+
let addr = expr as *const ast::Expr as usize;
|
|
2901
|
+
let call = OP_CALLS.with(|m| {
|
|
2902
|
+
m.borrow_mut()
|
|
2903
|
+
.entry(addr)
|
|
2904
|
+
.or_insert_with(|| {
|
|
2905
|
+
std::rc::Rc::new(ast::FnCall {
|
|
2906
|
+
name: method.to_string(),
|
|
2907
|
+
args: right.iter().map(|r| ast::Arg::Positional((*r).clone())).collect(),
|
|
2908
|
+
line: 0,
|
|
2909
|
+
column: 0,
|
|
2910
|
+
})
|
|
2911
|
+
})
|
|
2912
|
+
.clone()
|
|
2913
|
+
});
|
|
2914
|
+
inferMethodCall(lt, method, &call.args, env, ctx, expected)
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2869
2917
|
pub fn inferExpr(expr: &ast::Expr, env: &TypeEnv, ctx: &CheckCtx) -> Result<PlumType, String> {
|
|
2870
2918
|
inferExprWith(expr, env, ctx, None)
|
|
2871
2919
|
}
|
|
@@ -3091,10 +3139,38 @@ fn inferExprInner(
|
|
|
3091
3139
|
unify(&PlumType::TBool, &t, ctx)?;
|
|
3092
3140
|
Ok(PlumType::TBool)
|
|
3093
3141
|
}
|
|
3142
|
+
ast::Expr::Unary(u) => {
|
|
3094
|
-
|
|
3143
|
+
let t = inferExpr(&u.operand, env, ctx)?;
|
|
3144
|
+
if matches!(u.op, ast::UnOp::Neg) {
|
|
3145
|
+
if matches!(t, PlumType::TParam(_)) {
|
|
3146
|
+
return Ok(PlumType::TVar("_".to_string()));
|
|
3147
|
+
}
|
|
3148
|
+
if isOperatorOperand(&t) {
|
|
3149
|
+
return inferOperatorCall(expr, &t, "-", "neg", None, env, ctx, expected);
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
Ok(t)
|
|
3153
|
+
}
|
|
3095
3154
|
ast::Expr::Binary(b) => {
|
|
3096
3155
|
let lt = inferExpr(&b.left, env, ctx)?;
|
|
3097
3156
|
let rt = inferExpr(&b.right, env, ctx)?;
|
|
3157
|
+
let opn = match b.op {
|
|
3158
|
+
ast::BinOp::Add => Some(("+", "add")),
|
|
3159
|
+
ast::BinOp::Sub => Some(("-", "sub")),
|
|
3160
|
+
ast::BinOp::Mul => Some(("*", "mul")),
|
|
3161
|
+
ast::BinOp::Div => Some(("/", "div")),
|
|
3162
|
+
_ => None,
|
|
3163
|
+
};
|
|
3164
|
+
if let Some((sym, method)) = opn {
|
|
3165
|
+
if matches!(lt, PlumType::TParam(_)) && lt != rt {
|
|
3166
|
+
return Ok(PlumType::TVar("_".to_string()));
|
|
3167
|
+
}
|
|
3168
|
+
if isOperatorOperand(<) {
|
|
3169
|
+
return inferOperatorCall(
|
|
3170
|
+
expr, <, sym, method, Some(&b.right), env, ctx, expected,
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3098
3174
|
unify(<, &rt, ctx).map_err(|e| format!("binary op: {}", e))?;
|
|
3099
3175
|
bindAcross(<, &rt);
|
|
3100
3176
|
if matches!(lt, PlumType::TVar(_)) && !matches!(rt, PlumType::TVar(_)) {
|
|
@@ -3122,6 +3198,29 @@ fn inferExprInner(
|
|
|
3122
3198
|
}
|
|
3123
3199
|
return Ok(PlumType::TBool);
|
|
3124
3200
|
}
|
|
3201
|
+
let opn = match c.op {
|
|
3202
|
+
ast::CmpOp::Eq => Some(("==", "eq")),
|
|
3203
|
+
ast::CmpOp::Neq | ast::CmpOp::NotEq2 => Some(("!=", "eq")),
|
|
3204
|
+
ast::CmpOp::Lt => Some(("<", "lt")),
|
|
3205
|
+
ast::CmpOp::Lte => Some(("<=", "le")),
|
|
3206
|
+
ast::CmpOp::Gt => Some((">", "gt")),
|
|
3207
|
+
ast::CmpOp::Gte => Some((">=", "ge")),
|
|
3208
|
+
};
|
|
3209
|
+
if let Some((sym, method)) = opn {
|
|
3210
|
+
if matches!(lt, PlumType::TParam(_)) && lt != rt {
|
|
3211
|
+
return Ok(PlumType::TBool);
|
|
3212
|
+
}
|
|
3213
|
+
// `==`/`!=` keep their built-in meaning (variant/reference identity) on a
|
|
3214
|
+
// type that defines no `eq`; the ordering operators have no fallback.
|
|
3215
|
+
let has_eq = methodReceiverName(<)
|
|
3216
|
+
.is_some_and(|cl| ctx.methods.contains_key(&(cl, "eq".to_string())));
|
|
3217
|
+
if isOperatorOperand(<) && (method != "eq" || has_eq) {
|
|
3218
|
+
let r = inferOperatorCall(expr, <, sym, method, Some(&c.right), env, ctx, None)?;
|
|
3219
|
+
unify(&PlumType::TBool, &r, ctx)
|
|
3220
|
+
.map_err(|e| format!("operator '{}' ({}) must return Bool: {}", sym, method, e))?;
|
|
3221
|
+
return Ok(PlumType::TBool);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3125
3224
|
unify(<, &rt, ctx).map_err(|e| format!("compare op: {}", e))?;
|
|
3126
3225
|
bindAcross(<, &rt);
|
|
3127
3226
|
Ok(PlumType::TBool)
|
plum-checker/tests/checker_tests.rs
CHANGED
|
@@ -1889,6 +1889,18 @@ fn nested_instantiation_chain() {
|
|
|
1889
1889
|
assert!(msg.contains("Plain has no method 'lt'"), "{}", msg);
|
|
1890
1890
|
}
|
|
1891
1891
|
|
|
1892
|
+
#[test]
|
|
1893
|
+
fn operator_missing_method_error() {
|
|
1894
|
+
let src = format!(
|
|
1895
|
+
"{}fun twice(a: T) -> T =\n a + a\n\nfun main() -> Plain =\n twice(Plain(v: 1))\n",
|
|
1896
|
+
DUCK_DEFS
|
|
1897
|
+
);
|
|
1898
|
+
let msg = errText(checkSource(&parseClean(&src)));
|
|
1899
|
+
assert!(msg.contains("in twice[Plain]"), "{}", msg);
|
|
1900
|
+
assert!(msg.contains("called at"), "{}", msg);
|
|
1901
|
+
assert!(msg.contains("Plain has no operator '+' (define add)"), "{}", msg);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1892
1904
|
#[test]
|
|
1893
1905
|
fn instantiation_set_listed() {
|
|
1894
1906
|
let src = format!(
|
plum-examples/generics.plum
CHANGED
|
@@ -68,6 +68,15 @@ fun nameOf(s: T) -> Str =
|
|
|
68
68
|
|
|
69
69
|
# --- numeric -------------------------------------------------------------
|
|
70
70
|
|
|
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
|
|
73
|
+
# fields cannot hold `fn` types yet, so it is not bundled into a record).
|
|
74
|
+
fun sumWith(xs: List[T], zero: T, add: fn(T, T) -> T) -> T =
|
|
75
|
+
acc := zero
|
|
76
|
+
for i := range xs.length()
|
|
77
|
+
acc = add(acc, xs.get(i) ?? zero)
|
|
78
|
+
acc
|
|
79
|
+
|
|
71
80
|
test "Int extremes survive a generic fn"
|
|
72
81
|
big := 9223372036854775807
|
|
73
82
|
small := 0 - 9223372036854775807 - 1
|
|
@@ -209,3 +218,8 @@ test "== on T works for Int and Str"
|
|
|
209
218
|
assert !same("a", "b")
|
|
210
219
|
assert same(9007199254740993, 9007199254740993)
|
|
211
220
|
assert !same(9007199254740993, 9007199254740992)
|
|
221
|
+
|
|
222
|
+
test "num_record_passed_explicitly"
|
|
223
|
+
assert sumWith(List(1, 2, 3), 0, |a, b| a + b) == 6
|
|
224
|
+
assert sumWith(List("a", "b"), "", |a, b| a + b) == "ab"
|
|
225
|
+
assert sumWith(List(1.5, 2.5), 0.0, |a, b| a + b) == 4.0
|
plum-examples/operators.plum
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module operators
|
|
2
|
+
import std/Str
|
|
3
|
+
import std/Number
|
|
4
|
+
|
|
5
|
+
# Operators on non-primitive operands call methods: `a + b` is `a.add(b)`,
|
|
6
|
+
# `-a` is `a.neg()`, `a == b` is `a.eq(b)`, `a < b` is `a.lt(b)`.
|
|
7
|
+
enum Vec2 =
|
|
8
|
+
| Vec2(x: Float, y: Float)
|
|
9
|
+
|
|
10
|
+
fun toStr(self) -> Str =
|
|
11
|
+
"Vec2({self.x}, {self.y})"
|
|
12
|
+
|
|
13
|
+
fun add(self, o: Vec2) -> Vec2 =
|
|
14
|
+
Vec2(x: self.x + o.x, y: self.y + o.y)
|
|
15
|
+
|
|
16
|
+
fun mul(self, s: Float) -> Vec2 =
|
|
17
|
+
Vec2(x: self.x * s, y: self.y * s)
|
|
18
|
+
|
|
19
|
+
fun neg(self) -> Vec2 =
|
|
20
|
+
Vec2(x: 0.0 - self.x, y: 0.0 - self.y)
|
|
21
|
+
|
|
22
|
+
fun eq(self, o: Vec2) -> Bool =
|
|
23
|
+
self.x == o.x && self.y == o.y
|
|
24
|
+
|
|
25
|
+
fun lt(self, o: Vec2) -> Bool =
|
|
26
|
+
self.x < o.x
|
|
27
|
+
|
|
28
|
+
fun le(self, o: Vec2) -> Bool =
|
|
29
|
+
self.x <= o.x
|
|
30
|
+
|
|
31
|
+
enum Plain =
|
|
32
|
+
| Plain(v: Int)
|
|
33
|
+
|
|
34
|
+
fun double(a: T) -> T =
|
|
35
|
+
a + a
|
|
36
|
+
|
|
37
|
+
test "vec2_add_operator"
|
|
38
|
+
r := Vec2(x: 1.0, y: 2.0) + Vec2(x: 3.0, y: 4.0)
|
|
39
|
+
assert r.x == 4.0
|
|
40
|
+
assert r.y == 6.0
|
|
41
|
+
|
|
42
|
+
test "mul_scalar_rhs"
|
|
43
|
+
r := Vec2(x: 1.0, y: 2.0) * 2.0
|
|
44
|
+
assert r.x == 2.0
|
|
45
|
+
assert r.y == 4.0
|
|
46
|
+
|
|
47
|
+
test "neg"
|
|
48
|
+
r := -Vec2(x: 1.0, y: -2.0)
|
|
49
|
+
assert r.x == -1.0
|
|
50
|
+
assert r.y == 2.0
|
|
51
|
+
|
|
52
|
+
test "eq_and_neq"
|
|
53
|
+
a := Vec2(x: 1.0, y: 2.0)
|
|
54
|
+
assert a == Vec2(x: 1.0, y: 2.0)
|
|
55
|
+
assert a != Vec2(x: 1.0, y: 3.0)
|
|
56
|
+
assert !{a == Vec2(x: 0.0, y: 0.0)}
|
|
57
|
+
|
|
58
|
+
test "lt_ordering"
|
|
59
|
+
a := Vec2(x: 1.0, y: 0.0)
|
|
60
|
+
b := Vec2(x: 2.0, y: 0.0)
|
|
61
|
+
assert a < b
|
|
62
|
+
assert !{b < a}
|
|
63
|
+
assert a <= b
|
|
64
|
+
|
|
65
|
+
test "operators work through a generic"
|
|
66
|
+
d := double(Vec2(x: 1.0, y: 1.0))
|
|
67
|
+
assert d.x == 2.0
|
|
68
|
+
assert double(4) == 8
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -379,6 +379,8 @@ pub struct CompileCtx<'a> {
|
|
|
379
379
|
pub inst_funcs: HashMap<usize, (u32, FuncSig)>,
|
|
380
380
|
/// See `plum_checker::TypedInfo::call_inst`.
|
|
381
381
|
pub call_inst: HashMap<(Option<usize>, usize, bool), usize>,
|
|
382
|
+
/// See `plum_checker::TypedInfo::op_calls`.
|
|
383
|
+
pub op_calls: HashMap<usize, std::rc::Rc<ast::FnCall>>,
|
|
382
384
|
/// Every free (non-method) fn name, generic or not.
|
|
383
385
|
pub known_fns: HashSet<String>,
|
|
384
386
|
pub eg: EnumGenerics,
|
|
@@ -450,6 +452,7 @@ struct LocalCtx<'a> {
|
|
|
450
452
|
func_sigs: &'a HashMap<String, FuncSig>,
|
|
451
453
|
inst_funcs: &'a HashMap<usize, (u32, FuncSig)>,
|
|
452
454
|
call_inst: &'a HashMap<(Option<usize>, usize, bool), usize>,
|
|
455
|
+
op_calls: &'a HashMap<usize, std::rc::Rc<ast::FnCall>>,
|
|
453
456
|
known_fns: &'a HashSet<String>,
|
|
454
457
|
/// Index of the unit being compiled (closures are keyed by it) and its instantiation.
|
|
455
458
|
unit_idx: usize,
|
|
@@ -1641,6 +1644,7 @@ fn compileSourceInner(
|
|
|
1641
1644
|
instantiations,
|
|
1642
1645
|
inst_expr_types,
|
|
1643
1646
|
call_inst,
|
|
1647
|
+
op_calls,
|
|
1644
1648
|
..
|
|
1645
1649
|
} = typed;
|
|
1646
1650
|
// Per-instantiation expression types, split out by instantiation id.
|
|
@@ -2157,6 +2161,7 @@ fn compileSourceInner(
|
|
|
2157
2161
|
func_sigs,
|
|
2158
2162
|
inst_funcs,
|
|
2159
2163
|
call_inst,
|
|
2164
|
+
op_calls,
|
|
2160
2165
|
known_fns,
|
|
2161
2166
|
eg,
|
|
2162
2167
|
methods,
|
|
@@ -3807,6 +3812,7 @@ fn compileFnBody(
|
|
|
3807
3812
|
func_sigs: &ctx.func_sigs,
|
|
3808
3813
|
inst_funcs: &ctx.inst_funcs,
|
|
3809
3814
|
call_inst: &ctx.call_inst,
|
|
3815
|
+
op_calls: &ctx.op_calls,
|
|
3810
3816
|
known_fns: &ctx.known_fns,
|
|
3811
3817
|
unit_idx: unit.idx,
|
|
3812
3818
|
unit_inst: unit.inst,
|
|
@@ -5498,6 +5504,14 @@ fn plumTypeFromValtypeHint(vt: ValType) -> PlumType {
|
|
|
5498
5504
|
}
|
|
5499
5505
|
}
|
|
5500
5506
|
|
|
5507
|
+
/// An operator on a non-primitive operand compiled as its desugared method call. The
|
|
5508
|
+
/// checker's `op_calls` entry is shared by every instantiation of a generic body, so
|
|
5509
|
+
/// the operand's type in THIS unit decides.
|
|
5510
|
+
fn isOpCall(expr: &ast::Expr, operand: &ast::Expr, ctx: &LocalCtx) -> bool {
|
|
5511
|
+
ctx.op_calls.contains_key(&(expr as *const ast::Expr as usize))
|
|
5512
|
+
&& plum_checker::isOperatorOperand(&inferLocalType(operand, ctx))
|
|
5513
|
+
}
|
|
5514
|
+
|
|
5501
5515
|
fn compileExpr(
|
|
5502
5516
|
expr: &ast::Expr,
|
|
5503
5517
|
body: &mut Vec<u8>,
|
|
@@ -5695,6 +5709,24 @@ fn compileExpr(
|
|
|
5695
5709
|
}
|
|
5696
5710
|
Instruction::End.encode(body);
|
|
5697
5711
|
}
|
|
5712
|
+
ast::Expr::Unary(u) if isOpCall(expr, &u.operand, ctx) => {
|
|
5713
|
+
let call = &ctx.op_calls[&(expr as *const ast::Expr as usize)];
|
|
5714
|
+
let ty = inferLocalType(&u.operand, ctx);
|
|
5715
|
+
compileMethodCall(&u.operand, call, &ty, expr, false, body, ctx, state)?;
|
|
5716
|
+
}
|
|
5717
|
+
ast::Expr::Binary(b) if isOpCall(expr, &b.left, ctx) => {
|
|
5718
|
+
let call = &ctx.op_calls[&(expr as *const ast::Expr as usize)];
|
|
5719
|
+
let ty = inferLocalType(&b.left, ctx);
|
|
5720
|
+
compileMethodCall(&b.left, call, &ty, expr, false, body, ctx, state)?;
|
|
5721
|
+
}
|
|
5722
|
+
ast::Expr::Compare(c) if isOpCall(expr, &c.left, ctx) => {
|
|
5723
|
+
let call = &ctx.op_calls[&(expr as *const ast::Expr as usize)];
|
|
5724
|
+
let ty = inferLocalType(&c.left, ctx);
|
|
5725
|
+
compileMethodCall(&c.left, call, &ty, expr, false, body, ctx, state)?;
|
|
5726
|
+
if matches!(c.op, ast::CmpOp::Neq | ast::CmpOp::NotEq2) {
|
|
5727
|
+
Instruction::I32Eqz.encode(body);
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
5698
5730
|
ast::Expr::Unary(u) => match u.op {
|
|
5699
5731
|
ast::UnOp::Neg => {
|
|
5700
5732
|
if matches!(inferLocalType(&u.operand, ctx), PlumType::TFloat) {
|
|
@@ -6885,6 +6917,7 @@ fn compileClosureBody(
|
|
|
6885
6917
|
func_sigs: &ctx.func_sigs,
|
|
6886
6918
|
inst_funcs: &ctx.inst_funcs,
|
|
6887
6919
|
call_inst: &ctx.call_inst,
|
|
6920
|
+
op_calls: &ctx.op_calls,
|
|
6888
6921
|
known_fns: &ctx.known_fns,
|
|
6889
6922
|
unit_idx: unit.idx,
|
|
6890
6923
|
unit_inst: unit.inst,
|