plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-tooling/tree-sitter-plum/grammar.js
| 4bd2c1c | 1 | const PREC = { |
| 04d2270 | 2 | closure: -2, |
| 4bd2c1c | 3 | conditional: -1, |
| 4bd2c1c | 4 | parenthesized_expression: 1, |
| fc68ba7 | 5 | or: 10, |
| fc68ba7 | 6 | and: 11, |
| 4bd2c1c | 7 | not: 12, |
| 4bd2c1c | 8 | compare: 13, |
| 4bd2c1c | 9 | bitwise_or: 14, |
| 4bd2c1c | 10 | bitwise_and: 15, |
| 4bd2c1c | 11 | xor: 16, |
| 4bd2c1c | 12 | shift: 17, |
| 4bd2c1c | 13 | plus: 18, |
| 4bd2c1c | 14 | times: 19, |
| 4bd2c1c | 15 | unary: 20, |
| 4bd2c1c | 16 | power: 21, |
| efe15db | 17 | constructor: 22, |
| efe15db | 18 | call: 23, |
| 287b97c | 19 | // A bare `Name` inside an enum variant's payload parens (`Bar(Name)`) is |
| 287b97c | 20 | // ambiguous between "a positional payload type" and "a discriminant-value |
| 287b97c | 21 | // expression referencing a SCREAMING_CASE const" (a bare uppercase name |
| 287b97c | 22 | // lexes as `type_identifier` either way) — real usage only ever needs |
| 287b97c | 23 | // literal discriminant values (`ReadMin(10)`), never a bare-name one, so |
| 287b97c | 24 | // this always resolves in favor of "payload type". |
| 287b97c | 25 | variantType: 24, |
| 4bd2c1c | 26 | }; |
| 4bd2c1c | 27 | |
| efe15db | 28 | const DEC_DIGITS = token(sep1(/[0-9]+/, /_+/)); |
| efe15db | 29 | const HEX_DIGITS = token(sep1(/[0-9a-fA-F]+/, /_+/)); |
| efe15db | 30 | const BIN_DIGITS = token(sep1(/[01]/, /_+/)); |
| efe15db | 31 | const REAL_EXPONENT = token(seq(/[eE]/, optional(/[+-]/), DEC_DIGITS)); |
| efe15db | 32 | |
| 4bd2c1c | 33 | module.exports = grammar({ |
| f997bee | 34 | name: "plum", |
| f997bee | 35 | extras: $ => [ |
| f997bee | 36 | $.comment, |
| f997bee | 37 | /[\s\f\uFEFF\u2060\u200B]|\r?\n/, |
| f997bee | 38 | // $.line_continuation, |
| f997bee | 39 | ], |
| f997bee | 40 | externals: $ => [ |
| f997bee | 41 | $._newline, |
| f997bee | 42 | $._indent, |
| f997bee | 43 | $._dedent, |
| f997bee | 44 | $.string_start, |
| f997bee | 45 | $._string_content, |
| f997bee | 46 | $.escape_interpolation, |
| f997bee | 47 | $.string_end, |
| f997bee | 48 | |
| f997bee | 49 | // Mark comments as external tokens so that the external scanner is always |
| f997bee | 50 | // invoked, even if no external token is expected. This allows for better |
| f997bee | 51 | // error recovery, because the external scanner can maintain the overall |
| f997bee | 52 | // structure by returning dedent tokens whenever a dedent occurs, even |
| f997bee | 53 | // if no dedent is expected. |
| f997bee | 54 | $.comment, |
| f997bee | 55 | |
| f997bee | 56 | // Allow the external scanner to check for the validity of closing brackets |
| f997bee | 57 | // so that it can avoid returning dedent tokens between brackets. |
| f997bee | 58 | ']', |
| f997bee | 59 | ')', |
| f997bee | 60 | '}', |
| 3971d41 | 61 | |
| 3971d41 | 62 | // A zero-width marker the scanner emits only when requested (from the |
| 3971d41 | 63 | // guard_case-repeat position in `match`) and only when the next significant |
| 3971d41 | 64 | // token starts a fresh line at exactly the enclosing block's indentation. |
| 3971d41 | 65 | // Required between successive `guard_case` arms so an inline expression body |
| 3971d41 | 66 | // can't be mistaken for a continuation across the line boundary into the next |
| 3971d41 | 67 | // arm's leading `|` (which doubles as the bitwise-or operator) — see the |
| 3971d41 | 68 | // ARM_BREAK comment in scanner.c for the full rationale. |
| 3971d41 | 69 | $._arm_break, |
| f997bee | 70 | ], |
| a65c75b | 71 | // Conflict arises at the empty-argument-list state (e.g. `Foo()`): the parser cannot |
| a65c75b | 72 | // distinguish fn_argument_list from class_argument_list until it sees content or ')'. |
| a65c75b | 73 | // Declaring the conflict at the argument-list level (not the call level) resolves this. |
| af1776b | 74 | conflicts: ($) => [ |
| af1776b | 75 | [$.fn_argument_list, $.class_argument_list], |
| af1776b | 76 | // `cmp ? sum : bits` (ternary) vs `cmp?` (try-operator postfix) both start |
| af1776b | 77 | // with `primary_expression "?"` — genuinely ambiguous with only 1 token of |
| af1776b | 78 | // lookahead (a `:` may or may not follow much later); GLR resolves it by |
| af1776b | 79 | // trying both and keeping whichever completes. |
| af1776b | 80 | [$.expression, $.try_expression], |
| af1776b | 81 | ], |
| 8f91d27 | 82 | inline: ($) => [$.generic_type], |
| 4bd2c1c | 83 | rules: { |
| 4bd2c1c | 84 | source: ($) => |
| 4bd2c1c | 85 | seq( |
| 58cc293 | 86 | optional($.module), |
| 4bd2c1c | 87 | repeat($.import), |
| 5f2f962 | 88 | repeat(choice($.trait, $.enum, $.fn, $.const, $.test)), |
| 4bd2c1c | 89 | ), |
| 4bd2c1c | 90 | |
| 58cc293 | 91 | module: ($) => seq("module", $.mod_identifier), |
| 4bd2c1c | 92 | import: ($) => seq("import", $.url), |
| a271f34 | 93 | // Each path segment is its OWN named node (not a bare inline regex) so |
| a271f34 | 94 | // every byte of a `url` is owned by some child node — a bare regex used |
| a271f34 | 95 | // directly in a `seq` (as this used to be) leaves the text it matches |
| a271f34 | 96 | // with no node of its own, which Topiary's leaf-collection silently |
| a271f34 | 97 | // drops entirely (it only copies each CHILD's own span, never a parent's |
| a271f34 | 98 | // "gap" text) — `import std/os` formatted down to `import /`. |
| a271f34 | 99 | url: ($) => sep1($.url_segment, "/"), |
| a271f34 | 100 | url_segment: (_) => /[a-zA-Z_][a-zA-Z_0-9]*/, |
| 4bd2c1c | 101 | |
| 8f91d27 | 102 | generics: ($) => seq("[", commaSep1($.generic_type), "]"), |
| 4bd2c1c | 103 | generic_type: ($) => |
| f997bee | 104 | seq($.generic, optional(seq(":", sep1($.type_identifier, "+")))), |
| efe15db | 105 | type: ($) => |
| f997bee | 106 | choice( |
| 287b97c | 107 | // A bare `Name` (no `[...]` generics) is ambiguous with a |
| 287b97c | 108 | // `primary_expression` TypeName reference wherever both a `type` |
| 287b97c | 109 | // and an `expression` are legal in the same position (an enum |
| 287b97c | 110 | // variant's payload parens, which also accept discriminant-value |
| 287b97c | 111 | // expressions) — see `PREC.variantType`'s comment. Real discriminant |
| 287b97c | 112 | // values are always literals (`ReadMin(10)`), never a bare name, so |
| 287b97c | 113 | // this always resolves in favor of "payload type" there. |
| 287b97c | 114 | prec(PREC.variantType, seq( |
| f997bee | 115 | $.type_identifier, |
| f502d22 | 116 | field( |
| f502d22 | 117 | "generics", |
| 8f91d27 | 118 | optional(seq("[", commaSep1($.type), "]")), |
| f502d22 | 119 | ), |
| 287b97c | 120 | )), |
| f997bee | 121 | $.generic, |
| a271f34 | 122 | // A slice type, e.g. `[]Byte` — the only slice element type the |
| a271f34 | 123 | // checker currently accepts is `Byte` (see plum-checker), but the |
| a271f34 | 124 | // grammar itself doesn't restrict the element type. |
| a271f34 | 125 | seq("[", "]", field("element", $.type)), |
| 4bd2c1c | 126 | ), |
| f997bee | 127 | variadic_type: ($) => seq("...", $.type), |
| 4bd2c1c | 128 | |
| 4bd2c1c | 129 | trait: ($) => |
| 4bd2c1c | 130 | seq( |
| 4bd2c1c | 131 | "trait", |
| f997bee | 132 | field("name", $.type_identifier), |
| 4bd2c1c | 133 | field("generics", optional($.generics)), |
| 59e3de6 | 134 | "=", |
| 59e3de6 | 135 | $._indent, |
| 59e3de6 | 136 | field("fields", optional(repeat(alias($.trait_field, $.field)))), |
| 59e3de6 | 137 | $._dedent, |
| 4bd2c1c | 138 | ), |
| 4bd2c1c | 139 | |
| 4bd2c1c | 140 | trait_field: ($) => |
| 4bd2c1c | 141 | seq( |
| 9e7927c | 142 | field("name", $.fn_identifier), |
| 141de54 | 143 | field("params", seq("(", optional(commaSep1(choice($.self, $.param))), ")")), |
| 8f91d27 | 144 | field("returns", optional(seq("->", $.type))), |
| 4bd2c1c | 145 | ), |
| 4bd2c1c | 146 | |
| 4bd2c1c | 147 | param: ($) => |
| 419a88f | 148 | seq( |
| 9e7927c | 149 | field("name", $.var_identifier), |
| 419a88f | 150 | ":", |
| 906b92e | 151 | field("type", choice($.type, $.variadic_type, $.fn_value_type)), |
| 419a88f | 152 | optional(seq("=", field("value", $.expression))), |
| 4bd2c1c | 153 | ), |
| 4bd2c1c | 154 | |
| 906b92e | 155 | fn_value_type: ($) => |
| 906b92e | 156 | seq( |
| 906b92e | 157 | "fn", |
| 906b92e | 158 | "(", |
| 906b92e | 159 | field("params", optional(commaSep1($.type))), |
| 906b92e | 160 | ")", |
| 906b92e | 161 | optional(seq("->", field("returns", $.type))), |
| 906b92e | 162 | ), |
| 906b92e | 163 | |
| 4bd2c1c | 164 | enum: ($) => |
| 4bd2c1c | 165 | seq( |
| fd085d9 | 166 | "enum", |
| f997bee | 167 | field("name", $.type_identifier), |
| 287b97c | 168 | field("generics", optional($.generics)), |
| 5f2f962 | 169 | // Mutually exclusive with each other (an enum is either a plain sum |
| 5f2f962 | 170 | // type possibly claiming trait conformance, OR a discriminant enum |
| 5f2f962 | 171 | // with shared per-variant values — never both) — disambiguated by |
| 5f2f962 | 172 | // content, not just position: `implements` holds bare type names |
| 5f2f962 | 173 | // (`ToStr`), `params` holds `name: Type` pairs (`n: Int`), and |
| 5f2f962 | 174 | // `var_identifier`/`type_identifier` are distinct tokens. |
| 5f2f962 | 175 | optional(choice( |
| 5f2f962 | 176 | field("implements", seq("(", commaSep1($.type_identifier), ")")), |
| 5f2f962 | 177 | field("params", seq("(", commaSep1($.enum_param), ")")), |
| 5f2f962 | 178 | )), |
| f997bee | 179 | "=", |
| f997bee | 180 | $._indent, |
| 5de508e | 181 | optional(repeat(alias($.enum_field, $.field))), |
| f047082 | 182 | field("methods", optional(repeat($.fn))), |
| f997bee | 183 | $._dedent, |
| 4bd2c1c | 184 | ), |
| 4bd2c1c | 185 | |
| 8554271 | 186 | enum_param: ($) => |
| 8554271 | 187 | seq(field("name", $.var_identifier), ":", field("type", $.type)), |
| 8554271 | 188 | |
| 4bd2c1c | 189 | enum_field: ($) => |
| f997bee | 190 | seq( |
| f997bee | 191 | "|", |
| f997bee | 192 | field("name", $.type_identifier), |
| 8554271 | 193 | field( |
| 8554271 | 194 | "parameters", |
| 8554271 | 195 | optional(choice( |
| 287b97c | 196 | // Positional payload, e.g. `Some(Int)`/`Some(T)` — brackets are |
| 287b97c | 197 | // reserved for generics (a type/trait/enum's OWN declared |
| 287b97c | 198 | // params, and instantiating one, e.g. `List[Int]`); a variant's |
| 287b97c | 199 | // payload always uses parens, same as a class field's type. |
| 287b97c | 200 | seq("(", commaSep1($.type), ")"), |
| 3a2119e | 201 | seq("(", commaSep1($.enum_named_field), ")"), // named payload fields, e.g. `Circle(radius: Int)` |
| 287b97c | 202 | seq("(", commaSep1($.expression), ")"), // discriminant value literals |
| 8554271 | 203 | )), |
| 8554271 | 204 | ), |
| f997bee | 205 | ), |
| 4bd2c1c | 206 | |
| 3a2119e | 207 | // A named payload field on an enum variant — `radius: Int` in `Circle(radius: Int)`. |
| 3a2119e | 208 | // Distinct from `enum_param` (a shared param declared on the whole `enum`, e.g. |
| 3a2119e | 209 | // `enum Foo(n: Int) = ...`) even though the two rules look identical — this one's |
| 3a2119e | 210 | // scoped to a single variant's own payload instead. |
| 3a2119e | 211 | enum_named_field: ($) => seq(field("name", $.var_identifier), ":", field("type", $.type)), |
| 3a2119e | 212 | |
| 4bd2c1c | 213 | fn: ($) => |
| 9e7927c | 214 | prec.left( |
| 9e7927c | 215 | seq( |
| a271f34 | 216 | // `extern fun foo(...)` (no `=` body at all) declares a function backed |
| a271f34 | 217 | // by a host-provided wasm import instead of a compiled Plum body — see |
| a271f34 | 218 | // `libs/std/os.plum`'s `printLn`. Whether the body is actually present |
| a271f34 | 219 | // vs. legitimately absent is enforced by `plum-checker` (a clear semantic |
| a271f34 | 220 | // error either way), not the grammar, to keep this rule simple. |
| a271f34 | 221 | field("externKw", optional("extern")), |
| c06ddba | 222 | "fun", |
| 9e7927c | 223 | field("name", $.fn_identifier), |
| 6b5d9db | 224 | // A method's receiver is implicit from nesting the `fn` inside a |
| 6b5d9db | 225 | // `type`/`enum` body (see `class`/`enum` above) — there is no top-level |
| 6b5d9db | 226 | // `<Receiver>` annotation form. `self` may still appear as a bare first |
| 6b5d9db | 227 | // parameter to spell out that a nested `fn` is an instance method; it |
| 6b5d9db | 228 | // carries no type of its own, so the parser discards it rather than |
| 6b5d9db | 229 | // emitting a `param` — see `parse_fn`. |
| 141de54 | 230 | field("params", seq("(", optional(commaSep1(choice($.self, $.param))), ")")), |
| 8f91d27 | 231 | field("returns", optional(seq("->", $.type))), |
| a271f34 | 232 | optional(field("body", seq("=", choice($.expression, $.body)))), |
| 9e7927c | 233 | ) |
| 4bd2c1c | 234 | ), |
| 4bd2c1c | 235 | |
| f997bee | 236 | body: ($) => seq($._indent, repeat($._statement), $._dedent), |
| 4bd2c1c | 237 | |
| a271f34 | 238 | // A `test` block is a top-level, name-carrying group of assertions, compiled |
| a271f34 | 239 | // only when running `plum test` (stripped from normal builds). Its body is |
| a271f34 | 240 | // the same indented statement block as `fn`/`if`/etc. |
| a271f34 | 241 | test: ($) => |
| a271f34 | 242 | seq("test", field("name", $.string), field("body", $.body)), |
| a271f34 | 243 | |
| 5d12d71 | 244 | _statement: ($) => |
| 4bd2c1c | 245 | choice( |
| 5d12d71 | 246 | $.assign, |
| 5d12d71 | 247 | $.break, |
| 5d12d71 | 248 | $.continue, |
| 5d12d71 | 249 | $.assert, |
| 5d12d71 | 250 | $.for, |
| 5d12d71 | 251 | $.while, |
| fc68ba7 | 252 | $.if, |
| fc68ba7 | 253 | $.match, |
| fc68ba7 | 254 | $.return, |
| f997bee | 255 | $.todo, |
| aeaba6f | 256 | $.expression |
| 4bd2c1c | 257 | ), |
| 4bd2c1c | 258 | |
| 58cc293 | 259 | const: ($) => |
| 58cc293 | 260 | seq( |
| e9b4405 | 261 | $.const_identifier, |
| 58cc293 | 262 | "=", |
| e9b4405 | 263 | $.expression, |
| 58cc293 | 264 | ), |
| 58cc293 | 265 | |
| 391c8e4 | 266 | field_target: ($) => |
| 391c8e4 | 267 | seq( |
| 391c8e4 | 268 | field("object", $.primary_expression), |
| 391c8e4 | 269 | ".", |
| 391c8e4 | 270 | field("member", $.fn_identifier), |
| 391c8e4 | 271 | ), |
| 391c8e4 | 272 | |
| 5d12d71 | 273 | assign: ($) => |
| 9e7927c | 274 | seq( |
| 391c8e4 | 275 | commaSep1(choice($.var_identifier, $.field_target)), |
| a271f34 | 276 | field("op", choice("=", ":=")), |
| e9b4405 | 277 | commaSep1($.expression), |
| 4bd2c1c | 278 | ), |
| a9a0147 | 279 | // Outside a `test` body, traps on failure. Inside one, desugars to a |
| a9a0147 | 280 | // non-fatal recorded failure instead — the rest of the block keeps |
| a9a0147 | 281 | // running (see `plum-wasm-codegen`'s `desugarTestsToFns`). |
| f997bee | 282 | assert: ($) => seq("assert", $.expression), |
| f502d22 | 283 | return: ($) => prec.right(2, seq("return", optional($.expression))), |
| 5d12d71 | 284 | break: (_) => prec.left("break"), |
| 5d12d71 | 285 | continue: (_) => prec.left("continue"), |
| f997bee | 286 | todo: (_) => prec.left("todo"), |
| 4bd2c1c | 287 | |
| fc68ba7 | 288 | if: ($) => |
| 4bd2c1c | 289 | seq( |
| 4bd2c1c | 290 | "if", |
| 4bd2c1c | 291 | field("condition", $.expression), |
| 5d12d71 | 292 | field("body", $.body), |
| fc68ba7 | 293 | repeat(field("alternative", $.else_if)), |
| 9e7927c | 294 | optional(field("otherwise", $.else)), |
| 4bd2c1c | 295 | ), |
| fc68ba7 | 296 | else_if: ($) => |
| 5d12d71 | 297 | seq("else if", field("condition", $.expression), field("body", $.body)), |
| fc68ba7 | 298 | else: ($) => seq("else", field("body", $.body)), |
| 4bd2c1c | 299 | |
| 5d12d71 | 300 | for: ($) => |
| 4bd2c1c | 301 | seq( |
| 4bd2c1c | 302 | "for", |
| 9e7927c | 303 | field("left", commaSep1($.var_identifier)), |
| a271f34 | 304 | ":=", |
| a271f34 | 305 | "range", |
| 4bd2c1c | 306 | field("right", $.primary_expression), |
| 5d12d71 | 307 | field("body", $.body), |
| 4bd2c1c | 308 | ), |
| 4bd2c1c | 309 | |
| 5d12d71 | 310 | while: ($) => |
| f997bee | 311 | seq("while", field("condition", $.expression), field("body", $.body)), |
| 9e7927c | 312 | dotted_name: ($) => prec(1, sep1($.var_identifier, ".")), |
| 4bd2c1c | 313 | |
| 4bd2c1c | 314 | // Match cases |
| fc68ba7 | 315 | match: ($) => |
| f997bee | 316 | prec.left( |
| 608d862 | 317 | choice( |
| 608d862 | 318 | seq( |
| 608d862 | 319 | "match", |
| 608d862 | 320 | commaSep1(field("subject", $.expression)), // remove comma use tuples (a, b) and match against tuples |
| 608d862 | 321 | $._indent, |
| 608d862 | 322 | repeat(field("case", $.case)), |
| 608d862 | 323 | $._dedent, |
| 608d862 | 324 | ), |
| 608d862 | 325 | seq( |
| 608d862 | 326 | "match", |
| 608d862 | 327 | $._indent, |
| 3971d41 | 328 | optional( |
| 3971d41 | 329 | seq( |
| 3971d41 | 330 | field("case", $.guard_case), |
| 3971d41 | 331 | repeat(seq($._arm_break, field("case", $.guard_case))), |
| 3971d41 | 332 | ), |
| 3971d41 | 333 | ), |
| 608d862 | 334 | $._dedent, |
| 608d862 | 335 | ), |
| f997bee | 336 | ), |
| fc68ba7 | 337 | ), |
| 4bd2c1c | 338 | |
| 4a2384c | 339 | case: ($) => |
| 4a2384c | 340 | seq( |
| 4a2384c | 341 | commaSep1($.case_pattern), |
| 4a2384c | 342 | // Optional guard: `Fantasy(b) when b.hasMythicalCreatures => ...`. |
| 4a2384c | 343 | // Patterns must still match positionally for the guard to even run; |
| 4a2384c | 344 | // a matching case whose guard evaluates false falls through to the |
| 4a2384c | 345 | // NEXT case (same fallthrough compileCasePositions already gives a |
| 4a2384c | 346 | // plain pattern mismatch), not to a different position of this case. |
| 4a2384c | 347 | optional(seq("when", field("guard", $.expression))), |
| 4a2384c | 348 | "=>", |
| 4a2384c | 349 | field("body", choice($.expression, $.body)), |
| 4a2384c | 350 | ), |
| 4bd2c1c | 351 | |
| efe15db | 352 | case_pattern: ($) => |
| efe15db | 353 | prec( |
| efe15db | 354 | 1, |
| efe15db | 355 | choice( |
| efe15db | 356 | $.class_pattern, |
| f502d22 | 357 | $.type_identifier, |
| efe15db | 358 | $.string, |
| efe15db | 359 | $.integer, |
| efe15db | 360 | $.float, |
| efe15db | 361 | $.dotted_name, |
| efe15db | 362 | "_", |
| efe15db | 363 | ), |
| efe15db | 364 | ), |
| fc68ba7 | 365 | |
| 608d862 | 366 | guard_case: ($) => |
| 608d862 | 367 | seq( |
| 608d862 | 368 | "|", |
| 608d862 | 369 | field("guard", choice($.expression, "_")), |
| 608d862 | 370 | "=>", |
| 608d862 | 371 | field("body", choice($.expression, $.body)), |
| 608d862 | 372 | ), |
| 608d862 | 373 | |
| 4bd2c1c | 374 | class_pattern: ($) => |
| 4bd2c1c | 375 | seq( |
| f502d22 | 376 | $.type_identifier, |
| 4bd2c1c | 377 | "(", |
| 4bd2c1c | 378 | optional(seq(commaSep1($.case_pattern), optional(","))), |
| 5d12d71 | 379 | ")", |
| 4bd2c1c | 380 | ), |
| 4bd2c1c | 381 | |
| 4bd2c1c | 382 | expression: ($) => |
| 4bd2c1c | 383 | choice( |
| 4bd2c1c | 384 | $.comparison_operator, |
| 4bd2c1c | 385 | $.not_operator, |
| 4bd2c1c | 386 | $.boolean_operator, |
| 906b92e | 387 | $.closure, |
| 4bd2c1c | 388 | $.primary_expression, |
| 5d12d71 | 389 | $.ternary_expression, |
| ec5336f | 390 | $.elvis_expression, |
| 4bd2c1c | 391 | ), |
| 4bd2c1c | 392 | |
| 4bd2c1c | 393 | primary_expression: ($) => |
| 4bd2c1c | 394 | choice( |
| 4bd2c1c | 395 | $.binary_operator, |
| c05610c | 396 | $.self, |
| 9e7927c | 397 | $.var_identifier, |
| f997bee | 398 | $.type_identifier, |
| a271f34 | 399 | $.const_identifier, |
| 4bd2c1c | 400 | $.string, |
| 4bd2c1c | 401 | $.integer, |
| 4bd2c1c | 402 | $.float, |
| 4bd2c1c | 403 | $.unary_operator, |
| 4bd2c1c | 404 | $.attribute, |
| af1776b | 405 | $.try_expression, |
| 43e5250 | 406 | $.safe_attribute, |
| f997bee | 407 | $.fn_call, |
| d322d05 | 408 | $.class_call, |
| 5d12d71 | 409 | $.parenthesized_expression, |
| 4bd2c1c | 410 | ), |
| 4bd2c1c | 411 | |
| 4bd2c1c | 412 | parenthesized_expression: ($) => |
| f997bee | 413 | prec(PREC.parenthesized_expression, seq("{", $.expression, "}")), |
| 4bd2c1c | 414 | |
| 4bd2c1c | 415 | not_operator: ($) => |
| 4bd2c1c | 416 | prec(PREC.not, seq("!", field("argument", $.expression))), |
| 4bd2c1c | 417 | |
| 4bd2c1c | 418 | boolean_operator: ($) => |
| 4bd2c1c | 419 | choice( |
| fc68ba7 | 420 | prec.left( |
| fc68ba7 | 421 | PREC.and, |
| fc68ba7 | 422 | seq( |
| fc68ba7 | 423 | field("left", $.expression), |
| fc68ba7 | 424 | field("operator", "&&"), |
| fc68ba7 | 425 | field("right", $.expression), |
| fc68ba7 | 426 | ), |
| 5d12d71 | 427 | ), |
| fc68ba7 | 428 | prec.left( |
| fc68ba7 | 429 | PREC.or, |
| fc68ba7 | 430 | seq( |
| fc68ba7 | 431 | field("left", $.expression), |
| fc68ba7 | 432 | field("operator", "||"), |
| fc68ba7 | 433 | field("right", $.expression), |
| fc68ba7 | 434 | ), |
| 4bd2c1c | 435 | ), |
| 4bd2c1c | 436 | ), |
| 4bd2c1c | 437 | |
| 4bd2c1c | 438 | binary_operator: ($) => { |
| 4bd2c1c | 439 | const table = [ |
| 4bd2c1c | 440 | [prec.left, "+", PREC.plus], |
| 4bd2c1c | 441 | [prec.left, "-", PREC.plus], |
| 4bd2c1c | 442 | [prec.left, "*", PREC.times], |
| 4bd2c1c | 443 | [prec.left, "/", PREC.times], |
| 4bd2c1c | 444 | [prec.left, "%", PREC.times], |
| 4bd2c1c | 445 | [prec.left, "|", PREC.bitwise_or], |
| 4bd2c1c | 446 | [prec.left, "&", PREC.bitwise_and], |
| 4bd2c1c | 447 | [prec.left, "^", PREC.xor], |
| 4bd2c1c | 448 | [prec.left, "<<", PREC.shift], |
| 4bd2c1c | 449 | [prec.left, ">>", PREC.shift], |
| 4bd2c1c | 450 | ]; |
| 4bd2c1c | 451 | // @ts-ignore |
| 4bd2c1c | 452 | return choice( |
| 5d12d71 | 453 | ...table.map(([cb, operator, precedence]) => |
| 5d12d71 | 454 | cb( |
| 4bd2c1c | 455 | precedence, |
| 4bd2c1c | 456 | seq( |
| 4bd2c1c | 457 | field("left", $.primary_expression), |
| 4bd2c1c | 458 | field("operator", operator), |
| 5d12d71 | 459 | field("right", $.primary_expression), |
| 5d12d71 | 460 | ), |
| 5d12d71 | 461 | ), |
| 5d12d71 | 462 | ), |
| 4bd2c1c | 463 | ); |
| 4bd2c1c | 464 | }, |
| 4bd2c1c | 465 | |
| 4bd2c1c | 466 | unary_operator: ($) => |
| 4bd2c1c | 467 | prec( |
| 4bd2c1c | 468 | PREC.unary, |
| 4bd2c1c | 469 | seq( |
| 4bd2c1c | 470 | field("operator", choice("+", "-")), |
| 5d12d71 | 471 | field("argument", $.primary_expression), |
| 5d12d71 | 472 | ), |
| 4bd2c1c | 473 | ), |
| 4bd2c1c | 474 | |
| 4bd2c1c | 475 | comparison_operator: ($) => |
| 4bd2c1c | 476 | prec.left( |
| 4bd2c1c | 477 | PREC.compare, |
| 4bd2c1c | 478 | seq( |
| 4bd2c1c | 479 | $.primary_expression, |
| 5d12d71 | 480 | field("operator", choice("<", "<=", "==", "!=", ">=", ">", "<>")), |
| 5d12d71 | 481 | $.primary_expression, |
| 5d12d71 | 482 | ), |
| 4bd2c1c | 483 | ), |
| 4bd2c1c | 484 | |
| 5d12d71 | 485 | closure: ($) => |
| 04d2270 | 486 | // Lower than every other expression's precedence (including |
| 04d2270 | 487 | // `ternary_expression`'s own, deliberately low, `PREC.conditional`) so |
| 04d2270 | 488 | // that an inline ternary body (`|n| cond ? a : b`) is resolved as the |
| 04d2270 | 489 | // TERNARY EXTENDING the closure's own body, not as an outer ternary |
| 04d2270 | 490 | // wrapping the whole (already-complete) closure as its condition — |
| 04d2270 | 491 | // without this, the parser preferred reducing `closure` (default |
| 04d2270 | 492 | // precedence 0, higher than ternary_expression's -1) the moment it saw |
| 04d2270 | 493 | // `?`, producing `(|n| cond) ? a : b` instead of `|n| (cond ? a : b)`. |
| 04d2270 | 494 | prec( |
| 04d2270 | 495 | PREC.closure, |
| 04d2270 | 496 | seq( |
| 04d2270 | 497 | "|", |
| 04d2270 | 498 | field("parameters", optional(commaSep1($.var_identifier))), |
| 04d2270 | 499 | "|", |
| 04d2270 | 500 | // A closure body is either a single inline expression (`|v| v`, usable as an |
| 04d2270 | 501 | // ordinary call argument like `each(|v| v)`) or an indented block — the same |
| 04d2270 | 502 | // `choice($.expression, $.body)` shape already used by `fn` and `case` bodies. |
| 04d2270 | 503 | field("body", choice($.expression, $.body)), |
| 04d2270 | 504 | ), |
| 4bd2c1c | 505 | ), |
| 4bd2c1c | 506 | |
| f502d22 | 507 | // The member name always lexes as `fn_identifier` (a superset of `var_identifier`, |
| f502d22 | 508 | // since plain snake_case names are valid camelCase too) so the parser never has to |
| f502d22 | 509 | // pick between two identifier tokens that could both match the same text — that |
| f502d22 | 510 | // choice was ambiguous and broke `object.method(args)` parsing. |
| 4bd2c1c | 511 | attribute: ($) => |
| 4bd2c1c | 512 | prec( |
| 4bd2c1c | 513 | PREC.call, |
| 4bd2c1c | 514 | seq( |
| 4bd2c1c | 515 | field("object", $.primary_expression), |
| 4bd2c1c | 516 | ".", |
| f502d22 | 517 | field("member", $.fn_identifier), |
| f502d22 | 518 | field("arguments", optional($.fn_argument_list)), |
| 5d12d71 | 519 | ), |
| 4bd2c1c | 520 | ), |
| 4bd2c1c | 521 | |
| af1776b | 522 | // Rust-style error-propagation postfix: `expr?` — unwraps a `Result`'s `Ok` |
| af1776b | 523 | // or an `Option`'s `Some`, or exits the enclosing function early with the |
| af1776b | 524 | // `Err`/`None` value as-is otherwise. Same postfix shape/precedence as |
| af1776b | 525 | // `attribute` (`.`) just above. |
| af1776b | 526 | try_expression: ($) => |
| af1776b | 527 | seq(field("value", $.primary_expression), "?"), |
| af1776b | 528 | |
| 43e5250 | 529 | // Groovy/Kotlin-style safe navigation: `obj?.member` / `obj?.method(args)` |
| 43e5250 | 530 | // on a `Result`/`Option` value — same shape as `attribute` just above, but |
| 43e5250 | 531 | // with a `?.` token in place of `.`. `?.` is its own literal token, so the |
| 43e5250 | 532 | // lexer's usual longest-match rule already picks it over a bare `?` (the |
| 43e5250 | 533 | // `try_expression` token) with no extra grammar conflict needed, the same |
| 43e5250 | 534 | // way `elvis_expression`'s `?:` needed none. Desugars entirely in |
| 43e5250 | 535 | // `parser.rs` (into `.map(|v| v.member(...))`, reusing Option/Result's |
| 43e5250 | 536 | // existing generic `.map`) — nothing past the parser ever sees this node. |
| 43e5250 | 537 | safe_attribute: ($) => |
| 43e5250 | 538 | prec( |
| 43e5250 | 539 | PREC.call, |
| 43e5250 | 540 | seq( |
| 43e5250 | 541 | field("object", $.primary_expression), |
| 43e5250 | 542 | "?.", |
| 43e5250 | 543 | field("member", $.fn_identifier), |
| 43e5250 | 544 | field("arguments", optional($.fn_argument_list)), |
| 43e5250 | 545 | ), |
| 43e5250 | 546 | ), |
| 43e5250 | 547 | |
| f502d22 | 548 | // The callee name lexes as `var_identifier` (widened to a superset of |
| f502d22 | 549 | // `fn_identifier`'s charset below) rather than `fn_identifier` — using two |
| f502d22 | 550 | // different identifier tokens here was ambiguous for any all-lowercase, |
| f502d22 | 551 | // no-underscore callee (e.g. `factorial(...)`), since that text matches both |
| f502d22 | 552 | // token rules and the lexer would sometimes commit to the wrong one before |
| f502d22 | 553 | // the parser could see the following `(`. |
| f997bee | 554 | fn_call: ($) => |
| f997bee | 555 | prec(PREC.call, seq( |
| 96ec67f | 556 | field("function", choice($.var_identifier, $.type_identifier)), |
| f997bee | 557 | field( |
| f997bee | 558 | "arguments", |
| 58cc293 | 559 | $.fn_argument_list, |
| 5d12d71 | 560 | ), |
| f997bee | 561 | )), |
| f997bee | 562 | |
| 58cc293 | 563 | fn_argument_list: ($) => |
| 4bd2c1c | 564 | seq( |
| 4bd2c1c | 565 | "(", |
| 4bd2c1c | 566 | optional( |
| 5d12d71 | 567 | commaSep1(choice($.expression, $.keyword_argument, $.pair_argument)), |
| 4bd2c1c | 568 | ), |
| 4bd2c1c | 569 | optional(","), |
| 5d12d71 | 570 | ")", |
| 4bd2c1c | 571 | ), |
| 4bd2c1c | 572 | |
| 4bd2c1c | 573 | keyword_argument: ($) => |
| 9e7927c | 574 | seq(field("name", $.var_identifier), "=", field("value", $.expression)), |
| 4bd2c1c | 575 | |
| 4bd2c1c | 576 | pair_argument: ($) => |
| 4bd2c1c | 577 | seq(field("name", $.string), "=>", field("value", $.expression)), |
| 4bd2c1c | 578 | |
| d322d05 | 579 | class_call: ($) => |
| d322d05 | 580 | prec(PREC.call, seq( |
| d322d05 | 581 | field("type", $.type_identifier), |
| a271f34 | 582 | // Explicit type arguments (`List[Int](...)`) — needed whenever a |
| a271f34 | 583 | // generic class's fields alone can't pin down its type params (e.g. |
| a271f34 | 584 | // constructing an EMPTY `List` with no element anywhere in the call |
| a271f34 | 585 | // to infer `T` from). |
| a271f34 | 586 | field("generics", optional(seq("[", commaSep1($.type), "]"))), |
| d322d05 | 587 | field( |
| d322d05 | 588 | "arguments", |
| d322d05 | 589 | $.class_argument_list, |
| d322d05 | 590 | ), |
| d322d05 | 591 | )), |
| d322d05 | 592 | |
| d322d05 | 593 | class_argument_list: ($) => |
| d322d05 | 594 | seq( |
| d322d05 | 595 | "(", |
| 13507fa | 596 | optional(seq( |
| 13507fa | 597 | commaSep1(choice($.spread_argument, $.field_argument)), |
| 13507fa | 598 | optional(","), |
| 13507fa | 599 | )), |
| d322d05 | 600 | ")", |
| d322d05 | 601 | ), |
| d322d05 | 602 | |
| 13507fa | 603 | field_argument: ($) => |
| 13507fa | 604 | seq(field("name", $.var_identifier), ":", field("value", $.expression)), |
| 13507fa | 605 | |
| 13507fa | 606 | // Gleam-style functional-update entry: `Point(..p, x: 10)` — fields not |
| 13507fa | 607 | // named after it are read from `p` instead of being required. Only ever |
| 13507fa | 608 | // valid as the FIRST argument — enforced by `plum-core`'s parser, not here. |
| 13507fa | 609 | spread_argument: ($) => |
| 13507fa | 610 | seq("..", field("value", $.expression)), |
| 13507fa | 611 | |
| 4bd2c1c | 612 | ternary_expression: ($) => |
| 4bd2c1c | 613 | prec.right( |
| 4bd2c1c | 614 | PREC.conditional, |
| 5d12d71 | 615 | seq($.expression, "?", $.expression, ":", $.expression), |
| 4bd2c1c | 616 | ), |
| 4bd2c1c | 617 | |
| ec5336f | 618 | // Elvis / null-coalescing: `opt ?: default` — if `left` is `Ok(v)`/`Some(v)`, |
| ec5336f | 619 | // the value is `v`; if it's `Err(_)`/`None`, the value is `right`. A single |
| ec5336f | 620 | // atomic `"?:"` token (maximal munch out-munches the bare `"?"` ternary/ |
| ec5336f | 621 | // try-operator tokens at the same position, so — unlike `try_expression`, |
| ec5336f | 622 | // which genuinely shares ternary's leading `?` and needs the `conflicts` |
| ec5336f | 623 | // entry above — this needs no special conflict handling). |
| ec5336f | 624 | elvis_expression: ($) => |
| ec5336f | 625 | prec.right( |
| ec5336f | 626 | PREC.conditional, |
| ec5336f | 627 | seq($.expression, "?:", $.expression), |
| ec5336f | 628 | ), |
| ec5336f | 629 | |
| efe15db | 630 | // ========== |
| efe15db | 631 | // Literals |
| efe15db | 632 | // ========== |
| efe15db | 633 | |
| f997bee | 634 | string: $ => seq( |
| f997bee | 635 | $.string_start, |
| f997bee | 636 | repeat(choice($.interpolation, $.string_content)), |
| f997bee | 637 | $.string_end, |
| f997bee | 638 | ), |
| f997bee | 639 | string_content: $ => prec.right(repeat1( |
| 5d12d71 | 640 | choice( |
| f997bee | 641 | $.escape_interpolation, |
| f997bee | 642 | $.escape_sequence, |
| f997bee | 643 | $._not_escape_sequence, |
| f997bee | 644 | $._string_content, |
| f997bee | 645 | ))), |
| f997bee | 646 | |
| f997bee | 647 | interpolation: $ => seq( |
| f997bee | 648 | '{', |
| 2ec05c8 | 649 | $.expression, |
| f997bee | 650 | '}', |
| f997bee | 651 | ), |
| f997bee | 652 | |
| f997bee | 653 | escape_sequence: _ => token.immediate(prec(1, seq( |
| f997bee | 654 | '\\', |
| f997bee | 655 | choice( |
| f997bee | 656 | /u[a-fA-F\d]{4}/, |
| f997bee | 657 | /U[a-fA-F\d]{8}/, |
| f997bee | 658 | /x[a-fA-F\d]{2}/, |
| f997bee | 659 | /\d{1,3}/, |
| f997bee | 660 | /\r?\n/, |
| f997bee | 661 | /['"abfrntv\\]/, |
| f997bee | 662 | /N\{[^}]+\}/, |
| 4bd2c1c | 663 | ), |
| f997bee | 664 | ))), |
| f997bee | 665 | |
| f997bee | 666 | _not_escape_sequence: _ => token.immediate('\\'), |
| 2634d00 | 667 | |
| 2634d00 | 668 | float: ($) => |
| 2634d00 | 669 | token( |
| 2634d00 | 670 | choice( |
| 2634d00 | 671 | seq( |
| 2634d00 | 672 | choice( |
| 2634d00 | 673 | seq(DEC_DIGITS, REAL_EXPONENT), |
| 2634d00 | 674 | seq( |
| 2634d00 | 675 | optional(DEC_DIGITS), |
| 2634d00 | 676 | ".", |
| 2634d00 | 677 | DEC_DIGITS, |
| 2634d00 | 678 | optional(REAL_EXPONENT), |
| 2634d00 | 679 | ), |
| 2634d00 | 680 | ), |
| 2634d00 | 681 | optional(/[fF]/), |
| 2634d00 | 682 | ), |
| 2634d00 | 683 | seq(DEC_DIGITS, /[fF]/), |
| 2634d00 | 684 | ), |
| 2634d00 | 685 | ), |
| 2634d00 | 686 | integer: ($) => |
| 2634d00 | 687 | choice( |
| 2634d00 | 688 | token(seq(optional(/[1-9]/), DEC_DIGITS)), |
| 2634d00 | 689 | token(seq("0", /[xX]/, HEX_DIGITS)), |
| 2634d00 | 690 | token(seq("0", /[bB]/, BIN_DIGITS)), |
| 2634d00 | 691 | ), |
| c05610c | 692 | self: (_) => /self/, |
| 4aca0b2 | 693 | comment: _ => token(seq('#', /.*/)), |
| 5d12d71 | 694 | identifier: (_) => /[_a-z][_a-zA-Z0-9]*/, |
| 8f91d27 | 695 | generic: (_) => /[A-Z]/, // any single uppercase letter — reserved, illegal as a type_identifier |
| f502d22 | 696 | mod_identifier: () => /[a-z][a-z0-9]*(_[a-z0-9]+)*/, // lower snake case |
| f502d22 | 697 | const_identifier: (_) => /[A-Z][A-Z0-9]*(_[A-Z0-9]+)*/, // upper snake case |
| f502d22 | 698 | // Superset of fn_identifier's charset (adds "_") so fn_call's callee can share |
| f502d22 | 699 | // this single token instead of forcing the lexer to pick between two rules. |
| f502d22 | 700 | var_identifier: (_) => /[a-z][a-zA-Z0-9]*(_[a-zA-Z0-9]+)*/, // lower snake case (or camelCase, when used as a callee) |
| f997bee | 701 | fn_identifier: (_) => /[a-z][a-zA-Z0-9]*/, // camel case |
| 8f91d27 | 702 | type_identifier: (_) => /[A-Z][a-zA-Z0-9]+/, // capital case, 2+ chars (single uppercase letters are reserved for `generic`) |
| 4bd2c1c | 703 | }, |
| 4bd2c1c | 704 | }); |
| 4bd2c1c | 705 | |
| 4bd2c1c | 706 | function commaSep1(rule) { |
| 4bd2c1c | 707 | return sep1(rule, ","); |
| 4bd2c1c | 708 | } |
| 4bd2c1c | 709 | |
| f997bee | 710 | function newlineSep1(rule) { |
| f997bee | 711 | return sep1(rule, $._newline); |
| f997bee | 712 | } |
| f997bee | 713 | |
| 4bd2c1c | 714 | function sep1(rule, separator) { |
| 4bd2c1c | 715 | return seq(rule, repeat(seq(separator, rule))); |
| 5de508e | 716 | } |