@rcrsr/rill 0.1.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 (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +187 -0
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +69 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/demo.d.ts +6 -0
  8. package/dist/demo.d.ts.map +1 -0
  9. package/dist/demo.js +121 -0
  10. package/dist/demo.js.map +1 -0
  11. package/dist/index.d.ts +10 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +9 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/lexer/errors.d.ts +9 -0
  16. package/dist/lexer/errors.d.ts.map +1 -0
  17. package/dist/lexer/errors.js +12 -0
  18. package/dist/lexer/errors.js.map +1 -0
  19. package/dist/lexer/helpers.d.ts +14 -0
  20. package/dist/lexer/helpers.d.ts.map +1 -0
  21. package/dist/lexer/helpers.js +30 -0
  22. package/dist/lexer/helpers.js.map +1 -0
  23. package/dist/lexer/index.d.ts +8 -0
  24. package/dist/lexer/index.d.ts.map +1 -0
  25. package/dist/lexer/index.js +8 -0
  26. package/dist/lexer/index.js.map +1 -0
  27. package/dist/lexer/operators.d.ts +11 -0
  28. package/dist/lexer/operators.d.ts.map +1 -0
  29. package/dist/lexer/operators.js +58 -0
  30. package/dist/lexer/operators.js.map +1 -0
  31. package/dist/lexer/readers.d.ts +12 -0
  32. package/dist/lexer/readers.d.ts.map +1 -0
  33. package/dist/lexer/readers.js +144 -0
  34. package/dist/lexer/readers.js.map +1 -0
  35. package/dist/lexer/state.d.ts +18 -0
  36. package/dist/lexer/state.d.ts.map +1 -0
  37. package/dist/lexer/state.js +37 -0
  38. package/dist/lexer/state.js.map +1 -0
  39. package/dist/lexer/tokenizer.d.ts +9 -0
  40. package/dist/lexer/tokenizer.d.ts.map +1 -0
  41. package/dist/lexer/tokenizer.js +100 -0
  42. package/dist/lexer/tokenizer.js.map +1 -0
  43. package/dist/lexer.d.ts +19 -0
  44. package/dist/lexer.d.ts.map +1 -0
  45. package/dist/lexer.js +344 -0
  46. package/dist/lexer.js.map +1 -0
  47. package/dist/parser/arithmetic.d.ts +16 -0
  48. package/dist/parser/arithmetic.d.ts.map +1 -0
  49. package/dist/parser/arithmetic.js +128 -0
  50. package/dist/parser/arithmetic.js.map +1 -0
  51. package/dist/parser/boolean.d.ts +15 -0
  52. package/dist/parser/boolean.d.ts.map +1 -0
  53. package/dist/parser/boolean.js +20 -0
  54. package/dist/parser/boolean.js.map +1 -0
  55. package/dist/parser/control-flow.d.ts +56 -0
  56. package/dist/parser/control-flow.d.ts.map +1 -0
  57. package/dist/parser/control-flow.js +167 -0
  58. package/dist/parser/control-flow.js.map +1 -0
  59. package/dist/parser/expressions.d.ts +23 -0
  60. package/dist/parser/expressions.d.ts.map +1 -0
  61. package/dist/parser/expressions.js +950 -0
  62. package/dist/parser/expressions.js.map +1 -0
  63. package/dist/parser/extraction.d.ts +48 -0
  64. package/dist/parser/extraction.d.ts.map +1 -0
  65. package/dist/parser/extraction.js +279 -0
  66. package/dist/parser/extraction.js.map +1 -0
  67. package/dist/parser/functions.d.ts +20 -0
  68. package/dist/parser/functions.d.ts.map +1 -0
  69. package/dist/parser/functions.js +96 -0
  70. package/dist/parser/functions.js.map +1 -0
  71. package/dist/parser/helpers.d.ts +94 -0
  72. package/dist/parser/helpers.d.ts.map +1 -0
  73. package/dist/parser/helpers.js +225 -0
  74. package/dist/parser/helpers.js.map +1 -0
  75. package/dist/parser/index.d.ts +49 -0
  76. package/dist/parser/index.d.ts.map +1 -0
  77. package/dist/parser/index.js +73 -0
  78. package/dist/parser/index.js.map +1 -0
  79. package/dist/parser/literals.d.ts +37 -0
  80. package/dist/parser/literals.d.ts.map +1 -0
  81. package/dist/parser/literals.js +373 -0
  82. package/dist/parser/literals.js.map +1 -0
  83. package/dist/parser/parser-collect.d.ts +16 -0
  84. package/dist/parser/parser-collect.d.ts.map +1 -0
  85. package/dist/parser/parser-collect.js +125 -0
  86. package/dist/parser/parser-collect.js.map +1 -0
  87. package/dist/parser/parser-control.d.ts +20 -0
  88. package/dist/parser/parser-control.d.ts.map +1 -0
  89. package/dist/parser/parser-control.js +120 -0
  90. package/dist/parser/parser-control.js.map +1 -0
  91. package/dist/parser/parser-expr.d.ts +37 -0
  92. package/dist/parser/parser-expr.d.ts.map +1 -0
  93. package/dist/parser/parser-expr.js +639 -0
  94. package/dist/parser/parser-expr.js.map +1 -0
  95. package/dist/parser/parser-extract.d.ts +17 -0
  96. package/dist/parser/parser-extract.d.ts.map +1 -0
  97. package/dist/parser/parser-extract.js +222 -0
  98. package/dist/parser/parser-extract.js.map +1 -0
  99. package/dist/parser/parser-functions.d.ts +21 -0
  100. package/dist/parser/parser-functions.d.ts.map +1 -0
  101. package/dist/parser/parser-functions.js +155 -0
  102. package/dist/parser/parser-functions.js.map +1 -0
  103. package/dist/parser/parser-literals.d.ts +22 -0
  104. package/dist/parser/parser-literals.d.ts.map +1 -0
  105. package/dist/parser/parser-literals.js +288 -0
  106. package/dist/parser/parser-literals.js.map +1 -0
  107. package/dist/parser/parser-script.d.ts +21 -0
  108. package/dist/parser/parser-script.d.ts.map +1 -0
  109. package/dist/parser/parser-script.js +174 -0
  110. package/dist/parser/parser-script.js.map +1 -0
  111. package/dist/parser/parser-variables.d.ts +20 -0
  112. package/dist/parser/parser-variables.d.ts.map +1 -0
  113. package/dist/parser/parser-variables.js +146 -0
  114. package/dist/parser/parser-variables.js.map +1 -0
  115. package/dist/parser/parser.d.ts +49 -0
  116. package/dist/parser/parser.d.ts.map +1 -0
  117. package/dist/parser/parser.js +54 -0
  118. package/dist/parser/parser.js.map +1 -0
  119. package/dist/parser/script.d.ts +14 -0
  120. package/dist/parser/script.d.ts.map +1 -0
  121. package/dist/parser/script.js +196 -0
  122. package/dist/parser/script.js.map +1 -0
  123. package/dist/parser/state.d.ts +40 -0
  124. package/dist/parser/state.d.ts.map +1 -0
  125. package/dist/parser/state.js +129 -0
  126. package/dist/parser/state.js.map +1 -0
  127. package/dist/parser/variables.d.ts +10 -0
  128. package/dist/parser/variables.d.ts.map +1 -0
  129. package/dist/parser/variables.js +215 -0
  130. package/dist/parser/variables.js.map +1 -0
  131. package/dist/runtime/ast-equals.d.ts +13 -0
  132. package/dist/runtime/ast-equals.d.ts.map +1 -0
  133. package/dist/runtime/ast-equals.js +447 -0
  134. package/dist/runtime/ast-equals.js.map +1 -0
  135. package/dist/runtime/builtins.d.ts +13 -0
  136. package/dist/runtime/builtins.d.ts.map +1 -0
  137. package/dist/runtime/builtins.js +180 -0
  138. package/dist/runtime/builtins.js.map +1 -0
  139. package/dist/runtime/callable.d.ts +88 -0
  140. package/dist/runtime/callable.d.ts.map +1 -0
  141. package/dist/runtime/callable.js +98 -0
  142. package/dist/runtime/callable.js.map +1 -0
  143. package/dist/runtime/context.d.ts +13 -0
  144. package/dist/runtime/context.d.ts.map +1 -0
  145. package/dist/runtime/context.js +73 -0
  146. package/dist/runtime/context.js.map +1 -0
  147. package/dist/runtime/core/callable.d.ts +171 -0
  148. package/dist/runtime/core/callable.d.ts.map +1 -0
  149. package/dist/runtime/core/callable.js +246 -0
  150. package/dist/runtime/core/callable.js.map +1 -0
  151. package/dist/runtime/core/context.d.ts +29 -0
  152. package/dist/runtime/core/context.d.ts.map +1 -0
  153. package/dist/runtime/core/context.js +154 -0
  154. package/dist/runtime/core/context.js.map +1 -0
  155. package/dist/runtime/core/equals.d.ts +9 -0
  156. package/dist/runtime/core/equals.d.ts.map +1 -0
  157. package/dist/runtime/core/equals.js +381 -0
  158. package/dist/runtime/core/equals.js.map +1 -0
  159. package/dist/runtime/core/eval/base.d.ts +65 -0
  160. package/dist/runtime/core/eval/base.d.ts.map +1 -0
  161. package/dist/runtime/core/eval/base.js +112 -0
  162. package/dist/runtime/core/eval/base.js.map +1 -0
  163. package/dist/runtime/core/eval/evaluator.d.ts +47 -0
  164. package/dist/runtime/core/eval/evaluator.d.ts.map +1 -0
  165. package/dist/runtime/core/eval/evaluator.js +73 -0
  166. package/dist/runtime/core/eval/evaluator.js.map +1 -0
  167. package/dist/runtime/core/eval/index.d.ts +57 -0
  168. package/dist/runtime/core/eval/index.d.ts.map +1 -0
  169. package/dist/runtime/core/eval/index.js +95 -0
  170. package/dist/runtime/core/eval/index.js.map +1 -0
  171. package/dist/runtime/core/eval/mixins/annotations.d.ts +19 -0
  172. package/dist/runtime/core/eval/mixins/annotations.d.ts.map +1 -0
  173. package/dist/runtime/core/eval/mixins/annotations.js +146 -0
  174. package/dist/runtime/core/eval/mixins/annotations.js.map +1 -0
  175. package/dist/runtime/core/eval/mixins/closures.d.ts +49 -0
  176. package/dist/runtime/core/eval/mixins/closures.d.ts.map +1 -0
  177. package/dist/runtime/core/eval/mixins/closures.js +479 -0
  178. package/dist/runtime/core/eval/mixins/closures.js.map +1 -0
  179. package/dist/runtime/core/eval/mixins/collections.d.ts +24 -0
  180. package/dist/runtime/core/eval/mixins/collections.d.ts.map +1 -0
  181. package/dist/runtime/core/eval/mixins/collections.js +466 -0
  182. package/dist/runtime/core/eval/mixins/collections.js.map +1 -0
  183. package/dist/runtime/core/eval/mixins/control-flow.d.ts +27 -0
  184. package/dist/runtime/core/eval/mixins/control-flow.d.ts.map +1 -0
  185. package/dist/runtime/core/eval/mixins/control-flow.js +369 -0
  186. package/dist/runtime/core/eval/mixins/control-flow.js.map +1 -0
  187. package/dist/runtime/core/eval/mixins/core.d.ts +24 -0
  188. package/dist/runtime/core/eval/mixins/core.d.ts.map +1 -0
  189. package/dist/runtime/core/eval/mixins/core.js +335 -0
  190. package/dist/runtime/core/eval/mixins/core.js.map +1 -0
  191. package/dist/runtime/core/eval/mixins/expressions.d.ts +19 -0
  192. package/dist/runtime/core/eval/mixins/expressions.d.ts.map +1 -0
  193. package/dist/runtime/core/eval/mixins/expressions.js +202 -0
  194. package/dist/runtime/core/eval/mixins/expressions.js.map +1 -0
  195. package/dist/runtime/core/eval/mixins/extraction.d.ts +10 -0
  196. package/dist/runtime/core/eval/mixins/extraction.d.ts.map +1 -0
  197. package/dist/runtime/core/eval/mixins/extraction.js +250 -0
  198. package/dist/runtime/core/eval/mixins/extraction.js.map +1 -0
  199. package/dist/runtime/core/eval/mixins/literals.d.ts +23 -0
  200. package/dist/runtime/core/eval/mixins/literals.d.ts.map +1 -0
  201. package/dist/runtime/core/eval/mixins/literals.js +180 -0
  202. package/dist/runtime/core/eval/mixins/literals.js.map +1 -0
  203. package/dist/runtime/core/eval/mixins/types.d.ts +20 -0
  204. package/dist/runtime/core/eval/mixins/types.d.ts.map +1 -0
  205. package/dist/runtime/core/eval/mixins/types.js +109 -0
  206. package/dist/runtime/core/eval/mixins/types.js.map +1 -0
  207. package/dist/runtime/core/eval/mixins/variables.d.ts +34 -0
  208. package/dist/runtime/core/eval/mixins/variables.d.ts.map +1 -0
  209. package/dist/runtime/core/eval/mixins/variables.js +247 -0
  210. package/dist/runtime/core/eval/mixins/variables.js.map +1 -0
  211. package/dist/runtime/core/eval/types.d.ts +41 -0
  212. package/dist/runtime/core/eval/types.d.ts.map +1 -0
  213. package/dist/runtime/core/eval/types.js +10 -0
  214. package/dist/runtime/core/eval/types.js.map +1 -0
  215. package/dist/runtime/core/evaluate.d.ts +42 -0
  216. package/dist/runtime/core/evaluate.d.ts.map +1 -0
  217. package/dist/runtime/core/evaluate.debug.js +1251 -0
  218. package/dist/runtime/core/evaluate.js +1913 -0
  219. package/dist/runtime/core/evaluate.js.map +1 -0
  220. package/dist/runtime/core/execute.d.ts +26 -0
  221. package/dist/runtime/core/execute.d.ts.map +1 -0
  222. package/dist/runtime/core/execute.js +177 -0
  223. package/dist/runtime/core/execute.js.map +1 -0
  224. package/dist/runtime/core/signals.d.ts +19 -0
  225. package/dist/runtime/core/signals.d.ts.map +1 -0
  226. package/dist/runtime/core/signals.js +26 -0
  227. package/dist/runtime/core/signals.js.map +1 -0
  228. package/dist/runtime/core/types.d.ts +177 -0
  229. package/dist/runtime/core/types.d.ts.map +1 -0
  230. package/dist/runtime/core/types.js +50 -0
  231. package/dist/runtime/core/types.js.map +1 -0
  232. package/dist/runtime/core/values.d.ts +66 -0
  233. package/dist/runtime/core/values.d.ts.map +1 -0
  234. package/dist/runtime/core/values.js +240 -0
  235. package/dist/runtime/core/values.js.map +1 -0
  236. package/dist/runtime/evaluate.d.ts +32 -0
  237. package/dist/runtime/evaluate.d.ts.map +1 -0
  238. package/dist/runtime/evaluate.js +1111 -0
  239. package/dist/runtime/evaluate.js.map +1 -0
  240. package/dist/runtime/execute.d.ts +26 -0
  241. package/dist/runtime/execute.d.ts.map +1 -0
  242. package/dist/runtime/execute.js +121 -0
  243. package/dist/runtime/execute.js.map +1 -0
  244. package/dist/runtime/ext/builtins.d.ts +16 -0
  245. package/dist/runtime/ext/builtins.d.ts.map +1 -0
  246. package/dist/runtime/ext/builtins.js +528 -0
  247. package/dist/runtime/ext/builtins.js.map +1 -0
  248. package/dist/runtime/ext/content-parser.d.ts +83 -0
  249. package/dist/runtime/ext/content-parser.d.ts.map +1 -0
  250. package/dist/runtime/ext/content-parser.js +536 -0
  251. package/dist/runtime/ext/content-parser.js.map +1 -0
  252. package/dist/runtime/index.d.ts +28 -0
  253. package/dist/runtime/index.d.ts.map +1 -0
  254. package/dist/runtime/index.js +34 -0
  255. package/dist/runtime/index.js.map +1 -0
  256. package/dist/runtime/signals.d.ts +19 -0
  257. package/dist/runtime/signals.d.ts.map +1 -0
  258. package/dist/runtime/signals.js +26 -0
  259. package/dist/runtime/signals.js.map +1 -0
  260. package/dist/runtime/types.d.ts +169 -0
  261. package/dist/runtime/types.d.ts.map +1 -0
  262. package/dist/runtime/types.js +50 -0
  263. package/dist/runtime/types.js.map +1 -0
  264. package/dist/runtime/values.d.ts +50 -0
  265. package/dist/runtime/values.d.ts.map +1 -0
  266. package/dist/runtime/values.js +209 -0
  267. package/dist/runtime/values.js.map +1 -0
  268. package/dist/runtime.d.ts +254 -0
  269. package/dist/runtime.d.ts.map +1 -0
  270. package/dist/runtime.js +2014 -0
  271. package/dist/runtime.js.map +1 -0
  272. package/dist/types.d.ts +752 -0
  273. package/dist/types.d.ts.map +1 -0
  274. package/dist/types.js +189 -0
  275. package/dist/types.js.map +1 -0
  276. package/docs/00_INDEX.md +65 -0
  277. package/docs/01_guide.md +390 -0
  278. package/docs/02_types.md +399 -0
  279. package/docs/03_variables.md +314 -0
  280. package/docs/04_operators.md +551 -0
  281. package/docs/05_control-flow.md +350 -0
  282. package/docs/06_closures.md +353 -0
  283. package/docs/07_collections.md +686 -0
  284. package/docs/08_iterators.md +330 -0
  285. package/docs/09_strings.md +205 -0
  286. package/docs/10_parsing.md +366 -0
  287. package/docs/11_reference.md +350 -0
  288. package/docs/12_examples.md +771 -0
  289. package/docs/13_modules.md +519 -0
  290. package/docs/14_host-integration.md +826 -0
  291. package/docs/15_grammar.ebnf +693 -0
  292. package/docs/16_conventions.md +696 -0
  293. package/docs/99_llm-reference.txt +300 -0
  294. package/docs/assets/logo.png +0 -0
  295. package/package.json +70 -0
@@ -0,0 +1,950 @@
1
+ /**
2
+ * Expression Parsing
3
+ * Primary expressions, postfix expressions, pipe chains, and pipe targets
4
+ */
5
+ import { ParseError, TOKEN_TYPES } from '../types.js';
6
+ import { check, advance, expect, current, makeSpan, peek, } from './state.js';
7
+ import { isHostCall, isClosureCall, canStartPipeInvoke, isMethodCall, isTypedCaptureWithArrow, isInlineCaptureWithArrow, isClosureChainTarget, isNegativeNumber, isLiteralStart, isClosureStart, makeBoolLiteralBlock, VALID_TYPE_NAMES, parseTypeName, } from './helpers.js';
8
+ import { parseVariable, setParseBlock as setVariablesParseBlock, setParsePipeChain as setVariablesParsePipeChain, } from './variables.js';
9
+ import { parseLiteral, parseString, parseClosure, setParseExpression as setLiteralsParseExpression, setParseBlock as setLiteralsParseBlock, setParseGrouped as setLiteralsParseGrouped, setParsePostfixExpr as setLiteralsParsePostfixExpr, setLiteralsParsePipeChain, } from './literals.js';
10
+ import { parseHostCall, parseClosureCall, parsePipeInvoke, parseMethodCall, setParseExpression as setFunctionsParseExpression, } from './functions.js';
11
+ import { parsePipedConditional, parseConditionalWithCondition, parseConditionalRest, parseLoop, parseLoopWithInput, parseBlock, } from './control-flow.js';
12
+ import { parseClosureChain, parseDestructure, parseSlice, parseSpread, parseSpreadTarget, setParsePostfixExpr as setExtractionParsePostfixExpr, setParseGrouped as setExtractionParseGrouped, } from './extraction.js';
13
+ /**
14
+ * Parse constructs common to both primary expressions and pipe targets.
15
+ * Returns null if no common construct matches.
16
+ */
17
+ function parseCommonConstruct(state) {
18
+ // Boolean negation: !expr (for filter predicates like !.empty in pipes)
19
+ // Can be: !expr ? then ! else OR standalone !expr (returns true/false)
20
+ if (check(state, TOKEN_TYPES.BANG)) {
21
+ const start = current(state).span.start;
22
+ advance(state); // consume !
23
+ // Use parsePostfixExprBase to avoid consuming `?` - we handle it ourselves
24
+ const operand = parsePostfixExprBase(state);
25
+ const span = makeSpan(start, operand.span.end);
26
+ // Build the negation condition as unified expression
27
+ const unaryExpr = {
28
+ type: 'UnaryExpr',
29
+ op: '!',
30
+ operand,
31
+ span,
32
+ };
33
+ const negationCondition = {
34
+ type: 'GroupedExpr',
35
+ expression: {
36
+ type: 'PipeChain',
37
+ head: unaryExpr,
38
+ pipes: [],
39
+ terminator: null,
40
+ span,
41
+ },
42
+ span,
43
+ };
44
+ // Check for conditional: !expr ? then ! else
45
+ if (check(state, TOKEN_TYPES.QUESTION)) {
46
+ advance(state); // consume ?
47
+ return parseConditionalRest(state, negationCondition, start);
48
+ }
49
+ // Standalone negation: evaluates to true/false
50
+ return {
51
+ type: 'Conditional',
52
+ input: null,
53
+ condition: negationCondition,
54
+ thenBranch: makeBoolLiteralBlock(true, operand.span),
55
+ elseBranch: makeBoolLiteralBlock(false, operand.span),
56
+ span,
57
+ };
58
+ }
59
+ // Piped conditional: bare `?` uses $ as condition
60
+ if (check(state, TOKEN_TYPES.QUESTION)) {
61
+ return parsePipedConditional(state);
62
+ }
63
+ // Loop: @ body [? cond]
64
+ if (check(state, TOKEN_TYPES.AT)) {
65
+ return parseLoop(state, null);
66
+ }
67
+ // Block (may be followed by @ for loop with input, or ? for conditional)
68
+ if (check(state, TOKEN_TYPES.LBRACE)) {
69
+ const block = parseBlock(state);
70
+ // Check for loop: { input } @ body
71
+ if (check(state, TOKEN_TYPES.AT)) {
72
+ return parseLoopWithInput(state, block);
73
+ }
74
+ // Check for conditional: { expr } ? then ! else
75
+ if (check(state, TOKEN_TYPES.QUESTION)) {
76
+ return parseConditionalWithCondition(state, block);
77
+ }
78
+ return block;
79
+ }
80
+ // Grouped expression: ( inner-expr )
81
+ // Allows arithmetic, pipes, and compound expressions
82
+ // May be followed by: @ for loop, ? for conditional
83
+ if (check(state, TOKEN_TYPES.LPAREN)) {
84
+ const grouped = parseGrouped(state);
85
+ // Check for loop: (expr) @ body
86
+ if (check(state, TOKEN_TYPES.AT)) {
87
+ return parseLoopWithInput(state, grouped);
88
+ }
89
+ // Check for conditional: (expr) ? then ! else
90
+ if (check(state, TOKEN_TYPES.QUESTION)) {
91
+ return parseConditionalWithCondition(state, grouped);
92
+ }
93
+ return grouped;
94
+ }
95
+ return null;
96
+ }
97
+ // ============================================================
98
+ // EXPRESSION PARSING
99
+ // ============================================================
100
+ export function parseExpression(state) {
101
+ return parsePipeChain(state);
102
+ }
103
+ /**
104
+ * Helper to create implicit pipe variable ($) for bare break/return
105
+ */
106
+ function implicitPipeVar(span) {
107
+ const varNode = {
108
+ type: 'Variable',
109
+ name: null,
110
+ isPipeVar: true,
111
+ accessChain: [],
112
+ fieldAccess: [],
113
+ bracketAccess: [],
114
+ defaultValue: null,
115
+ existenceCheck: null,
116
+ span,
117
+ };
118
+ return {
119
+ type: 'PostfixExpr',
120
+ primary: varNode,
121
+ methods: [],
122
+ span,
123
+ };
124
+ }
125
+ export function parsePipeChain(state) {
126
+ const start = current(state).span.start;
127
+ // Handle bare break: "break" ≡ "$ -> break"
128
+ if (check(state, TOKEN_TYPES.BREAK)) {
129
+ const token = advance(state);
130
+ return {
131
+ type: 'PipeChain',
132
+ head: implicitPipeVar(token.span),
133
+ pipes: [],
134
+ terminator: { type: 'Break', span: token.span },
135
+ span: token.span,
136
+ };
137
+ }
138
+ // Handle bare return: "return" ≡ "$ -> return"
139
+ if (check(state, TOKEN_TYPES.RETURN)) {
140
+ const token = advance(state);
141
+ return {
142
+ type: 'PipeChain',
143
+ head: implicitPipeVar(token.span),
144
+ pipes: [],
145
+ terminator: { type: 'Return', span: token.span },
146
+ span: token.span,
147
+ };
148
+ }
149
+ // Parse expression head with full precedence chain:
150
+ // logical-or -> logical-and -> comparison -> additive -> multiplicative -> unary -> postfix
151
+ let head = parseLogicalOr(state);
152
+ // Check for loop: expr @ body
153
+ // This allows: $status.pending @ { ... }, ($x < 10) @ { ... }
154
+ if (check(state, TOKEN_TYPES.AT)) {
155
+ const headAsPipeChain = {
156
+ type: 'PipeChain',
157
+ head,
158
+ pipes: [],
159
+ terminator: null,
160
+ span: head.span,
161
+ };
162
+ const loop = parseLoopWithInput(state, headAsPipeChain);
163
+ const span = makeSpan(head.span.start, current(state).span.end);
164
+ head = wrapLoopInPostfixExpr(loop, span);
165
+ }
166
+ // Check for conditional: expr ? then ! else
167
+ // This allows: 5 + 3 ? "big" ! "small", $ready ? "go" ! "wait"
168
+ if (check(state, TOKEN_TYPES.QUESTION)) {
169
+ const headAsPipeChain = {
170
+ type: 'PipeChain',
171
+ head,
172
+ pipes: [],
173
+ terminator: null,
174
+ span: head.span,
175
+ };
176
+ const conditional = parseConditionalWithCondition(state, headAsPipeChain);
177
+ const span = makeSpan(head.span.start, current(state).span.end);
178
+ head = wrapConditionalInPostfixExpr(conditional, span);
179
+ }
180
+ const pipes = [];
181
+ let terminator = null;
182
+ while (check(state, TOKEN_TYPES.ARROW)) {
183
+ advance(state);
184
+ // Check for break terminator: -> break
185
+ if (check(state, TOKEN_TYPES.BREAK)) {
186
+ const token = advance(state);
187
+ terminator = { type: 'Break', span: token.span };
188
+ break;
189
+ }
190
+ // Check for return terminator: -> return
191
+ if (check(state, TOKEN_TYPES.RETURN)) {
192
+ const token = advance(state);
193
+ terminator = { type: 'Return', span: token.span };
194
+ break;
195
+ }
196
+ // Check for capture vs ClosureCall: $identifier
197
+ if (check(state, TOKEN_TYPES.DOLLAR)) {
198
+ // ClosureCall: $name( - pass to parsePipeTarget
199
+ if (isClosureCall(state)) {
200
+ pipes.push(parsePipeTarget(state));
201
+ continue;
202
+ }
203
+ // Inline capture: $name -> (followed by arrow)
204
+ if (isInlineCaptureWithArrow(state)) {
205
+ pipes.push(parseCapture(state));
206
+ continue;
207
+ }
208
+ // Inline capture with type: $name:type -> (followed by arrow)
209
+ if (isTypedCaptureWithArrow(state)) {
210
+ pipes.push(parseCapture(state));
211
+ continue;
212
+ }
213
+ // Terminal capture: $name or $name:type (end of chain)
214
+ terminator = parseCapture(state);
215
+ break;
216
+ }
217
+ pipes.push(parsePipeTarget(state));
218
+ }
219
+ // Check for conditional after pipe chain: $val -> :?string ? then ! else
220
+ if (check(state, TOKEN_TYPES.QUESTION) && pipes.length > 0) {
221
+ const span = makeSpan(start, current(state).span.end);
222
+ const chainAsCondition = {
223
+ type: 'PipeChain',
224
+ head,
225
+ pipes,
226
+ terminator: null,
227
+ span,
228
+ };
229
+ const conditional = parseConditionalWithCondition(state, chainAsCondition);
230
+ const resultSpan = makeSpan(start, current(state).span.end);
231
+ return {
232
+ type: 'PipeChain',
233
+ head: wrapConditionalInPostfixExpr(conditional, resultSpan),
234
+ pipes: [],
235
+ terminator: null,
236
+ span: resultSpan,
237
+ };
238
+ }
239
+ return {
240
+ type: 'PipeChain',
241
+ head,
242
+ pipes,
243
+ terminator,
244
+ span: makeSpan(start, current(state).span.end),
245
+ };
246
+ }
247
+ export function parsePostfixExpr(state) {
248
+ const postfixExpr = parsePostfixExprBase(state);
249
+ // Check if this postfix-expr is a condition for a conditional: expr ? then ! else
250
+ // This allows: $ready ? "go" ! "wait", $data.valid ? process() ! skip()
251
+ if (check(state, TOKEN_TYPES.QUESTION)) {
252
+ const conditional = parseConditionalWithCondition(state, postfixExpr);
253
+ const span = makeSpan(postfixExpr.span.start, current(state).span.end);
254
+ return wrapConditionalInPostfixExpr(conditional, span);
255
+ }
256
+ return postfixExpr;
257
+ }
258
+ /**
259
+ * Parse postfix expression without checking for trailing `?` conditional.
260
+ * Used when the caller needs to handle the `?` themselves (e.g., for negation).
261
+ */
262
+ function parsePostfixExprBase(state) {
263
+ const start = current(state).span.start;
264
+ let primary = parsePrimary(state);
265
+ // Check for postfix type assertion: expr:type or expr:?type
266
+ // This binds tighter than method calls: 42:number.str means (42:number).str
267
+ if (check(state, TOKEN_TYPES.COLON)) {
268
+ primary = parsePostfixTypeOperation(state, primary, start);
269
+ }
270
+ const methods = [];
271
+ // Parse method calls and invocations
272
+ // Method call: .name(args) or .name
273
+ // Invocation: (args) - calls the result as a closure
274
+ while (isMethodCall(state) || check(state, TOKEN_TYPES.LPAREN)) {
275
+ if (isMethodCall(state)) {
276
+ methods.push(parseMethodCall(state));
277
+ }
278
+ else {
279
+ // Postfix invocation: expr(args)
280
+ methods.push(parseInvoke(state));
281
+ }
282
+ }
283
+ return {
284
+ type: 'PostfixExpr',
285
+ primary,
286
+ methods,
287
+ span: makeSpan(start, current(state).span.end),
288
+ };
289
+ }
290
+ /**
291
+ * Parse postfix invocation: (args)
292
+ * This allows calling the result of any expression as a closure.
293
+ * Examples: $handlers[0](), $dict.method()(), ($closure)()
294
+ */
295
+ function parseInvoke(state) {
296
+ const start = current(state).span.start;
297
+ expect(state, TOKEN_TYPES.LPAREN, 'Expected (');
298
+ const args = [];
299
+ if (!check(state, TOKEN_TYPES.RPAREN)) {
300
+ args.push(parsePipeChain(state));
301
+ while (check(state, TOKEN_TYPES.COMMA)) {
302
+ advance(state); // consume ,
303
+ args.push(parsePipeChain(state));
304
+ }
305
+ }
306
+ expect(state, TOKEN_TYPES.RPAREN, 'Expected )');
307
+ return {
308
+ type: 'Invoke',
309
+ args,
310
+ span: makeSpan(start, current(state).span.end),
311
+ };
312
+ }
313
+ /**
314
+ * Parse postfix type operation: primary:type or primary:?type
315
+ * Creates TypeAssertion or TypeCheck node with the primary as operand.
316
+ */
317
+ function parsePostfixTypeOperation(state, primary, start) {
318
+ expect(state, TOKEN_TYPES.COLON, 'Expected :');
319
+ // Check for type check (question mark)
320
+ const isCheck = check(state, TOKEN_TYPES.QUESTION);
321
+ if (isCheck) {
322
+ advance(state); // consume ?
323
+ }
324
+ // Parse type name
325
+ const typeName = parseTypeName(state, VALID_TYPE_NAMES);
326
+ // Wrap primary in PostfixExprNode for the operand
327
+ const operand = {
328
+ type: 'PostfixExpr',
329
+ primary,
330
+ methods: [],
331
+ span: makeSpan(start, current(state).span.end),
332
+ };
333
+ const span = makeSpan(start, current(state).span.end);
334
+ if (isCheck) {
335
+ return {
336
+ type: 'TypeCheck',
337
+ operand,
338
+ typeName,
339
+ span,
340
+ };
341
+ }
342
+ return {
343
+ type: 'TypeAssertion',
344
+ operand,
345
+ typeName,
346
+ span,
347
+ };
348
+ }
349
+ // ============================================================
350
+ // PRIMARY PARSING
351
+ // ============================================================
352
+ export function parsePrimary(state) {
353
+ // Spread operator: *expr - convert tuple/dict to args
354
+ if (check(state, TOKEN_TYPES.STAR)) {
355
+ return parseSpread(state);
356
+ }
357
+ // Unary minus for negative numbers: -42
358
+ if (isNegativeNumber(state)) {
359
+ const start = current(state).span.start;
360
+ advance(state); // consume -
361
+ const numToken = advance(state); // consume number
362
+ return {
363
+ type: 'NumberLiteral',
364
+ value: -parseFloat(numToken.value),
365
+ span: makeSpan(start, numToken.span.end),
366
+ };
367
+ }
368
+ // Closure: |params| body or || body
369
+ if (isClosureStart(state)) {
370
+ return parseClosure(state);
371
+ }
372
+ // Literal (strings, numbers, booleans, tuples/dicts)
373
+ if (isLiteralStart(state)) {
374
+ return parseLiteral(state);
375
+ }
376
+ // Closure call: $fn(args) - closure invocation
377
+ if (isClosureCall(state)) {
378
+ return parseClosureCall(state);
379
+ }
380
+ // Variable
381
+ if (check(state, TOKEN_TYPES.DOLLAR, TOKEN_TYPES.PIPE_VAR)) {
382
+ return parseVariable(state);
383
+ }
384
+ // Bare method call: .method ≡ $ -> .method (implicit pipe var receiver)
385
+ if (isMethodCall(state)) {
386
+ return parseMethodCall(state);
387
+ }
388
+ // Function call (identifier followed by paren)
389
+ if (isHostCall(state)) {
390
+ return parseHostCall(state);
391
+ }
392
+ // Common constructs: conditionals, loops, blocks, grouped expressions
393
+ const common = parseCommonConstruct(state);
394
+ if (common)
395
+ return common;
396
+ throw new ParseError(`Unexpected token: ${current(state).value}`, current(state).span.start);
397
+ }
398
+ // ============================================================
399
+ // PIPE TARGET PARSING
400
+ // ============================================================
401
+ export function parsePipeTarget(state) {
402
+ // Type operations: -> :type or -> :?type
403
+ if (check(state, TOKEN_TYPES.COLON)) {
404
+ return parseTypeOperation(state);
405
+ }
406
+ // Spread as pipe target: -> * (convert pipe value to args)
407
+ if (check(state, TOKEN_TYPES.STAR)) {
408
+ return parseSpreadTarget(state);
409
+ }
410
+ // Extraction operators
411
+ if (check(state, TOKEN_TYPES.STAR_LT)) {
412
+ return parseDestructure(state);
413
+ }
414
+ if (check(state, TOKEN_TYPES.SLASH_LT)) {
415
+ return parseSlice(state);
416
+ }
417
+ // Collection operators: -> each, -> map, -> fold, -> filter
418
+ if (check(state, TOKEN_TYPES.EACH)) {
419
+ return parseEachExpr(state);
420
+ }
421
+ if (check(state, TOKEN_TYPES.MAP)) {
422
+ return parseMapExpr(state);
423
+ }
424
+ if (check(state, TOKEN_TYPES.FOLD)) {
425
+ return parseFoldExpr(state);
426
+ }
427
+ if (check(state, TOKEN_TYPES.FILTER)) {
428
+ return parseFilterExpr(state);
429
+ }
430
+ // Method call (starts with .) - may be condition for conditional
431
+ if (check(state, TOKEN_TYPES.DOT)) {
432
+ const methodCall = parseMethodCall(state);
433
+ // Check if this is a condition for a conditional: .valid ? then ! else
434
+ if (check(state, TOKEN_TYPES.QUESTION)) {
435
+ // Wrap in PostfixExpr with implicit $ receiver for the condition
436
+ const postfixExpr = {
437
+ type: 'PostfixExpr',
438
+ primary: {
439
+ type: 'Variable',
440
+ name: null,
441
+ isPipeVar: true,
442
+ accessChain: [],
443
+ fieldAccess: [],
444
+ bracketAccess: [],
445
+ defaultValue: null,
446
+ existenceCheck: null,
447
+ span: methodCall.span,
448
+ },
449
+ methods: [methodCall],
450
+ span: methodCall.span,
451
+ };
452
+ return parseConditionalWithCondition(state, postfixExpr);
453
+ }
454
+ return methodCall;
455
+ }
456
+ // Closure call: $fn(args) - closure invocation as pipe target
457
+ if (isClosureCall(state)) {
458
+ return parseClosureCall(state);
459
+ }
460
+ // Sequential spread: -> @$var or -> @[closures] (not @{ } which is for-loop, not @( which is while)
461
+ if (isClosureChainTarget(state)) {
462
+ return parseClosureChain(state);
463
+ }
464
+ // Pipe invoke: -> $() or -> $(args) - invoke pipe value as closure
465
+ // The $ prefix distinguishes from grouped expressions: -> (expr)
466
+ if (canStartPipeInvoke(state)) {
467
+ return parsePipeInvoke(state);
468
+ }
469
+ // String literal (template with {$} interpolation)
470
+ if (check(state, TOKEN_TYPES.STRING)) {
471
+ return parseString(state);
472
+ }
473
+ // Function call with parens
474
+ if (isHostCall(state)) {
475
+ return parseHostCall(state);
476
+ }
477
+ // Bare function name: "-> greet" ≡ "-> greet()" with $ as implicit arg
478
+ if (check(state, TOKEN_TYPES.IDENTIFIER)) {
479
+ const start = current(state).span.start;
480
+ const nameToken = advance(state);
481
+ return {
482
+ type: 'HostCall',
483
+ name: nameToken.value,
484
+ args: [],
485
+ span: makeSpan(start, current(state).span.end),
486
+ };
487
+ }
488
+ // Common constructs: conditionals, loops, blocks, arithmetic
489
+ const common = parseCommonConstruct(state);
490
+ if (common) {
491
+ // Check for postfix type assertion on the common construct: (expr):type
492
+ if (check(state, TOKEN_TYPES.COLON)) {
493
+ return parsePostfixTypeOperation(state, common, common.span.start);
494
+ }
495
+ return common;
496
+ }
497
+ throw new ParseError(`Expected pipe target, got: ${current(state).value}`, current(state).span.start);
498
+ }
499
+ // ============================================================
500
+ // CAPTURE PARSING
501
+ // ============================================================
502
+ export function parseCapture(state) {
503
+ const start = current(state).span.start;
504
+ expect(state, TOKEN_TYPES.DOLLAR, 'Expected $');
505
+ const nameToken = expect(state, TOKEN_TYPES.IDENTIFIER, 'Expected variable name');
506
+ // Optional type annotation: $name:type
507
+ let typeName = null;
508
+ if (check(state, TOKEN_TYPES.COLON)) {
509
+ advance(state);
510
+ typeName = parseTypeName(state, VALID_TYPE_NAMES);
511
+ }
512
+ return {
513
+ type: 'Capture',
514
+ name: nameToken.value,
515
+ typeName,
516
+ span: makeSpan(start, current(state).span.end),
517
+ };
518
+ }
519
+ // ============================================================
520
+ // HELPER: Create pipe chain from single primary
521
+ // ============================================================
522
+ export function makePipeChain(primary, start) {
523
+ return {
524
+ type: 'PipeChain',
525
+ head: {
526
+ type: 'PostfixExpr',
527
+ primary,
528
+ methods: [],
529
+ span: makeSpan(start, start),
530
+ },
531
+ pipes: [],
532
+ terminator: null,
533
+ span: makeSpan(start, start),
534
+ };
535
+ }
536
+ // ============================================================
537
+ // GROUPED EXPRESSION & ARITHMETIC PARSING
538
+ // ============================================================
539
+ /**
540
+ * Grouped expression: ( expression )
541
+ * Single-expression block with () delimiters.
542
+ * Provides scoping — captures inside are local.
543
+ *
544
+ * Note: Boolean operators (&&, ||, !) are only supported in while loop
545
+ * conditions @(condition), not in general grouped expressions.
546
+ */
547
+ export function parseGrouped(state) {
548
+ const start = current(state).span.start;
549
+ expect(state, TOKEN_TYPES.LPAREN, 'Expected (');
550
+ const expression = parsePipeChain(state);
551
+ expect(state, TOKEN_TYPES.RPAREN, 'Expected )');
552
+ return {
553
+ type: 'GroupedExpr',
554
+ expression,
555
+ span: makeSpan(start, current(state).span.end),
556
+ };
557
+ }
558
+ /**
559
+ * Check if current token is a comparison operator.
560
+ */
561
+ function isComparisonOp(state) {
562
+ return check(state, TOKEN_TYPES.EQ, TOKEN_TYPES.NE, TOKEN_TYPES.LT, TOKEN_TYPES.GT, TOKEN_TYPES.LE, TOKEN_TYPES.GE);
563
+ }
564
+ /** Map token type to comparison operator string */
565
+ function tokenToComparisonOp(tokenType) {
566
+ switch (tokenType) {
567
+ case TOKEN_TYPES.EQ:
568
+ return '==';
569
+ case TOKEN_TYPES.NE:
570
+ return '!=';
571
+ case TOKEN_TYPES.LT:
572
+ return '<';
573
+ case TOKEN_TYPES.GT:
574
+ return '>';
575
+ case TOKEN_TYPES.LE:
576
+ return '<=';
577
+ default:
578
+ return '>=';
579
+ }
580
+ }
581
+ /** Wrap a conditional node in a PostfixExpr */
582
+ function wrapConditionalInPostfixExpr(conditional, span) {
583
+ return {
584
+ type: 'PostfixExpr',
585
+ primary: conditional,
586
+ methods: [],
587
+ span,
588
+ };
589
+ }
590
+ /** Wrap a loop node in a PostfixExpr */
591
+ function wrapLoopInPostfixExpr(loop, span) {
592
+ return {
593
+ type: 'PostfixExpr',
594
+ primary: loop,
595
+ methods: [],
596
+ span,
597
+ };
598
+ }
599
+ // ============================================================
600
+ // EXPRESSION PRECEDENCE CHAIN
601
+ // ============================================================
602
+ // Precedence (lowest to highest):
603
+ // logical-or (||) -> logical-and (&&) -> comparison -> additive -> multiplicative -> unary -> postfix
604
+ /**
605
+ * Parse logical OR expression: logical-and ('||' logical-and)*
606
+ */
607
+ function parseLogicalOr(state) {
608
+ const start = current(state).span.start;
609
+ let left = parseLogicalAnd(state);
610
+ while (check(state, TOKEN_TYPES.OR)) {
611
+ advance(state);
612
+ const right = parseLogicalAnd(state);
613
+ left = {
614
+ type: 'BinaryExpr',
615
+ op: '||',
616
+ left,
617
+ right,
618
+ span: makeSpan(start, current(state).span.end),
619
+ };
620
+ }
621
+ return left;
622
+ }
623
+ /**
624
+ * Parse logical AND expression: comparison ('&&' comparison)*
625
+ */
626
+ function parseLogicalAnd(state) {
627
+ const start = current(state).span.start;
628
+ let left = parseComparison(state);
629
+ while (check(state, TOKEN_TYPES.AND)) {
630
+ advance(state);
631
+ const right = parseComparison(state);
632
+ left = {
633
+ type: 'BinaryExpr',
634
+ op: '&&',
635
+ left,
636
+ right,
637
+ span: makeSpan(start, current(state).span.end),
638
+ };
639
+ }
640
+ return left;
641
+ }
642
+ /**
643
+ * Parse comparison expression: additive (comp-op additive)?
644
+ */
645
+ function parseComparison(state) {
646
+ const start = current(state).span.start;
647
+ let left = parseAdditive(state);
648
+ if (isComparisonOp(state)) {
649
+ const opToken = advance(state);
650
+ const op = tokenToComparisonOp(opToken.type);
651
+ const right = parseAdditive(state);
652
+ left = {
653
+ type: 'BinaryExpr',
654
+ op,
655
+ left,
656
+ right,
657
+ span: makeSpan(start, current(state).span.end),
658
+ };
659
+ }
660
+ return left;
661
+ }
662
+ /**
663
+ * Parse additive expression: multiplicative (('+' | '-') multiplicative)*
664
+ */
665
+ function parseAdditive(state) {
666
+ const start = current(state).span.start;
667
+ let left = parseMultiplicative(state);
668
+ while (check(state, TOKEN_TYPES.PLUS, TOKEN_TYPES.MINUS)) {
669
+ const opToken = advance(state);
670
+ const op = opToken.type === TOKEN_TYPES.PLUS ? '+' : '-';
671
+ const right = parseMultiplicative(state);
672
+ left = {
673
+ type: 'BinaryExpr',
674
+ op,
675
+ left,
676
+ right,
677
+ span: makeSpan(start, current(state).span.end),
678
+ };
679
+ }
680
+ return left;
681
+ }
682
+ /**
683
+ * Parse multiplicative expression: unary (('*' | '/' | '%') unary)*
684
+ */
685
+ function parseMultiplicative(state) {
686
+ const start = current(state).span.start;
687
+ let left = parseUnary(state);
688
+ while (check(state, TOKEN_TYPES.STAR, TOKEN_TYPES.SLASH, TOKEN_TYPES.PERCENT)) {
689
+ const opToken = advance(state);
690
+ const op = opToken.type === TOKEN_TYPES.STAR
691
+ ? '*'
692
+ : opToken.type === TOKEN_TYPES.SLASH
693
+ ? '/'
694
+ : '%';
695
+ const right = parseUnary(state);
696
+ left = {
697
+ type: 'BinaryExpr',
698
+ op,
699
+ left,
700
+ right,
701
+ span: makeSpan(start, current(state).span.end),
702
+ };
703
+ }
704
+ return left;
705
+ }
706
+ /**
707
+ * Parse unary expression: ('-' | '!') unary | postfix-expr
708
+ */
709
+ function parseUnary(state) {
710
+ if (check(state, TOKEN_TYPES.MINUS)) {
711
+ const start = current(state).span.start;
712
+ advance(state);
713
+ const operand = parseUnary(state);
714
+ return {
715
+ type: 'UnaryExpr',
716
+ op: '-',
717
+ operand,
718
+ span: makeSpan(start, operand.span.end),
719
+ };
720
+ }
721
+ if (check(state, TOKEN_TYPES.BANG)) {
722
+ const start = current(state).span.start;
723
+ advance(state);
724
+ const operand = parseUnary(state);
725
+ return {
726
+ type: 'UnaryExpr',
727
+ op: '!',
728
+ operand,
729
+ span: makeSpan(start, operand.span.end),
730
+ };
731
+ }
732
+ return parsePostfixExpr(state);
733
+ }
734
+ // ============================================================
735
+ // TYPE OPERATIONS
736
+ // ============================================================
737
+ /**
738
+ * Parse type operation as pipe target: :type or :?type
739
+ * These are shorthand for $:type and $:?type (type assertion/check on pipe value).
740
+ */
741
+ function parseTypeOperation(state) {
742
+ const start = current(state).span.start;
743
+ expect(state, TOKEN_TYPES.COLON, 'Expected :');
744
+ // Check for type check (question mark)
745
+ const isCheck = check(state, TOKEN_TYPES.QUESTION);
746
+ if (isCheck) {
747
+ advance(state); // consume ?
748
+ }
749
+ // Parse type name
750
+ const typeName = parseTypeName(state, VALID_TYPE_NAMES);
751
+ const span = makeSpan(start, current(state).span.end);
752
+ if (isCheck) {
753
+ return {
754
+ type: 'TypeCheck',
755
+ operand: null, // null means use pipe value ($)
756
+ typeName,
757
+ span,
758
+ };
759
+ }
760
+ return {
761
+ type: 'TypeAssertion',
762
+ operand: null, // null means use pipe value ($)
763
+ typeName,
764
+ span,
765
+ };
766
+ }
767
+ // ============================================================
768
+ // COLLECTION OPERATORS (each, map, fold)
769
+ // ============================================================
770
+ /**
771
+ * Parse collection body: the body for each/map/fold operators.
772
+ * Valid forms:
773
+ * - |x| body -- inline closure
774
+ * - { body } -- block expression
775
+ * - (expr) -- grouped expression
776
+ * - $fn -- variable closure
777
+ * - $ -- identity (returns element)
778
+ * - * -- spread (converts element to tuple)
779
+ */
780
+ function parseIteratorBody(state) {
781
+ // Inline closure: |x| body or |x, acc = init| body
782
+ if (isClosureStart(state)) {
783
+ return parseClosure(state);
784
+ }
785
+ // Block: { body }
786
+ if (check(state, TOKEN_TYPES.LBRACE)) {
787
+ return parseBlock(state);
788
+ }
789
+ // Grouped: (expr)
790
+ if (check(state, TOKEN_TYPES.LPAREN)) {
791
+ return parseGrouped(state);
792
+ }
793
+ // Variable closure: $fn or identity $
794
+ if (check(state, TOKEN_TYPES.DOLLAR) || check(state, TOKEN_TYPES.PIPE_VAR)) {
795
+ return parseVariable(state);
796
+ }
797
+ // Spread: * (converts element to tuple)
798
+ if (check(state, TOKEN_TYPES.STAR)) {
799
+ return parseSpread(state);
800
+ }
801
+ throw new ParseError(`Expected collection body (closure, block, grouped, variable, or spread), got: ${current(state).value}`, current(state).span.start);
802
+ }
803
+ /**
804
+ * Check if the next token sequence indicates an accumulator followed by a body.
805
+ * Disambiguation rule from spec:
806
+ * - (expr) at end of statement or before -> → grouped expression (body)
807
+ * - (expr) { block } → accumulator, block body
808
+ * - (expr) |x| body → accumulator, closure body
809
+ * - (expr1) (expr2) → accumulator, grouped body
810
+ */
811
+ function hasAccumulatorPrefix(state) {
812
+ if (!check(state, TOKEN_TYPES.LPAREN)) {
813
+ return false;
814
+ }
815
+ // Find matching close paren
816
+ let depth = 1;
817
+ let i = 1;
818
+ while (depth > 0) {
819
+ const token = peek(state, i);
820
+ if (!token)
821
+ return false;
822
+ if (token.type === TOKEN_TYPES.LPAREN)
823
+ depth++;
824
+ else if (token.type === TOKEN_TYPES.RPAREN)
825
+ depth--;
826
+ i++;
827
+ }
828
+ // Look at what follows the closing paren
829
+ const afterParen = peek(state, i);
830
+ if (!afterParen)
831
+ return false;
832
+ // If followed by body starters, this paren is accumulator
833
+ return (afterParen.type === TOKEN_TYPES.LBRACE || // (init) { body }
834
+ afterParen.type === TOKEN_TYPES.PIPE_BAR || // (init) |x| body
835
+ afterParen.type === TOKEN_TYPES.OR || // (init) || body
836
+ afterParen.type === TOKEN_TYPES.LPAREN // (init) (expr)
837
+ );
838
+ }
839
+ /**
840
+ * Parse each expression: -> each [accumulator] body
841
+ *
842
+ * Syntax:
843
+ * -> each |x| body
844
+ * -> each { body }
845
+ * -> each (expr)
846
+ * -> each $fn
847
+ * -> each $
848
+ * -> each(init) { body } -- with accumulator ($@ in body)
849
+ * -> each |x, acc = init| body -- with accumulator (closure param)
850
+ */
851
+ function parseEachExpr(state) {
852
+ const start = current(state).span.start;
853
+ expect(state, TOKEN_TYPES.EACH, 'Expected each');
854
+ let accumulator = null;
855
+ // Check for accumulator prefix: (init) followed by body
856
+ if (hasAccumulatorPrefix(state)) {
857
+ accumulator = parseGrouped(state).expression;
858
+ }
859
+ const body = parseIteratorBody(state);
860
+ return {
861
+ type: 'EachExpr',
862
+ body,
863
+ accumulator,
864
+ span: makeSpan(start, current(state).span.end),
865
+ };
866
+ }
867
+ /**
868
+ * Parse map expression: -> map body
869
+ *
870
+ * Syntax:
871
+ * -> map |x| body
872
+ * -> map { body }
873
+ * -> map (expr)
874
+ * -> map $fn
875
+ * -> map $
876
+ *
877
+ * No accumulator (parallel execution has no "previous").
878
+ */
879
+ function parseMapExpr(state) {
880
+ const start = current(state).span.start;
881
+ expect(state, TOKEN_TYPES.MAP, 'Expected map');
882
+ const body = parseIteratorBody(state);
883
+ return {
884
+ type: 'MapExpr',
885
+ body,
886
+ span: makeSpan(start, current(state).span.end),
887
+ };
888
+ }
889
+ /**
890
+ * Parse fold expression: -> fold body
891
+ *
892
+ * Syntax:
893
+ * -> fold |x, acc = init| body -- accumulator in closure params
894
+ * -> fold(init) { body } -- accumulator via $@
895
+ * -> fold $fn -- fn must have accumulator param
896
+ *
897
+ * Accumulator is required.
898
+ */
899
+ function parseFoldExpr(state) {
900
+ const start = current(state).span.start;
901
+ expect(state, TOKEN_TYPES.FOLD, 'Expected fold');
902
+ let accumulator = null;
903
+ // Check for accumulator prefix: (init) followed by body
904
+ if (hasAccumulatorPrefix(state)) {
905
+ accumulator = parseGrouped(state).expression;
906
+ }
907
+ const body = parseIteratorBody(state);
908
+ return {
909
+ type: 'FoldExpr',
910
+ body,
911
+ accumulator,
912
+ span: makeSpan(start, current(state).span.end),
913
+ };
914
+ }
915
+ /**
916
+ * Parse filter expression: -> filter body
917
+ *
918
+ * Syntax:
919
+ * -> filter |x| body
920
+ * -> filter { body }
921
+ * -> filter (expr)
922
+ * -> filter $fn
923
+ *
924
+ * Predicate returns truthy/falsy. Elements where predicate is truthy are kept.
925
+ */
926
+ function parseFilterExpr(state) {
927
+ const start = current(state).span.start;
928
+ expect(state, TOKEN_TYPES.FILTER, 'Expected filter');
929
+ const body = parseIteratorBody(state);
930
+ return {
931
+ type: 'FilterExpr',
932
+ body,
933
+ span: makeSpan(start, current(state).span.end),
934
+ };
935
+ }
936
+ // ============================================================
937
+ // WIRE UP CIRCULAR DEPENDENCIES
938
+ // ============================================================
939
+ // Set up all circular dependency injections
940
+ setLiteralsParseExpression(parseExpression);
941
+ setLiteralsParseBlock(parseBlock);
942
+ setLiteralsParseGrouped(parseGrouped);
943
+ setLiteralsParsePostfixExpr(parsePostfixExpr);
944
+ setLiteralsParsePipeChain(parsePipeChain);
945
+ setFunctionsParseExpression(parseExpression);
946
+ setExtractionParsePostfixExpr(parsePostfixExpr);
947
+ setExtractionParseGrouped(parseGrouped);
948
+ setVariablesParseBlock(parseBlock);
949
+ setVariablesParsePipeChain(parsePipeChain);
950
+ //# sourceMappingURL=expressions.js.map