plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
README.md
| fa31ba4 | 1 | # 👾 Plum Programming Language |
| c8e4165 | 2 | |
| fa31ba4 | 3 | - A statically typed, imperative programming language with ADT's (Algebraic Data Types) inspired by rust, gleam |
| fa31ba4 | 4 | - The compiler is built upon the tree-sitter parser so has out of the box syntax highlighting support for helix and zed editor |
| fa31ba4 | 5 | - Plans to be compiled to amd64,arm64, and riscv64 using QBE maintaining C-ABI compatibility |
| fa31ba4 | 6 | |
| fa31ba4 | 7 | |
| fa31ba4 | 8 | ## Requirements |
| fa31ba4 | 9 | ```sh |
| fa31ba4 | 10 | node >= 23.1.0 |
| fa31ba4 | 11 | npm >= 10.9.0 |
| fa31ba4 | 12 | qbe >= 1.2 |
| fa31ba4 | 13 | clang >= 16.0.0 |
| 222801d | 14 | ``` |
| 222801d | 15 | |
| 222801d | 16 | ## Language Syntax |
| 222801d | 17 | |
| 222801d | 18 | This section documents the syntax currently implemented by the [tree-sitter grammar](tooling/tree-sitter-plum/grammar.js), [`plum-checker`](plum-checker), and [`plum-wasm-codegen`](plum-wasm-codegen). Every construct below has a runnable, verified example in [`examples/`](examples) — the two test suites [`plum-checker/tests/examples_test.rs`](plum-checker/tests/examples_test.rs) and [`plum-wasm-codegen/tests/examples_test.rs`](plum-wasm-codegen/tests/examples_test.rs) parse, type-check, and (where noted) compile-and-execute every file in that directory, so this documentation can't silently drift out of sync with what actually works. |
| 222801d | 19 | |
| 222801d | 20 | > The [docs website](website/src/content/docs) describes a considerably more elaborate *future* syntax (`fn`, `record`, arrow return types, `{}` blocks, generic `[T]` params). That syntax isn't implemented yet — everything below is what the compiler accepts **today**. |
| 222801d | 21 | |
| 222801d | 22 | ### Table of contents |
| 222801d | 23 | |
| 222801d | 24 | - [Layout and comments](#layout-and-comments) |
| 222801d | 25 | - [Naming conventions](#naming-conventions) |
| 222801d | 26 | - [Modules and imports](#modules-and-imports) |
| 222801d | 27 | - [Constants](#constants) |
| 222801d | 28 | - [Literals](#literals) |
| 222801d | 29 | - [Operators](#operators) |
| 222801d | 30 | - [Variables and assignment](#variables-and-assignment) |
| 222801d | 31 | - [Control flow](#control-flow) |
| 222801d | 32 | - [Functions](#functions) |
| 222801d | 33 | - [Types: records, traits, enums](#types-records-traits-enums) |
| 222801d | 34 | - [Generics](#generics) |
| 222801d | 35 | - [`self`, field access, and methods](#self-field-access-and-methods) |
| 222801d | 36 | - [Constructing values](#constructing-values) |
| 222801d | 37 | - [`match`](#match) |
| 222801d | 38 | - [String interpolation](#string-interpolation) |
| 0000000 | 39 | - [`extern` functions and printing](#extern-functions-and-printing) |
| 222801d | 40 | - [Known gaps](#known-gaps) |
| 222801d | 41 | |
| 222801d | 42 | ### Layout and comments |
| 222801d | 43 | |
| 222801d | 44 | Blocks are indentation-sensitive (2 spaces), like Python — there's no `{ }` for grouping statements. A `#` starts a line comment. |
| 222801d | 45 | |
| 222801d | 46 | ```plum |
| 222801d | 47 | # this is a comment |
| 0fe3528 | 48 | fun main() = |
| 222801d | 49 | x = 1 |
| 222801d | 50 | if x > 0 |
| 222801d | 51 | x = x + 1 |
| 222801d | 52 | ``` |
| 222801d | 53 | |
| 222801d | 54 | ### Naming conventions |
| 222801d | 55 | |
| 222801d | 56 | The lexer enforces case by construct — using the wrong case for a position is a parse error, not just a style nit: |
| 222801d | 57 | |
| 222801d | 58 | | Token | Case | Used for | |
| 222801d | 59 | |---|---|---| |
| 222801d | 60 | | `type_identifier` | `PascalCase` | type/trait/enum names, generic bounds | |
| 222801d | 61 | | `fn_identifier` | `camelCase` | function/method names, `.field`/`.method()` member names | |
| 222801d | 62 | | `var_identifier` | `snake_case` | variables, params, fields (also accepts camelCase — see below) | |
| 222801d | 63 | | `const_identifier` | `SCREAMING_SNAKE_CASE` | top-level constants | |
| 222801d | 64 | | `mod_identifier` | `snake_case`, single word | `module` declarations only (no `/`) | |
| 222801d | 65 | |
| 222801d | 66 | `var_identifier`'s regex is deliberately a superset of `fn_identifier`'s (it allows both `_` and mixed case) so that a bare function call like `factorial(x)` — an all-lowercase, no-underscore name — isn't ambiguous between "a variable" and "a function" at the lexer level. |
| 222801d | 67 | |
| 222801d | 68 | ### Modules and imports |
| 222801d | 69 | |
| 222801d | 70 | ```plum |
| 222801d | 71 | module basics |
| 222801d | 72 | |
| 222801d | 73 | import std/io |
| 222801d | 74 | ``` |
| 222801d | 75 | |
| 222801d | 76 | `module` takes exactly one identifier (no path segments). `import` takes a `/`-separated path; there's no module resolution yet — it's parsed but not loaded. |
| 222801d | 77 | |
| 222801d | 78 | ### Constants |
| 222801d | 79 | |
| 222801d | 80 | ```plum |
| 222801d | 81 | MAX_RETRIES = 3 |
| 222801d | 82 | PI = 3.14159 |
| 222801d | 83 | GREETING = "hello" |
| 222801d | 84 | ``` |
| 222801d | 85 | |
| 222801d | 86 | Top-level only, `SCREAMING_SNAKE_CASE`, one expression. |
| 222801d | 87 | |
| 222801d | 88 | ### Literals |
| 222801d | 89 | |
| 222801d | 90 | ```plum |
| 222801d | 91 | dec = 42 |
| 222801d | 92 | hex = 0xFF |
| 222801d | 93 | bin = 0b1010 |
| 222801d | 94 | big = 1_000_000 # underscores allowed as digit separators |
| 222801d | 95 | |
| 222801d | 96 | flt = 3.14 |
| 222801d | 97 | flt2 = 12.0f # trailing f/F suffix |
| 222801d | 98 | exp = 6.022e23 |
| 222801d | 99 | |
| 222801d | 100 | name = "plum" |
| 222801d | 101 | empty = "" |
| 222801d | 102 | escaped = "line one\nline two\ttabbed \"quoted\"" |
| 222801d | 103 | |
| 222801d | 104 | yes = True |
| 222801d | 105 | no = False |
| 222801d | 106 | ``` |
| 222801d | 107 | |
| 660674c | 108 | `True`/`False` are built into the type checker/codegen as `Bool`'s two variants — you don't need to declare `enum Bool` yourself to use them. |
| 222801d | 109 | |
| 222801d | 110 | Full example: [`examples/basics.plum`](examples/basics.plum), [`examples/strings.plum`](examples/strings.plum). |
| 222801d | 111 | |
| 222801d | 112 | ### Operators |
| 222801d | 113 | |
| 222801d | 114 | From tightest to loosest binding: |
| 222801d | 115 | |
| 222801d | 116 | | Precedence | Operators | Notes | |
| 222801d | 117 | |---|---|---| |
| 222801d | 118 | | highest | `.` | attribute access / method call | |
| 222801d | 119 | | | unary `+` `-` | | |
| 222801d | 120 | | | `*` `/` `%` | | |
| 222801d | 121 | | | `+` `-` | | |
| 222801d | 122 | | | `<<` `>>` | | |
| 222801d | 123 | | | `^` | | |
| 222801d | 124 | | | `&` | | |
| 222801d | 125 | | | `\|` | | |
| 222801d | 126 | | | `<` `<=` `==` `!=` `>=` `>` `<>` | comparisons | |
| 222801d | 127 | | | `!` | boolean not | |
| 222801d | 128 | | | `&&` | | |
| 222801d | 129 | | | `\|\|` | | |
| 222801d | 130 | | | `..` | range (e.g. `for i in 0..5`) | |
| 222801d | 131 | | lowest | `? :` | ternary | |
| 222801d | 132 | |
| 222801d | 133 | `!` binds **looser** than comparisons — `!a == b` means `!(a == b)`, not `(!a) == b`. Whenever you mix categories (especially with `!`), group explicitly rather than relying on memorized precedence: |
| 222801d | 134 | |
| 222801d | 135 | ```plum |
| 222801d | 136 | grouped = {1 + 2} * {3 - 1} # {expr} groups, like (expr) in most languages |
| 222801d | 137 | picked = cmp ? sum : bits |
| 222801d | 138 | negated = -sum |
| 222801d | 139 | inverted = !cmp |
| 222801d | 140 | ``` |
| 222801d | 141 | |
| 222801d | 142 | Note: this language uses `{ }` for grouping a sub-expression, not `( )` — parens are reserved for call/constructor argument lists. |
| 222801d | 143 | |
| 222801d | 144 | Full example: [`examples/basics.plum`](examples/basics.plum). |
| 222801d | 145 | |
| 222801d | 146 | ### Variables and assignment |
| 222801d | 147 | |
| 222801d | 148 | ```plum |
| 222801d | 149 | x = 1 |
| 222801d | 150 | a, b = 1, 2 # multiple targets, positionally paired with multiple values |
| 222801d | 151 | ``` |
| 222801d | 152 | |
| 222801d | 153 | Assignment introduces a binding if `x` isn't already in scope, or updates it otherwise — there's no separate `let`/`var` keyword. |
| 222801d | 154 | |
| 222801d | 155 | ### Control flow |
| 222801d | 156 | |
| 222801d | 157 | ```plum |
| 222801d | 158 | if n < 0 |
| 222801d | 159 | return "negative" |
| 222801d | 160 | else if n == 0 |
| 222801d | 161 | return "zero" |
| 222801d | 162 | else |
| 222801d | 163 | return "positive" |
| 222801d | 164 | |
| 222801d | 165 | while i > 0 |
| 222801d | 166 | i = i - 1 |
| 222801d | 167 | |
| 222801d | 168 | for i in 0..limit |
| 222801d | 169 | if i == 3 |
| 222801d | 170 | continue |
| 222801d | 171 | if i == 8 |
| 222801d | 172 | break |
| 222801d | 173 | total = total + i |
| 222801d | 174 | |
| 222801d | 175 | assert n > 0 # traps at runtime if false |
| 222801d | 176 | todo # traps at runtime — marks a body as not yet implemented |
| 222801d | 177 | ``` |
| 222801d | 178 | |
| 222801d | 179 | `for x in <range>` only accepts a range expression (`a..b`) or another primary expression on the right — there's no iterator protocol yet. |
| 222801d | 180 | |
| 222801d | 181 | Full example: [`examples/control_flow.plum`](examples/control_flow.plum). |
| 222801d | 182 | |
| 222801d | 183 | ### Functions |
| 222801d | 184 | |
| 222801d | 185 | ```plum |
| 0fe3528 | 186 | fun addInts(a: Int, b: Int) -> Int = |
| 222801d | 187 | a + b |
| 222801d | 188 | |
| 0fe3528 | 189 | fun greet() = # no return type => Unit |
| 222801d | 190 | todo |
| 222801d | 191 | |
| 0fe3528 | 192 | fun withDefault(a: Int, step: Int = 1) -> Int = |
| 222801d | 193 | a + step |
| 222801d | 194 | |
| 0fe3528 | 195 | fun sumAll(nums: ...Int) -> Int = # variadic param |
| 222801d | 196 | todo |
| 222801d | 197 | ``` |
| 222801d | 198 | |
| 222801d | 199 | The body always follows `=` on the next, indented line (`= expr` on the same line is grammatically accepted too, but every example and test in this repo uses the indented form — prefer it for consistency). |
| 222801d | 200 | |
| 222801d | 201 | Full example: [`examples/functions.plum`](examples/functions.plum). |
| 222801d | 202 | |
| 222801d | 203 | ### Types: records, traits, enums |
| 222801d | 204 | |
| 222801d | 205 | ```plum |
| 222801d | 206 | type Point = |
| 222801d | 207 | x: Int |
| 222801d | 208 | y: Int |
| 222801d | 209 | |
| 222801d | 210 | type Named(Stringable) = # implements Stringable |
| 222801d | 211 | name: Str |
| 222801d | 212 | |
| 222801d | 213 | trait Shape = |
| 222801d | 214 | area() -> Float |
| 222801d | 215 | perimeter() -> Float |
| 222801d | 216 | |
| 222801d | 217 | enum Color = |
| 222801d | 218 | | Red |
| 222801d | 219 | | Green |
| 222801d | 220 | | Blue |
| 222801d | 221 | |
| 222801d | 222 | enum Option = |
| 222801d | 223 | | Some(Int) # variant with payload |
| 222801d | 224 | | None |
| 222801d | 225 | ``` |
| 222801d | 226 | |
| 222801d | 227 | Full example: [`examples/types.plum`](examples/types.plum). |
| 222801d | 228 | |
| 222801d | 229 | ### Generics |
| 222801d | 230 | |
| 222801d | 231 | Generic type parameters are single letters only: `a`, `b`, `c`, `d`. |
| 222801d | 232 | |
| 222801d | 233 | ```plum |
| 222801d | 234 | type Box(a) = |
| 222801d | 235 | value: a |
| 222801d | 236 | |
| 222801d | 237 | trait Comparable(a: Ord) = # bounded generic param |
| 222801d | 238 | compareTo(other: a) -> Int |
| 222801d | 239 | |
| 0fe3528 | 240 | fun wrap(value: a) -> Bool = # generic param type |
| 222801d | 241 | True |
| 222801d | 242 | ``` |
| 222801d | 243 | |
| 2216237 | 244 | Generic **arguments** (instantiating a generic type) accept either bracket or paren syntax: `List[Int]` and `List(Int)` both parse. User-defined generics (classes, their methods, free functions, and enums) are monomorphized: each concrete-type-argument combination actually used in the program gets its own specialized, fully-concrete copy, which then type-checks and compiles to wasm through the normal, unmodified pipeline. See `useWrap`/`usePair` in [`examples/functions.plum`](examples/functions.plum) and `makeIntBox`/`makeStrBox` in [`examples/types.plum`](examples/types.plum) for real instantiation sites. Generic enums support any number of concrete instantiations coexisting in one program (variant names are mangled per instantiation, e.g. `Some` -> `Some$Int`/`Some$Str`, internally — invisible to user code). One narrower residual limitation: a payload-free variant (e.g. `None`) used as a bare value *outside* of a `match` pattern can't be disambiguated between multiple concrete instantiations of its enum from that expression alone; constructing via a payload-carrying sibling (`Some(5)`) and matching (`Some(v) => ...`, `None => ...`) is fully supported and is the overwhelmingly common usage pattern. |
| 222801d | 245 | |
| 222801d | 246 | Full example: [`examples/types.plum`](examples/types.plum), [`examples/functions.plum`](examples/functions.plum). |
| 222801d | 247 | |
| 6341c74 | 248 | ### Closures |
| 6341c74 | 249 | |
| 6341c74 | 250 | ```plum |
| 0fe3528 | 251 | fun each(cb: fn(Int) -> Int) -> Int = |
| 6341c74 | 252 | cb(5) |
| 6341c74 | 253 | |
| 0fe3528 | 254 | fun useCapturingClosure() -> Int = |
| 6341c74 | 255 | offset = 100 |
| 6341c74 | 256 | cb = |v| |
| 6341c74 | 257 | v + offset |
| 6341c74 | 258 | cb(5) |
| 6341c74 | 259 | |
| 0fe3528 | 260 | fun main() -> Int = |
| 6341c74 | 261 | each(|v| v * 3) + useCapturingClosure() |
| 6341c74 | 262 | ``` |
| 6341c74 | 263 | |
| 6341c74 | 264 | A closure literal is `|params| body`: zero or more bare parameter names between pipes, followed by an expression or an indented block body. A parameter's type is inferred from context (e.g. the declared type of the `fn(...)` slot it's passed into) rather than annotated inline. |
| 6341c74 | 265 | |
| 35af6cf | 266 | A `fn(...)` / `fn(...) -> T` type annotates a closure-typed parameter or field: `fn(Int) -> Int` is a function from one `Int` to an `Int`; parameter types are positional only (no names). Closures compile to a `{table_index, env_pointer}` pair and are called via `call_indirect` through a function table, so a `fn(...)`-typed value can hold a closure literal, a plain top-level named function (`each(double)` — compiled as a zero-capture "trampoline" closure), or a closure nested inside another closure's body. |
| 6341c74 | 267 | |
| 6341c74 | 268 | Capture is **snapshot-by-value**: any outer variable a closure body references is copied into the closure's environment at the moment the closure literal is evaluated, not re-read live later. Reassigning the captured variable afterward does not change what the closure sees. |
| 6341c74 | 269 | |
| 35af6cf | 270 | Closures parse and compile in two shapes: passed directly as a call argument (`each(|v| v * 3)`), or assigned to a local first and called or passed on later (`cb = |v|\n v + offset` followed by `cb(5)` or `each(cb)`). Both support a multi-line indented body. |
| 6341c74 | 271 | |
| 6341c74 | 272 | Full example: [`examples/closures.plum`](examples/closures.plum). |
| 6341c74 | 273 | |
| 222801d | 274 | ### `self`, field access, and methods |
| 222801d | 275 | |
| 41e0859 | 276 | A `fun` declared indented directly inside a `type`/`enum` body is a method on that type, with an implicit `self`: |
| 222801d | 277 | |
| 222801d | 278 | ```plum |
| 222801d | 279 | type Cat = |
| 222801d | 280 | name: Str |
| 222801d | 281 | age: Int |
| 222801d | 282 | |
| 41e0859 | 283 | fun getAge(self) -> Int = |
| 41e0859 | 284 | self.age |
| 222801d | 285 | |
| 41e0859 | 286 | fun birthday(self) -> Int = |
| 41e0859 | 287 | self.age + 1 |
| 222801d | 288 | |
| 0fe3528 | 289 | fun main() -> Int = |
| 222801d | 290 | c = Cat(name: "Whiskers", age: 3) |
| 222801d | 291 | a = c.getAge() # method call |
| 222801d | 292 | w = Wrapper(inner: c, tag: 1) |
| 222801d | 293 | w.inner.age # chained field access |
| 222801d | 294 | ``` |
| 222801d | 295 | |
| 222801d | 296 | Methods are dispatched by declared receiver type, not by name alone — two types can each define a method with the same name without colliding. |
| 222801d | 297 | |
| 222801d | 298 | Full example: [`examples/methods.plum`](examples/methods.plum). |
| 222801d | 299 | |
| 222801d | 300 | ### Constructing values |
| 222801d | 301 | |
| 222801d | 302 | ```plum |
| 222801d | 303 | Cat(name: "Whiskers", age: 3) |
| 222801d | 304 | ``` |
| 222801d | 305 | |
| 222801d | 306 | Construct a `type` value by calling its name with `field: value` pairs (any order; every field must be provided). |
| 222801d | 307 | |
| 222801d | 308 | ### `match` |
| 222801d | 309 | |
| 222801d | 310 | ```plum |
| 222801d | 311 | match n |
| 660674c | 312 | 0 => "zero" # inline body |
| 222801d | 313 | 1 => |
| 660674c | 314 | "one" # indented block body — both forms are accepted |
| 222801d | 315 | _ => |
| 222801d | 316 | "many" |
| 222801d | 317 | |
| 222801d | 318 | match b |
| 222801d | 319 | True => |
| 222801d | 320 | 1 |
| 222801d | 321 | False => |
| 222801d | 322 | 0 |
| 222801d | 323 | |
| 222801d | 324 | match opt |
| 222801d | 325 | Some(v) => |
| 222801d | 326 | v |
| 222801d | 327 | None => |
| 222801d | 328 | 0 |
| 222801d | 329 | ``` |
| 222801d | 330 | |
| 35af6cf | 331 | A case body can be a single inline expression right after `=>`, or an indented block — pick whichever reads better for that arm. Patterns can be: integer/float/string literals, a bare identifier (binds a new local to the subject's value), a bare capitalized tag (`True`, `False`, or any declared `enum` variant with no payload — compared, not bound), a constructor pattern (`Some(v)`, binding its argument — sub-patterns can themselves be constructor patterns, e.g. `Wrap(Some(v))`), or `_` (wildcard). `match a, b, ...` against multiple comma-separated subjects is also supported — each case supplies one pattern per subject, and every position must match for that case to apply. |
| 222801d | 332 | |
| 222801d | 333 | Full example: [`examples/match.plum`](examples/match.plum). |
| 222801d | 334 | |
| 222801d | 335 | ### String interpolation |
| 222801d | 336 | |
| 222801d | 337 | ```plum |
| 0fe3528 | 338 | fun greet(name: Str) -> Str = |
| 222801d | 339 | "Hello, {name}!" |
| 222801d | 340 | ``` |
| 222801d | 341 | |
| 35af6cf | 342 | `{expr}` inside a string interpolates a single primary expression (a variable, literal, attribute access, etc). Parses, type-checks, and compiles for `Str`, `Int`, and `Bool` values; interpolating a `Float` reports a clear error (see below). |
| 222801d | 343 | |
| 222801d | 344 | Full example: [`examples/strings.plum`](examples/strings.plum). |
| 222801d | 345 | |
| 0000000 | 346 | ### `extern` functions and printing |
| 0000000 | 347 | |
| 0000000 | 348 | ```plum |
| 0000000 | 349 | extern fun printLn(s: Str) |
| 0000000 | 350 | ``` |
| 0000000 | 351 | |
| 0000000 | 352 | `extern fun` declares a function with no Plum body at all — no `=`, no `todo` — backed instead by a host-provided wasm import. wasm has no built-in notion of stdout, so this is how `printLn` exists: `plum-checker` requires an `extern` fn to have no body and no receiver (it can't be a method), and `plum-wasm-codegen` compiles it to a genuine wasm import (`plum::printLn`) instead of a normal function body. Both `plum run` (JIT-executes immediately under an embedded `wasmtime`) and `plum build` (AOT-compiles to wasm and links it into a standalone executable via the `plum-runtime` crate) supply this import; a bare `.wasm` file run through an unmodified `wasmtime` CLI will fail to instantiate for lack of it. `libs/std/os.plum` declares `printLn` for use via `import std/os`; [`examples/io.plum`](examples/io.plum) declares an equivalent local copy (avoiding the import, so it stays runnable by the single-file test harnesses in `plum-checker`/`plum-wasm-codegen`) and is the smallest example to run for a smoke test: |
| 0000000 | 353 | |
| 0000000 | 354 | ``` |
| 0000000 | 355 | $ plum run examples/io.plum |
| 0000000 | 356 | hello from plum |
| 0000000 | 357 | hello, world! |
| 0000000 | 358 | ``` |
| 0000000 | 359 | |
| 222801d | 360 | ### Known gaps |
| 222801d | 361 | |
| 222801d | 362 | Some things parse and type-check but don't compile to wasm yet — `plum-wasm-codegen` reports a clear error rather than silently producing wrong code: |
| 222801d | 363 | |
| 35af6cf | 364 | - interpolating a `Float` value in a string (`Str`/`Int`/`Bool` interpolation, and plain non-interpolated literals, all compile) — correct decimal formatting of a float is a substantial separate undertaking (something like Grisu/Ryu), scoped out for now |
| ecd2178 | 365 | - `libs/std`'s actual `List`/`Map` still don't compile — `List`'s methods (`get`/`length`/`add`/`set`/`removeAt`/`remove`/`clear`/`reverse`/`each`/`map`) are all implemented now, but a class or enum-variant field declared with a concrete instantiation of another generic type (`Node.next: Option[Node]`, needed by `List`/`Node`'s own linked-list shape) doesn't survive monomorphization — `plumTypeFromAst` drops generic type arguments when building field types, so once the referenced generic type is specialized (and its unspecialized original removed), the field is left pointing at a name that no longer exists. This is a `plum-checker` generics gap, not a codegen one — the underlying wasm-gc struct/array machinery it would need is fully implemented and tested (see `plum-wasm-codegen/tests/codegen_tests.rs`'s `listAddSetRemoveAtRemoveClearReverseAllWorkCorrectly` and `removingEveryNodeInALoopLeavesAnEmptyCorrectlyFunctioningList`, which port `List`'s methods onto an equivalent non-generic shape to prove it). Separately, `List`'s own `join` method (and `Map`) reference a `Buffer` type and trait-bounded dispatch (`Stringable`) that don't exist yet — `plum-checker` doesn't process trait declarations at all currently |