openalgo-script 0.5.0 → 0.7.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 (343) hide show
  1. package/CHANGELOG.md +226 -0
  2. package/README.md +53 -22
  3. package/dist/adapters/charts/run.d.ts.map +1 -1
  4. package/dist/adapters/charts/run.js +6 -0
  5. package/dist/adapters/charts/run.js.map +1 -1
  6. package/dist/adapters/charts/tables.d.ts +6 -19
  7. package/dist/adapters/charts/tables.d.ts.map +1 -1
  8. package/dist/adapters/charts/tables.js.map +1 -1
  9. package/dist/adapters/charts/undrawable.d.ts +4 -0
  10. package/dist/adapters/charts/undrawable.d.ts.map +1 -0
  11. package/dist/adapters/charts/undrawable.js +47 -0
  12. package/dist/adapters/charts/undrawable.js.map +1 -0
  13. package/dist/core/backtest/drive.d.ts +36 -1
  14. package/dist/core/backtest/drive.d.ts.map +1 -1
  15. package/dist/core/backtest/drive.js +13 -107
  16. package/dist/core/backtest/drive.js.map +1 -1
  17. package/dist/core/backtest/index.d.ts +2 -0
  18. package/dist/core/backtest/index.d.ts.map +1 -1
  19. package/dist/core/backtest/index.js.map +1 -1
  20. package/dist/core/backtest/walk.d.ts +39 -0
  21. package/dist/core/backtest/walk.d.ts.map +1 -0
  22. package/dist/core/backtest/walk.js +125 -0
  23. package/dist/core/backtest/walk.js.map +1 -0
  24. package/dist/core/catalogue/catalogue.generated.d.ts +178 -1
  25. package/dist/core/catalogue/catalogue.generated.d.ts.map +1 -1
  26. package/dist/core/catalogue/catalogue.generated.js +18 -1
  27. package/dist/core/catalogue/catalogue.generated.js.map +1 -1
  28. package/dist/core/catalogue/types.d.ts +8 -2
  29. package/dist/core/catalogue/types.d.ts.map +1 -1
  30. package/dist/core/catalogue/values.generated.d.ts +51 -0
  31. package/dist/core/catalogue/values.generated.d.ts.map +1 -1
  32. package/dist/core/check/assigned.d.ts +19 -0
  33. package/dist/core/check/assigned.d.ts.map +1 -0
  34. package/dist/core/check/assigned.js +14 -0
  35. package/dist/core/check/assigned.js.map +1 -0
  36. package/dist/core/check/call-sites.d.ts.map +1 -1
  37. package/dist/core/check/call-sites.js +9 -4
  38. package/dist/core/check/call-sites.js.map +1 -1
  39. package/dist/core/check/calls.d.ts.map +1 -1
  40. package/dist/core/check/calls.js +12 -4
  41. package/dist/core/check/calls.js.map +1 -1
  42. package/dist/core/check/check.d.ts.map +1 -1
  43. package/dist/core/check/check.js +15 -2
  44. package/dist/core/check/check.js.map +1 -1
  45. package/dist/core/check/checker.d.ts +26 -0
  46. package/dist/core/check/checker.d.ts.map +1 -1
  47. package/dist/core/check/checker.js +38 -2
  48. package/dist/core/check/checker.js.map +1 -1
  49. package/dist/core/check/constant.d.ts +16 -0
  50. package/dist/core/check/constant.d.ts.map +1 -1
  51. package/dist/core/check/constant.js +52 -0
  52. package/dist/core/check/constant.js.map +1 -1
  53. package/dist/core/check/deleted.d.ts +3 -0
  54. package/dist/core/check/deleted.d.ts.map +1 -0
  55. package/dist/core/check/deleted.js +109 -0
  56. package/dist/core/check/deleted.js.map +1 -0
  57. package/dist/core/check/expressions.js +1 -1
  58. package/dist/core/check/expressions.js.map +1 -1
  59. package/dist/core/check/library-output.d.ts.map +1 -1
  60. package/dist/core/check/library-output.js +1 -0
  61. package/dist/core/check/library-output.js.map +1 -1
  62. package/dist/core/check/library-prose.generated.js +10 -10
  63. package/dist/core/check/library-prose.generated.js.map +1 -1
  64. package/dist/core/check/library.d.ts +7 -0
  65. package/dist/core/check/library.d.ts.map +1 -1
  66. package/dist/core/check/library.js +1 -0
  67. package/dist/core/check/library.js.map +1 -1
  68. package/dist/core/check/surface.d.ts +9 -0
  69. package/dist/core/check/surface.d.ts.map +1 -1
  70. package/dist/core/check/surface.js +13 -0
  71. package/dist/core/check/surface.js.map +1 -1
  72. package/dist/core/engine/bar-source.d.ts +65 -0
  73. package/dist/core/engine/bar-source.d.ts.map +1 -0
  74. package/dist/core/engine/bar-source.js +58 -0
  75. package/dist/core/engine/bar-source.js.map +1 -0
  76. package/dist/core/engine/channels.js +2 -2
  77. package/dist/core/engine/channels.js.map +1 -1
  78. package/dist/core/engine/engine.d.ts +9 -11
  79. package/dist/core/engine/engine.d.ts.map +1 -1
  80. package/dist/core/engine/engine.js +26 -27
  81. package/dist/core/engine/engine.js.map +1 -1
  82. package/dist/core/engine/guard.d.ts.map +1 -1
  83. package/dist/core/engine/guard.js +10 -0
  84. package/dist/core/engine/guard.js.map +1 -1
  85. package/dist/core/engine/host.d.ts +4 -2
  86. package/dist/core/engine/host.d.ts.map +1 -1
  87. package/dist/core/engine/host.js.map +1 -1
  88. package/dist/core/engine/index.d.ts +1 -0
  89. package/dist/core/engine/index.d.ts.map +1 -1
  90. package/dist/core/engine/index.js.map +1 -1
  91. package/dist/core/engine/library/binding.d.ts +2 -0
  92. package/dist/core/engine/library/binding.d.ts.map +1 -1
  93. package/dist/core/engine/library/binding.js.map +1 -1
  94. package/dist/core/engine/library/index.d.ts +8 -0
  95. package/dist/core/engine/library/index.d.ts.map +1 -1
  96. package/dist/core/engine/library/index.js +10 -0
  97. package/dist/core/engine/library/index.js.map +1 -1
  98. package/dist/core/engine/library/maths.js +2 -2
  99. package/dist/core/engine/library/maths.js.map +1 -1
  100. package/dist/core/engine/library/objects.d.ts.map +1 -1
  101. package/dist/core/engine/library/objects.js +1 -1
  102. package/dist/core/engine/library/objects.js.map +1 -1
  103. package/dist/core/engine/machine.js +5 -5
  104. package/dist/core/engine/machine.js.map +1 -1
  105. package/dist/core/engine/memory.js +2 -2
  106. package/dist/core/engine/memory.js.map +1 -1
  107. package/dist/core/engine/registers.js +2 -2
  108. package/dist/core/engine/registers.js.map +1 -1
  109. package/dist/core/engine/request-body.d.ts +3 -1
  110. package/dist/core/engine/request-body.d.ts.map +1 -1
  111. package/dist/core/engine/request-body.js +3 -1
  112. package/dist/core/engine/request-body.js.map +1 -1
  113. package/dist/core/engine/request-plan.d.ts +3 -3
  114. package/dist/core/engine/request-plan.js +3 -3
  115. package/dist/core/engine/requests.d.ts +2 -2
  116. package/dist/core/engine/requests.d.ts.map +1 -1
  117. package/dist/core/engine/requests.js +5 -4
  118. package/dist/core/engine/requests.js.map +1 -1
  119. package/dist/core/engine/series.d.ts +18 -6
  120. package/dist/core/engine/series.d.ts.map +1 -1
  121. package/dist/core/engine/series.js +24 -5
  122. package/dist/core/engine/series.js.map +1 -1
  123. package/dist/core/engine/session/record.d.ts +4 -4
  124. package/dist/core/engine/session/record.js +4 -4
  125. package/dist/core/engine/values/index.d.ts +1 -1
  126. package/dist/core/engine/values/index.d.ts.map +1 -1
  127. package/dist/core/engine/values/index.js +1 -1
  128. package/dist/core/engine/values/index.js.map +1 -1
  129. package/dist/core/engine/values/value.d.ts +2 -0
  130. package/dist/core/engine/values/value.d.ts.map +1 -1
  131. package/dist/core/engine/values/value.js +4 -0
  132. package/dist/core/engine/values/value.js.map +1 -1
  133. package/dist/core/importer/calls.d.ts +29 -0
  134. package/dist/core/importer/calls.d.ts.map +1 -0
  135. package/dist/core/importer/calls.js +267 -0
  136. package/dist/core/importer/calls.js.map +1 -0
  137. package/dist/core/importer/context.d.ts +118 -0
  138. package/dist/core/importer/context.d.ts.map +1 -0
  139. package/dist/core/importer/context.js +120 -0
  140. package/dist/core/importer/context.js.map +1 -0
  141. package/dist/core/importer/expressions.d.ts +44 -0
  142. package/dist/core/importer/expressions.d.ts.map +1 -0
  143. package/dist/core/importer/expressions.js +223 -0
  144. package/dist/core/importer/expressions.js.map +1 -0
  145. package/dist/core/importer/importer.d.ts +13 -0
  146. package/dist/core/importer/importer.d.ts.map +1 -0
  147. package/dist/core/importer/importer.js +220 -0
  148. package/dist/core/importer/importer.js.map +1 -0
  149. package/dist/core/importer/index.d.ts +20 -0
  150. package/dist/core/importer/index.d.ts.map +1 -0
  151. package/dist/core/importer/index.js +18 -0
  152. package/dist/core/importer/index.js.map +1 -0
  153. package/dist/core/importer/lexer.d.ts +68 -0
  154. package/dist/core/importer/lexer.d.ts.map +1 -0
  155. package/dist/core/importer/lexer.js +207 -0
  156. package/dist/core/importer/lexer.js.map +1 -0
  157. package/dist/core/importer/names.d.ts +41 -0
  158. package/dist/core/importer/names.d.ts.map +1 -0
  159. package/dist/core/importer/names.js +78 -0
  160. package/dist/core/importer/names.js.map +1 -0
  161. package/dist/core/importer/orders.d.ts +8 -0
  162. package/dist/core/importer/orders.d.ts.map +1 -0
  163. package/dist/core/importer/orders.js +332 -0
  164. package/dist/core/importer/orders.js.map +1 -0
  165. package/dist/core/importer/outputs.d.ts +19 -0
  166. package/dist/core/importer/outputs.d.ts.map +1 -0
  167. package/dist/core/importer/outputs.js +266 -0
  168. package/dist/core/importer/outputs.js.map +1 -0
  169. package/dist/core/importer/parser.d.ts +5 -0
  170. package/dist/core/importer/parser.d.ts.map +1 -0
  171. package/dist/core/importer/parser.js +439 -0
  172. package/dist/core/importer/parser.js.map +1 -0
  173. package/dist/core/importer/presence.d.ts +32 -0
  174. package/dist/core/importer/presence.d.ts.map +1 -0
  175. package/dist/core/importer/presence.js +146 -0
  176. package/dist/core/importer/presence.js.map +1 -0
  177. package/dist/core/importer/settle.d.ts +23 -0
  178. package/dist/core/importer/settle.d.ts.map +1 -0
  179. package/dist/core/importer/settle.js +135 -0
  180. package/dist/core/importer/settle.js.map +1 -0
  181. package/dist/core/importer/statements.d.ts +4 -0
  182. package/dist/core/importer/statements.d.ts.map +1 -0
  183. package/dist/core/importer/statements.js +339 -0
  184. package/dist/core/importer/statements.js.map +1 -0
  185. package/dist/core/importer/syntax.d.ts +182 -0
  186. package/dist/core/importer/syntax.d.ts.map +1 -0
  187. package/dist/core/importer/syntax.js +14 -0
  188. package/dist/core/importer/syntax.js.map +1 -0
  189. package/dist/core/importer/table.d.ts +52 -0
  190. package/dist/core/importer/table.d.ts.map +1 -0
  191. package/dist/core/importer/table.js +156 -0
  192. package/dist/core/importer/table.js.map +1 -0
  193. package/dist/core/importer/values.d.ts +27 -0
  194. package/dist/core/importer/values.d.ts.map +1 -0
  195. package/dist/core/importer/values.js +236 -0
  196. package/dist/core/importer/values.js.map +1 -0
  197. package/dist/core/index.d.ts +12 -0
  198. package/dist/core/index.d.ts.map +1 -1
  199. package/dist/core/index.js +11 -0
  200. package/dist/core/index.js.map +1 -1
  201. package/dist/core/stdlib/averages/exponential.d.ts.map +1 -1
  202. package/dist/core/stdlib/averages/exponential.js +3 -6
  203. package/dist/core/stdlib/averages/exponential.js.map +1 -1
  204. package/dist/core/stdlib/averages/gaussian.d.ts +15 -0
  205. package/dist/core/stdlib/averages/gaussian.d.ts.map +1 -0
  206. package/dist/core/stdlib/averages/gaussian.js +55 -0
  207. package/dist/core/stdlib/averages/gaussian.js.map +1 -0
  208. package/dist/core/stdlib/averages/shaped.d.ts +2 -6
  209. package/dist/core/stdlib/averages/shaped.d.ts.map +1 -1
  210. package/dist/core/stdlib/averages/shaped.js +8 -22
  211. package/dist/core/stdlib/averages/shaped.js.map +1 -1
  212. package/dist/core/stdlib/calendar/civil.d.ts.map +1 -1
  213. package/dist/core/stdlib/calendar/civil.js +5 -2
  214. package/dist/core/stdlib/calendar/civil.js.map +1 -1
  215. package/dist/core/stdlib/maths/arithmetic.d.ts.map +1 -1
  216. package/dist/core/stdlib/maths/arithmetic.js +2 -1
  217. package/dist/core/stdlib/maths/arithmetic.js.map +1 -1
  218. package/dist/core/stdlib/maths/elementary.d.ts +3 -20
  219. package/dist/core/stdlib/maths/elementary.d.ts.map +1 -1
  220. package/dist/core/stdlib/maths/elementary.js +17 -13
  221. package/dist/core/stdlib/maths/elementary.js.map +1 -1
  222. package/dist/core/stdlib/maths/hypot.d.ts +8 -0
  223. package/dist/core/stdlib/maths/hypot.d.ts.map +1 -0
  224. package/dist/core/stdlib/maths/hypot.js +68 -0
  225. package/dist/core/stdlib/maths/hypot.js.map +1 -0
  226. package/dist/core/stdlib/maths/power.d.ts +3 -0
  227. package/dist/core/stdlib/maths/power.d.ts.map +1 -0
  228. package/dist/core/stdlib/maths/power.js +83 -0
  229. package/dist/core/stdlib/maths/power.js.map +1 -0
  230. package/dist/core/stdlib/maths/transcendental.d.ts +15 -0
  231. package/dist/core/stdlib/maths/transcendental.d.ts.map +1 -0
  232. package/dist/core/stdlib/maths/transcendental.js +185 -0
  233. package/dist/core/stdlib/maths/transcendental.js.map +1 -0
  234. package/dist/core/stdlib/maths/trigonometric.d.ts +4 -0
  235. package/dist/core/stdlib/maths/trigonometric.d.ts.map +1 -0
  236. package/dist/core/stdlib/maths/trigonometric.js +196 -0
  237. package/dist/core/stdlib/maths/trigonometric.js.map +1 -0
  238. package/dist/core/stdlib/series/conditions.d.ts +2 -4
  239. package/dist/core/stdlib/series/conditions.d.ts.map +1 -1
  240. package/dist/core/stdlib/series/conditions.js +9 -13
  241. package/dist/core/stdlib/series/conditions.js.map +1 -1
  242. package/dist/core/stdlib/series/statistics.d.ts.map +1 -1
  243. package/dist/core/stdlib/series/statistics.js +2 -0
  244. package/dist/core/stdlib/series/statistics.js.map +1 -1
  245. package/dist/core/stdlib/values/history.d.ts +26 -0
  246. package/dist/core/stdlib/values/history.d.ts.map +1 -0
  247. package/dist/core/stdlib/values/history.js +52 -0
  248. package/dist/core/stdlib/values/history.js.map +1 -0
  249. package/dist/core/stdlib/values/index.d.ts +1 -0
  250. package/dist/core/stdlib/values/index.d.ts.map +1 -1
  251. package/dist/core/stdlib/values/index.js +1 -0
  252. package/dist/core/stdlib/values/index.js.map +1 -1
  253. package/dist/core/stdlib/values/lookback.d.ts +4 -1
  254. package/dist/core/stdlib/values/lookback.d.ts.map +1 -1
  255. package/dist/core/stdlib/values/lookback.js +48 -45
  256. package/dist/core/stdlib/values/lookback.js.map +1 -1
  257. package/dist/core/stdlib/values/region.d.ts +6 -5
  258. package/dist/core/stdlib/values/region.d.ts.map +1 -1
  259. package/dist/core/stdlib/values/region.js +3 -3
  260. package/dist/core/stdlib/values/region.js.map +1 -1
  261. package/dist/core/stdlib/volatility/measures.d.ts.map +1 -1
  262. package/dist/core/stdlib/volatility/measures.js +6 -4
  263. package/dist/core/stdlib/volatility/measures.js.map +1 -1
  264. package/dist/core/version/version.generated.d.ts +1 -1
  265. package/dist/core/version/version.generated.js +1 -1
  266. package/package.json +13 -2
  267. package/spec/errors.json +380 -20
  268. package/src/adapters/charts/run.ts +6 -0
  269. package/src/adapters/charts/tables.ts +6 -19
  270. package/src/adapters/charts/undrawable.ts +51 -0
  271. package/src/core/backtest/drive.ts +66 -154
  272. package/src/core/backtest/index.ts +2 -0
  273. package/src/core/backtest/walk.ts +182 -0
  274. package/src/core/catalogue/catalogue.generated.ts +18 -1
  275. package/src/core/catalogue/types.ts +8 -2
  276. package/src/core/catalogue/values.generated.ts +16 -0
  277. package/src/core/check/assigned.ts +31 -0
  278. package/src/core/check/call-sites.ts +11 -4
  279. package/src/core/check/calls.ts +13 -4
  280. package/src/core/check/check.ts +14 -2
  281. package/src/core/check/checker.ts +39 -2
  282. package/src/core/check/constant.ts +57 -0
  283. package/src/core/check/deleted.ts +135 -0
  284. package/src/core/check/expressions.ts +1 -1
  285. package/src/core/check/library-output.ts +1 -0
  286. package/src/core/check/library-prose.generated.ts +10 -10
  287. package/src/core/check/library.ts +8 -0
  288. package/src/core/check/surface.ts +16 -0
  289. package/src/core/engine/bar-source.ts +113 -0
  290. package/src/core/engine/channels.ts +2 -2
  291. package/src/core/engine/engine.ts +29 -28
  292. package/src/core/engine/guard.ts +12 -1
  293. package/src/core/engine/host.ts +4 -2
  294. package/src/core/engine/index.ts +1 -0
  295. package/src/core/engine/library/binding.ts +2 -0
  296. package/src/core/engine/library/index.ts +11 -0
  297. package/src/core/engine/library/maths.ts +2 -2
  298. package/src/core/engine/library/objects.ts +1 -6
  299. package/src/core/engine/machine.ts +5 -5
  300. package/src/core/engine/memory.ts +2 -2
  301. package/src/core/engine/registers.ts +2 -2
  302. package/src/core/engine/request-body.ts +5 -7
  303. package/src/core/engine/request-plan.ts +3 -3
  304. package/src/core/engine/requests.ts +11 -8
  305. package/src/core/engine/series.ts +30 -6
  306. package/src/core/engine/session/record.ts +4 -4
  307. package/src/core/engine/values/index.ts +1 -0
  308. package/src/core/engine/values/value.ts +5 -0
  309. package/src/core/importer/calls.ts +274 -0
  310. package/src/core/importer/context.ts +197 -0
  311. package/src/core/importer/expressions.ts +247 -0
  312. package/src/core/importer/importer.ts +226 -0
  313. package/src/core/importer/index.ts +19 -0
  314. package/src/core/importer/lexer.ts +285 -0
  315. package/src/core/importer/names.ts +104 -0
  316. package/src/core/importer/orders.ts +307 -0
  317. package/src/core/importer/outputs.ts +245 -0
  318. package/src/core/importer/parser.ts +436 -0
  319. package/src/core/importer/presence.ts +164 -0
  320. package/src/core/importer/settle.ts +141 -0
  321. package/src/core/importer/statements.ts +331 -0
  322. package/src/core/importer/syntax.ts +144 -0
  323. package/src/core/importer/table.ts +191 -0
  324. package/src/core/importer/values.ts +241 -0
  325. package/src/core/index.ts +13 -0
  326. package/src/core/stdlib/averages/exponential.ts +3 -4
  327. package/src/core/stdlib/averages/gaussian.ts +52 -0
  328. package/src/core/stdlib/averages/shaped.ts +8 -21
  329. package/src/core/stdlib/calendar/civil.ts +5 -2
  330. package/src/core/stdlib/maths/arithmetic.ts +2 -1
  331. package/src/core/stdlib/maths/elementary.ts +20 -33
  332. package/src/core/stdlib/maths/hypot.ts +63 -0
  333. package/src/core/stdlib/maths/power.ts +57 -0
  334. package/src/core/stdlib/maths/transcendental.ts +160 -0
  335. package/src/core/stdlib/maths/trigonometric.ts +160 -0
  336. package/src/core/stdlib/series/conditions.ts +9 -12
  337. package/src/core/stdlib/series/statistics.ts +1 -0
  338. package/src/core/stdlib/values/history.ts +70 -0
  339. package/src/core/stdlib/values/index.ts +1 -0
  340. package/src/core/stdlib/values/lookback.ts +51 -47
  341. package/src/core/stdlib/values/region.ts +7 -5
  342. package/src/core/stdlib/volatility/measures.ts +6 -4
  343. package/src/core/version/version.generated.ts +1 -1
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Inputs, and what lands on the chart.
3
+ *
4
+ * Every argument of a drawing call is about the picture, so an argument with no
5
+ * OpenScript spelling is left out with OS9009 and the call is kept. An input is
6
+ * kept as well when one of its arguments cannot be carried, because the rest of
7
+ * the script reads the value it declares, and that argument is reported with
8
+ * OS9006 instead.
9
+ *
10
+ * OpenScript keys a column, a level, a marker and an input by its title and
11
+ * refuses two of one kind with one title (OS3017), where the source dialect
12
+ * accepts both. A repeated title is therefore numbered, which changes a label
13
+ * and nothing a script computes.
14
+ */
15
+ import { diagnosticFor } from '../diagnostics/index.js';
16
+ import { bind, onlyKnown } from './calls.js';
17
+ import type { Context, OutLine } from './context.js';
18
+ import { quoted } from './context.js';
19
+ import { pathOf } from './syntax.js';
20
+ import type { Arg, CallExpr, Expr } from './syntax.js';
21
+ import { NOTHING, atom, translate } from './values.js';
22
+ import type { Out } from './values.js';
23
+
24
+ const INPUT_ARGUMENTS = ['defval', 'title', 'minval', 'maxval', 'step', 'tooltip', 'inline', 'group', 'confirm', 'options', 'display'];
25
+ const PRESENTATION_ONLY = new Set(['inline', 'confirm', 'display']);
26
+
27
+ function literalString(expr: Expr | undefined): string | undefined {
28
+ return expr?.kind === 'string' ? expr.value : undefined;
29
+ }
30
+
31
+ /** OS9009 for an argument that changes only the picture. */
32
+ function leftOut(ctx: Context, arg: Arg, name: string, call: string): void {
33
+ ctx.note(diagnosticFor('OS9009', ctx.span(arg.at), { argument: name, call }));
34
+ }
35
+
36
+ /**
37
+ * `input.int(14, "Length", minval = 1)` and its siblings, as one `input()`.
38
+ *
39
+ * `assigned` is whether the input is the whole value of a declaration, where
40
+ * OpenScript takes the name as the title and a title may be left out.
41
+ */
42
+ export function translateInput(ctx: Context, call: CallExpr, path: string, assigned: boolean): Out {
43
+ const bound = bind(call, ['defval', 'title']);
44
+ if (!onlyKnown(ctx, path, bound, INPUT_ARGUMENTS)) return NOTHING;
45
+ const defval = bound.byName.get('defval');
46
+ if (defval === undefined) {
47
+ ctx.refuse(diagnosticFor('OS9006', ctx.span(call.at), { argument: 'defval', call: path }));
48
+ return NOTHING;
49
+ }
50
+ const parts = [translate(ctx, defval.value).text];
51
+ const titleArg = bound.byName.get('title');
52
+ const written = literalString(titleArg?.value);
53
+ if (titleArg !== undefined && written === undefined) leftOut(ctx, titleArg, 'title', path);
54
+ if (written !== undefined && written !== '') parts.push(quoted(ctx.title('input', written)));
55
+ else if (!assigned) parts.push(quoted(ctx.title('input', 'Input')));
56
+
57
+ const numeric = path === 'input.int' || path === 'input.float' || (path === 'input' && defval.value.kind === 'number');
58
+ for (const [source, target] of [['minval', 'min'], ['maxval', 'max'], ['step', 'step']] as const) {
59
+ const arg = bound.byName.get(source);
60
+ if (arg !== undefined) parts.push(`${target} = ${translate(ctx, arg.value).text}`);
61
+ }
62
+ const options = bound.byName.get('options');
63
+ if (options !== undefined) {
64
+ if (numeric) ctx.note(diagnosticFor('OS9006', ctx.span(options.at), { argument: 'options', call: path }));
65
+ else parts.push(`options = ${listText(ctx, options.value)}`);
66
+ }
67
+ for (const name of ['group', 'tooltip']) {
68
+ const arg = bound.byName.get(name);
69
+ if (arg !== undefined) parts.push(`${name} = ${translate(ctx, arg.value).text}`);
70
+ }
71
+ for (const [name, arg] of bound.byName) if (PRESENTATION_ONLY.has(name)) leftOut(ctx, arg, name, path);
72
+ return atom(`input(${parts.join(', ')})`);
73
+ }
74
+
75
+ function listText(ctx: Context, expr: Expr): string {
76
+ if (expr.kind !== 'list') return translate(ctx, expr).text;
77
+ return `[${expr.items.map((item) => translate(ctx, item).text).join(', ')}]`;
78
+ }
79
+
80
+ /** A member constant's last word, when the argument is `namespace.word`. */
81
+ function constantOf(expr: Expr | undefined, namespace: string): string | undefined {
82
+ const path = expr === undefined ? undefined : pathOf(expr);
83
+ return path?.startsWith(`${namespace}.`) ? path.slice(namespace.length + 1) : undefined;
84
+ }
85
+
86
+ const PLOT_STYLES: Readonly<Record<string, string | null>> = {
87
+ style_line: null,
88
+ style_linebr: null,
89
+ style_stepline: 'step',
90
+ style_steplinebr: 'step',
91
+ style_histogram: 'histogram',
92
+ style_columns: 'column',
93
+ style_area: 'area',
94
+ style_areabr: 'area',
95
+ };
96
+
97
+ const FORMATS = new Set(['price', 'percent', 'volume']);
98
+
99
+ /** The title a declared output is given: the source's literal, or a name made from what it draws. */
100
+ function outputTitle(ctx: Context, kind: string, arg: Arg | undefined, drawn: Expr | undefined, fallback: string, call: string): string {
101
+ const written = literalString(arg?.value);
102
+ if (arg !== undefined && written === undefined) leftOut(ctx, arg, 'title', call);
103
+ const base = written !== undefined && written !== '' ? written : drawn?.kind === 'name' ? drawn.name : fallback;
104
+ return ctx.title(kind, base);
105
+ }
106
+
107
+ const PLOT_ARGUMENTS = ['series', 'title', 'color', 'linewidth', 'style', 'trackprice', 'histbase', 'offset', 'join', 'editable', 'show_last', 'display'];
108
+
109
+ export function plotCall(ctx: Context, call: CallExpr, path: string): Out {
110
+ const bound = bind(call, PLOT_ARGUMENTS);
111
+ const series = bound.byName.get('series');
112
+ if (series === undefined || bound.stray.length > 0) {
113
+ ctx.refuse(diagnosticFor('OS9006', ctx.span(call.at), { argument: 'series', call: path }));
114
+ return NOTHING;
115
+ }
116
+ const parts = [translate(ctx, series.value).text];
117
+ parts.push(quoted(outputTitle(ctx, 'plot', bound.byName.get('title'), series.value, 'Plot', path)));
118
+ for (const [name, arg] of bound.byName) {
119
+ if (name === 'series' || name === 'title') continue;
120
+ if (name === 'color') parts.splice(2, 0, translate(ctx, arg.value).text);
121
+ else if (name === 'linewidth') parts.push(`width = ${translate(ctx, arg.value).text}`);
122
+ else if (name === 'offset' || name === 'precision') parts.push(`${name} = ${translate(ctx, arg.value).text}`);
123
+ else if (name === 'style' && PLOT_STYLES[constantOf(arg.value, 'plot') ?? ''] !== undefined) {
124
+ const style = PLOT_STYLES[constantOf(arg.value, 'plot') ?? ''];
125
+ if (style !== null && style !== undefined) parts.push(`style = ${quoted(style)}`);
126
+ } else if (name === 'format' && FORMATS.has(constantOf(arg.value, 'format') ?? '')) {
127
+ parts.push(`format = ${quoted(constantOf(arg.value, 'format') ?? '')}`);
128
+ } else leftOut(ctx, arg, name, path);
129
+ }
130
+ return atom(`plot(${parts.join(', ')})`);
131
+ }
132
+
133
+ const LINE_STYLES: Readonly<Record<string, string>> = { style_solid: 'solid', style_dotted: 'dotted', style_dashed: 'dashed' };
134
+
135
+ export function levelCall(ctx: Context, call: CallExpr, path: string): Out {
136
+ const bound = bind(call, ['price', 'title', 'color', 'linestyle', 'linewidth', 'editable', 'display']);
137
+ const price = bound.byName.get('price');
138
+ if (price === undefined || bound.stray.length > 0) {
139
+ ctx.refuse(diagnosticFor('OS9006', ctx.span(call.at), { argument: 'price', call: path }));
140
+ return NOTHING;
141
+ }
142
+ const parts = [translate(ctx, price.value).text];
143
+ const titleArg = bound.byName.get('title');
144
+ const written = literalString(titleArg?.value);
145
+ if (titleArg !== undefined && written === undefined) leftOut(ctx, titleArg, 'title', path);
146
+ if (written !== undefined && written !== '') parts.push(quoted(ctx.title('level', written)));
147
+ for (const [name, arg] of bound.byName) {
148
+ if (name === 'price' || name === 'title') continue;
149
+ const style = LINE_STYLES[constantOf(arg.value, 'hline') ?? ''];
150
+ if (name === 'color') parts.push(`color = ${translate(ctx, arg.value).text}`);
151
+ else if (name === 'linewidth') parts.push(`width = ${translate(ctx, arg.value).text}`);
152
+ else if (name === 'linestyle' && style !== undefined) parts.push(`style = ${quoted(style)}`);
153
+ else leftOut(ctx, arg, name, path);
154
+ }
155
+ return atom(`level(${parts.join(', ')})`);
156
+ }
157
+
158
+ /** `fill` between two named plots; a level or an expression in either place is refused. */
159
+ export function fillLine(ctx: Context, call: CallExpr, path: string, depth: number): OutLine[] {
160
+ const bound = bind(call, ['plot1', 'plot2', 'color']);
161
+ const ends = [bound.byName.get('plot1') ?? bound.byName.get('hline1'), bound.byName.get('plot2') ?? bound.byName.get('hline2')];
162
+ const names: string[] = [];
163
+ for (const end of ends) {
164
+ const binding = end?.value.kind === 'name' ? ctx.lookup(end.value.name, end.value.at) : undefined;
165
+ if (end === undefined || binding === undefined || binding.handle === undefined) {
166
+ ctx.refuse(diagnosticFor('OS9002', ctx.span(call.at), { construct: 'a fill between values that are not plots' }));
167
+ return [];
168
+ }
169
+ if (binding.handle === 'level') {
170
+ ctx.refuse(diagnosticFor('OS9002', ctx.span(call.at), { construct: 'a fill that ends at a horizontal line' }));
171
+ return [];
172
+ }
173
+ names.push(binding.output);
174
+ }
175
+ const parts = [...names];
176
+ for (const [name, arg] of bound.byName) {
177
+ if (['plot1', 'plot2', 'hline1', 'hline2'].includes(name)) continue;
178
+ if (name === 'color') parts.push(`color = ${translate(ctx, arg.value).text}`);
179
+ else leftOut(ctx, arg, name, path);
180
+ }
181
+ for (const one of bound.stray) leftOut(ctx, one.arg, one.name, path);
182
+ return [{ depth, text: `fill(${parts.join(', ')})` }];
183
+ }
184
+
185
+ /** `bgcolor` and `barcolor`, which paint one bar and take a colour. */
186
+ export function paintLine(ctx: Context, call: CallExpr, path: string, target: string, depth: number): OutLine[] {
187
+ const bound = bind(call, ['color']);
188
+ const colour = bound.byName.get('color');
189
+ if (colour === undefined) {
190
+ ctx.refuse(diagnosticFor('OS9006', ctx.span(call.at), { argument: 'color', call: path }));
191
+ return [];
192
+ }
193
+ for (const [name, arg] of bound.byName) if (name !== 'color') leftOut(ctx, arg, name, path);
194
+ for (const one of bound.stray) leftOut(ctx, one.arg, one.name, path);
195
+ return [{ depth, text: `${target}(${translate(ctx, colour.value).text})` }];
196
+ }
197
+
198
+ const SHAPES: Readonly<Record<string, string>> = {
199
+ triangleup: 'triangleUp',
200
+ triangledown: 'triangleDown',
201
+ arrowup: 'arrowUp',
202
+ arrowdown: 'arrowDown',
203
+ circle: 'circle',
204
+ square: 'square',
205
+ diamond: 'diamond',
206
+ cross: 'cross',
207
+ flag: 'flag',
208
+ labelup: 'label',
209
+ labeldown: 'label',
210
+ };
211
+
212
+ const LOCATIONS: Readonly<Record<string, string>> = { abovebar: 'above', belowbar: 'below' };
213
+
214
+ /** `plotshape(cond, ...)`, which draws on the bars its series holds: a marker behind an if. */
215
+ export function shapeLines(ctx: Context, call: CallExpr, path: string, depth: number): OutLine[] {
216
+ const bound = bind(call, ['series', 'title', 'style', 'location', 'color']);
217
+ const series = bound.byName.get('series');
218
+ if (series === undefined) {
219
+ ctx.refuse(diagnosticFor('OS9006', ctx.span(call.at), { argument: 'series', call: path }));
220
+ return [];
221
+ }
222
+ const condition = translate(ctx, series.value).text;
223
+ const textArg = bound.byName.get('text');
224
+ const text = literalString(textArg?.value);
225
+ const shown = text !== undefined && text !== '';
226
+ const titleArg = bound.byName.get('title');
227
+ if (textArg !== undefined && !shown) leftOut(ctx, textArg, 'text', path);
228
+ if (titleArg !== undefined && shown) leftOut(ctx, titleArg, 'title', path);
229
+ const title = outputTitle(ctx, 'signal', shown ? textArg : titleArg, undefined, 'Shape', path);
230
+ const parts = [quoted(title)];
231
+ for (const [name, arg] of bound.byName) {
232
+ if (name === 'series' || name === 'title' || name === 'text') continue;
233
+ const shape = SHAPES[constantOf(arg.value, 'shape') ?? ''];
234
+ const location = LOCATIONS[constantOf(arg.value, 'location') ?? ''];
235
+ if (name === 'color') parts.push(`color = ${translate(ctx, arg.value).text}`);
236
+ else if (name === 'style' && shape !== undefined) parts.push(`shape = ${quoted(shape)}`);
237
+ else if (name === 'location' && location !== undefined) parts.push(`at = ${quoted(location)}`);
238
+ else leftOut(ctx, arg, name, path);
239
+ }
240
+ for (const one of bound.stray) leftOut(ctx, one.arg, one.name, path);
241
+ return [
242
+ { depth, text: `if ${condition}` },
243
+ { depth: depth + 1, text: `signal(${parts.join(', ')})` },
244
+ ];
245
+ }
@@ -0,0 +1,436 @@
1
+ /**
2
+ * Statements and blocks in the source dialect.
3
+ *
4
+ * A block is the run of lines indented past its header, as in OpenScript. A
5
+ * statement the reader cannot take apart is not a parse error that stops the
6
+ * file: it becomes one `refused` statement covering its own lines and its
7
+ * block, so the translator can keep it as a comment and go on to the next.
8
+ */
9
+ import { Cursor, Unreadable, UNREAD, badConstruct, expression, placeOf } from './expressions.js';
10
+ import type { Item, Logical, Remark, Token } from './lexer.js';
11
+ import { joined } from './syntax.js';
12
+ import type { Arm, Body, Branch, Entry, Expr, Named, Param, Place, Stmt } from './syntax.js';
13
+
14
+ const KEYWORD_FORMS: Readonly<Record<string, string>> = {
15
+ import: 'an import',
16
+ export: 'an export',
17
+ method: 'a method declaration',
18
+ type: 'a type declaration',
19
+ enum: 'an enum declaration',
20
+ else: 'an else with no if before it',
21
+ };
22
+
23
+ const QUALIFIERS = new Set(['const', 'simple', 'series']);
24
+ const TYPES = new Set([
25
+ 'int', 'float', 'bool', 'color', 'string', 'line', 'label', 'box', 'table', 'linefill',
26
+ 'polyline', 'array', 'matrix', 'map',
27
+ ]);
28
+ const ASSIGNMENTS = new Set([':=', '+=', '-=', '*=', '/=', '%=']);
29
+
30
+ interface Parsed {
31
+ readonly stmt: Stmt;
32
+ readonly next: number;
33
+ }
34
+
35
+ function placeOfLogical(logical: Logical): Place {
36
+ const first = logical.tokens[0];
37
+ const last = logical.tokens[logical.tokens.length - 1];
38
+ if (first === undefined || last === undefined) return { offset: 0, length: 0 };
39
+ return joined(placeOf(first), placeOf(last));
40
+ }
41
+
42
+ /** The last input line a body reaches, or the fallback when it holds no code. */
43
+ function lastLineOf(body: Body, fallback: number): number {
44
+ let last = fallback;
45
+ for (const entry of body) {
46
+ if (entry.kind === 'stmt') last = Math.max(last, entry.stmt.lines.last);
47
+ }
48
+ return last;
49
+ }
50
+
51
+ function nextCode(items: readonly Item[], from: number): Logical | undefined {
52
+ for (let i = from; i < items.length; i += 1) {
53
+ const item = items[i];
54
+ if (item !== undefined && item.kind === 'code') return item.logical;
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ function entryOf(item: Item): Entry | undefined {
60
+ if (item.kind === 'remark') return { kind: 'remark', remark: item.remark };
61
+ if (item.kind === 'blank') return { kind: 'blank', line: item.line };
62
+ return undefined;
63
+ }
64
+
65
+ /** The statements indented past a header at `headerIndent`, starting at item `from`. */
66
+ function blockAfter(items: readonly Item[], from: number, headerIndent: number): { body: Entry[]; next: number } {
67
+ const body: Entry[] = [];
68
+ let i = from;
69
+ while (i < items.length) {
70
+ const item = items[i];
71
+ if (item === undefined) break;
72
+ if (item.kind !== 'code') {
73
+ const code = nextCode(items, i);
74
+ if (code === undefined || code.indent <= headerIndent) break;
75
+ const entry = entryOf(item);
76
+ if (entry !== undefined) body.push(entry);
77
+ i += 1;
78
+ continue;
79
+ }
80
+ if (item.logical.indent <= headerIndent) break;
81
+ const parsed = statementAt(items, i, item.logical);
82
+ body.push({ kind: 'stmt', stmt: parsed.stmt });
83
+ i = parsed.next;
84
+ }
85
+ return { body, next: i };
86
+ }
87
+
88
+ /** The whole file as one top level body. */
89
+ export function readScript(items: readonly Item[]): Body {
90
+ return blockAfter(items, 0, -1).body;
91
+ }
92
+
93
+ /** Names a statement the reader refused would have declared, read off its first tokens. */
94
+ function declaredBy(tokens: readonly Token[]): Named[] {
95
+ let i = 0;
96
+ const at = (k: number): Token | undefined => tokens[k];
97
+ if (at(i)?.text === 'var' || at(i)?.text === 'varip') i += 1;
98
+ while (at(i) !== undefined && (QUALIFIERS.has(at(i)?.text ?? '') || TYPES.has(at(i)?.text ?? ''))) i += 1;
99
+ const head = at(i);
100
+ if (head === undefined) return [];
101
+ if (head.kind === 'name' && at(i + 1)?.text === '=') return [{ name: head.text, at: placeOf(head) }];
102
+ if (head.text === '[') {
103
+ const names: Named[] = [];
104
+ for (let k = i + 1; k < tokens.length; k += 1) {
105
+ const token = tokens[k];
106
+ if (token === undefined || token.text === ']') break;
107
+ if (token.kind === 'name') names.push({ name: token.text, at: placeOf(token) });
108
+ }
109
+ return names;
110
+ }
111
+ if (head.kind === 'name' && at(i + 1)?.text === '(') {
112
+ if (tokens.some((token) => token.text === '=>')) return [{ name: head.text, at: placeOf(head) }];
113
+ }
114
+ return [];
115
+ }
116
+
117
+ /** A statement kept whole, with the block below it swallowed so its lines stay together. */
118
+ function refusedAt(items: readonly Item[], index: number, logical: Logical, at: Place, construct: string): Parsed {
119
+ const { body, next } = blockAfter(items, index + 1, logical.indent);
120
+ const lines = { first: logical.firstLine, last: lastLineOf(body, logical.lastLine) };
121
+ const stmt: Stmt = { kind: 'refused', construct, declares: declaredBy(logical.tokens), at, lines, remarks: logical.remarks };
122
+ return { stmt, next };
123
+ }
124
+
125
+ /** The statement starting at item `index`, whose logical line is `logical`. */
126
+ function statementAt(items: readonly Item[], index: number, logical: Logical): Parsed {
127
+ try {
128
+ return statementOf(items, index, logical);
129
+ } catch (error) {
130
+ if (!(error instanceof Unreadable)) throw error;
131
+ return refusedAt(items, index, logical, error.at, error.construct);
132
+ }
133
+ }
134
+
135
+ function common(logical: Logical, last: number = logical.lastLine): {
136
+ at: Place;
137
+ lines: { first: number; last: number };
138
+ remarks: readonly Remark[];
139
+ } {
140
+ return { at: placeOfLogical(logical), lines: { first: logical.firstLine, last }, remarks: logical.remarks };
141
+ }
142
+
143
+ function wholeLine(c: Cursor): void {
144
+ if (!c.done()) c.fail();
145
+ }
146
+
147
+ function statementOf(items: readonly Item[], index: number, logical: Logical): Parsed {
148
+ const c = new Cursor(logical.tokens);
149
+ const first = c.peek();
150
+ if (first === undefined) throw new Unreadable(placeOfLogical(logical), UNREAD);
151
+ if (first.kind === 'bad') throw new Unreadable(placeOf(first), badConstruct(first));
152
+ const word = first.kind === 'name' ? first.text : '';
153
+ const keyword = KEYWORD_FORMS[word];
154
+ if (keyword !== undefined) throw new Unreadable(placeOf(first), keyword);
155
+
156
+ switch (word) {
157
+ case 'if':
158
+ return ifAt(items, index, logical, c);
159
+ case 'for':
160
+ return forAt(items, index, logical, c);
161
+ case 'while': {
162
+ c.take();
163
+ const condition = expression(c);
164
+ wholeLine(c);
165
+ const { body, next } = blockAfter(items, index + 1, logical.indent);
166
+ return { stmt: { kind: 'while', condition, body, ...common(logical, lastLineOf(body, logical.lastLine)) }, next };
167
+ }
168
+ case 'switch':
169
+ return switchAt(items, index, logical, c);
170
+ case 'break':
171
+ case 'continue':
172
+ c.take();
173
+ wholeLine(c);
174
+ return { stmt: { kind: word, ...common(logical) }, next: index + 1 };
175
+ }
176
+ return simpleAt(items, index, logical, c);
177
+ }
178
+
179
+ function ifAt(items: readonly Item[], index: number, logical: Logical, c: Cursor): Parsed {
180
+ c.take();
181
+ const branches: Branch[] = [];
182
+ let condition = expression(c);
183
+ wholeLine(c);
184
+ let { body, next } = blockAfter(items, index + 1, logical.indent);
185
+ let otherwise: Entry[] | undefined;
186
+ let last = lastLineOf(body, logical.lastLine);
187
+ for (;;) {
188
+ branches.push({ condition, body });
189
+ // Comment and blank lines before an else belong to the branch they follow.
190
+ let ahead = next;
191
+ const between: Entry[] = [];
192
+ for (let item = items[ahead]; item !== undefined && item.kind !== 'code'; item = items[++ahead]) {
193
+ const entry = entryOf(item);
194
+ if (entry !== undefined) between.push(entry);
195
+ }
196
+ const following = items[ahead];
197
+ if (following === undefined || following.kind !== 'code') break;
198
+ const line = following.logical;
199
+ if (line.indent !== logical.indent || line.tokens[0]?.text !== 'else') break;
200
+ body.push(...between);
201
+ next = ahead;
202
+ const tail = new Cursor(line.tokens);
203
+ tail.take();
204
+ if (tail.done()) {
205
+ const block = blockAfter(items, next + 1, logical.indent);
206
+ otherwise = block.body;
207
+ last = lastLineOf(block.body, line.lastLine);
208
+ next = block.next;
209
+ break;
210
+ }
211
+ tail.expect('if');
212
+ condition = expression(tail);
213
+ wholeLine(tail);
214
+ const block = blockAfter(items, next + 1, logical.indent);
215
+ body = block.body;
216
+ last = lastLineOf(block.body, line.lastLine);
217
+ next = block.next;
218
+ }
219
+ return { stmt: { kind: 'if', branches, otherwise, ...common(logical, last) }, next };
220
+ }
221
+
222
+ function forAt(items: readonly Item[], index: number, logical: Logical, c: Cursor): Parsed {
223
+ const head = c.take();
224
+ const variable = c.take();
225
+ if (variable.kind !== 'name' || c.sees('in')) {
226
+ throw new Unreadable(joined(placeOf(head), placeOf(variable)), 'a for loop over a collection');
227
+ }
228
+ c.expect('=');
229
+ const from = expression(c);
230
+ c.expect('to');
231
+ const to = expression(c);
232
+ let by: Expr | undefined;
233
+ if (c.sees('by')) {
234
+ c.take();
235
+ by = expression(c);
236
+ }
237
+ wholeLine(c);
238
+ const { body, next } = blockAfter(items, index + 1, logical.indent);
239
+ const named = { name: variable.text, at: placeOf(variable) };
240
+ const stmt: Stmt = { kind: 'for', variable: named, from, to, by, body, ...common(logical, lastLineOf(body, logical.lastLine)) };
241
+ return { stmt, next };
242
+ }
243
+
244
+ function switchAt(items: readonly Item[], index: number, logical: Logical, c: Cursor): Parsed {
245
+ c.take();
246
+ const subject = c.done() ? undefined : expression(c);
247
+ wholeLine(c);
248
+ const arms: { match: Expr | undefined; body: Entry[] }[] = [];
249
+ // Comment and blank lines between arms stay with the arm before them, and
250
+ // any before the first arm go at the start of its body.
251
+ const pending: Entry[] = [];
252
+ let i = index + 1;
253
+ let last = logical.lastLine;
254
+ while (i < items.length) {
255
+ const item = items[i];
256
+ if (item === undefined) break;
257
+ if (item.kind !== 'code') {
258
+ const code = nextCode(items, i);
259
+ if (code === undefined || code.indent <= logical.indent) break;
260
+ const entry = entryOf(item);
261
+ if (entry !== undefined) (arms[arms.length - 1]?.body ?? pending).push(entry);
262
+ i += 1;
263
+ continue;
264
+ }
265
+ const line = item.logical;
266
+ if (line.indent <= logical.indent) break;
267
+ const arm = new Cursor(line.tokens);
268
+ const match = arm.sees('=>') ? undefined : expression(arm);
269
+ arm.expect('=>');
270
+ if (arm.done()) {
271
+ const block = blockAfter(items, i + 1, line.indent);
272
+ arms.push({ match, body: [...pending.splice(0), ...block.body] });
273
+ last = lastLineOf(block.body, line.lastLine);
274
+ i = block.next;
275
+ continue;
276
+ }
277
+ const rest: Logical = { ...line, tokens: line.tokens.slice(arm.index) };
278
+ const inner = simpleAt(items, i, rest, new Cursor(rest.tokens));
279
+ arms.push({ match, body: [...pending.splice(0), { kind: 'stmt', stmt: inner.stmt }] });
280
+ last = line.lastLine;
281
+ i += 1;
282
+ }
283
+ const read: Arm[] = arms;
284
+ return { stmt: { kind: 'switch', subject, arms: read, ...common(logical, last) }, next: i };
285
+ }
286
+
287
+ /** Skips `var`, qualifiers and a type; returns the mode and whether a type was written. */
288
+ function declarationHead(c: Cursor): { mode: 'plain' | 'var' | 'varip'; typed: boolean } {
289
+ let mode: 'plain' | 'var' | 'varip' = 'plain';
290
+ if (c.sees('var') || c.sees('varip')) mode = c.take().text === 'var' ? 'var' : 'varip';
291
+ let typed = false;
292
+ while (c.peek()?.kind === 'name' && QUALIFIERS.has(c.peek()?.text ?? '')) c.take();
293
+ const type = c.peek();
294
+ if (type !== undefined && type.kind === 'name' && TYPES.has(type.text)) {
295
+ const after = c.peek(1);
296
+ const generic = after?.text === '<';
297
+ if (generic || (after?.kind === 'name' && after.text !== 'and' && after.text !== 'or')) {
298
+ c.take();
299
+ typed = true;
300
+ if (generic) {
301
+ while (!c.done() && !c.sees('>')) c.take();
302
+ c.expect('>');
303
+ }
304
+ }
305
+ }
306
+ return { mode, typed };
307
+ }
308
+
309
+ function simpleAt(items: readonly Item[], index: number, logical: Logical, c: Cursor): Parsed {
310
+ const next = index + 1;
311
+ const { mode, typed } = declarationHead(c);
312
+ const head = c.peek();
313
+ if (head === undefined) c.fail();
314
+
315
+ if (head.text === '[' && mode === 'plain' && !typed) return tupleAt(index, logical, c);
316
+
317
+ if (head.kind === 'name' && c.sees('(', 1) && mode === 'plain' && !typed) {
318
+ const fn = functionAt(items, index, logical, c);
319
+ if (fn !== undefined) return fn;
320
+ }
321
+
322
+ if (head.kind === 'name' && c.sees('=', 1)) {
323
+ c.take();
324
+ c.take();
325
+ const valueStart = c.peek();
326
+ if (valueStart !== undefined && (valueStart.text === 'if' || valueStart.text === 'switch')) {
327
+ return valueFormAt(items, index, logical, valueStart);
328
+ }
329
+ const value = expression(c);
330
+ wholeLine(c);
331
+ const name = { name: head.text, at: placeOf(head) };
332
+ return { stmt: { kind: 'declare', mode, name, value, ...common(logical) }, next };
333
+ }
334
+
335
+ if (mode !== 'plain' || typed) c.fail();
336
+
337
+ if (head.kind === 'name' && c.peek(1)?.kind === 'op' && ASSIGNMENTS.has(c.peek(1)?.text ?? '')) {
338
+ c.take();
339
+ const op = c.take().text;
340
+ const value = expression(c);
341
+ wholeLine(c);
342
+ const name = { name: head.text, at: placeOf(head) };
343
+ return { stmt: { kind: 'assign', op, name, value, ...common(logical) }, next };
344
+ }
345
+
346
+ const expr = expression(c);
347
+ wholeLine(c);
348
+ return { stmt: { kind: 'evaluate', expr, ...common(logical) }, next };
349
+ }
350
+
351
+ /** `x = if ...` and `x = switch ...`, kept whole with every branch they own. */
352
+ function valueFormAt(items: readonly Item[], index: number, logical: Logical, word: Token): Parsed {
353
+ const rest: Logical = { ...logical, tokens: logical.tokens.slice(logical.tokens.indexOf(word)) };
354
+ const inner = word.text === 'if' ? ifAt(items, index, rest, new Cursor(rest.tokens)) : switchAt(items, index, rest, new Cursor(rest.tokens));
355
+ const construct = word.text === 'if' ? 'an if used as a value' : 'a switch used as a value';
356
+ const stmt: Stmt = {
357
+ kind: 'refused',
358
+ construct,
359
+ declares: declaredBy(logical.tokens),
360
+ at: placeOf(word),
361
+ lines: { first: logical.firstLine, last: inner.stmt.lines.last },
362
+ remarks: logical.remarks,
363
+ };
364
+ return { stmt, next: inner.next };
365
+ }
366
+
367
+ function tupleAt(index: number, logical: Logical, c: Cursor): Parsed {
368
+ const open = c.take();
369
+ const names: Named[] = [];
370
+ while (!c.sees(']')) {
371
+ const token = c.take();
372
+ if (token.kind !== 'name') throw new Unreadable(placeOf(token), UNREAD);
373
+ names.push({ name: token.text, at: placeOf(token) });
374
+ if (!c.sees(',')) break;
375
+ c.take();
376
+ }
377
+ const close = c.expect(']');
378
+ if (!c.sees('=')) throw new Unreadable(joined(placeOf(open), placeOf(close)), 'a tuple reassignment');
379
+ c.take();
380
+ const value = expression(c);
381
+ wholeLine(c);
382
+ return { stmt: { kind: 'tuple', names, value, ...common(logical) }, next: index + 1 };
383
+ }
384
+
385
+ /** A function declaration, or undefined when the line is a call rather than one. */
386
+ function functionAt(items: readonly Item[], index: number, logical: Logical, c: Cursor): Parsed | undefined {
387
+ const tokens = logical.tokens;
388
+ let depth = 0;
389
+ let close = -1;
390
+ for (let k = c.index + 1; k < tokens.length; k += 1) {
391
+ const text = tokens[k]?.text;
392
+ if (text === '(') depth += 1;
393
+ else if (text === ')') {
394
+ depth -= 1;
395
+ if (depth === 0) {
396
+ close = k;
397
+ break;
398
+ }
399
+ }
400
+ }
401
+ if (close < 0 || tokens[close + 1]?.text !== '=>') return undefined;
402
+
403
+ const nameToken = c.take();
404
+ c.expect('(');
405
+ const params: Param[] = [];
406
+ while (!c.sees(')')) {
407
+ declarationHead(c);
408
+ const token = c.take();
409
+ if (token.kind !== 'name') throw new Unreadable(placeOf(token), UNREAD);
410
+ let fallback: Expr | undefined;
411
+ if (c.sees('=')) {
412
+ c.take();
413
+ fallback = expression(c);
414
+ }
415
+ params.push({ name: token.text, at: placeOf(token), fallback });
416
+ if (!c.sees(',')) break;
417
+ c.take();
418
+ }
419
+ c.expect(')');
420
+ c.expect('=>');
421
+ const name = { name: nameToken.text, at: placeOf(nameToken) };
422
+ if (!c.done()) {
423
+ const body = expression(c);
424
+ wholeLine(c);
425
+ return { stmt: { kind: 'function', name, params, body, ...common(logical) }, next: index + 1 };
426
+ }
427
+ const block = blockAfter(items, index + 1, logical.indent);
428
+ const stmt: Stmt = {
429
+ kind: 'function',
430
+ name,
431
+ params,
432
+ body: block.body,
433
+ ...common(logical, lastLineOf(block.body, logical.lastLine)),
434
+ };
435
+ return { stmt, next: block.next };
436
+ }