plum

#treesitter#compiler#wasm

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
}