@rcrsr/rill 0.4.4 → 0.5.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.
Files changed (141) hide show
  1. package/dist/check/config.d.ts.map +1 -1
  2. package/dist/check/config.js +9 -9
  3. package/dist/check/config.js.map +1 -1
  4. package/dist/check/visitor.d.ts.map +1 -1
  5. package/dist/check/visitor.js +1 -0
  6. package/dist/check/visitor.js.map +1 -1
  7. package/dist/cli-check.js +8 -9
  8. package/dist/cli-check.js.map +1 -1
  9. package/dist/cli-eval.js +4 -5
  10. package/dist/cli-eval.js.map +1 -1
  11. package/dist/cli-exec.d.ts.map +1 -1
  12. package/dist/cli-exec.js +2 -3
  13. package/dist/cli-exec.js.map +1 -1
  14. package/dist/cli-shared.d.ts +5 -3
  15. package/dist/cli-shared.d.ts.map +1 -1
  16. package/dist/cli-shared.js +5 -9
  17. package/dist/cli-shared.js.map +1 -1
  18. package/dist/generated/introspection-data.d.ts +2 -0
  19. package/dist/generated/introspection-data.d.ts.map +1 -0
  20. package/dist/generated/introspection-data.js +618 -0
  21. package/dist/generated/introspection-data.js.map +1 -0
  22. package/dist/generated/version-data.d.ts +18 -0
  23. package/dist/generated/version-data.d.ts.map +1 -0
  24. package/dist/generated/version-data.js +16 -0
  25. package/dist/generated/version-data.js.map +1 -0
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +5 -1
  29. package/dist/index.js.map +1 -1
  30. package/dist/lexer/errors.d.ts +3 -2
  31. package/dist/lexer/errors.d.ts.map +1 -1
  32. package/dist/lexer/errors.js +19 -4
  33. package/dist/lexer/errors.js.map +1 -1
  34. package/dist/lexer/operators.d.ts.map +1 -1
  35. package/dist/lexer/operators.js +1 -0
  36. package/dist/lexer/operators.js.map +1 -1
  37. package/dist/lexer/readers.d.ts.map +1 -1
  38. package/dist/lexer/readers.js +4 -4
  39. package/dist/lexer/readers.js.map +1 -1
  40. package/dist/lexer/tokenizer.js +1 -1
  41. package/dist/lexer/tokenizer.js.map +1 -1
  42. package/dist/parser/helpers.d.ts.map +1 -1
  43. package/dist/parser/helpers.js +5 -3
  44. package/dist/parser/helpers.js.map +1 -1
  45. package/dist/parser/parser-collect.js +1 -1
  46. package/dist/parser/parser-collect.js.map +1 -1
  47. package/dist/parser/parser-control.js +6 -4
  48. package/dist/parser/parser-control.js.map +1 -1
  49. package/dist/parser/parser-expr.d.ts.map +1 -1
  50. package/dist/parser/parser-expr.js +24 -4
  51. package/dist/parser/parser-expr.js.map +1 -1
  52. package/dist/parser/parser-extract.js +1 -1
  53. package/dist/parser/parser-extract.js.map +1 -1
  54. package/dist/parser/parser-functions.js +2 -1
  55. package/dist/parser/parser-functions.js.map +1 -1
  56. package/dist/parser/parser-literals.d.ts.map +1 -1
  57. package/dist/parser/parser-literals.js +39 -10
  58. package/dist/parser/parser-literals.js.map +1 -1
  59. package/dist/parser/parser-script.js +2 -0
  60. package/dist/parser/parser-script.js.map +1 -1
  61. package/dist/parser/parser-variables.d.ts +2 -2
  62. package/dist/parser/parser-variables.d.ts.map +1 -1
  63. package/dist/parser/parser-variables.js +28 -12
  64. package/dist/parser/parser-variables.js.map +1 -1
  65. package/dist/parser/state.js +1 -1
  66. package/dist/parser/state.js.map +1 -1
  67. package/dist/runtime/core/callable.d.ts +8 -0
  68. package/dist/runtime/core/callable.d.ts.map +1 -1
  69. package/dist/runtime/core/callable.js +6 -6
  70. package/dist/runtime/core/callable.js.map +1 -1
  71. package/dist/runtime/core/context.d.ts.map +1 -1
  72. package/dist/runtime/core/context.js +17 -8
  73. package/dist/runtime/core/context.js.map +1 -1
  74. package/dist/runtime/core/equals.d.ts.map +1 -1
  75. package/dist/runtime/core/equals.js +7 -1
  76. package/dist/runtime/core/equals.js.map +1 -1
  77. package/dist/runtime/core/eval/base.js +2 -2
  78. package/dist/runtime/core/eval/base.js.map +1 -1
  79. package/dist/runtime/core/eval/mixins/annotations.js +2 -2
  80. package/dist/runtime/core/eval/mixins/annotations.js.map +1 -1
  81. package/dist/runtime/core/eval/mixins/closures.d.ts.map +1 -1
  82. package/dist/runtime/core/eval/mixins/closures.js +32 -26
  83. package/dist/runtime/core/eval/mixins/closures.js.map +1 -1
  84. package/dist/runtime/core/eval/mixins/collections.js +13 -13
  85. package/dist/runtime/core/eval/mixins/collections.js.map +1 -1
  86. package/dist/runtime/core/eval/mixins/control-flow.js +1 -1
  87. package/dist/runtime/core/eval/mixins/control-flow.js.map +1 -1
  88. package/dist/runtime/core/eval/mixins/core.d.ts.map +1 -1
  89. package/dist/runtime/core/eval/mixins/core.js +37 -14
  90. package/dist/runtime/core/eval/mixins/core.js.map +1 -1
  91. package/dist/runtime/core/eval/mixins/expressions.d.ts +2 -0
  92. package/dist/runtime/core/eval/mixins/expressions.d.ts.map +1 -1
  93. package/dist/runtime/core/eval/mixins/expressions.js +80 -27
  94. package/dist/runtime/core/eval/mixins/expressions.js.map +1 -1
  95. package/dist/runtime/core/eval/mixins/extraction.js +14 -14
  96. package/dist/runtime/core/eval/mixins/extraction.js.map +1 -1
  97. package/dist/runtime/core/eval/mixins/literals.d.ts +4 -1
  98. package/dist/runtime/core/eval/mixins/literals.d.ts.map +1 -1
  99. package/dist/runtime/core/eval/mixins/literals.js +117 -14
  100. package/dist/runtime/core/eval/mixins/literals.js.map +1 -1
  101. package/dist/runtime/core/eval/mixins/types.js +3 -3
  102. package/dist/runtime/core/eval/mixins/types.js.map +1 -1
  103. package/dist/runtime/core/eval/mixins/variables.d.ts.map +1 -1
  104. package/dist/runtime/core/eval/mixins/variables.js +121 -31
  105. package/dist/runtime/core/eval/mixins/variables.js.map +1 -1
  106. package/dist/runtime/core/execute.js +2 -2
  107. package/dist/runtime/core/execute.js.map +1 -1
  108. package/dist/runtime/core/introspection-data.d.ts +2 -0
  109. package/dist/runtime/core/introspection-data.d.ts.map +1 -0
  110. package/dist/runtime/core/introspection-data.js +618 -0
  111. package/dist/runtime/core/introspection-data.js.map +1 -0
  112. package/dist/runtime/core/introspection.d.ts +59 -0
  113. package/dist/runtime/core/introspection.d.ts.map +1 -0
  114. package/dist/runtime/core/introspection.js +120 -0
  115. package/dist/runtime/core/introspection.js.map +1 -0
  116. package/dist/runtime/core/version-data.d.ts +18 -0
  117. package/dist/runtime/core/version-data.d.ts.map +1 -0
  118. package/dist/runtime/core/version-data.js +16 -0
  119. package/dist/runtime/core/version-data.js.map +1 -0
  120. package/dist/runtime/ext/builtins.js +21 -21
  121. package/dist/runtime/ext/builtins.js.map +1 -1
  122. package/dist/runtime/ext/extensions.js +1 -1
  123. package/dist/runtime/ext/extensions.js.map +1 -1
  124. package/dist/runtime/index.d.ts +4 -0
  125. package/dist/runtime/index.d.ts.map +1 -1
  126. package/dist/runtime/index.js +2 -0
  127. package/dist/runtime/index.js.map +1 -1
  128. package/dist/types.d.ts +127 -9
  129. package/dist/types.d.ts.map +1 -1
  130. package/dist/types.js +457 -11
  131. package/dist/types.js.map +1 -1
  132. package/docs/02_types.md +35 -3
  133. package/docs/04_operators.md +22 -1
  134. package/docs/05_control-flow.md +62 -0
  135. package/docs/11_reference.md +9 -5
  136. package/docs/12_examples.md +4 -4
  137. package/docs/14_host-integration.md +152 -0
  138. package/docs/15_grammar.ebnf +33 -6
  139. package/docs/88_errors.md +902 -0
  140. package/docs/99_llm-reference.txt +19 -0
  141. package/package.json +4 -2
