@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,693 @@
1
+ (* rill v0.1.0 Grammar - EBNF Notation *)
2
+ (* Pipe-based scripting language for prompt-drive workflows *)
3
+
4
+ (* ============================================================ *)
5
+ (* DESIGN PRINCIPLES *)
6
+ (* ============================================================ *)
7
+
8
+ (* No Null:rill has no null or nil type. Operations that might produce
9
+ "nothing" in other languages instead stop execution with an error.
10
+ This eliminates null-checking boilerplate and makes failures explicit
11
+ at the point of origin rather than propagating silently. *)
12
+
13
+ (* Content Equality: All comparisons (==, !=, .eq, .ne) use structural
14
+ equality, not reference identity. Two values are equal if they have
15
+ the same content:
16
+ - Primitives: value equality
17
+ - Lists: same length + element-wise equality
18
+ - Dicts: same keys + value-wise equality (order-independent)
19
+ - Closures: same params + body AST structure + captured values
20
+ (Source location does not affect equality - two identical closures
21
+ defined in different places are equal if their structure matches) *)
22
+
23
+ (* ============================================================ *)
24
+ (* SCRIPT STRUCTURE *)
25
+ (* ============================================================ *)
26
+
27
+ script = [ frontmatter ] , { statement } ;
28
+ (* Semantic: Returns the value of the last statement.
29
+ "5" evaluates to 5, equivalent to "5 -> return".
30
+ Empty script returns null at the API boundary. *)
31
+
32
+ (* Frontmatter is syntactically recognized but semantically opaque torill.
33
+ The content between --- markers is passed through to the runtime/caller
34
+ for application-specific interpretation.rill does not parse or validate
35
+ frontmatter fields — that responsibility belongs to the embedding application. *)
36
+ frontmatter = "---" , newline , { yaml-line } , "---" , newline ;
37
+ yaml-line = { yaml-char } , newline ;
38
+ yaml-char = letter | digit | ":" | " " | "-" | "_" | "." | "[" | "]" | "," | "'" | '"' ;
39
+
40
+ (* ============================================================ *)
41
+ (* STATEMENTS *)
42
+ (* ============================================================ *)
43
+
44
+ (* Statement boundaries:rill requires no semicolons or explicit terminators.
45
+ The grammar is unambiguous because continuation and start tokens are disjoint.
46
+
47
+ Continuation tokens (extend current statement):
48
+ "->" pipe operator (to pipe-target or capture)
49
+ ":>" capture operator (captures value and continues chain)
50
+ "." method chain
51
+ "?" conditional operator (expr ? then ! else)
52
+ "!" else clause (after conditional then-branch)
53
+
54
+ Statement-start tokens (begin new statement):
55
+ "$" variable or closure-call (if followed by "name(")
56
+ identifier host call (followed by "(")
57
+ "@" for-loop or while-loop
58
+ "{" block
59
+ "|" closure (|params| body)
60
+ literals strings, numbers, bools, tuples, dicts
61
+
62
+ After any complete expression, if the next token is not a continuation,
63
+ it must begin a new statement. No lookahead beyond one token is needed.
64
+
65
+ Line continuation: -> or :> at the beginning of a line continues the
66
+ previous line's pipe chain. This allows readable multi-line chains:
67
+ "hello"
68
+ :> $greeting
69
+ -> .upper *)
70
+
71
+ (* Parsing note: "->" capture vs "->" pipe-target is unambiguous because
72
+ capture ("$" identifier) and pipe-target are disjoint sets. Parsers
73
+ should greedily consume pipe-chain, then check for trailing "-> $id". *)
74
+
75
+ (* Capture operators:
76
+ - ":>" (capture-arrow) captures value and continues chain
77
+ - "->" to "$var" (terminal) captures value and ends chain
78
+
79
+ ":>" is preferred for most captures because it's clearer:
80
+ "hello" :> $x -> .upper -- capture "hello", chain continues, result is "HELLO"
81
+ "hello" :> $x -- capture and emit (result is "hello")
82
+
83
+ Chain terminators: break and return terminate pipe chains.
84
+
85
+ "hello" -> $x -- terminal capture (ends chain)
86
+ "hello" -> $x -> .len -- inline capture (-> $x ->, value flows through)
87
+ "hello" :> $x -> .len -- capture arrow (clearer, same effect)
88
+ [1,2,3] -> each { break } -- break terminates (exits loop)
89
+ "result" -> return -- return terminates (exits function) *)
90
+
91
+ statement = [ annotation ] , pipe-chain ;
92
+ (* pipe-chain includes optional terminator: capture | "break" | "return" *)
93
+
94
+ (* Annotations: ^(key: value, ...) prefix modifying statement behavior.
95
+ Common use: loop limits via ^(limit: N).
96
+ annotation = "^" , "(" , [ annotation-arg , { "," , annotation-arg } ] , ")" ;
97
+ annotation-arg = identifier , ":" , literal ;
98
+ *)
99
+ annotation = "^" , "(" , [ annotation-arg , { "," , annotation-arg } ] , ")" ;
100
+ annotation-arg = identifier , ":" , literal ;
101
+
102
+ (* ============================================================ *)
103
+ (* EXPRESSIONS *)
104
+ (* ============================================================ *)
105
+
106
+ expression = pipe-chain ;
107
+
108
+ pipe-chain = value-expr , [ "?" , body , [ "!" , body ] ]
109
+ , { ( "->" , ( pipe-target | capture ) ) | ( ":>" , capture ) }
110
+ , [ "->" , chain-terminator ] ;
111
+ (* Expression head can include arithmetic, comparisons, and logical ops.
112
+ 5 + 3 -- standalone arithmetic
113
+ 5 > 3 -- comparison (returns bool)
114
+ $a && $b -- logical and
115
+ $x > 0 && $x < 10 -- comparison with logical ops
116
+ !$ready || $timeout -- logical or with negation
117
+ 5 > 3 ? "yes" ! "no" -- expression as conditional
118
+ When the head depends on $, "$ ->" is implied:
119
+ .len ≡ $ -> .len, ? "yes" ! "no" ≡ $ -> ? "yes" ! "no" *)
120
+
121
+ (* Expression precedence (lowest to highest):
122
+ || -> && -> comparison -> + - -> * / % -> unary (- !) -> postfix *)
123
+ (* value-expr is the expression before any pipes or conditionals *)
124
+ value-expr = logical-or ;
125
+ logical-or = logical-and , { "||" , logical-and } ;
126
+ logical-and = comparison , { "&&" , comparison } ;
127
+ comparison = additive , [ comparison-op , additive ] ;
128
+ additive = multiplicative , { ( "+" | "-" ) , multiplicative } ;
129
+ multiplicative = unary , { ( "*" | "/" | "%" ) , unary } ;
130
+ unary = [ "-" | "!" ] , postfix-expr ;
131
+
132
+ comparison-op = "==" | "!=" | "<" | ">" | "<=" | ">=" ;
133
+
134
+ chain-terminator = "break" | "return" ;
135
+ (* Note: capture can appear mid-chain or at end; handled in pipe-chain *)
136
+
137
+ postfix-expr = primary , { "." , method-name , [ "(" , [ arguments ] , ")" ] } ;
138
+
139
+ primary = literal
140
+ | variable
141
+ | closure-call
142
+ | host-call
143
+ | conditional
144
+ | loop
145
+ | block
146
+ | grouped-expr
147
+ | spread ; (* *expr - convert list/dict to tuple *)
148
+
149
+ (* implicit-primary: used in contexts where implicit $ is available (body, pipe targets).
150
+ Includes method-call because .method() expands to $ -> .method().
151
+ primary excludes method-call since statements cannot start with a bare method call. *)
152
+ implicit-primary = literal | variable | closure-call | host-call | method-call | block | grouped-expr ;
153
+
154
+ (* ============================================================ *)
155
+ (* LITERALS *)
156
+ (* ============================================================ *)
157
+
158
+ literal = string | number | bool | list | dict | closure ;
159
+
160
+ (* Closures are first-class values with optional typed parameters.
161
+ The body can be a block, grouped expression, or postfix expression.
162
+
163
+ || { "hello" } -> $greet -- no params, block body
164
+ |x| { $x -> .len } -> $strlen -- untyped param
165
+ |x: string| { $x -> .len } -> $strlen -- typed param
166
+ |x: string = "default"| { $x } -> $fn -- typed with default
167
+ |x = "default"| { $x } -> $fn -- default with inferred type
168
+ |a: number, b: number = 0| { } -> $add -- mixed params
169
+ |x| ($x + 1) -> $inc -- grouped expression body
170
+ |x| $x -> $identity -- postfix expression body
171
+
172
+ Scope rules (same as blocks):
173
+ - Captures outer scope variables (read-only)
174
+ - Local variables (declared inside) are mutable
175
+ - Cannot modify outer scope variables
176
+
177
+ Invocation:
178
+ - $fn() -- call with no args
179
+ - $fn("arg") -- call with args
180
+ - $fn -> $() -- pipe-style invoke (equivalent)
181
+ - "value" -> $fn() -- pipe value as first arg
182
+ - *[1, 2] -> $fn() -- tuple unpacking at invocation *)
183
+
184
+ closure-param = identifier , [ ":" , param-type ] , [ "=" , literal ]
185
+ | identifier , "=" , literal ; (* type inferred from default *)
186
+ param-type = "string" | "number" | "bool" ; (* primitives only for closure params *)
187
+ body = block | grouped-expr | postfix-expr ;
188
+ closure = "|" , [ closure-param , { "," , closure-param } ] , "|" , body ;
189
+
190
+ string = '"' , { char | escape | brace-escape | interpolation } , '"'
191
+ | "<<" , delimiter , newline , heredoc-body , delimiter ;
192
+
193
+ char = letter | digit | " " | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")"
194
+ | "*" | "+" | "," | "-" | "." | "/" | ":" | ";" | "<" | "=" | ">"
195
+ | "?" | "@" | "[" | "]" | "^" | "_" | "`" | "|" | "~" | newline ;
196
+ (* excludes " \ { } which have special meaning in strings *)
197
+ escape = "\\" , ( "n" | "r" | "t" | "\\" | '"' ) ;
198
+ brace-escape = "{{" | "}}" ; (* {{ -> literal {, }} -> literal } *)
199
+ delimiter = identifier ;
200
+ heredoc-body = { heredoc-line } ; (* heredocs also support interpolation *)
201
+ heredoc-line = { heredoc-char | interpolation | brace-escape } , newline ;
202
+ heredoc-char = char | '"' | "\\" ; (* excludes { } which have special meaning *)
203
+
204
+ interpolation = "{" , expression , "}" ;
205
+
206
+ number = [ "-" ] , digit , { digit } , [ "." , digit , { digit } ] ;
207
+ bool = "true" | "false" ;
208
+
209
+ list = "[" , "]"
210
+ | "[" , expression , { "," , expression } , "]" ;
211
+
212
+ dict = "[" , ":" , "]"
213
+ | "[" , dict-entry , { "," , dict-entry } , "]" ;
214
+ dict-entry = identifier , ":" , expression ; (* intentional: keys are static identifiers *)
215
+ (* Semantic: keys "keys", "values", "entries" are reserved (cannot be used as dict keys).
216
+ Semantic: closure values become "dict closures" where $ is late-bound
217
+ to the containing dict at invocation time (like "this" in other languages). *)
218
+
219
+ (* ============================================================ *)
220
+ (* VARIABLES *)
221
+ (* ============================================================ *)
222
+
223
+ variable = "$" , identifier , { property-access } , [ default-value ] , [ existence-check ]
224
+ | "$" , { property-access } , [ default-value ] , [ existence-check ] ;
225
+ (* $ARGS and $ENV are outer-scope variables provided by the runtime,
226
+ not grammar-level constructs. Same scoping rules apply. *)
227
+
228
+ (* Closure calls invoke closures stored in variables:
229
+ $strlen("hello") -- call closure with string arg
230
+ $add(1, 2) -- call closure with multiple args
231
+ $transform($data) -- call closure with variable arg
232
+ In string interpolation: "Length: {$strlen("word")}" *)
233
+ closure-call = "$" , identifier , "(" , [ arguments ] , ")" ;
234
+
235
+ (* Property access chain supports both dot and bracket access in any order:
236
+ $data.user.name -- literal fields
237
+ $data.items[0] -- bracket index
238
+ $data.items[-1] -- negative index (from end)
239
+ $data.user.$field -- variable as key
240
+ $data.items.($i + 1) -- computed expression
241
+ $data.{get_key()} -- block returning key
242
+ $data.user.(nick || name) -- alternatives (try left-to-right)
243
+ *)
244
+ property-access = field-access | bracket-access ;
245
+
246
+ field-access = "." , ( identifier
247
+ | "$" , identifier (* variable as key *)
248
+ | "(" , pipe-chain , ")" (* computed expression *)
249
+ | "{" , block , "}" (* block returning key *)
250
+ | "(" , identifier , { "||" , identifier } , ")" ) ; (* alternatives *)
251
+
252
+ bracket-access = "[" , pipe-chain , "]" ; (* index access, supports negative *)
253
+
254
+ (* Default value if property is missing: $data.name ?? "Anonymous" *)
255
+ default-value = "??" , body ;
256
+
257
+ (* Existence check: $data.user.?email returns bool
258
+ Type narrowing uses & (not :) because the expression returns bool.
259
+ Using :type would suggest type assertion on a non-bool value.
260
+ $data.?field&string means "does field exist and is it a string?" -> bool *)
261
+ existence-check = ".?" , ( identifier | "$" , identifier ) , [ "&" , type-name ] ;
262
+
263
+ digits = digit , { digit } ;
264
+
265
+ (* Capture with optional type annotation.
266
+ Variables are type-locked after first assignment.
267
+
268
+ Capture arrow (:>) captures value into variable:
269
+ "hello" :> $name -- capture, chain continues with "hello"
270
+ "hello" :> $name -> .upper -- capture, then transform (result: "HELLO")
271
+ "hello" :> $name:string -- explicit type (validated on assignment)
272
+
273
+ Pipe arrow (-> $var) invokes closure stored in $var:
274
+ "hello" -> $fn -- invoke $fn("hello") if $fn is closure
275
+ "hello" -> $fn -> .len -- invoke, then continue chain
276
+
277
+ Type locking:
278
+ "hello" :> $name -- $name is now type-locked to string
279
+ 5 :> $name -- ERROR: $name was previously string *)
280
+ capture = "$" , identifier , [ ":" , capture-type ] ;
281
+ capture-type = "string" | "number" | "bool" | "closure" | "list" | "dict" | "tuple" ;
282
+
283
+ (* ============================================================ *)
284
+ (* HOST FUNCTIONS *)
285
+ (* ============================================================ *)
286
+
287
+ host-call = host-name , "(" , [ arguments ] , ")" ;
288
+
289
+ host-name = identifier , { "::" , identifier } ;
290
+ (* Namespaced functions use :: separator: math::add, io::file::read
291
+ rill is a vanilla language. The host provides all domain functions.
292
+
293
+ Core global functions (built-in):
294
+ - type($value) -- returns type name: "string", "number", "bool", "closure", "list", "dict", "tuple"
295
+ - log($value) -- logs value using .str, returns value unchanged (passthrough)
296
+ - json($value) -- converts any value to JSON string
297
+ - parse_json($text) -- parses JSON string to value (with repair)
298
+ - identity($value) -- returns value unchanged
299
+ - range(start, end, step?) -- iterator from start (inclusive) to end (exclusive)
300
+ - repeat(value, count) -- iterator repeating value count times
301
+
302
+ Content parsing functions (built-in):
303
+ - parse_auto($text) -- auto-detect and parse structured content
304
+ - parse_xml($text, tag?) -- extract content from XML tags
305
+ - parse_fence($text, lang?) -- extract fenced code block content
306
+ - parse_fences($text) -- extract all fenced code blocks
307
+ - parse_frontmatter($text) -- parse YAML frontmatter and body
308
+ - parse_checklist($text) -- parse markdown checklist items
309
+
310
+ All other functions are registered by the host via RuntimeContext.functions. *)
311
+
312
+ arguments = expression , { "," , expression } ;
313
+
314
+ (* ============================================================ *)
315
+ (* METHODS *)
316
+ (* ============================================================ *)
317
+
318
+ (* Methods: ".method" or ".method()" with implied $ receiver.
319
+ 0-arg methods may omit parens: .empty ≡ .empty() ≡ $.empty()
320
+ In pipe: "x -> .str" passes x as receiver to str method.
321
+ In conditional: ".empty ? then ! else" tests $.empty for truthiness.
322
+ Pipe semantics: "$ -> fn" ≡ "fn($)" — piped value becomes first arg.
323
+ Global functions (type, log, json) are not methods — use as: "x -> log" *)
324
+
325
+ method-call = "." , method-name , [ "(" , [ arguments ] , ")" ] ;
326
+
327
+ method-name = "contains" | "match" | "is_match" (* pattern: 1 arg *)
328
+ | "starts_with" | "ends_with" (* prefix/suffix: 1 arg *)
329
+ | "lower" | "upper" (* case: 0 args *)
330
+ | "replace" | "replace_all" (* regex replace: 2 args *)
331
+ | "index_of" (* position: 1 arg *)
332
+ | "repeat" (* repeat: 1 arg *)
333
+ | "pad_start" | "pad_end" (* padding: 1-2 args *)
334
+ | "empty" | "lines" (* utility: 0 args *)
335
+ | "eq" | "ne" | "lt" | "gt" | "le" | "ge" (* comparison: 1 arg *)
336
+ | "str" | "num" | "len" | "trim" (* conversion: 0 args *)
337
+ | "head" | "tail" (* element access: 0 args *)
338
+ | "first" (* iterator: 0 args, returns iterator *)
339
+ | "at" (* element access: 1 arg *)
340
+ | "split" | "join" (* string ops: 0-1 args *)
341
+ | "keys" | "values" | "entries" ; (* dict: 0 args *)
342
+ (* Note: .str is built-in on ALL types including tuple.
343
+ Use global log() and json() functions instead of methods. *)
344
+
345
+ (* Arity constraints (semantic check, not grammar-enforced):
346
+ The grammar allows [ arguments ] for flexibility, but these methods
347
+ have fixed arity that parsers/interpreters must validate:
348
+ 0 args: empty, lines, str, num, len, trim, head, tail, first, keys, values, entries, lower, upper
349
+ 0-1 args: split, join (separator defaults to newline/comma)
350
+ 1 arg: contains, match, is_match, starts_with, ends_with, index_of, repeat, eq, ne, lt, gt, le, ge, at
351
+ 1-2 args: pad_start, pad_end (fill defaults to space)
352
+ 2 args: replace, replace_all *)
353
+
354
+ (* ============================================================ *)
355
+ (* PIPES *)
356
+ (* ============================================================ *)
357
+
358
+ (* pipe-chain is defined in EXPRESSIONS section *)
359
+
360
+ (* Capture: stores value in a variable and terminates the pipe chain.
361
+ The stored value also becomes the expression result.
362
+
363
+ Example: fetch("url") -> $result
364
+ The value is stored in $result and returned.
365
+
366
+ Note: Unlike earlier versions, capture ALWAYS terminates the chain.
367
+ Use explicit chaining for multi-step operations:
368
+ fetch("url") -> $result
369
+ $result -> log -> ?(.contains("x")) { } *)
370
+
371
+ (* Pipe targets: bare host names allowed without parens.
372
+ "$ -> greet" ≡ "greet($)" — piped value becomes implicit first arg. *)
373
+
374
+ pipe-target = host-call (* $ -> greet("world") — call with explicit args *)
375
+ | closure-call (* $ -> $fn("arg") — invoke closure with args *)
376
+ | pipe-invoke (* $ -> $() — invoke $ as closure, $ -> $(args) with args *)
377
+ | variable (* $ -> $fn — invoke closure stored in $fn with $ as arg *)
378
+ | host-name (* $ -> greet — bare name, $ becomes implicit first arg *)
379
+ | method-call (* $ -> .len — method on $, $ -> .at(0) with args *)
380
+ | string (* $ -> "hello {$}" — template with interpolation *)
381
+ | conditional (* $ -> ? "yes" ! "no" — conditional on $ *)
382
+ | loop (* $ -> @(cond) { body } — while loop *)
383
+ | each-expr (* $ -> each { body } — sequential iteration *)
384
+ | map-expr (* $ -> map { body } — parallel iteration *)
385
+ | fold-expr (* $ -> fold(init) { body } — reduction *)
386
+ | filter-expr (* $ -> filter { cond } — parallel filtering *)
387
+ | block (* $ -> { .len } — block with implicit $ head *)
388
+ | grouped-expr (* $ -> ($ + 1) — arithmetic/compound expression *)
389
+ | closure-chain (* $ -> @[$f, $g] — chain: $g($f($)) *)
390
+ | destructure (* $ -> *<$a, $b> — extract elements into vars *)
391
+ | slice (* $ -> /<0:3> — extract portion of $ *)
392
+ | type-assertion (* $ -> :string — assert $ is string type *)
393
+ | type-check (* $ -> :?string — check if $ is string type *)
394
+ | "*" ; (* $ -> * — convert $ to tuple for unpacking *)
395
+
396
+ (* Closure chain (@): compose closures left-to-right.
397
+ - Chain: x -> @[$f,$g,$h] -- $h($g($f(x)))
398
+ The target must be a closure or list of closures. *)
399
+
400
+ closure-chain = "@" , ( variable | list ) ; (* @$var or @[closures] *)
401
+
402
+ (* ============================================================ *)
403
+ (* EXTRACTION OPERATORS *)
404
+ (* ============================================================ *)
405
+
406
+ (* Extraction operators transform collections by extracting elements.
407
+ They return the extracted values while optionally binding to variables.
408
+
409
+ Destructure: extract elements into variables
410
+ - List: [1, 2, 3] -> *<$a, $b, $c>
411
+ - Dict: [name: "x"] -> *<name: $n>
412
+ - Skip: [1, 2, 3] -> *<$a, _, $c>
413
+ - Nested: [[1, 2], 3] -> *<*<$a, $b>, $c>
414
+
415
+ Slice: extract portions using Python-style start:stop:step
416
+ - [1, 2, 3, 4, 5] -> /<1:4> -> [2, 3, 4]
417
+ - [1, 2, 3, 4, 5] -> /<::-1> -> [5, 4, 3, 2, 1]
418
+ - "hello" -> /<1:4> -> "ell"
419
+
420
+ Enumerate: use global enumerate() function instead
421
+ - enumerate([10, 20]) -> [[index: 0, value: 10], [index: 1, value: 20]]
422
+ - enumerate([a: 1]) -> [[index: 0, key: "a", value: 1]] *)
423
+
424
+ destructure = "*<" , [ destruct-pattern , { "," , destruct-pattern } ] , ">" ;
425
+ destruct-pattern = "$" , identifier , [ ":" , capture-type ] (* variable *)
426
+ | identifier , ":" , "$" , identifier , [ ":" , capture-type ] (* key: $var *)
427
+ | "_" (* skip placeholder *)
428
+ | destructure ; (* nested pattern *)
429
+
430
+ slice = "/<" , [ slice-bound ] , ":" , [ slice-bound ] , [ ":" , [ slice-bound ] ] , ">" ;
431
+ slice-bound = number | variable | grouped-expr ;
432
+
433
+ (* Spread: convert list/dict to tuple for unpacking at closure invocation.
434
+ The tuple type exists solely to signal "unpack these values as separate arguments."
435
+
436
+ Syntax:
437
+ *[1, 2, 3] -- positional tuple from list literal
438
+ *[x: 1, y: 2] -- named tuple from dict literal
439
+ *$myList -- positional tuple from list variable
440
+ *$myDict -- named tuple from dict variable
441
+ [1, 2, 3] -> * -- convert pipe value to tuple
442
+
443
+ Key-based matching at invocation:
444
+ - Numeric keys (from list): match parameters by position
445
+ - String keys (from dict): match parameters by name
446
+
447
+ Example:
448
+ |a, b| { [$b, $a] } -> $flip
449
+ *[1, 2] -> $flip() -- $a=1, $b=2, returns [2, 1]
450
+ *[a: 1, b: 2] -> $flip() -- named tuple, same result
451
+
452
+ Strict validation: missing or extra arguments error at invocation.
453
+ Parameter defaults provide explicit opt-in leniency. *)
454
+
455
+ spread = "*" , [ postfix-expr ] ;
456
+ (* Bare * uses implicit $: each { * } ≡ each { $ -> * } *)
457
+
458
+ (* Pipe-invoke: call the pipe value ($) as a closure
459
+ ||{ "hello" } -> $() -- invoke with no args
460
+ |x|$x -> $("world") -- invoke with args
461
+ $closure -> $() -- invoke variable that holds a closure *)
462
+ pipe-invoke = "$" , "(" , [ arguments ] , ")" ;
463
+
464
+ (* Type assertion: assert value is a specific type, error if mismatch.
465
+ Returns value unchanged on success. Can be used as:
466
+ - Postfix: 42:number, (expr):type - binds tighter than method calls
467
+ - Pipe target: -> :type - asserts on pipe value ($)
468
+
469
+ Examples:
470
+ 42:number -- postfix assertion
471
+ (1 + 2):number -- postfix on grouped expr
472
+ (($ < 5) @ ($ + 1)):number -- postfix on loop result
473
+ "hello" -> :string -- pipe target assertion
474
+ $val -> :dict -> .keys -- assert then continue chain *)
475
+ type-assertion = ":" , type-name ;
476
+
477
+ (* Type check: check if value is a specific type, returns bool.
478
+ Can be used as postfix (expr:?type) or pipe target (-> :?type).
479
+
480
+ Examples:
481
+ 42:?number -- true (postfix)
482
+ "hello":?number -- false (postfix)
483
+ $val -> :?list ? process() -- conditional on type *)
484
+ type-check = ":" , "?" , type-name ;
485
+
486
+ type-name = "string" | "number" | "bool" | "closure" | "list" | "dict" | "tuple" ;
487
+
488
+ (* ============================================================ *)
489
+ (* CONTROL FLOW *)
490
+ (* ============================================================ *)
491
+
492
+ (* Conditional syntax: condition ? then-branch ! else-branch
493
+ The condition MUST evaluate to a boolean value (true or false).
494
+ Non-boolean values will cause a runtime error.
495
+
496
+ Examples:
497
+ $ready ? "go" ! "wait" -- variable must be boolean
498
+ .valid ? process() ! reject() -- method must return boolean
499
+ ($x > 10) ? "big" ! "small" -- comparison returns boolean
500
+ true ? "yes" ! "no" -- literal boolean
501
+
502
+ Piped form (? alone uses $ as condition):
503
+ true -> ? "yes" ! "no" -- pipe value must be boolean
504
+ ($x > 5) -> ? "big" ! "small" -- expression evaluates to boolean
505
+
506
+ Else-if chaining:
507
+ .eq("A") ? 1 ! .eq("B") ? 2 ! 3 -- chained conditions *)
508
+
509
+ conditional = body , "?" , body , [ "!" , else-clause ]
510
+ | "?" , body , [ "!" , else-clause ] ; (* piped form: condition is $ *)
511
+ else-clause = conditional | body ;
512
+
513
+ (* Loop syntax: condition @ body [? condition]
514
+ The @ operator is for while loops only. For iteration, use collection operators.
515
+ Both while and do-while conditions MUST evaluate to boolean (true or false).
516
+ Non-boolean values will cause a runtime error.
517
+
518
+ - bool input: while loop (re-evaluates condition each iteration)
519
+ - trailing ? condition: do-while (body first, then check)
520
+
521
+ For-each iteration uses collection operators (each, map, filter, fold).
522
+ See docs/collections.md for collection operators.
523
+
524
+ Examples:
525
+ ($x < 10) @ { ($x + 1) :> $x } -- while loop (condition is bool)
526
+ @ { $ } ? ($x < 10) -- do-while (body, then check)
527
+ [1, 2, 3] -> each { $ * 2 } -- for-each (use each operator)
528
+ "abc" -> each { $ } -- iterate over string chars
529
+ [a: 1, b: 2] -> each { $.key } -- iterate over dict entries
530
+ $items -> each { process($) } -- iterate over variable *)
531
+
532
+ loop = [ body ] , "@" , body , [ "?" , body ] ;
533
+ (* First body: input (condition for while, iterable for for-each)
534
+ Second body: loop body
535
+ Third body (after ?): do-while condition *)
536
+
537
+ (* ============================================================ *)
538
+ (* COLLECTION OPERATORS *)
539
+ (* ============================================================ *)
540
+
541
+ (* Collection operators for iteration and reduction.
542
+ See docs/collections.md for complete documentation.
543
+
544
+ Operator comparison:
545
+ - each: Sequential iteration, returns list of all results
546
+ - map: Parallel iteration (Promise.all), returns list of all results
547
+ - filter: Parallel filtering, returns elements where predicate is truthy
548
+ - fold: Sequential reduction, returns final result only
549
+
550
+ Body forms (all operators):
551
+ - block: { body } -- $ is current element
552
+ - grouped: ( expr ) -- $ is current element
553
+ - closure: |x| body -- named parameter
554
+ - variable: $fn -- pre-defined closure
555
+ - identity: $ -- return elements unchanged
556
+
557
+ Accumulator (each and fold only):
558
+ - Block form: (init) { body } -- $@ is accumulator, $ is element
559
+ - Closure: |x, acc = init| -- named accumulator parameter
560
+
561
+ Dict iteration binds $ to { key, value } objects.
562
+
563
+ Examples:
564
+ [1, 2, 3] -> each { $ * 2 } -- [2, 4, 6]
565
+ [1, 2, 3] -> map ($ + 10) -- [11, 12, 13]
566
+ [1, 2, 3, 4, 5] -> filter { $ > 2 } -- [3, 4, 5]
567
+ [1, 2, 3] -> fold(0) { $@ + $ } -- 6
568
+ [1, 2, 3] -> each(0) { $@ + $ } -- [1, 3, 6] (running sum)
569
+ [1, 2, 3] -> each |x, s = 0| ($s + $x) -- [1, 3, 6]
570
+ *)
571
+
572
+ iterator-body = closure | block | grouped-expr | postfix-expr | host-name ;
573
+ (* Valid body forms for each, map, fold, filter operators.
574
+ closure: |x| body or |x, acc = init| body
575
+ block: { body } where $ is current element
576
+ grouped-expr: ( expr ) where $ is current element
577
+ postfix-expr: includes variables ($fn), bare $ for identity, and .method shorthand
578
+ host-name: bare function name (func or ns::func), element passed as first arg
579
+ Note: variable not listed separately since postfix-expr -> primary -> variable
580
+ .method shorthand applies method to each element: ["a","b"] -> map .upper *)
581
+
582
+ each-expr = "each" , [ "(" , expression , ")" ] , iterator-body ;
583
+ (* Sequential iteration returning list of all body results.
584
+ Optional (expression) provides initial accumulator value.
585
+ With accumulator: $@ is accumulator, $ is current element.
586
+ Supports break for early termination.
587
+ [1, 2, 3] -> each { $ * 2 } -- [2, 4, 6]
588
+ [1, 2, 3] -> each(0) { $@ + $ } -- [1, 3, 6] *)
589
+
590
+ map-expr = "map" , iterator-body ;
591
+ (* Parallel iteration returning list of all body results.
592
+ Executes all iterations concurrently via Promise.all.
593
+ Order is preserved despite parallel execution.
594
+ No accumulator (parallel has no "previous" value).
595
+ No break support.
596
+ [1, 2, 3] -> map { $ * 2 } -- [2, 4, 6]
597
+ $urls -> map |url| fetch($url) -- concurrent fetch *)
598
+
599
+ fold-expr = "fold" , ( "(" , expression , ")" , iterator-body | closure | variable ) ;
600
+ (* Sequential reduction returning final accumulated value.
601
+ Requires accumulator (reduction semantics need initial value).
602
+ Block form: (init) { body } where $@ is accumulator.
603
+ Closure form: |x, acc = init| body.
604
+ Variable form: $fn where closure defines accumulator.
605
+ No break support.
606
+ [1, 2, 3] -> fold(0) { $@ + $ } -- 6
607
+ [1, 2, 3] -> fold |x, s = 0| ($s + $x) -- 6 *)
608
+
609
+ filter-expr = "filter" , iterator-body ;
610
+ (* Parallel filtering returning elements where predicate is true.
611
+ Predicate MUST return boolean (true or false).
612
+ Non-boolean values will cause a runtime error.
613
+ Executes all predicates concurrently via Promise.all.
614
+ Order is preserved despite parallel execution.
615
+ No accumulator (each element evaluated independently).
616
+ [1, 2, 3, 4, 5] -> filter { $ > 2 } -- [3, 4, 5]
617
+ $users -> filter |u| ($u.active) -- active users (must be bool) *)
618
+
619
+ block = "{" , statement , { statement } , "}" ;
620
+ (* Empty blocks not allowed. Blocks return their last expression's value,
621
+ enabling use in boolean contexts and as pipe sources. *)
622
+
623
+ (* ============================================================ *)
624
+ (* NOTE: BOOLEAN/LOGICAL OPERATORS *)
625
+ (* ============================================================ *)
626
+
627
+ (* Logical operators (&&, ||, !) are integrated into the main expression
628
+ grammar (see value-expr above). They work everywhere expressions
629
+ work — no special-casing needed.
630
+
631
+ Precedence is standard: || < && < comparison < arithmetic < unary
632
+
633
+ Examples:
634
+ $a && $b -- logical and
635
+ $x > 0 || $x < -10 -- comparison with logical or
636
+ !$ready && $timeout -- negation with logical and
637
+ ($a || $b) && $c -- grouped for precedence
638
+ $valid && $ready ? "go" ! "wait" -- used as conditional *)
639
+
640
+ (* ============================================================ *)
641
+ (* ARITHMETIC EXPRESSIONS *)
642
+ (* ============================================================ *)
643
+
644
+ (* Arithmetic uses parenthesis grouping: ( expr )
645
+ Operators follow standard precedence: * / % before + -
646
+
647
+ (5 + 3) -> 8
648
+ (2 + 3 * 4) -> 14
649
+ ((2 + 3) * 4) -> 20
650
+ 10 -> ($ + 5) -> 15 ($ is pipe value)
651
+ 3 -> $x ($ * 2) -> 6 (variables in expressions)
652
+ (10 / 0) -> ERROR (division by zero)
653
+
654
+ Type constraint: all operands must be numbers.
655
+ "5" -> ($ + 1) -> ERROR (string is not a number)
656
+
657
+ Grouped expressions can appear:
658
+ - As standalone expressions: (5 + 3)
659
+ - As pipe targets: 5 -> ($ * 2)
660
+ - Nested: ((1 + 2) * 3)
661
+ - As closure bodies: |x| ($x + 1)
662
+
663
+ Note: Arithmetic works everywhere now, not just in grouped expressions.
664
+ 5 + 3, { 5 + 3 }, and (5 + 3) all work. Grouped expressions provide
665
+ explicit scoping for captures. *)
666
+
667
+ grouped-expr = "(" , pipe-chain , ")" ;
668
+ (* Single-expression block with () delimiters.
669
+ Uses the same parsing as any expression. *)
670
+
671
+ (* ============================================================ *)
672
+ (* LEXICAL *)
673
+ (* ============================================================ *)
674
+
675
+ identifier = ( letter | "_" ) , { letter | digit | "_" } ;
676
+ (* Style: snake_case is idiomatic for identifiers.
677
+ Functions: parse_json, replace_all, pad_start
678
+ Variables: $user_name, $total_count, $api_response
679
+ Methods: .starts_with, .ends_with, .index_of *)
680
+
681
+ letter = "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M"
682
+ | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z"
683
+ | "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m"
684
+ | "n" | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" | "y" | "z" ;
685
+ digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ;
686
+ newline = "\n" | "\r\n" ;
687
+
688
+ (* ============================================================ *)
689
+ (* WHITESPACE & COMMENTS *)
690
+ (* ============================================================ *)
691
+
692
+ (* Whitespace (spaces, tabs, newlines) between tokens is insignificant.
693
+ Comments start with # and extend to end of line. *)