tree-sitter-bynk 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/grammar.js ADDED
@@ -0,0 +1,1062 @@
1
+ /**
2
+ * @file Tree-sitter grammar for Bynk.
3
+ *
4
+ * Covers the syntactic surface defined by the normative specification
5
+ * (docs/src/spec/, §3–§4), which is generated from this grammar and kept
6
+ * current per increment. Implements the highlighting / structural shape the
7
+ * editor needs; semantic rules (type checking, exhaustiveness, effect
8
+ * propagation, `given` matching) are intentionally left to the LSP.
9
+ *
10
+ * The grammar is permissive in the places where the type checker would
11
+ * reject code anyway — e.g., `capability` declarations parse inside any
12
+ * declaration body; the LSP surfaces the placement error.
13
+ *
14
+ * Validated by parsing the bynkc fixture corpus (tests/fixtures) to zero
15
+ * ERROR/MISSING nodes.
16
+ *
17
+ * @author Bynk project
18
+ * @license see project root
19
+ */
20
+
21
+ /// <reference types="tree-sitter-cli/dsl" />
22
+ // @ts-check
23
+
24
+ const PREC = {
25
+ assert: 0,
26
+ or: 1,
27
+ and: 2,
28
+ is: 3,
29
+ cmp: 4,
30
+ rel: 5,
31
+ add: 6,
32
+ mul: 7,
33
+ unary: 8,
34
+ postfix: 9,
35
+ };
36
+
37
+ module.exports = grammar({
38
+ name: "bynk",
39
+
40
+ externals: ($) => [$.doc_block],
41
+
42
+ extras: ($) => [/\s+/, $.line_comment, $.doc_block],
43
+
44
+ // The DSL allows reserved words to be referenced inside specific rules via
45
+ // tokens; we declare keywords so they take precedence over `identifier`.
46
+ word: ($) => $.identifier,
47
+
48
+ conflicts: ($) => [
49
+ // `match e { … }` / `if e { … }` / `commit e`: after a bare identifier `e`,
50
+ // the `{` is ambiguous between opening the match/if body and opening a
51
+ // record construction/spread `e { … }`. Keep all parses alive; the body
52
+ // content (match arms / block statements vs field inits) decides which one
53
+ // survives at parse time.
54
+ [$.record_construction, $.record_spread, $._primary],
55
+ // v0.20a: `(x …` is ambiguous between a lambda parameter list and a
56
+ // parenthesised expression (and `()` the unit literal) until the
57
+ // closing `)` is followed — or not — by `=>`.
58
+ [$.lambda_param, $._primary],
59
+ [$._primary, $.lambda_expr],
60
+ ],
61
+
62
+ rules: {
63
+ // -- Top level --
64
+
65
+ // A source file is normally one or more top-level units. To keep the
66
+ // highlighter from painting every documentation snippet as one big ERROR,
67
+ // we also parse *fragments* — pieces lifted out of their enclosing unit,
68
+ // which never occur in a real `.bynk` file but appear throughout the docs
69
+ // and in editor scratch buffers. The LSP still flags the structural
70
+ // placement error semantically.
71
+ //
72
+ // The three branches are disjoint on their first token, so no real input
73
+ // is ambiguous between them: a unit opens with `commons`/`context`/`test`;
74
+ // an item fragment opens with another declaration keyword (`type`, `fn`,
75
+ // `service`, `on`, …); and a statement/expression fragment opens with a
76
+ // value keyword (`let`, `if`, `match`, `assert`) or a bare term. Keeping
77
+ // item and expression fragments in separate branches (rather than one
78
+ // mixed `repeat1`) avoids spurious boundary conflicts where a completed
79
+ // declaration abuts a following parenthesised expression.
80
+ source_file: ($) =>
81
+ choice(
82
+ repeat1(
83
+ choice(
84
+ $.commons_decl,
85
+ $.context_decl,
86
+ $.adapter_decl,
87
+ $.integration_decl,
88
+ $.test_decl,
89
+ ),
90
+ ),
91
+ repeat1($._item_fragment),
92
+ $._expr_fragment,
93
+ ),
94
+
95
+ _item_fragment: ($) =>
96
+ choice($._context_body_item, $.handler, $.state_decl, $.key_decl),
97
+
98
+ // The body of a fragment block: statements then an optional tail value,
99
+ // mirroring `block` so multi-statement and bare-expression snippets parse.
100
+ // Spelled as a non-empty choice so the rule never matches the empty input.
101
+ _expr_fragment: ($) =>
102
+ choice(
103
+ seq(repeat1($._statement), optional(field("tail", $._expression))),
104
+ field("tail", $._expression),
105
+ ),
106
+
107
+ commons_decl: ($) =>
108
+ seq(
109
+ "commons",
110
+ field("name", $.qualified_name),
111
+ choice(
112
+ seq("{", repeat($._commons_body_item), "}"),
113
+ // Fragment form: header followed by items to EOF (or the next
114
+ // top-level header).
115
+ repeat($._commons_body_item),
116
+ ),
117
+ ),
118
+
119
+ context_decl: ($) =>
120
+ seq(
121
+ "context",
122
+ field("name", $.qualified_name),
123
+ choice(
124
+ seq("{", repeat($._context_body_item), "}"),
125
+ repeat($._context_body_item),
126
+ ),
127
+ ),
128
+
129
+ // v0.17: an adapter — the host boundary (capability contract + binding).
130
+ // Brace or fragment form, like commons/context.
131
+ adapter_decl: ($) =>
132
+ seq(
133
+ "adapter",
134
+ field("name", $.qualified_name),
135
+ choice(
136
+ seq("{", repeat($._adapter_body_item), "}"),
137
+ repeat($._adapter_body_item),
138
+ ),
139
+ ),
140
+
141
+ // v0.7: test unit targeting a commons or context. Brace or fragment form
142
+ // (a test case is `test "string" …`, so it never collides with a new
143
+ // `test <qualified_name>` unit — but the disambiguation needs one extra
144
+ // token of lookahead, handled by a declared conflict).
145
+ test_decl: ($) =>
146
+ prec.right(
147
+ seq(
148
+ "test",
149
+ field("target", $.qualified_name),
150
+ choice(
151
+ seq("{", repeat($._test_body_item), "}"),
152
+ repeat($._test_body_item),
153
+ ),
154
+ ),
155
+ ),
156
+
157
+ // v0.16: a multi-Worker integration test. `integration` is contextual
158
+ // after `test` and before the suite-name string; `wires` lists the
159
+ // participating contexts. Body holds `uses` and `test "name"` cases (no
160
+ // `mocks` — integration tests wire real implementations).
161
+ integration_decl: ($) =>
162
+ prec.right(
163
+ seq(
164
+ "test",
165
+ "integration",
166
+ field("name", $.string_literal),
167
+ choice(
168
+ seq("{", $.wires_decl, repeat($._integration_body_item), "}"),
169
+ seq($.wires_decl, repeat($._integration_body_item)),
170
+ ),
171
+ ),
172
+ ),
173
+
174
+ wires_decl: ($) => seq("wires", sep1(field("participant", $.qualified_name), ",")),
175
+
176
+ _integration_body_item: ($) => choice($.uses_decl, $.test_case),
177
+
178
+ qualified_name: ($) => sep1($.identifier, "."),
179
+
180
+ _commons_body_item: ($) =>
181
+ choice(
182
+ $.uses_decl,
183
+ $.type_decl,
184
+ $.fn_decl,
185
+ // Permissive: capability/service/etc. can syntactically appear
186
+ // anywhere — the LSP reports semantic placement errors.
187
+ $.capability_decl,
188
+ $.provider_decl,
189
+ $.service_decl,
190
+ $.agent_decl,
191
+ $.actor_decl,
192
+ ),
193
+
194
+ _context_body_item: ($) =>
195
+ choice(
196
+ $.uses_decl,
197
+ $.consumes_decl,
198
+ $.exports_decl,
199
+ $.type_decl,
200
+ $.fn_decl,
201
+ $.capability_decl,
202
+ $.provider_decl,
203
+ $.service_decl,
204
+ $.agent_decl,
205
+ $.actor_decl,
206
+ ),
207
+
208
+ // v0.17: adapter items — a binding clause, capabilities, boundary types,
209
+ // inline pure helpers and `uses`, external providers, and `exports`.
210
+ // v0.18 adds `consumes` (adapter-to-adapter capability dependencies).
211
+ // Permissive (service/agent parse here too); the LSP reports the semantic
212
+ // placement error.
213
+ _adapter_body_item: ($) =>
214
+ choice(
215
+ $.binding_decl,
216
+ $.uses_decl,
217
+ $.consumes_decl,
218
+ $.exports_decl,
219
+ $.type_decl,
220
+ $.fn_decl,
221
+ $.capability_decl,
222
+ $.provider_decl,
223
+ $.service_decl,
224
+ $.agent_decl,
225
+ $.actor_decl,
226
+ ),
227
+
228
+ _test_body_item: ($) =>
229
+ choice($.uses_decl, $.consumes_decl, $.mocks_decl, $.test_case),
230
+
231
+ // -- Headers / clauses --
232
+
233
+ uses_decl: ($) => seq("uses", field("target", $.qualified_name)),
234
+ consumes_decl: ($) =>
235
+ seq(
236
+ "consumes",
237
+ field("target", $.qualified_name),
238
+ optional(
239
+ choice(
240
+ // v0.6: `consumes a.b as Alias`.
241
+ seq("as", field("alias", $.identifier)),
242
+ // v0.17: `consumes a.b { Cap, … }` — selected capabilities.
243
+ seq(
244
+ "{",
245
+ optional(sep1(field("capability", $.identifier), ",")),
246
+ optional(","),
247
+ "}",
248
+ ),
249
+ ),
250
+ ),
251
+ ),
252
+
253
+ // v0.17: an adapter's binding module + optional npm dependency map.
254
+ binding_decl: ($) =>
255
+ seq(
256
+ "binding",
257
+ field("module", $.string_literal),
258
+ optional(
259
+ seq(
260
+ "requires",
261
+ "{",
262
+ optional(sep1($.binding_requirement, ",")),
263
+ optional(","),
264
+ "}",
265
+ ),
266
+ ),
267
+ ),
268
+ binding_requirement: ($) =>
269
+ seq(field("package", $.string_literal), ":", field("range", $.string_literal)),
270
+ exports_decl: ($) =>
271
+ seq(
272
+ "exports",
273
+ // v0.15: `capability` joins the type-visibility keywords here.
274
+ field("kind", choice("opaque", "transparent", "capability")),
275
+ "{",
276
+ optional(sep1(field("name", $.identifier), ",")),
277
+ optional(","),
278
+ "}",
279
+ ),
280
+
281
+ // -- Type declarations --
282
+
283
+ type_decl: ($) =>
284
+ seq(
285
+ "type",
286
+ field("name", $.identifier),
287
+ "=",
288
+ field("body", $._type_body),
289
+ ),
290
+
291
+ _type_body: ($) =>
292
+ choice(
293
+ $.opaque_type,
294
+ $.refined_type,
295
+ $.record_type,
296
+ $.sum_type,
297
+ $.enum_type,
298
+ ),
299
+
300
+ opaque_type: ($) =>
301
+ seq(
302
+ "opaque",
303
+ field("base", $._base_type),
304
+ optional(seq("where", field("refinement", $.refinement))),
305
+ ),
306
+
307
+ refined_type: ($) =>
308
+ prec(
309
+ 1,
310
+ seq(
311
+ field("base", $._base_type),
312
+ optional(seq("where", field("refinement", $.refinement))),
313
+ ),
314
+ ),
315
+
316
+ record_type: ($) =>
317
+ seq(
318
+ "{",
319
+ optional(sep1($.record_field, ",")),
320
+ optional(","),
321
+ "}",
322
+ ),
323
+
324
+ record_field: ($) =>
325
+ seq(
326
+ field("name", $.identifier),
327
+ ":",
328
+ field("type", $._type_ref),
329
+ optional(seq("where", field("refinement", $.refinement))),
330
+ // v0.11: an optional initial-value expression. Meaningful on agent
331
+ // `state` fields; the checker restricts where it applies.
332
+ optional(seq("=", field("init", $._expression))),
333
+ ),
334
+
335
+ sum_type: ($) => prec.right(repeat1($.sum_variant)),
336
+ sum_variant: ($) =>
337
+ seq(
338
+ "|",
339
+ field("name", $.constant_name),
340
+ optional(
341
+ seq(
342
+ "(",
343
+ optional(sep1($.variant_payload_field, ",")),
344
+ optional(","),
345
+ ")",
346
+ ),
347
+ ),
348
+ ),
349
+ variant_payload_field: ($) =>
350
+ seq(
351
+ field("name", $.identifier),
352
+ ":",
353
+ field("type", $._type_ref),
354
+ ),
355
+
356
+ enum_type: ($) =>
357
+ seq(
358
+ "enum",
359
+ "{",
360
+ optional(sep1(field("variant", $.constant_name), ",")),
361
+ optional(","),
362
+ "}",
363
+ ),
364
+
365
+ refinement: ($) => sep1($._refinement_pred, "and"),
366
+ _refinement_pred: ($) =>
367
+ choice(
368
+ $.pred_call,
369
+ $.pred_atom,
370
+ ),
371
+ pred_call: ($) =>
372
+ seq(field("name", $.predicate_name), "(", optional(sep1($._pred_arg, ",")), ")"),
373
+ pred_atom: ($) => $.predicate_name,
374
+ predicate_name: ($) =>
375
+ choice(
376
+ "Matches",
377
+ "InRange",
378
+ "MinLength",
379
+ "MaxLength",
380
+ "Length",
381
+ "NonNegative",
382
+ "Positive",
383
+ "NonEmpty",
384
+ ),
385
+ _pred_arg: ($) => choice($.number_literal, $.float_literal, $.string_literal),
386
+
387
+ _base_type: ($) => $.base_type,
388
+ base_type: () => choice("Int", "String", "Bool", "Float"),
389
+
390
+ _type_ref: ($) =>
391
+ choice(
392
+ $.function_type_ref,
393
+ $._base_type,
394
+ $.unit_type,
395
+ $.validation_error_type,
396
+ $.generic_type_ref,
397
+ $.identifier,
398
+ ),
399
+
400
+ // v0.20a: a function type — `A -> B`, `(A, B) -> C`, `() -> B`,
401
+ // right-associative (prec.right). A parenthesised parameter list is only
402
+ // a function type when followed by `->`; bare `()` stays the unit type.
403
+ function_type_ref: ($) =>
404
+ prec.right(
405
+ seq(
406
+ field(
407
+ "params",
408
+ choice(
409
+ $._base_type,
410
+ $.unit_type,
411
+ $.validation_error_type,
412
+ $.generic_type_ref,
413
+ $.identifier,
414
+ seq("(", sep1($._type_ref, ","), optional(","), ")"),
415
+ ),
416
+ ),
417
+ "->",
418
+ field("return_type", $._type_ref),
419
+ ),
420
+ ),
421
+
422
+ unit_type: () => seq("(", ")"),
423
+ validation_error_type: () => "ValidationError",
424
+ generic_type_ref: ($) =>
425
+ seq(
426
+ field(
427
+ "name",
428
+ choice(
429
+ alias("Result", $.builtin_type),
430
+ alias("Option", $.builtin_type),
431
+ alias("Effect", $.builtin_type),
432
+ // v0.9: HTTP result type.
433
+ alias("HttpResult", $.builtin_type),
434
+ // v0.20b: the built-in collection types.
435
+ alias("List", $.builtin_type),
436
+ alias("Map", $.builtin_type),
437
+ ),
438
+ ),
439
+ "[",
440
+ sep1(field("arg", $._type_ref), ","),
441
+ "]",
442
+ ),
443
+
444
+ // -- Function declarations --
445
+
446
+ fn_decl: ($) =>
447
+ seq(
448
+ "fn",
449
+ field("name", choice($.method_name, $.identifier)),
450
+ // v0.20a: optional `[A, B]` type parameters (functions only).
451
+ optional(seq("[", sep1(field("type_param", $.identifier), ","), "]")),
452
+ "(",
453
+ optional($._params),
454
+ ")",
455
+ "->",
456
+ field("return_type", $._type_ref),
457
+ field("body", $.block),
458
+ ),
459
+ method_name: ($) =>
460
+ seq(field("type", $.identifier), ".", field("method", $.identifier)),
461
+
462
+ _params: ($) =>
463
+ seq(
464
+ choice($.self_param, $.param),
465
+ repeat(seq(",", $.param)),
466
+ optional(","),
467
+ ),
468
+ self_param: () => "self",
469
+ param: ($) =>
470
+ seq(
471
+ field("name", $.identifier),
472
+ ":",
473
+ field("type", $._type_ref),
474
+ ),
475
+
476
+ // -- v0.5: capabilities, providers, services, agents --
477
+
478
+ capability_decl: ($) =>
479
+ seq(
480
+ "capability",
481
+ field("name", $.identifier),
482
+ "{",
483
+ repeat($.capability_op),
484
+ "}",
485
+ ),
486
+ capability_op: ($) =>
487
+ seq(
488
+ "fn",
489
+ field("name", $.identifier),
490
+ "(",
491
+ optional(sep1($.param, ",")),
492
+ optional(","),
493
+ ")",
494
+ "->",
495
+ field("return_type", $._type_ref),
496
+ ),
497
+
498
+ provider_decl: ($) =>
499
+ seq(
500
+ "provides",
501
+ field("capability", $.identifier),
502
+ "=",
503
+ field("provider", $.identifier),
504
+ // v0.12: a provider may depend on other capabilities.
505
+ optional(field("given", $.given_clause)),
506
+ // v0.17: a bodiless provider is *external* — supplied by an adapter's
507
+ // binding. The absence of the brace block (not an empty one) is the
508
+ // signal; the LSP reports placement errors (body in an adapter, or
509
+ // bodiless outside one).
510
+ optional(seq("{", repeat($.provider_op), "}")),
511
+ ),
512
+ provider_op: ($) =>
513
+ seq(
514
+ "fn",
515
+ field("name", $.identifier),
516
+ "(",
517
+ optional(sep1($.param, ",")),
518
+ optional(","),
519
+ ")",
520
+ "->",
521
+ field("return_type", $._type_ref),
522
+ field("body", $.block),
523
+ ),
524
+
525
+ service_decl: ($) =>
526
+ seq(
527
+ "service",
528
+ field("name", $.identifier),
529
+ optional(field("protocol", $.service_protocol)),
530
+ "{",
531
+ repeat($.handler),
532
+ "}",
533
+ ),
534
+ // v0.44: `from <protocol>` on the service header.
535
+ service_protocol: ($) =>
536
+ seq(
537
+ "from",
538
+ choice(
539
+ "http",
540
+ "cron",
541
+ seq("queue", "(", field("name", $.string_literal), ")"),
542
+ ),
543
+ ),
544
+
545
+ agent_decl: ($) =>
546
+ seq(
547
+ "agent",
548
+ field("name", $.identifier),
549
+ "{",
550
+ field("key", $.key_decl),
551
+ field("state", $.state_decl),
552
+ repeat($.handler),
553
+ "}",
554
+ ),
555
+ key_decl: ($) =>
556
+ seq("key", field("name", $.identifier), ":", field("type", $._type_ref)),
557
+ state_decl: ($) =>
558
+ seq(
559
+ "state",
560
+ "{",
561
+ optional(sep1($.record_field, ",")),
562
+ optional(","),
563
+ "}",
564
+ ),
565
+
566
+ // v0.5 `on call`; v0.44 `on GET("path")` / `on schedule("expr")` /
567
+ // `on message(...)` — the protocol lives on the service header.
568
+ handler: ($) =>
569
+ choice($.call_handler, $.http_handler, $.cron_handler, $.queue_handler),
570
+ call_handler: ($) =>
571
+ seq(
572
+ "on",
573
+ "call",
574
+ optional(field("method", $.identifier)),
575
+ optional(field("by", $.by_clause)),
576
+ "(",
577
+ optional(sep1($.param, ",")),
578
+ optional(","),
579
+ ")",
580
+ "->",
581
+ field("return_type", $._type_ref),
582
+ optional(field("given", $.given_clause)),
583
+ field("body", $.block),
584
+ ),
585
+ // v0.44: `on GET("/path") (params) -> …` — the method-builder collapses
586
+ // verb+route into one config expression in the handler-config slot.
587
+ http_handler: ($) =>
588
+ seq(
589
+ "on",
590
+ field("method", $.http_method),
591
+ "(",
592
+ field("path", $.string_literal),
593
+ ")",
594
+ optional(field("by", $.by_clause)),
595
+ "(",
596
+ optional(sep1($.param, ",")),
597
+ optional(","),
598
+ ")",
599
+ "->",
600
+ field("return_type", $._type_ref),
601
+ optional(field("given", $.given_clause)),
602
+ field("body", $.block),
603
+ ),
604
+ http_method: () => choice("GET", "POST", "PUT", "PATCH", "DELETE"),
605
+ // v0.44: `on schedule("<expr>") (at: Int?) -> Effect[Result[(), E]] { … }`.
606
+ cron_handler: ($) =>
607
+ seq(
608
+ "on",
609
+ "schedule",
610
+ "(",
611
+ field("schedule", $.string_literal),
612
+ ")",
613
+ optional(field("by", $.by_clause)),
614
+ "(",
615
+ optional(sep1($.param, ",")),
616
+ optional(","),
617
+ ")",
618
+ "->",
619
+ field("return_type", $._type_ref),
620
+ optional(field("given", $.given_clause)),
621
+ field("body", $.block),
622
+ ),
623
+ // v0.44: `on message(m: T) -> Effect[QueueResult] { … }`. The queue binding
624
+ // lives on the service header (`from queue("name")`).
625
+ queue_handler: ($) =>
626
+ seq(
627
+ "on",
628
+ "message",
629
+ optional(field("by", $.by_clause)),
630
+ "(",
631
+ optional(sep1($.param, ",")),
632
+ optional(","),
633
+ ")",
634
+ "->",
635
+ field("return_type", $._type_ref),
636
+ optional(field("given", $.given_clause)),
637
+ field("body", $.block),
638
+ ),
639
+ given_clause: ($) =>
640
+ // v0.15: a capability may be a bare local name or a dotted cross-context
641
+ // reference (`B.Cap` / `platform.time.Clock`).
642
+ seq("given", sep1(field("capability", $.qualified_name), ",")),
643
+
644
+ // v0.45: an actor declaration — a boundary contract. Normal form
645
+ // `actor Name { auth = Scheme (, identity = Type)? }`; the reserved
646
+ // refinement form `actor Admin = Base where <predicate>` is parsed (and
647
+ // rejected by the checker in Foundations).
648
+ actor_decl: ($) =>
649
+ seq(
650
+ "actor",
651
+ field("name", $.identifier),
652
+ choice(
653
+ seq(
654
+ "{",
655
+ "auth",
656
+ "=",
657
+ field("scheme", $.scheme),
658
+ // v0.47: the Bearer scheme takes a `(secret = "<env>")` config.
659
+ // v0.51: generalised to keyed args — `Scheme(key = value, …)` with
660
+ // string or integer values (Signature: secret/header/timestamp +
661
+ // integer tolerance).
662
+ optional(field("config", $.scheme_config)),
663
+ optional(seq(",", "identity", "=", field("identity", $._type_ref))),
664
+ "}",
665
+ ),
666
+ seq(
667
+ "=",
668
+ field("base", $.identifier),
669
+ "where",
670
+ field("predicate", $.refinement),
671
+ ),
672
+ ),
673
+ ),
674
+ // The closed, compiler-known scheme set. `None`/`Internal` are admitted in
675
+ // Foundations; `Bearer`/`Signature` are reserved-and-rejected.
676
+ scheme: () => choice("None", "Internal", "Bearer", "Signature"),
677
+ // v0.51: the keyed-args scheme config — `(key = value, …)`. Values are
678
+ // string or integer literals; the checker validates which keys each scheme
679
+ // admits (Bearer: secret; Signature: secret/header/timestamp/tolerance).
680
+ scheme_config: ($) =>
681
+ seq(
682
+ "(",
683
+ sep1($.scheme_arg, ","),
684
+ ")",
685
+ ),
686
+ scheme_arg: ($) =>
687
+ seq(
688
+ field("key", $.identifier),
689
+ "=",
690
+ field("value", choice($.string_literal, $.number_literal)),
691
+ ),
692
+ // v0.45: the handler `by <binder>: <Actor>` clause — sits after the protocol
693
+ // config and before the parameters.
694
+ // v0.50: the binder is optional — `by <Actor>` (verify, don't capture the
695
+ // identity) or `by <binder>: <Actor>` (capture it).
696
+ // v0.52: the actor position is a `|`-separated list. One name is the
697
+ // ordinary single-actor handler; more than one is an ordered sum of peer
698
+ // actors (`by who: User | Visitor`), resolved first-wins.
699
+ by_clause: ($) =>
700
+ seq(
701
+ "by",
702
+ optional(seq(field("binder", $.identifier), ":")),
703
+ field("actor", $.identifier),
704
+ repeat(seq("|", field("actor", $.identifier))),
705
+ ),
706
+
707
+ // -- v0.7: test bodies --
708
+
709
+ mocks_decl: ($) =>
710
+ seq(
711
+ "mocks",
712
+ field("capability", $.identifier),
713
+ "=",
714
+ field("impl", $.identifier),
715
+ "{",
716
+ repeat($.provider_op),
717
+ "}",
718
+ ),
719
+ test_case: ($) =>
720
+ seq("test", field("name", $.string_literal), field("body", $.block)),
721
+
722
+ // -- Block & statements --
723
+
724
+ // A block is a run of statements ending in a tail expression (the block's
725
+ // value). The tail is optional because a test body may end in a statement
726
+ // such as a bare `assert`.
727
+ block: ($) =>
728
+ seq("{", repeat($._statement), optional(field("tail", $._expression)), "}"),
729
+
730
+ // `assert` is an expression (v0.9.1) but also appears in statement
731
+ // position within a test body (an expression-statement of type `()`).
732
+ _statement: ($) =>
733
+ choice($.let_stmt, $.effect_let_stmt, $.commit_stmt, prec(1, $.assert_expr)),
734
+
735
+ let_stmt: ($) =>
736
+ seq(
737
+ "let",
738
+ field("name", $._binding_name),
739
+ optional(seq(":", field("type", $._type_ref))),
740
+ "=",
741
+ field("value", $._expression),
742
+ ),
743
+ effect_let_stmt: ($) =>
744
+ seq(
745
+ "let",
746
+ field("name", $._binding_name),
747
+ optional(seq(":", field("type", $._type_ref))),
748
+ "<-",
749
+ field("value", $._expression),
750
+ ),
751
+ commit_stmt: ($) => seq("commit", field("value", $._expression)),
752
+
753
+ // A let/effect-let may bind the discard name `_`.
754
+ _binding_name: ($) => choice($.identifier, alias("_", $.wildcard)),
755
+
756
+ // -- Expressions --
757
+
758
+ _expression: ($) =>
759
+ choice(
760
+ $.if_expr,
761
+ $.match_expr,
762
+ $.is_expr,
763
+ $.assert_expr,
764
+ $.binary_expr,
765
+ $.unary_expr,
766
+ $._primary,
767
+ ),
768
+
769
+ // v0.9.1: `assert` is an expression of type `()`.
770
+ assert_expr: ($) =>
771
+ prec.right(PREC.assert, seq("assert", field("cond", $._expression))),
772
+
773
+ if_expr: ($) =>
774
+ seq(
775
+ "if",
776
+ field("cond", $._expression),
777
+ field("then", $.block),
778
+ "else",
779
+ field("else", choice($.if_expr, $.block)),
780
+ ),
781
+
782
+ match_expr: ($) =>
783
+ seq(
784
+ "match",
785
+ field("disc", $._expression),
786
+ "{",
787
+ repeat($.match_arm),
788
+ "}",
789
+ ),
790
+ // Arms are newline-separated; a trailing comma is permitted but not
791
+ // required.
792
+ match_arm: ($) =>
793
+ prec.right(
794
+ seq(
795
+ field("pattern", $._pattern),
796
+ "=>",
797
+ // `_expression` already includes `block` via `_primary`, so a
798
+ // `{ … }` arm body is covered without a separate alternative.
799
+ field("body", $._expression),
800
+ optional(","),
801
+ ),
802
+ ),
803
+
804
+ _pattern: ($) => choice($.wildcard_pattern, $.variant_pattern),
805
+ wildcard_pattern: () => "_",
806
+ // The variant name is an `identifier`, not `constant_name`: because
807
+ // `word` unifies all word-shaped lexemes to `identifier`, a `constant_name`
808
+ // here can never out-lex `identifier` after `is`/`=>`, which previously
809
+ // mis-parsed `x is Miss` as a `type.` prefix awaiting a dot. Capitalisation
810
+ // is recovered in the highlight query, not the grammar.
811
+ variant_pattern: ($) =>
812
+ prec.right(
813
+ seq(
814
+ optional(seq(field("type", $.identifier), ".")),
815
+ field("variant", $.identifier),
816
+ optional(
817
+ seq(
818
+ "(",
819
+ optional(sep1($._pattern_binding, ",")),
820
+ optional(","),
821
+ ")",
822
+ ),
823
+ ),
824
+ ),
825
+ ),
826
+ _pattern_binding: ($) =>
827
+ choice(
828
+ $.named_binding,
829
+ $.positional_binding,
830
+ ),
831
+ named_binding: ($) =>
832
+ seq(field("field", $.identifier), ":", field("name", choice($.identifier, "_"))),
833
+ positional_binding: ($) => choice($.identifier, "_"),
834
+
835
+ is_expr: ($) =>
836
+ prec.left(
837
+ PREC.is,
838
+ seq(field("value", $._expression), "is", field("pattern", $._pattern)),
839
+ ),
840
+
841
+ binary_expr: ($) =>
842
+ choice(
843
+ prec.left(PREC.or, seq($._expression, "||", $._expression)),
844
+ prec.left(PREC.and, seq($._expression, "&&", $._expression)),
845
+ prec.left(PREC.cmp, seq($._expression, choice("==", "!="), $._expression)),
846
+ prec.left(
847
+ PREC.rel,
848
+ seq($._expression, choice("<", "<=", ">", ">="), $._expression),
849
+ ),
850
+ prec.left(PREC.add, seq($._expression, choice("+", "-"), $._expression)),
851
+ prec.left(PREC.mul, seq($._expression, choice("*", "/"), $._expression)),
852
+ ),
853
+
854
+ unary_expr: ($) =>
855
+ prec.right(PREC.unary, seq(choice("!", "-"), $._expression)),
856
+
857
+ _primary: ($) =>
858
+ choice(
859
+ $.lambda_expr,
860
+ $.paren_expr,
861
+ $.method_call,
862
+ $.field_access,
863
+ $.call,
864
+ $.record_construction,
865
+ $.record_spread,
866
+ $.question_expr,
867
+ $.ok_expr,
868
+ $.err_expr,
869
+ $.some_expr,
870
+ $.none_expr,
871
+ $.effect_pure_expr,
872
+ $.mock_expr,
873
+ $.list_literal,
874
+ $.block,
875
+ $.number_literal,
876
+ $.float_literal,
877
+ $.string_literal,
878
+ $.boolean_literal,
879
+ $.unit_literal,
880
+ $.self_expr,
881
+ $.identifier,
882
+ ),
883
+
884
+ // v0.20a: a lambda — `(params) => expr | block`. Conflicts with
885
+ // paren_expr/unit_literal until the `=>` disambiguates (GLR conflict
886
+ // declared below).
887
+ lambda_expr: ($) =>
888
+ seq(
889
+ "(",
890
+ optional(sep1($.lambda_param, ",")),
891
+ ")",
892
+ "=>",
893
+ field("body", choice($._expression, $.block)),
894
+ ),
895
+
896
+ lambda_param: ($) =>
897
+ seq(field("name", $.identifier), optional(seq(":", field("type", $._type_ref)))),
898
+
899
+ paren_expr: ($) => seq("(", $._expression, ")"),
900
+
901
+ // Higher precedence than `field_access`: when a `(` follows `recv.name`,
902
+ // shift into the call; when it doesn't, fall back to field access. This is
903
+ // what lets `Reps.of(n)` be a call while `p.x` is a field access.
904
+ // `method_call` also subsumes the old `constructor_call` (`Type.method(…)`).
905
+ method_call: ($) =>
906
+ prec.left(
907
+ PREC.postfix + 1,
908
+ seq(
909
+ field("receiver", $._primary),
910
+ ".",
911
+ field("method", $.identifier),
912
+ // v0.22b: optional explicit type arguments on a qualified static —
913
+ // `Json.decode[T](…)` (same-line rule as `call` type application).
914
+ optional(seq("[", sep1(field("type_arg", $._type_ref), ","), "]")),
915
+ "(",
916
+ optional(sep1(field("arg", $._expression), ",")),
917
+ optional(","),
918
+ ")",
919
+ ),
920
+ ),
921
+
922
+ field_access: ($) =>
923
+ prec.left(
924
+ PREC.postfix,
925
+ seq(field("receiver", $._primary), ".", field("field", $.identifier)),
926
+ ),
927
+
928
+ call: ($) =>
929
+ prec.left(
930
+ PREC.postfix,
931
+ seq(
932
+ field("name", $.identifier),
933
+ // v0.20a: optional explicit type arguments — `name[T, U](…)`.
934
+ optional(seq("[", sep1(field("type_arg", $._type_ref), ","), "]")),
935
+ "(",
936
+ optional(sep1(field("arg", $._expression), ",")),
937
+ optional(","),
938
+ ")",
939
+ ),
940
+ ),
941
+
942
+ // v0.20b: a list literal — `[a, b, c]`, optional trailing comma. A
943
+ // *leading* `[` only; type application stays a postfix form on a callee
944
+ // identifier (see `call`).
945
+ list_literal: ($) =>
946
+ seq(
947
+ "[",
948
+ optional(seq(sep1(field("element", $._expression), ","), optional(","))),
949
+ "]",
950
+ ),
951
+
952
+ record_construction: ($) =>
953
+ seq(
954
+ field("type", $.identifier),
955
+ "{",
956
+ optional(sep1($.field_init, ",")),
957
+ optional(","),
958
+ "}",
959
+ ),
960
+ field_init: ($) =>
961
+ choice(
962
+ seq(field("name", $.identifier), ":", field("value", $._expression)),
963
+ field("shorthand", $.identifier),
964
+ ),
965
+
966
+ record_spread: ($) =>
967
+ choice(
968
+ seq(
969
+ field("type", $.identifier),
970
+ "{",
971
+ "...",
972
+ field("base", $._expression),
973
+ repeat(seq(",", $.field_init)),
974
+ optional(","),
975
+ "}",
976
+ ),
977
+ seq(
978
+ "{",
979
+ "...",
980
+ field("base", $._expression),
981
+ repeat(seq(",", $.field_init)),
982
+ optional(","),
983
+ "}",
984
+ ),
985
+ ),
986
+
987
+ question_expr: ($) =>
988
+ prec.left(PREC.postfix, seq($._expression, "?")),
989
+
990
+ ok_expr: ($) => seq("Ok", "(", $._expression, ")"),
991
+ err_expr: ($) => seq("Err", "(", $._expression, ")"),
992
+ some_expr: ($) => seq("Some", "(", $._expression, ")"),
993
+ none_expr: () => "None",
994
+ effect_pure_expr: ($) =>
995
+ seq("Effect", ".", "pure", "(", $._expression, ")"),
996
+
997
+ // v0.9.4 Part B: `Mock[T]` test-context construction. The `[ … ]` here is
998
+ // the bracket syntax otherwise reserved for generics; in expression
999
+ // position it carries the mocked type. The optional argument is either a
1000
+ // parenthesised literal-/variant-pin or a brace record-override (the latter
1001
+ // reuses `field_init`, identical to record construction). The test-context
1002
+ // restriction is semantic and left to the LSP.
1003
+ mock_expr: ($) =>
1004
+ prec.right(
1005
+ seq(
1006
+ "Mock",
1007
+ "[",
1008
+ field("type", $._type_ref),
1009
+ "]",
1010
+ optional(field("arg", $.mock_arg)),
1011
+ ),
1012
+ ),
1013
+ mock_arg: ($) =>
1014
+ choice(
1015
+ seq("(", sep1(field("pin", $._expression), ","), optional(","), ")"),
1016
+ seq("{", optional(sep1($.field_init, ",")), optional(","), "}"),
1017
+ ),
1018
+
1019
+ self_expr: () => "self",
1020
+
1021
+ // -- Tokens --
1022
+
1023
+ identifier: () => /[A-Za-z][A-Za-z0-9_]*/,
1024
+
1025
+ // Constant-style names (capitalised idents used as variant names).
1026
+ // Tree-sitter cannot enforce capitalisation at the lex level without
1027
+ // overshadowing `identifier`; we use a regex.
1028
+ constant_name: () => /[A-Z][A-Za-z0-9_]*/,
1029
+
1030
+ number_literal: () => /[0-9]+/,
1031
+ // v0.21: a float literal — fraction with a digit on both sides of the
1032
+ // `.`, an exponent, or both. Longest-match keeps `1.toFloat()` lexing
1033
+ // as a method call on an integer literal.
1034
+ float_literal: () =>
1035
+ /[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?|[0-9]+[eE][+-]?[0-9]+/,
1036
+ string_literal: ($) =>
1037
+ seq(
1038
+ '"',
1039
+ repeat(choice(/[^"\\\n]/, /\\[nt"\\]/, $.string_interpolation)),
1040
+ '"',
1041
+ ),
1042
+ // v0.43: an interpolation hole `\(expr)`. A bare `\(` was an invalid
1043
+ // escape, so this is backward-compatible; `\\(` is an escaped backslash
1044
+ // followed by a literal `(`, so it is not a hole.
1045
+ string_interpolation: ($) => seq('\\(', $._expression, ')'),
1046
+ boolean_literal: () => choice("true", "false"),
1047
+ unit_literal: () => seq("(", ")"),
1048
+
1049
+ // `--` line comment. The external scanner consumes `---+`-starting
1050
+ // lines as `doc_block` tokens before the regex tokenizer runs, so
1051
+ // line_comment sees only `--` (or `-` chars followed by a non-third
1052
+ // dash sequence that's already been gobbled by the external scanner).
1053
+ line_comment: () => token(seq("--", /[^\n]*/)),
1054
+
1055
+ // `doc_block` is provided by the external scanner — see src/scanner.c.
1056
+ // The grammar references it via the `externals` array above.
1057
+ },
1058
+ });
1059
+
1060
+ function sep1(rule, separator) {
1061
+ return seq(rule, repeat(seq(separator, rule)));
1062
+ }