@@ -562,6 +562,67 @@ Error halts the loop immediately:
562
562
 
563
563
  ---
564
564
 
565
+ ## Pass
566
+
567
+ The `pass` keyword returns the current pipe value (`$`) unchanged. Use it for explicit identity pass-through in conditional branches and dict values.
568
+
569
+ ### In Conditionals
570
+
571
+ Use `pass` when one branch should preserve the piped value:
572
+
573
+ ```rill
574
+ "input" -> .contains("in") ? pass ! "fallback"
575
+ # Returns "input" (condition true, pass preserves $)
576
+ ```
577
+
578
+ ```rill
579
+ "data" -> .empty ? { error "Empty input" } ! pass
580
+ # Returns "data" (condition false, pass preserves $)
581
+ ```
582
+
583
+ ### In Dict Values
584
+
585
+ Use `pass` to include the piped value in dict construction:
586
+
587
+ ```rill
588
+ "success" -> { [status: pass, code: 0] }
589
+ # Returns [status: "success", code: 0]
590
+ ```
591
+
592
+ ### In Collection Operators
593
+
594
+ Preserve elements conditionally:
595
+
596
+ ```rill
597
+ [1, -2, 3, -4] -> map { ($ > 0) ? pass ! 0 }
598
+ # Returns [1, 0, 3, 0]
599
+ ```
600
+
601
+ ### Why Use Pass?
602
+
603
+ The `pass` keyword provides clearer intent than bare `$`:
604
+
605
+ ```text
606
+ # Less clear - what does $ mean here?
607
+ $cond ? do_something() ! $
608
+
609
+ # More explicit - reader knows this is intentional no-op
610
+ $cond ? do_something() ! pass
611
+ ```
612
+
613
+ ### Pass Behavior
614
+
615
+ | Pattern | Returns | Context |
616
+ |---------|---------|---------|
617
+ | `cond ? pass ! alt` | `$` if true, `alt` if false | Conditional branch |
618
+ | `cond ? alt ! pass` | `alt` if true, `$` if false | Conditional branch |
619
+ | `[key: pass]` | Dict with `$` as value | Dict construction |
620
+ | `-> { pass }` | `$` | Block body |
621
+
622
+ **Note:** `pass` requires pipe context. Using `pass` without `$` bound throws an error.
623
+
624
+ ---
625
+
565
626
  ## Control Flow Summary
