plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
ff759b0
— Peter John
2026-10-27T17:48:08+05:30
chore: audit docs, comments and editor support against the current language
- README.md +21 -21
- plum-checker/src/lib.rs +5 -5
- plum-core/src/ast.rs +1 -1
- plum-core/src/forms.rs +1 -1
- plum-examples/dop_visitor.plum +4 -4
- plum-examples/numbers.plum +3 -7
- plum-examples/oop_visitor.plum +12 -13
- plum-lsp/src/completion.rs +11 -3
- plum-lsp/src/main.rs +26 -0
- plum-std/Json.plum +2 -4
- plum-std/List.plum +1 -1
- plum-std/Str.plum +3 -3
- plum-tooling/tree-sitter-plum/queries/plum/format.scm +3 -3
- plum-tooling/tree-sitter-plum/queries/plum/highlights.scm +12 -8
- plum-tooling/tree-sitter-plum/queries/plum/tags.scm +3 -0
- plum-tooling/tree-sitter-plum/test/aa.plum +19 -12
- plum-tooling/tree-sitter-plum/test/corpus/function.txt +2 -2
- plum-tooling/tree-sitter-plum/test/corpus/safe_attribute.txt +1 -1
- plum-tooling/tree-sitter-plum/test/corpus/try.txt +5 -14
- plum-tooling/tree-sitter-plum/test/highlight/sample.plum +4 -4
- plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json +1 -1
- plum-wasm-codegen/src/lib.rs +3 -10
README.md
CHANGED
|
@@ -51,7 +51,7 @@ Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` fo
|
|
|
51
51
|
```plum
|
|
52
52
|
# this is a comment
|
|
53
53
|
fun main() =
|
|
54
|
-
x = 1
|
|
54
|
+
x := 1
|
|
55
55
|
if x > 0
|
|
56
56
|
x = x + 1
|
|
57
57
|
```
|
|
@@ -71,7 +71,7 @@ fun main() =
|
|
|
71
71
|
```plum
|
|
72
72
|
module basics
|
|
73
73
|
|
|
74
|
-
import std/
|
|
74
|
+
import std/Os
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
## Constants
|
|
@@ -85,17 +85,17 @@ GREETING = "hello"
|
|
|
85
85
|
## Literals
|
|
86
86
|
|
|
87
87
|
```plum
|
|
88
|
-
dec = 42
|
|
88
|
+
dec := 42
|
|
89
|
-
hex = 0xFF
|
|
89
|
+
hex := 0xFF
|
|
90
|
-
bin = 0b1010
|
|
90
|
+
bin := 0b1010
|
|
91
|
-
big = 1_000_000
|
|
91
|
+
big := 1_000_000
|
|
92
|
-
flt = 3.14
|
|
92
|
+
flt := 3.14
|
|
93
|
-
flt2 = 12.0f
|
|
93
|
+
flt2 := 12.0f
|
|
94
|
-
exp = 6.022e23
|
|
94
|
+
exp := 6.022e23
|
|
95
|
-
name = "plum"
|
|
95
|
+
name := "plum"
|
|
96
|
-
escaped = "line one\nline two\ttabbed \"quoted\""
|
|
96
|
+
escaped := "line one\nline two\ttabbed \"quoted\""
|
|
97
|
-
yes = true
|
|
97
|
+
yes := true
|
|
98
|
-
no = false
|
|
98
|
+
no := false
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
## Operators
|
|
@@ -122,10 +122,10 @@ From tightest to loosest binding:
|
|
|
122
122
|
There's no range operator — every `for` is `for <binders> := range <source>`: `for i := range n` counts 0 to n-1, and the `Int` methods `a.until(b)` (a to b-1), `a.to(b)` (inclusive) and `a.downTo(b)` give other ranges (`for i := range 1.to(10)`). `{ }` is used to group a sub-expression (not `( )`, which is reserved for calls):
|
|
123
123
|
|
|
124
124
|
```plum
|
|
125
|
-
grouped = {1 + 2} * {3 - 1}
|
|
125
|
+
grouped := {1 + 2} * {3 - 1}
|
|
126
|
-
picked = if cmp then sum else bits
|
|
126
|
+
picked := if cmp then sum else bits
|
|
127
|
-
negated = -sum
|
|
127
|
+
negated := -sum
|
|
128
|
-
inverted = !cmp
|
|
128
|
+
inverted := !cmp
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
### Operators on your own types
|
|
@@ -407,7 +407,7 @@ Generic types, methods, and free functions are duck-typed and compiled once per
|
|
|
407
407
|
|
|
408
408
|
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.
|
|
409
409
|
|
|
410
|
-
- **No bounds
|
|
410
|
+
- **No bounds.** `[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.
|
|
411
411
|
- **Errors point at the use site.** A missing method is reported against the concrete type, with the chain of instantiations that led there:
|
|
412
412
|
|
|
413
413
|
```
|
|
@@ -418,7 +418,7 @@ in outer[Plain] (called at main.plum:9:3):
|
|
|
418
418
|
|
|
419
419
|
- **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.
|
|
420
420
|
- **Typed arrays are native.** Each instantiation stores its elements in an array of its own element type, with no boxing at generic edges.
|
|
421
|
-
- **Explicit
|
|
421
|
+
- **Explicit behavior 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:
|
|
422
422
|
|
|
423
423
|
```plum
|
|
424
424
|
enum Num[T] =
|
|
@@ -536,7 +536,7 @@ fun greet(name: Str) -> Str =
|
|
|
536
536
|
"Hello, {name}!"
|
|
537
537
|
```
|
|
538
538
|
|
|
539
|
-
`{expr}` interpolates a variable, literal, attribute access, `&&`/`||`/comparison
|
|
539
|
+
`{expr}` interpolates any expression: a variable, literal, attribute access, method call, `&&`/`||`/comparison or an inline `if`.
|
|
540
540
|
|
|
541
541
|
## `extern` functions and printing
|
|
542
542
|
|
|
@@ -544,7 +544,7 @@ fun greet(name: Str) -> Str =
|
|
|
544
544
|
extern fun printLn(s: Str)
|
|
545
545
|
```
|
|
546
546
|
|
|
547
|
-
`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/
|
|
547
|
+
`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`.
|
|
548
548
|
|
|
549
549
|
```sh
|
|
550
550
|
$ plum run plum-examples/io.plum
|
plum-checker/src/lib.rs
CHANGED
|
@@ -39,7 +39,7 @@ thread_local! {
|
|
|
39
39
|
static ERR_ACC: RefCell<BTreeSet<String>> = const { RefCell::new(BTreeSet::new()) };
|
|
40
40
|
/// Call edges `(caller key, callee key)` seen while collecting error sets.
|
|
41
41
|
static ERR_EDGES: RefCell<BTreeSet<(String, String)>> = const { RefCell::new(BTreeSet::new()) };
|
|
42
|
-
/// Error-union sites (`?`, `!`, `catch`
|
|
42
|
+
/// Error-union sites (`?`, `!`, `catch`) recorded for codegen; `None` while collecting.
|
|
43
43
|
static ERR_SITES: RefCell<Option<HashMap<usize, ErrSite>>> = const { RefCell::new(None) };
|
|
44
44
|
static OP_CALLS: RefCell<HashMap<usize, std::rc::Rc<ast::FnCall>>> = RefCell::new(HashMap::new());
|
|
45
45
|
}
|
|
@@ -138,7 +138,7 @@ pub struct TypedInfo {
|
|
|
138
138
|
/// The program with its `for` loops lowered (see `plum_core::forms`): the
|
|
139
139
|
/// source the types above belong to, and the one codegen must compile.
|
|
140
140
|
pub lowered: Option<std::rc::Rc<ast::Source>>,
|
|
141
|
-
/// `?` / `!` / `catch`
|
|
141
|
+
/// `?` / `!` / `catch` expression address -> its error-union facts.
|
|
142
142
|
pub err_sites: HashMap<usize, ErrSite>,
|
|
143
143
|
/// Final error set (sorted names) of every fn returning `!T`, keyed `"f"` / `"Recv.f"`.
|
|
144
144
|
pub fn_error_sets: BTreeMap<String, Vec<String>>,
|
|
@@ -459,7 +459,7 @@ pub fn variantFields(info: &EnumVariantInfo, ty: &PlumType) -> Vec<PlumType> {
|
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
/// `
|
|
462
|
+
/// `Just(3)`-style result type of a variant of enum `info.enum_name`.
|
|
463
463
|
fn enumTypeWith(info: &EnumVariantInfo, owner: &BTreeMap<String, PlumType>) -> PlumType {
|
|
464
464
|
if info.generics.is_empty() {
|
|
465
465
|
PlumType::TNamed(info.enum_name.clone())
|
|
@@ -1968,7 +1968,7 @@ fn checkFn(
|
|
|
1968
1968
|
.as_ref()
|
|
1969
1969
|
.map(|r| bindFnSet(plumTypeFromAstG(r), f))
|
|
1970
1970
|
.unwrap_or(PlumType::TUnit);
|
|
1971
|
-
// Reserved env key read by `?` (see `inferExpr`'s `Try` arm); closures remove it.
|
|
1971
|
+
// Reserved env key read by `?` (see `inferExpr`'s `Expr::Try` arm); closures remove it.
|
|
1972
1972
|
env.insert(RETURN_KEY.to_string(), TypeScheme::mono(declared_ret.clone()));
|
|
1973
1973
|
if f.is_star {
|
|
1974
1974
|
env.insert(STAR_KEY.to_string(), TypeScheme::mono(PlumType::TNamed(f.name.clone())));
|
|
@@ -2406,7 +2406,7 @@ fn patCatchAll(p: &ast::CasePattern) -> bool {
|
|
|
2406
2406
|
}
|
|
2407
2407
|
}
|
|
2408
2408
|
|
|
2409
|
-
/// A constructor pattern: `
|
|
2409
|
+
/// A constructor pattern: `Just(n)` / `Red` -> (name, sub-patterns).
|
|
2410
2410
|
fn patCtor(p: &ast::CasePattern) -> Option<(&str, &[ast::CasePattern])> {
|
|
2411
2411
|
match p {
|
|
2412
2412
|
ast::CasePattern::Class { name, fields } => Some((name.as_str(), fields.as_slice())),
|
plum-core/src/ast.rs
CHANGED
|
@@ -11,7 +11,7 @@ pub struct Source {
|
|
|
11
11
|
/// the end of the file.
|
|
12
12
|
pub topLevelOrder: Vec<TopLevelKind>,
|
|
13
13
|
/// Errors found while expanding `for` forms (see `forms.rs`): block escapes,
|
|
14
|
-
/// recursive
|
|
14
|
+
/// recursive `fun*`s, misuse of a `fun*`. The checker reports them.
|
|
15
15
|
pub formErrors: Vec<(usize, usize, String)>,
|
|
16
16
|
/// Every expanded for-form: (hygiene prefix of its renamed locals, form name,
|
|
17
17
|
/// line, column of the `for`), so checker errors can name the form.
|
plum-core/src/forms.rs
CHANGED
|
@@ -1720,7 +1720,7 @@ fn convertExpr(e: &mut Expr, uses: &mut YieldUses) {
|
|
|
1720
1720
|
}
|
|
1721
1721
|
|
|
1722
1722
|
/// Locals are function-wide and typed once, so two `match` arms that bind the
|
|
1723
|
-
/// same name (`
|
|
1723
|
+
/// same name (`Circle(v)` of an `Int` in one match, of a `Str` in the next) would
|
|
1724
1724
|
/// share one wasm local of the wrong type. Every pattern binder that was
|
|
1725
1725
|
/// already bound earlier in the fn is renamed (in its arm only) to a fresh
|
|
1726
1726
|
/// `name__N`.
|
plum-examples/dop_visitor.plum
CHANGED
|
@@ -21,18 +21,18 @@
|
|
|
21
21
|
# The checker's exhaustiveness check on enums stands in for the compile-time
|
|
22
22
|
# guarantee sealed interfaces + switch give in Java.
|
|
23
23
|
#
|
|
24
|
-
# DOP vs. OOP (see the `
|
|
24
|
+
# DOP vs. OOP (see the one-`enum`-per-shape rewrite in
|
|
25
25
|
# `oop_visitor.plum`):
|
|
26
26
|
# - Adding a new operation (a new function that matches on `Book`) touches
|
|
27
27
|
# one place here, and the exhaustiveness checker forces every variant to
|
|
28
28
|
# be handled. In the OOP version it means adding a new method to every
|
|
29
|
-
#
|
|
29
|
+
# shape enum, with nothing catching a variant you forgot to update.
|
|
30
30
|
# - Adding a new variant instead means touching every `match` on `Book`
|
|
31
31
|
# here (again caught by exhaustiveness if missed); the OOP version adds
|
|
32
|
-
# it in one new, self-contained `
|
|
32
|
+
# it in one new, self-contained `enum`, untouched elsewhere.
|
|
33
33
|
# - So DOP fits best when operations change often and the shapes are
|
|
34
34
|
# stable; OOP fits best when shapes multiply and operations are stable.
|
|
35
|
-
# plum has no dynamic dispatch through
|
|
35
|
+
# plum has no dynamic dispatch through an interface (see
|
|
36
36
|
# `oop_visitor.plum`'s header), which tilts things further toward DOP
|
|
37
37
|
# for anything that needs to treat the shapes polymorphically.
|
|
38
38
|
|
plum-examples/numbers.plum
CHANGED
|
@@ -95,11 +95,7 @@ test "string interpolation of a Float value works, via Number.toStr"
|
|
|
95
95
|
assert "computed is {y}" == "computed is 3.0"
|
|
96
96
|
|
|
97
97
|
test "assert of two directly-chained Float method calls renders a real failure message"
|
|
98
|
-
# Previously crashed the whole compile with "interpolating a Float value is
|
|
99
|
-
#
|
|
98
|
+
# `"{expr}"` interpolation of any Float-typed expression works (via
|
|
100
|
-
# only caught an OBVIOUS float literal on either side of the comparison,
|
|
101
|
-
# missing any Float-typed expression that wasn't written as one (like these
|
|
102
|
-
# chained method calls). Fixed at the root: `"{expr}"` interpolation of a
|
|
103
|
-
#
|
|
99
|
+
# `Number.toStr`), so the "Expected/Actual" failure message renders even for
|
|
104
|
-
#
|
|
100
|
+
# chained method calls like these.
|
|
105
101
|
assert 8.log2() == 8.0.log2()
|
plum-examples/oop_visitor.plum
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
# Classic OOP alternative to the DOP version in `dop_visitor.plum`: instead of
|
|
2
2
|
# one sealed `enum` + one exhaustive `match`, each book shape gets its own
|
|
3
|
+
# single-variant `enum` and defines its own `collectInterestingInfo` method —
|
|
3
|
-
#
|
|
4
|
+
# the direct translation of the blog post's Java (sealed interface implemented
|
|
4
|
-
# `collectInterestingInfo` method — the direct translation of the blog post's
|
|
5
|
-
#
|
|
5
|
+
# by records, each overriding a method).
|
|
6
6
|
# Based on: https://wimdetroyer.com/blog/visitor-pattern-in-dop
|
|
7
7
|
#
|
|
8
|
-
# One real limitation this example runs into: plum
|
|
8
|
+
# One real limitation this example runs into: plum has no interfaces and no
|
|
9
|
-
# compile-time structural check only ("does this type declare these
|
|
10
|
-
#
|
|
9
|
+
# runtime dispatch. Generics are duck-typed per instantiation, so a function
|
|
11
|
-
#
|
|
10
|
+
# can't take a bare `book: Book` parameter and call
|
|
12
11
|
# `book.collectInterestingInfo()` polymorphically across different concrete
|
|
13
12
|
# types, and there's no way to hold a heterogeneous list of `Book`s. So
|
|
14
13
|
# unlike the Java original (or the enum/match version), there's no single
|
|
@@ -16,16 +15,16 @@
|
|
|
16
15
|
# called directly at its own call site.
|
|
17
16
|
#
|
|
18
17
|
# OOP vs. DOP (see the `enum`/`match` version in `dop_visitor.plum`):
|
|
19
|
-
# - Adding a new variant (a new
|
|
18
|
+
# - Adding a new variant (a new single-variant `enum`) is self-contained
|
|
20
|
-
# and doesn't touch existing types. In the DOP version it means
|
|
19
|
+
# here and doesn't touch existing types. In the DOP version it means
|
|
21
|
-
# a new arm to every `match` on `Book` (caught by the checker's
|
|
20
|
+
# adding a new arm to every `match` on `Book` (caught by the checker's
|
|
22
21
|
# exhaustiveness check if missed).
|
|
23
|
-
# - Adding a new operation instead means adding a method to every
|
|
22
|
+
# - Adding a new operation instead means adding a method to every shape
|
|
24
|
-
# here, with nothing enforcing that you updated all of them; the DOP
|
|
23
|
+
# enum here, with nothing enforcing that you updated all of them; the DOP
|
|
25
24
|
# version adds one new function with one exhaustive `match`.
|
|
26
25
|
# - So OOP fits best when shapes multiply and operations are stable; DOP
|
|
27
26
|
# fits best when operations change often and the shapes are stable.
|
|
28
|
-
# plum's lack of dynamic dispatch through
|
|
27
|
+
# plum's lack of dynamic dispatch through an interface (see above)
|
|
29
28
|
# removes OOP's usual "call through one interface" benefit entirely,
|
|
30
29
|
# which is a real cost specific to this language.
|
|
31
30
|
|
plum-lsp/src/completion.rs
CHANGED
|
@@ -25,7 +25,7 @@ use plum_core::ast;
|
|
|
25
25
|
use tower_lsp::lsp_types::*;
|
|
26
26
|
|
|
27
27
|
const KEYWORDS: &[&str] = &[
|
|
28
|
-
"fun", "fun*", "yield", "crash", "error", "catch", "value", "enum", "extend", "import", "module", "extern", "if", "then", "else", "while", "for", "range", "match",
|
|
28
|
+
"fun", "fun*", "fn", "yield", "crash", "error", "catch", "value", "enum", "extend", "import", "module", "extern", "if", "then", "else", "while", "for", "range", "match",
|
|
29
29
|
"when", "break", "continue", "return", "todo", "assert", "test", "self", "true",
|
|
30
30
|
"false",
|
|
31
31
|
];
|
|
@@ -285,9 +285,17 @@ pub fn memberItems(
|
|
|
285
285
|
return Vec::new();
|
|
286
286
|
};
|
|
287
287
|
|
|
288
|
+
// A primitive's methods may live on the "union" enum that bare-wraps it
|
|
289
|
+
// (`Int` / `Float` -> `Number`), exactly as the checker's method fallback.
|
|
288
|
-
let mut
|
|
290
|
+
let mut owners = vec![recv_name.clone()];
|
|
291
|
+
if let Some(info) = ctx.enum_variants.get(recv_name.as_str()) {
|
|
292
|
+
if info.enum_name != recv_name {
|
|
293
|
+
owners.push(info.enum_name.to_string());
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
let mut items: Vec<CompletionItem> = Vec::new();
|
|
289
297
|
for ((r, name), fn_ty) in ctx.methods.iter() {
|
|
290
|
-
if r != &recv_name {
|
|
298
|
+
if !owners.contains(r) || (r != &recv_name && items.iter().any(|i| i.label == *name)) {
|
|
291
299
|
continue;
|
|
292
300
|
}
|
|
293
301
|
let detail = match fn_ty {
|
plum-lsp/src/main.rs
CHANGED
|
@@ -432,6 +432,32 @@ mod tests {
|
|
|
432
432
|
let _ = std::fs::remove_dir_all(&dir);
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
+
/// `.` on Float / Str / Map offers their current methods (and none of the removed ones).
|
|
436
|
+
#[test]
|
|
437
|
+
fn memberCompletionOnFloatStrMap() {
|
|
438
|
+
let dir = std::env::temp_dir().join(format!("plum-lsp-fsm-{}", std::process::id()));
|
|
439
|
+
std::fs::create_dir_all(&dir).unwrap();
|
|
440
|
+
let path = dir.join("t.plum");
|
|
441
|
+
let text = "module t\nimport std/Number\nimport std/Str\nimport std/List\nimport std/Map\n\nfun main() =\n f := 1.5\n f.zzzzzz\n s := \"x\"\n s.zzzzzz\n m := Map[Str, Int]()\n m.zzzzzz\n";
|
|
442
|
+
std::fs::write(&path, text).unwrap();
|
|
443
|
+
let cases: [(usize, &str, &[&str]); 3] = [
|
|
444
|
+
(9, "f", &["sqrt", "toStr"]),
|
|
445
|
+
(11, "s", &["length", "split", "parseBool"]),
|
|
446
|
+
(13, "m", &["get", "keys", "values", "entries"]),
|
|
447
|
+
];
|
|
448
|
+
for (line, recv, wants) in cases {
|
|
449
|
+
let items = memberCompletionFor(&path, &repoRoot(), text, line, recv);
|
|
450
|
+
let labels: Vec<&str> = items.iter().map(|i| i.label.as_str()).collect();
|
|
451
|
+
for want in wants {
|
|
452
|
+
assert!(labels.contains(want), "missing {want} for {recv} in {labels:?}");
|
|
453
|
+
}
|
|
454
|
+
for gone in ["andThen", "unwrapOr", "orElse"] {
|
|
455
|
+
assert!(!labels.contains(&gone), "stale {gone} for {recv}: {labels:?}");
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
let _ = std::fs::remove_dir_all(&dir);
|
|
459
|
+
}
|
|
460
|
+
|
|
435
461
|
#[test]
|
|
436
462
|
fn keywordsCurrent() {
|
|
437
463
|
let labels: Vec<String> = completion::keywordItems().into_iter().map(|i| i.label).collect();
|
plum-std/Json.plum
CHANGED
|
@@ -82,10 +82,8 @@ enum JsonParse =
|
|
|
82
82
|
| JsonParseFailed(pos: Int, text: Str)
|
|
83
83
|
|
|
84
84
|
# A recursive-descent parser over a `Str` source, tracking its read position
|
|
85
|
-
# in `pos`.
|
|
85
|
+
# in `pos`. It reads directly from a `Str`, the same way every other stdlib
|
|
86
|
-
# but `Readable` is an undefined trait with no methods anywhere in `libs/std`
|
|
87
|
-
# — there is nothing to call on it — so this reads directly from a `Str`
|
|
88
|
-
#
|
|
86
|
+
# parser (e.g. `parseInt`) does.
|
|
89
87
|
enum JsonParser =
|
|
90
88
|
| JsonParser(src: Str, pos: Int, text: Str)
|
|
91
89
|
|
plum-std/List.plum
CHANGED
|
@@ -330,7 +330,7 @@ test "chunk splits into sublists of at most size elements each"
|
|
|
330
330
|
assert chunks.get(2)!.join(",") == "5"
|
|
331
331
|
assert l.chunk(0).length() == 0
|
|
332
332
|
|
|
333
|
-
test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty is
|
|
333
|
+
test "list_get_set_push_pop: push 1000, get(999), pop until empty, get on empty is an error"
|
|
334
334
|
l := List[Int]()
|
|
335
335
|
i := 0
|
|
336
336
|
while i < 1000
|
plum-std/Str.plum
CHANGED
|
@@ -4,8 +4,8 @@ import std/List
|
|
|
4
4
|
import std/Buffer
|
|
5
5
|
import std/Number
|
|
6
6
|
|
|
7
|
-
# A Str is an immutable byte sequence, backed by a real `Buffer`
|
|
7
|
+
# A Str is an immutable byte sequence, backed by a real `Buffer` in its
|
|
8
|
-
# `
|
|
8
|
+
# `data` field, not a compiler special case: `Str` is an ordinary
|
|
9
9
|
# wasm-gc struct wrapping a `Buffer` (itself a wasm-gc struct wrapping a raw
|
|
10
10
|
# `[]Byte`), the same generic struct-of-fields codegen every other class
|
|
11
11
|
# gets. Only `byteToStr` (build a length-1 `Str` from a raw byte) and the
|
|
@@ -591,7 +591,7 @@ test "string interpolation supports a method call taking a closure literal"
|
|
|
591
591
|
assert "{l.findValue(3)!}" == "3"
|
|
592
592
|
assert "{l.contains(100)}" == "false"
|
|
593
593
|
|
|
594
|
-
test "string interpolation supports
|
|
594
|
+
test "string interpolation supports an inline if expression"
|
|
595
595
|
n := 5
|
|
596
596
|
assert "{if n > 0 then "positive" else "non-positive"}" == "positive"
|
|
597
597
|
|
plum-tooling/tree-sitter-plum/queries/plum/format.scm
CHANGED
|
@@ -226,9 +226,9 @@
|
|
|
226
226
|
(_) @append_indent_end
|
|
227
227
|
.)
|
|
228
228
|
|
|
229
|
-
; `enum_field` (a variant like `|
|
|
229
|
+
; `enum_field` (a variant like `| Circle(r: Float)`) is ALIASED to the visible
|
|
230
|
-
; node kind
|
|
230
|
+
; node kind "field"; only it has a literal "|" — safe to target unscoped by
|
|
231
|
-
;
|
|
231
|
+
; parent.
|
|
232
232
|
(field
|
|
233
233
|
"|" @append_space)
|
|
234
234
|
|
plum-tooling/tree-sitter-plum/queries/plum/highlights.scm
CHANGED
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
((var_identifier) @variable.builtin
|
|
8
8
|
(#eq? @variable.builtin "_"))
|
|
9
9
|
|
|
10
|
+
; Error names: `error.Name` and the members of `error Set = A | B` are constants
|
|
11
|
+
; (these must come before the generic `(type_identifier) @type`).
|
|
12
|
+
(error_value "." @punctuation.delimiter)
|
|
13
|
+
|
|
14
|
+
(error_value (type_identifier) @constant)
|
|
15
|
+
|
|
16
|
+
(error_decl name: (type_identifier) @type)
|
|
17
|
+
|
|
18
|
+
(error_decl member: (type_identifier) @constant)
|
|
19
|
+
|
|
10
20
|
; (mod_identifier) @constant
|
|
11
21
|
(type_identifier) @type
|
|
12
22
|
(fn_identifier) @function
|
|
@@ -82,14 +92,6 @@
|
|
|
82
92
|
|
|
83
93
|
"error" @keyword
|
|
84
94
|
|
|
85
|
-
(error_value "." @punctuation.delimiter)
|
|
86
|
-
|
|
87
|
-
(error_value (type_identifier) @constant)
|
|
88
|
-
|
|
89
|
-
(error_decl (type_identifier) @type)
|
|
90
|
-
|
|
91
|
-
(error_decl member: (type_identifier) @constant)
|
|
92
|
-
|
|
93
95
|
|
|
94
96
|
(safe_attribute "?." @operator)
|
|
95
97
|
|
|
@@ -108,6 +110,8 @@
|
|
|
108
110
|
"value"
|
|
109
111
|
"fun"
|
|
110
112
|
"fun*"
|
|
113
|
+
"extern"
|
|
114
|
+
"fn"
|
|
111
115
|
"test"
|
|
112
116
|
] @keyword
|
|
113
117
|
|
plum-tooling/tree-sitter-plum/queries/plum/tags.scm
CHANGED
|
@@ -15,3 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
(extend
|
|
17
17
|
name: (type_identifier) @name) @definition.class
|
|
18
|
+
|
|
19
|
+
(error_decl
|
|
20
|
+
name: (type_identifier) @name) @definition.class
|
plum-tooling/tree-sitter-plum/test/aa.plum
CHANGED
|
@@ -1,31 +1,38 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
import std/
|
|
3
|
+
import std/Number
|
|
4
|
-
import std/
|
|
4
|
+
import std/Str
|
|
5
|
+
import std/Os
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
PI_APPROX = 3.14
|
|
7
8
|
MSG = "Hello World"
|
|
8
9
|
SUM = 1 + {{2 * 3} / 4}
|
|
9
10
|
ENABLED = !false
|
|
10
11
|
|
|
12
|
+
error CatError = Hungry | Asleep
|
|
13
|
+
|
|
11
14
|
# Cat is an animal that meows always
|
|
15
|
+
enum Cat =
|
|
12
|
-
|
|
16
|
+
| Cat(name: Str, age: Int)
|
|
13
|
-
name: Str
|
|
14
|
-
age: Int
|
|
15
17
|
|
|
16
|
-
fun withName(name: Str) -> Cat =
|
|
18
|
+
fun withName(self, name: Str) -> Cat =
|
|
17
19
|
Cat(name: name, age: self.age)
|
|
18
20
|
|
|
19
|
-
fun withAge(age: Int) -> Cat =
|
|
21
|
+
fun withAge(self, age: Int) -> Cat =
|
|
20
22
|
Cat(name: self.name, age: age)
|
|
21
23
|
|
|
22
|
-
fun speak() -> Str =
|
|
24
|
+
fun speak(self) -> Str =
|
|
23
25
|
"meow"
|
|
24
26
|
|
|
27
|
+
fun feed(self, food: Int) -> CatError!Int =
|
|
28
|
+
if food < 0
|
|
29
|
+
return error.Hungry
|
|
30
|
+
food
|
|
31
|
+
|
|
25
|
-
fun toStr() -> Str =
|
|
32
|
+
fun toStr(self) -> Str =
|
|
26
|
-
for i
|
|
33
|
+
for i := range 10
|
|
27
34
|
printLn("delta")
|
|
28
|
-
if
|
|
35
|
+
if self.age > 1
|
|
29
36
|
printLn("Hellow")
|
|
30
37
|
else
|
|
31
38
|
printLn("New")
|
plum-tooling/tree-sitter-plum/test/corpus/function.txt
CHANGED
|
@@ -204,7 +204,7 @@ fun bothTrue(a: Bool, b: Bool) -> Bool =
|
|
|
204
204
|
(var_identifier))))))))
|
|
205
205
|
|
|
206
206
|
================================================================================
|
|
207
|
-
function - bare
|
|
207
|
+
function - bare inline if as body's trailing statement
|
|
208
208
|
================================================================================
|
|
209
209
|
|
|
210
210
|
fun pick(cond: Bool, a: Int, b: Int) -> Int =
|
|
@@ -309,7 +309,7 @@ fun useClosure() -> Bool =
|
|
|
309
309
|
(fn_argument_list)))))))
|
|
310
310
|
|
|
311
311
|
================================================================================
|
|
312
|
-
function - closure with an inline
|
|
312
|
+
function - closure with an inline if body
|
|
313
313
|
================================================================================
|
|
314
314
|
|
|
315
315
|
fun useClosure() -> Str =
|
plum-tooling/tree-sitter-plum/test/corpus/safe_attribute.txt
CHANGED
|
@@ -66,7 +66,7 @@ fun greetCall(person: Option[Person]) -> Option[Str] =
|
|
|
66
66
|
(string_end)))))))))))
|
|
67
67
|
|
|
68
68
|
================================================================================
|
|
69
|
-
safe navigation operator - coexists with
|
|
69
|
+
safe navigation operator - coexists with inline if and try, all sharing `?`-prefixed tokens
|
|
70
70
|
================================================================================
|
|
71
71
|
|
|
72
72
|
fun classify(n: Int) -> Int =
|
plum-tooling/tree-sitter-plum/test/corpus/try.txt
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
try operator - postfix ? unwraps or returns early
|
|
3
3
|
================================================================================
|
|
4
4
|
|
|
5
|
-
fun parsePositive(s: Str) ->
|
|
5
|
+
fun parsePositive(s: Str) -> !Int =
|
|
6
6
|
n := parseInt(s)?
|
|
7
|
-
return
|
|
7
|
+
return n
|
|
8
8
|
|
|
9
9
|
--------------------------------------------------------------------------------
|
|
10
10
|
|
|
@@ -16,11 +16,7 @@ fun parsePositive(s: Str) -> Result[Int, Str] =
|
|
|
16
16
|
type: (type
|
|
17
17
|
(type_identifier)))
|
|
18
18
|
returns: (type
|
|
19
|
-
(type_identifier)
|
|
20
|
-
generics: (type
|
|
21
|
-
|
|
19
|
+
(type_identifier))
|
|
22
|
-
generics: (type
|
|
23
|
-
(type_identifier)))
|
|
24
20
|
body: (body
|
|
25
21
|
(assign
|
|
26
22
|
(var_identifier)
|
|
@@ -37,15 +33,10 @@ fun parsePositive(s: Str) -> Result[Int, Str] =
|
|
|
37
33
|
(return
|
|
38
34
|
(expression
|
|
39
35
|
(primary_expression
|
|
40
|
-
(fn_call
|
|
41
|
-
function: (type_identifier)
|
|
42
|
-
arguments: (fn_argument_list
|
|
43
|
-
(expression
|
|
44
|
-
(primary_expression
|
|
45
|
-
|
|
36
|
+
(var_identifier)))))))
|
|
46
37
|
|
|
47
38
|
================================================================================
|
|
48
|
-
try operator - coexists with the
|
|
39
|
+
try operator - coexists with the inline if expression
|
|
49
40
|
================================================================================
|
|
50
41
|
|
|
51
42
|
fun classify(n: Int) -> Int =
|
plum-tooling/tree-sitter-plum/test/highlight/sample.plum
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
module std
|
|
2
2
|
|
|
3
|
-
enum Cat
|
|
3
|
+
enum Cat =
|
|
4
4
|
| Cat(name: Str, age: Int)
|
|
5
5
|
|
|
6
|
-
fun withName(name: Str) -> Cat =
|
|
6
|
+
fun withName(self, name: Str) -> Cat =
|
|
7
7
|
Cat(name: name, age: self.age)
|
|
8
8
|
|
|
9
|
-
fun withAge(age: Int) -> Cat =
|
|
9
|
+
fun withAge(self, age: Int) -> Cat =
|
|
10
10
|
Cat(name: self.name, age: age)
|
|
11
11
|
|
|
12
|
-
fun toStr() -> Str =
|
|
12
|
+
fun toStr(self) -> Str =
|
|
13
13
|
"Cat({self.name}, {self.age})"
|
|
14
14
|
|
|
15
15
|
fun main() =
|
plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json
CHANGED
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
|
-
"match": "\\b(module|value|enum|error|extend|fun\\*?|extern|self|test)(?![A-Za-z0-9_])",
|
|
70
|
+
"match": "\\b(module|value|enum|error|extend|fun\\*?|fn|extern|self|test)(?![A-Za-z0-9_])",
|
|
71
71
|
"name": "keyword.const.plum"
|
|
72
72
|
},
|
|
73
73
|
{
|
plum-wasm-codegen/src/lib.rs
CHANGED
|
@@ -485,7 +485,7 @@ struct LocalCtx<'a> {
|
|
|
485
485
|
/// `plum_compiler_bugs_found_2026_08` memory.
|
|
486
486
|
loop_extra_depth: Cell<u32>,
|
|
487
487
|
/// The declared return type of the function being compiled (`None` inside a
|
|
488
|
-
/// closure body) — `?` builds the OUTER function's
|
|
488
|
+
/// closure body) — `?` builds the OUTER function's error result from it.
|
|
489
489
|
ret_type: Option<PlumType>,
|
|
490
490
|
/// Match-subject scratch local -> its error-name type, for subjects whose
|
|
491
491
|
/// static type is a `TErrName` (their `Name` arms test the i32 code).
|
|
@@ -6378,15 +6378,8 @@ fn compileExprCore(
|
|
|
6378
6378
|
ast::Expr::Paren(inner) => {
|
|
6379
6379
|
compileExpr(inner, body, ctx, state)?;
|
|
6380
6380
|
}
|
|
6381
|
-
// `expr?` —
|
|
6381
|
+
// `expr?` — if the error code is non-zero, return it from the enclosing
|
|
6382
|
-
// field) vs Err (return the already-constructed value AS-IS,
|
|
6383
|
-
// no reconstruction needed). The `then`/`else` branches don't need to
|
|
6384
|
-
// agree in wasm's usual sense: after `return`, the rest of that
|
|
6385
|
-
//
|
|
6382
|
+
// function (see `compileErrTry`); otherwise the success value is the result.
|
|
6386
|
-
// declared result type (the success field's) coming only from the
|
|
6387
|
-
// `else` branch — the same stack-polymorphism-after-`return` rule
|
|
6388
|
-
// `compileStmt`'s own `Return` arm relies on, just reached from
|
|
6389
|
-
// inside an expression here for the first time.
|
|
6390
6383
|
ast::Expr::Try(inner) => compileErrTry(expr, inner, body, ctx, state)?,
|
|
6391
6384
|
ast::Expr::Unary(u) if isOpCall(expr, &u.operand, ctx) => {
|
|
6392
6385
|
let call = &ctx.op_calls[&(expr as *const ast::Expr as usize)];
|