plum

#treesitter#compiler#wasm

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 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/io
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, 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.
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 "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:
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, an inline `if`, or a closure-taking method call.
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/os`.
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`, `fail`) recorded for codegen; `None` while collecting.
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` / `fail` expression address -> its error-union facts.
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
- /// `Ok(3)`-style result type of a variant of enum `info.enum_name`.
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: `Ok(n)` / `Red` / `#Tag` -> (name, sub-patterns).
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 inline funs, misuse of an inline fun. The checker reports them.
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 (`Ok(v)` of an `Int` in one match, of a `Str` in the next) would
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 `trait`/`type`-per-shape rewrite in
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
- # `type`, with nothing catching a variant you forgot to update.
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 `type`, untouched elsewhere.
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 a trait type (see
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
- # not yet supported" instead of a normal pass/fail report — `looksLikeFloat`
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
- # Float now genuinely works (via `Number.toStr`), so the "Expected/Actual"
99
+ # `Number.toStr`), so the "Expected/Actual" failure message renders even for
104
- # failure-message machinery no longer needs to special-case Float at all.
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
- # `type`, declares it implements the `Book` trait, and defines its own
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
- # Java (sealed interface implemented by records, each overriding a method).
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's `trait` is a
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
- # methods with matching signatures?") — there is no vtable or runtime
9
+ # runtime dispatch. Generics are duck-typed per instantiation, so a function
11
- # dispatch. A function can't take a bare `book: Book` parameter and call
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 `type(Book) = ...`) is self-contained here
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 adding
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 `type`
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 a trait type (see above)
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 items = Vec::new();
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`. (The original design threaded a `Readable` stream through here,
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
- # instead, the same way every other stdlib parser, e.g. `Int.fromStr`, does.)
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 Err"
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` — a genuine
7
+ # A Str is an immutable byte sequence, backed by a real `Buffer` in its
8
- # `type`/`data` field, not a compiler special case: `Str` is an ordinary
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 a ternary expression"
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 `| Some[T]`) is ALIASED to the same visible
229
+ ; `enum_field` (a variant like `| Circle(r: Float)`) is ALIASED to the visible
230
- ; node kind ("field") as the old trait_field, but only it has a literal
230
+ ; node kind "field"; only it has a literal "|" — safe to target unscoped by
231
- ; "|" — safe to target unscoped by parent.
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/int
3
+ import std/Number
4
- import std/str
4
+ import std/Str
5
+ import std/Os
5
6
 
6
- PI = 3.14
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
- type Cat(IAnimal, IToStr) =
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 in 0..10
33
+ for i := range 10
27
34
  printLn("delta")
28
- if dog > 1
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 ternary as body's trailing statement
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 ternary body
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 ternary, and try, all sharing `?`-prefixed tokens
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) -> Result[Int, Str] =
5
+ fun parsePositive(s: Str) -> !Int =
6
6
  n := parseInt(s)?
7
- return Ok(n)
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
- (type_identifier))
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
- (var_identifier)))))))))))
36
+ (var_identifier)))))))
46
37
 
47
38
  ================================================================================
48
- try operator - coexists with the unrelated ternary operator, same `?` token
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(ToStr) =
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 `Err` variant from it.
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?` — `ref.test` decides Ok (fall through, extract the
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
- // branch is unreachable, so the validator accepts the `if`'s single
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)];