566
627
 
567
628
  | Statement | Scope | Effect |
@@ -570,6 +631,7 @@ Error halts the loop immediately:
570
631
  | `$val -> break` | Loop | Exit loop with value |
571
632
  | `return` | Block/Script | Exit block or script with current `$` |
572
633
  | `$val -> return` | Block/Script | Exit block or script with value |
634
+ | `pass` | Any | Returns current `$` unchanged |
573
635
  | `assert cond` | Any | Halt if condition false, pass through on success |
574
636
  | `assert cond "msg"` | Any | Halt with custom message if condition false |
575
637
  | `error "msg"` | Any | Always halt with error message |
@@ -1,10 +1,10 @@
1
- # rill Core Language Specification v0.4.0
1
+ # rill Core Language Specification v0.5.0
2
2
 
3
3
  *From prompts to workflows*
4
4
 
5
5
  rill is a pipe-based scripting language for orchestrating workflows.
6
6
 
7
- > **Experimental (v0.4.0).** Active development. Breaking changes until v1.0.
7
+ > **Experimental (v0.5.0).** Active development. Breaking changes until v1.0.
8
8
 
9
9
  ## Overview
10
10
 
@@ -46,7 +46,7 @@ rill is an imperative scripting language that is dynamically typed and type-safe
46
46
  | Type | `:type` (assert), `:?type` (check) |
47
47
  | Member | `.field`, `[index]` |
48
48
  | Default | `?? value` |
49
- | Existence | `.?field`, `.?field&type` |
49
+ | Existence | `.?field`, `.?$var`, `.?($expr)`, `.?field&type` |
50
50
 
51
51
  See [Operators](04_operators.md) for detailed documentation.
52
52
 
@@ -60,6 +60,7 @@ See [Operators](04_operators.md) for detailed documentation.
60
60
  | `@ body ? cond` | Do-while |
61
61
  | `break` / `$val -> break` | Exit loop |
62
62
  | `return` / `$val -> return` | Exit block or script |
63
+ | `pass` | Returns current `$` unchanged (use in conditionals, dicts) |
63
64
  | `assert cond` / `assert cond "msg"` | Validate condition, halt on failure |
64
65
  | `error "msg"` / `$val -> error` | Halt execution with error message |
65
66
 
@@ -85,7 +86,7 @@ See [Collections](07_collections.md) for detailed documentation.
85
86
  | Number | `123`, `0.5` | `42`, `0.9` | Number value |
86
87
  | Bool | `true`, `false` | `true` | Boolean value |
87
88
  | List | `[a, b]`, `[...$list]` | `["file.ts", 42]`, `[...$a, 3]` | List value |
88
- | Dict | `[k: v]`, `[[k1, k2]: v]` | `[output: "text"]`, `[["a", "b"]: 1]` | Dict value |
89
+ | Dict | `[k: v]`, `[$k: v]`, `[($e): v]` | `[output: "text"]`, `[$key: 1]` | Dict value |
89
90
  | Tuple | `*[...]` | `*[1, 2]`, `*[x: 1, y: 2]` | Tuple value |
90
91
  | Closure | `\|\|{ }` | `\|x\|($x * 2)` | `ScriptCallable` |
91
92
  | Block | `{ body }` | `{ $ + 1 }` | `ScriptCallable` |
@@ -139,7 +140,10 @@ See [Variables](03_variables.md) for detailed documentation.
139
140
  | `$data.($i + 1)` | Computed key |
140
141
  | `$data.(a \|\| b)` | Alternative keys |
141
142
  | `$data.field ?? default` | Default if missing |
142
- | `$data.?field` | Existence check |
143
+ | `$data.?field` | Existence check (literal) |
144
+ | `$data.?$key` | Existence check (variable) |
145
+ | `$data.?($expr)` | Existence check (computed) |
146
+ | `$data.?field&type` | Existence + type check |
143
147
  | `$data.^key` | Annotation reflection |
144
148
 
145
149
  ### Dispatch
@@ -75,7 +75,7 @@ $verification -> ?(.contains("PASS")) {
75
75
 
76
76
  Works through a checklist until complete.
77
77
 
78
- ```rill
78
+ ```text
79
79
  ---
80
80
  args: plan: string
81
81
  ---
@@ -84,7 +84,7 @@ args: plan: string
84
84
  app::prompt("Read {$plan} and find the first unchecked item (- [ ])") :> $status
85
85
 
86
86
  # Work loop
87
- (!$status -> .contains("ALL COMPLETE")) @ {
87
+ $status -> (!.contains("ALL COMPLETE")) @ {
88
88
  """
89
89
  Based on this status:
90
90
  {$}
@@ -387,7 +387,7 @@ Items {$start} through {$end}
387
387
 
388
388
  Uses explicit signals for workflow control.
389
389
 
390
- ```rill
390
+ ```text
391
391
  ---
392
392
  args: task: string
393
393
  exceptions:
@@ -404,7 +404,7 @@ Rules:
404
404
  - Output :::DONE::: when complete
405
405
  """ -> app::prompt() :> $result
406
406
 
407
- (!$result -> .contains(":::DONE:::")) @ {
407
+ $result -> (!.contains(":::DONE:::")) @ {
408
408
  """
409
409
  Continue working on: {$task}
410
410
 
@@ -593,6 +593,150 @@ interface ExecutionResult {
593
593
  }
594
594
  ```
595
595
 
596
+ ## Introspection
597
+
598
+ Discover available functions, access language documentation, and check runtime version at runtime.
599
+
600
+ ### getFunctions()
601
+
602
+ Enumerate all callable functions registered in the runtime context:
603
+
604
+ ```typescript
605
+ import { createRuntimeContext, getFunctions } from '@rcrsr/rill';
606
+
607
+ const ctx = createRuntimeContext({
608
+ functions: {
609
+ greet: {
610
+ params: [
611
+ { name: 'name', type: 'string', description: 'Person to greet' },
612
+ ],
613
+ description: 'Generate a greeting message',
614
+ fn: (args) => `Hello, ${args[0]}!`,
615
+ },
616
+ },
617
+ });
618
+
619
+ const functions = getFunctions(ctx);
620
+ // [
621
+ // {
622
+ // name: 'greet',
623
+ // description: 'Generate a greeting message',
624
+ // params: [{ name: 'name', type: 'string', description: 'Person to greet', defaultValue: undefined }]
625
+ // },
626
+ // ... built-in functions
627
+ // ]
628
+ ```
629
+
630
+ Returns `FunctionMetadata[]` combining:
631
+ 1. Host functions (with full parameter metadata)
632
+ 2. Built-in functions
633
+ 3. Script closures (reads `^(doc: "...")` annotation for description)
634
+
635
+ ### getLanguageReference()
636
+
637
+ Access the bundled rill language reference for LLM prompt context:
638
+
639
+ ```typescript
640
+ import { getLanguageReference } from '@rcrsr/rill';
641
+
642
+ const reference = getLanguageReference();
643
+ // Returns complete language reference text (syntax, operators, types, etc.)
644
+
645
+ // Use in LLM system prompts:
646
+ const systemPrompt = `You are a rill script assistant.
647
+
648
+ ${reference}
649
+
650
+ Help the user write rill scripts.`;
651
+ ```
652
+
653
+ ### VERSION and VERSION_INFO
654
+
655
+ Access runtime version information for logging, diagnostics, or version checks:
656
+
657
+ ```typescript
658
+ import { VERSION, VERSION_INFO } from '@rcrsr/rill';
659
+
660
+ // VERSION: Semver string for display
661
+ console.log(`Running rill ${VERSION}`); // "Running rill 0.5.0"
662
+
663
+ // VERSION_INFO: Structured components for programmatic comparison
664
+ if (VERSION_INFO.major === 0 && VERSION_INFO.minor < 4) {
665
+ console.warn('Rill version too old, upgrade required');
666
+ }
667
+
668
+ // Log full version info
669
+ console.log('Runtime:', {
670
+ version: VERSION,
671
+ major: VERSION_INFO.major,
672
+ minor: VERSION_INFO.minor,
673
+ patch: VERSION_INFO.patch,
674
+ prerelease: VERSION_INFO.prerelease,
675
+ });
676
+ ```
677
+
678
+ **VERSION Constant:**
679
+ - Type: `string`
680
+ - Format: Semver (e.g., `"0.5.0"`, `"1.0.0-beta.1"`)
681
+ - Use: Display in logs, error messages, diagnostics
682
+
683
+ **VERSION_INFO Constant:**
684
+ - Type: `VersionInfo`
685
+ - Fields:
686
+ - `major: number` - Major version (breaking changes)
687
+ - `minor: number` - Minor version (new features)
688
+ - `patch: number` - Patch version (bug fixes)
689
+ - `prerelease?: string` - Prerelease tag if present
690
+ - Use: Programmatic version comparison, compatibility checks
691
+
692
+ **Version Comparison Example:**
693
+
694
+ ```typescript
695
+ import { VERSION_INFO } from '@rcrsr/rill';
696
+
697
+ function checkCompatibility(): boolean {
698
+ const required = { major: 0, minor: 4, patch: 0 };
699
+
700
+ if (VERSION_INFO.major !== required.major) {
701
+ return false; // Breaking change
702
+ }
703
+
704
+ if (VERSION_INFO.minor < required.minor) {
705
+ return false; // Missing features
706
+ }
707
+
708
+ return true;
709
+ }
710
+
711
+ if (!checkCompatibility()) {
712
+ throw new Error(`Requires rill >= 0.4.0, found ${VERSION}`);
713
+ }
714
+ ```
715
+
716
+ ### Introspection Types
717
+
718
+ ```typescript
719
+ interface FunctionMetadata {
720
+ readonly name: string; // Function name (e.g., "math::add")
721
+ readonly description: string; // Human-readable description
722
+ readonly params: readonly ParamMetadata[];
723
+ }
724
+
725
+ interface ParamMetadata {
726
+ readonly name: string; // Parameter name
727
+ readonly type: string; // Type constraint (e.g., "string")
728
+ readonly description: string; // Parameter description
729
+ readonly defaultValue: RillValue | undefined; // Default if optional
730
+ }
731
+
732
+ interface VersionInfo {
733
+ readonly major: number; // Major version (breaking changes)
734
+ readonly minor: number; // Minor version (new features)
735
+ readonly patch: number; // Patch version (bug fixes)
736
+ readonly prerelease?: string; // Prerelease tag if present
737
+ }
738
+ ```
739
+
596
740
  ## I/O Callbacks
597
741
 
598
742
  Handle script I/O through callbacks:
@@ -813,6 +957,14 @@ export { validateHostFunctionArgs };
813
957
  // Value types
814
958
  export type { RillValue, RillArgs };
815
959
 
960
+ // Introspection
961
+ export { getFunctions, getLanguageReference };
962
+ export type { FunctionMetadata, ParamMetadata };
963
+
964
+ // Version information
965
+ export { VERSION, VERSION_INFO };
966
+ export type { VersionInfo };
967
+
816
968
  // Callbacks
817
969
  export type { RuntimeCallbacks, ObservabilityCallbacks };
818
970
  export type { StepStartEvent, StepEndEvent, FunctionCallEvent, FunctionReturnEvent };
@@ -1,4 +1,4 @@
1
- (* rill v0.4.2 Grammar - EBNF Notation *)
1
+ (* rill v0.4.5 Grammar - EBNF Notation *)
2
2
  (* Pipe-based scripting language for prompt-drive workflows *)
3
3
 
4
4
  (* ============================================================ *)
@@ -128,6 +128,10 @@ comparison = additive , [ comparison-op , additive ] ;
128
128
  additive = multiplicative , { ( "+" | "-" ) , multiplicative } ;
129
129
  multiplicative = unary , { ( "*" | "/" | "%" ) , unary } ;
130
130
  unary = [ "-" | "!" ] , postfix-expr ;
131
+ (* Semantic: "!" requires boolean operand. No truthiness coercion.
132
+ !true -> false, !false -> true
133
+ !"string" -> RUNTIME_TYPE_ERROR (Negation requires boolean, got string)
134
+ !0 -> RUNTIME_TYPE_ERROR (Negation requires boolean, got number) *)
131
135
 
132
136
  comparison-op = "==" | "!=" | "<" | ">" | "<=" | ">=" ;
133
137
 
@@ -151,7 +155,21 @@ primary = literal
151
155
  | loop
152
156
  | block
153
157
  | grouped-expr
154
- | spread ; (* *expr - convert list/dict to tuple *)
158
+ | spread (* *expr - convert list/dict to tuple *)
159
+ | pass ;
160
+
161
+ (* Pass: returns current pipe value ($) unchanged.
162
+ Used for explicit identity pass-through in conditionals and dict values.
163
+
164
+ Examples:
165
+ cond ? pass ! "fallback" -- preserve $ when condition true
166
+ cond ? "value" ! pass -- preserve $ when condition false
167
+ [key: pass] -- include $ as dict value
168
+ -> { pass } -- block returns $
169
+
170
+ Semantic: pass requires pipe context. Using pass without $ bound
171
+ throws RUNTIME_UNDEFINED_VARIABLE: Variable '$' not defined *)
172
+ pass = "pass" ;
155
173
 
156
174
  (* implicit-primary: used in contexts where implicit $ is available (body, pipe targets).
157
175
  Includes method-call because .method() expands to $ -> .method().
@@ -220,9 +238,13 @@ list-element = "..." , expression (* spread: inline list elements *)
220
238
 
221
239
  dict = "[" , ":" , "]"
222
240
  | "[" , dict-entry , { "," , dict-entry } , "]" ;
223
- dict-key = identifier | number | bool ;
241
+ dict-key = identifier | number | bool
242
+ | "$" , identifier (* variable key: [$keyName: value] *)
243
+ | "(" , pipe-chain , ")" ; (* computed key: [($expr): value] *)
224
244
  dict-entry = ( dict-key | "[" , expression , { "," , expression } , "]" ) , ":" , expression ;
225
- (* Keys can be identifiers, numbers, booleans, or multi-key lists.
245
+ (* Keys can be identifiers, numbers, booleans, variables, computed expressions, or multi-key lists.
246
+ Variable key: [$varName: value] uses variable value as key (must be string).
247
+ Computed key: [($x -> .upper): value] evaluates expression as key (must be string).
226
248
  Multi-key syntax [k1, k2]: expr maps multiple keys to same value.
227
249
  Semantic: keys "keys", "values", "entries" are reserved (cannot be used as dict keys).
228
250
  Semantic: closure values become "dict closures" where $ is late-bound
@@ -269,8 +291,13 @@ default-value = "??" , body ;
269
291
  (* Existence check: $data.user.?email returns bool
270
292
  Type narrowing uses & (not :) because the expression returns bool.
271
293
  Using :type would suggest type assertion on a non-bool value.
272
- $data.?field&string means "does field exist and is it a string?" -> bool *)
273
- existence-check = ".?" , ( identifier | "$" , identifier ) , [ "&" , type-name ] ;
294
+ $data.?field&string means "does field exist and is it a string?" -> bool
295
+ Variable existence: $data.?$keyName checks existence using variable value.
296
+ Computed existence: $data.?($expr) checks existence using expression result. *)
297
+ existence-check = ".?" , ( identifier
298
+ | "$" , identifier (* variable key *)
299
+ | "(" , pipe-chain , ")" ) (* computed expression *)
300
+ , [ "&" , type-name ] ;
274
301
 
275
302
  digits = digit , { digit } ;
276
303