openalgo-script 0.5.0 → 0.6.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 (242) hide show
  1. package/CHANGELOG.md +121 -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/engine.d.ts +9 -11
  77. package/dist/core/engine/engine.d.ts.map +1 -1
  78. package/dist/core/engine/engine.js +24 -24
  79. package/dist/core/engine/engine.js.map +1 -1
  80. package/dist/core/engine/guard.d.ts.map +1 -1
  81. package/dist/core/engine/guard.js +10 -0
  82. package/dist/core/engine/guard.js.map +1 -1
  83. package/dist/core/engine/host.d.ts +4 -2
  84. package/dist/core/engine/host.d.ts.map +1 -1
  85. package/dist/core/engine/host.js.map +1 -1
  86. package/dist/core/engine/index.d.ts +1 -0
  87. package/dist/core/engine/index.d.ts.map +1 -1
  88. package/dist/core/engine/index.js.map +1 -1
  89. package/dist/core/engine/library/binding.d.ts +2 -0
  90. package/dist/core/engine/library/binding.d.ts.map +1 -1
  91. package/dist/core/engine/library/binding.js.map +1 -1
  92. package/dist/core/engine/library/index.d.ts +8 -0
  93. package/dist/core/engine/library/index.d.ts.map +1 -1
  94. package/dist/core/engine/library/index.js +10 -0
  95. package/dist/core/engine/library/index.js.map +1 -1
  96. package/dist/core/engine/library/objects.d.ts.map +1 -1
  97. package/dist/core/engine/library/objects.js +1 -1
  98. package/dist/core/engine/library/objects.js.map +1 -1
  99. package/dist/core/engine/request-body.d.ts +3 -1
  100. package/dist/core/engine/request-body.d.ts.map +1 -1
  101. package/dist/core/engine/request-body.js +3 -1
  102. package/dist/core/engine/request-body.js.map +1 -1
  103. package/dist/core/engine/request-plan.d.ts +3 -3
  104. package/dist/core/engine/request-plan.js +3 -3
  105. package/dist/core/engine/requests.d.ts +2 -2
  106. package/dist/core/engine/requests.d.ts.map +1 -1
  107. package/dist/core/engine/requests.js +5 -4
  108. package/dist/core/engine/requests.js.map +1 -1
  109. package/dist/core/engine/series.d.ts +18 -6
  110. package/dist/core/engine/series.d.ts.map +1 -1
  111. package/dist/core/engine/series.js +24 -5
  112. package/dist/core/engine/series.js.map +1 -1
  113. package/dist/core/engine/session/record.d.ts +4 -4
  114. package/dist/core/engine/session/record.js +4 -4
  115. package/dist/core/importer/calls.d.ts +29 -0
  116. package/dist/core/importer/calls.d.ts.map +1 -0
  117. package/dist/core/importer/calls.js +267 -0
  118. package/dist/core/importer/calls.js.map +1 -0
  119. package/dist/core/importer/context.d.ts +118 -0
  120. package/dist/core/importer/context.d.ts.map +1 -0
  121. package/dist/core/importer/context.js +120 -0
  122. package/dist/core/importer/context.js.map +1 -0
  123. package/dist/core/importer/expressions.d.ts +44 -0
  124. package/dist/core/importer/expressions.d.ts.map +1 -0
  125. package/dist/core/importer/expressions.js +223 -0
  126. package/dist/core/importer/expressions.js.map +1 -0
  127. package/dist/core/importer/importer.d.ts +13 -0
  128. package/dist/core/importer/importer.d.ts.map +1 -0
  129. package/dist/core/importer/importer.js +220 -0
  130. package/dist/core/importer/importer.js.map +1 -0
  131. package/dist/core/importer/index.d.ts +20 -0
  132. package/dist/core/importer/index.d.ts.map +1 -0
  133. package/dist/core/importer/index.js +18 -0
  134. package/dist/core/importer/index.js.map +1 -0
  135. package/dist/core/importer/lexer.d.ts +68 -0
  136. package/dist/core/importer/lexer.d.ts.map +1 -0
  137. package/dist/core/importer/lexer.js +207 -0
  138. package/dist/core/importer/lexer.js.map +1 -0
  139. package/dist/core/importer/names.d.ts +41 -0
  140. package/dist/core/importer/names.d.ts.map +1 -0
  141. package/dist/core/importer/names.js +78 -0
  142. package/dist/core/importer/names.js.map +1 -0
  143. package/dist/core/importer/orders.d.ts +8 -0
  144. package/dist/core/importer/orders.d.ts.map +1 -0
  145. package/dist/core/importer/orders.js +332 -0
  146. package/dist/core/importer/orders.js.map +1 -0
  147. package/dist/core/importer/outputs.d.ts +19 -0
  148. package/dist/core/importer/outputs.d.ts.map +1 -0
  149. package/dist/core/importer/outputs.js +266 -0
  150. package/dist/core/importer/outputs.js.map +1 -0
  151. package/dist/core/importer/parser.d.ts +5 -0
  152. package/dist/core/importer/parser.d.ts.map +1 -0
  153. package/dist/core/importer/parser.js +439 -0
  154. package/dist/core/importer/parser.js.map +1 -0
  155. package/dist/core/importer/presence.d.ts +32 -0
  156. package/dist/core/importer/presence.d.ts.map +1 -0
  157. package/dist/core/importer/presence.js +146 -0
  158. package/dist/core/importer/presence.js.map +1 -0
  159. package/dist/core/importer/settle.d.ts +23 -0
  160. package/dist/core/importer/settle.d.ts.map +1 -0
  161. package/dist/core/importer/settle.js +135 -0
  162. package/dist/core/importer/settle.js.map +1 -0
  163. package/dist/core/importer/statements.d.ts +4 -0
  164. package/dist/core/importer/statements.d.ts.map +1 -0
  165. package/dist/core/importer/statements.js +339 -0
  166. package/dist/core/importer/statements.js.map +1 -0
  167. package/dist/core/importer/syntax.d.ts +182 -0
  168. package/dist/core/importer/syntax.d.ts.map +1 -0
  169. package/dist/core/importer/syntax.js +14 -0
  170. package/dist/core/importer/syntax.js.map +1 -0
  171. package/dist/core/importer/table.d.ts +52 -0
  172. package/dist/core/importer/table.d.ts.map +1 -0
  173. package/dist/core/importer/table.js +156 -0
  174. package/dist/core/importer/table.js.map +1 -0
  175. package/dist/core/importer/values.d.ts +27 -0
  176. package/dist/core/importer/values.d.ts.map +1 -0
  177. package/dist/core/importer/values.js +236 -0
  178. package/dist/core/importer/values.js.map +1 -0
  179. package/dist/core/index.d.ts +12 -0
  180. package/dist/core/index.d.ts.map +1 -1
  181. package/dist/core/index.js +11 -0
  182. package/dist/core/index.js.map +1 -1
  183. package/dist/core/stdlib/calendar/civil.d.ts.map +1 -1
  184. package/dist/core/stdlib/calendar/civil.js +5 -2
  185. package/dist/core/stdlib/calendar/civil.js.map +1 -1
  186. package/dist/core/version/version.generated.d.ts +1 -1
  187. package/dist/core/version/version.generated.js +1 -1
  188. package/package.json +5 -2
  189. package/spec/errors.json +380 -20
  190. package/src/adapters/charts/run.ts +6 -0
  191. package/src/adapters/charts/tables.ts +6 -19
  192. package/src/adapters/charts/undrawable.ts +51 -0
  193. package/src/core/backtest/drive.ts +66 -154
  194. package/src/core/backtest/index.ts +2 -0
  195. package/src/core/backtest/walk.ts +182 -0
  196. package/src/core/catalogue/catalogue.generated.ts +18 -1
  197. package/src/core/catalogue/types.ts +8 -2
  198. package/src/core/catalogue/values.generated.ts +16 -0
  199. package/src/core/check/assigned.ts +31 -0
  200. package/src/core/check/call-sites.ts +11 -4
  201. package/src/core/check/calls.ts +13 -4
  202. package/src/core/check/check.ts +14 -2
  203. package/src/core/check/checker.ts +39 -2
  204. package/src/core/check/constant.ts +57 -0
  205. package/src/core/check/deleted.ts +135 -0
  206. package/src/core/check/expressions.ts +1 -1
  207. package/src/core/check/library-output.ts +1 -0
  208. package/src/core/check/library-prose.generated.ts +10 -10
  209. package/src/core/check/library.ts +8 -0
  210. package/src/core/check/surface.ts +16 -0
  211. package/src/core/engine/bar-source.ts +113 -0
  212. package/src/core/engine/engine.ts +26 -25
  213. package/src/core/engine/guard.ts +12 -1
  214. package/src/core/engine/host.ts +4 -2
  215. package/src/core/engine/index.ts +1 -0
  216. package/src/core/engine/library/binding.ts +2 -0
  217. package/src/core/engine/library/index.ts +11 -0
  218. package/src/core/engine/library/objects.ts +1 -6
  219. package/src/core/engine/request-body.ts +5 -7
  220. package/src/core/engine/request-plan.ts +3 -3
  221. package/src/core/engine/requests.ts +11 -8
  222. package/src/core/engine/series.ts +30 -6
  223. package/src/core/engine/session/record.ts +4 -4
  224. package/src/core/importer/calls.ts +274 -0
  225. package/src/core/importer/context.ts +197 -0
  226. package/src/core/importer/expressions.ts +247 -0
  227. package/src/core/importer/importer.ts +226 -0
  228. package/src/core/importer/index.ts +19 -0
  229. package/src/core/importer/lexer.ts +285 -0
  230. package/src/core/importer/names.ts +104 -0
  231. package/src/core/importer/orders.ts +307 -0
  232. package/src/core/importer/outputs.ts +245 -0
  233. package/src/core/importer/parser.ts +436 -0
  234. package/src/core/importer/presence.ts +164 -0
  235. package/src/core/importer/settle.ts +141 -0
  236. package/src/core/importer/statements.ts +331 -0
  237. package/src/core/importer/syntax.ts +144 -0
  238. package/src/core/importer/table.ts +191 -0
  239. package/src/core/importer/values.ts +241 -0
  240. package/src/core/index.ts +13 -0
  241. package/src/core/stdlib/calendar/civil.ts +5 -2
  242. package/src/core/version/version.generated.ts +1 -1
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Three questions about a source expression that decide where the two
3
+ * languages part, answered from the source tree before anything is written.
4
+ *
5
+ * **Can it be absent?** The source dialect answers a comparison with an absent
6
+ * side `false`, where OpenScript answers `none` (`language.md` 6.4). Most
7
+ * places treat the two alike, because a condition that is absent takes the
8
+ * false branch; the places that do not are `not`, an equality with a boolean
9
+ * literal and a condition handed to a counting built-in, and there the
10
+ * translation writes `orElse(x, false)`, which is the source dialect's answer
11
+ * exactly. The analysis is conservative: anything it cannot show is present is
12
+ * treated as possibly absent, which costs an `orElse` that changes nothing.
13
+ *
14
+ * **Does it hold state?** A call that keeps state at its call site, or a user
15
+ * function that may, is one whose value depends on which bars it ran on
16
+ * (`language.md` 11.4). That is where the order and laziness of evaluation
17
+ * stop being invisible.
18
+ *
19
+ * **Is it a whole-number constant?** Version 5 of the source dialect divides
20
+ * two of those without a fraction.
21
+ */
22
+ import type { Context } from './context.js';
23
+ import { pathOf } from './syntax.js';
24
+ import type { Expr } from './syntax.js';
25
+ import { CALLS, COLOURS } from './table.js';
26
+
27
+ /** Built-in series that hold a value on every bar, from bar 0. Volume can be absent. */
28
+ const PRESENT_SERIES = new Set(['open', 'high', 'low', 'close', 'hl2', 'hlc3', 'ohlc4', 'hlcc4', 'time', 'bar_index']);
29
+
30
+ /** Calls that return a present value whenever their arguments are present. */
31
+ const PRESENT_WHEN_ARGUMENTS_ARE = new Set([
32
+ 'math.abs', 'math.max', 'math.min', 'math.floor', 'math.ceil', 'math.sign', 'int', 'float',
33
+ ]);
34
+
35
+ function namePresent(ctx: Context, name: string): boolean {
36
+ const binding = ctx.scope.find(name);
37
+ if (binding !== undefined) return binding.present;
38
+ return PRESENT_SERIES.has(name);
39
+ }
40
+
41
+ function callPresent(ctx: Context, expr: Extract<Expr, { kind: 'call' }>): boolean {
42
+ const path = pathOf(expr.callee);
43
+ if (path === undefined || ctx.scope.find(path.split('.')[0] ?? '') !== undefined) return false;
44
+ if (path === 'na') return true;
45
+ if (path === 'nz') return expr.args.length < 2 || neverAbsent(ctx, expr.args[1]?.value);
46
+ if (path.startsWith('input')) return true;
47
+ if (path === 'color.new' || path === 'color.rgb') return true;
48
+ if (PRESENT_WHEN_ARGUMENTS_ARE.has(path)) return expr.args.every((arg) => neverAbsent(ctx, arg.value));
49
+ return false;
50
+ }
51
+
52
+ /** Whether an expression holds a value on every bar, as far as the tree can show. */
53
+ export function neverAbsent(ctx: Context, expr: Expr | undefined): boolean {
54
+ if (expr === undefined) return false;
55
+ switch (expr.kind) {
56
+ case 'number':
57
+ case 'string':
58
+ case 'bool':
59
+ case 'color':
60
+ return true;
61
+ case 'name':
62
+ return namePresent(ctx, expr.name);
63
+ case 'member': {
64
+ const path = pathOf(expr) ?? '';
65
+ if (path.startsWith('barstate.') || path === 'math.pi' || path === 'math.e') return true;
66
+ if (path === 'strategy.position_size') return true;
67
+ return path.startsWith('color.') && COLOURS.has(expr.property);
68
+ }
69
+ case 'unary':
70
+ return neverAbsent(ctx, expr.operand);
71
+ case 'binary':
72
+ if (expr.op === '/' || expr.op === '%') return false;
73
+ return neverAbsent(ctx, expr.left) && neverAbsent(ctx, expr.right);
74
+ case 'ternary':
75
+ return neverAbsent(ctx, expr.then) && neverAbsent(ctx, expr.otherwise);
76
+ case 'group':
77
+ return neverAbsent(ctx, expr.inner);
78
+ case 'call':
79
+ return callPresent(ctx, expr);
80
+ case 'index':
81
+ case 'list':
82
+ return false;
83
+ }
84
+ }
85
+
86
+ /** Whether evaluating an expression runs a call that keeps state at its call site. */
87
+ export function holdsState(ctx: Context, expr: Expr): boolean {
88
+ switch (expr.kind) {
89
+ case 'call': {
90
+ const path = pathOf(expr.callee);
91
+ if (path !== undefined) {
92
+ const binding = ctx.scope.find(path);
93
+ if (binding !== undefined && binding.kind === 'function') return true;
94
+ if (CALLS.get(path)?.stateful === true) return true;
95
+ }
96
+ return holdsState(ctx, expr.callee) || expr.args.some((arg) => holdsState(ctx, arg.value));
97
+ }
98
+ case 'member':
99
+ return holdsState(ctx, expr.object);
100
+ case 'index':
101
+ return holdsState(ctx, expr.target) || holdsState(ctx, expr.index);
102
+ case 'unary':
103
+ return holdsState(ctx, expr.operand);
104
+ case 'binary':
105
+ return holdsState(ctx, expr.left) || holdsState(ctx, expr.right);
106
+ case 'ternary':
107
+ return holdsState(ctx, expr.condition) || holdsState(ctx, expr.then) || holdsState(ctx, expr.otherwise);
108
+ case 'group':
109
+ return holdsState(ctx, expr.inner);
110
+ case 'list':
111
+ return expr.items.some((item) => holdsState(ctx, item));
112
+ default:
113
+ return false;
114
+ }
115
+ }
116
+
117
+ /** A whole-number literal, as the source writes one: no point and no exponent. */
118
+ function wholeLiteral(text: string): boolean {
119
+ return /^[0-9]+$/.test(text);
120
+ }
121
+
122
+ /** Whether an expression is a whole-number constant in version 5's sense. */
123
+ export function wholeConstant(ctx: Context, expr: Expr): boolean {
124
+ switch (expr.kind) {
125
+ case 'number':
126
+ return wholeLiteral(expr.text);
127
+ case 'name':
128
+ return ctx.scope.find(expr.name)?.whole === true;
129
+ case 'unary':
130
+ return expr.op !== 'not' && wholeConstant(ctx, expr.operand);
131
+ case 'group':
132
+ return wholeConstant(ctx, expr.inner);
133
+ case 'binary':
134
+ return ['+', '-', '*', '/', '%'].includes(expr.op) && wholeConstant(ctx, expr.left) && wholeConstant(ctx, expr.right);
135
+ default:
136
+ return false;
137
+ }
138
+ }
139
+
140
+ /** Whether a name is read anywhere inside an expression. */
141
+ export function mentions(expr: Expr, name: string): boolean {
142
+ switch (expr.kind) {
143
+ case 'name':
144
+ return expr.name === name;
145
+ case 'member':
146
+ return mentions(expr.object, name);
147
+ case 'call':
148
+ return mentions(expr.callee, name) || expr.args.some((arg) => mentions(arg.value, name));
149
+ case 'index':
150
+ return mentions(expr.target, name) || mentions(expr.index, name);
151
+ case 'unary':
152
+ return mentions(expr.operand, name);
153
+ case 'binary':
154
+ return mentions(expr.left, name) || mentions(expr.right, name);
155
+ case 'ternary':
156
+ return mentions(expr.condition, name) || mentions(expr.then, name) || mentions(expr.otherwise, name);
157
+ case 'group':
158
+ return mentions(expr.inner, name);
159
+ case 'list':
160
+ return expr.items.some((item) => mentions(item, name));
161
+ default:
162
+ return false;
163
+ }
164
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Settling a translation: what is kept, what it reads, and whether it compiles.
3
+ *
4
+ * The importer promises that what it hands back compiles, and the promise is
5
+ * kept by compiling it rather than by trusting the translators. Three passes
6
+ * repeat until nothing changes:
7
+ *
8
+ * 1. **The cascade.** A statement that reads a top level name declared by a
9
+ * statement that was not kept is not kept either (OS9005), in source order,
10
+ * so one refusal is reported once at its cause and once at each statement
11
+ * that follows from it.
12
+ * 2. **The rendering.** Kept statements are written out; the rest are written
13
+ * as their source lines behind a comment marker, so the original is there to
14
+ * translate from and nothing in them is compiled.
15
+ * 3. **The compile.** Every error the compiler raises is laid at the statement
16
+ * whose line it is on, which is then not kept (OS9012), and the loop runs
17
+ * again. It ends when the output compiles, or when an error lies somewhere no
18
+ * statement owns, which only the declaration can be.
19
+ *
20
+ * Warnings the compiler raises are left for the reader's own compile to show:
21
+ * they are about the output as OpenScript and say so in its own words.
22
+ */
23
+ import { check } from '../check/index.js';
24
+ import { DiagnosticBag, diagnosticFor } from '../diagnostics/index.js';
25
+ import type { Diagnostic } from '../diagnostics/index.js';
26
+ import { emit } from '../emit/index.js';
27
+ import { lex } from '../lex/index.js';
28
+ import { parseTokens } from '../parse/index.js';
29
+ import { sourceFile } from '../source/index.js';
30
+ import type { SourceFile } from '../source/index.js';
31
+ import { isKept, reasonOf } from './context.js';
32
+ import type { Context, Node } from './context.js';
33
+ import type { Remark } from './lexer.js';
34
+
35
+ /** One line of the output that is not a statement. */
36
+ export type Loose =
37
+ | { readonly kind: 'remark'; readonly remark: Remark; readonly depth: number }
38
+ | { readonly kind: 'blank' }
39
+ | { readonly kind: 'text'; readonly text: string };
40
+
41
+ export type Piece = Loose | { readonly kind: 'node'; readonly node: Node };
42
+
43
+ const INDENT = ' ';
44
+
45
+ function cascade(ctx: Context, nodes: readonly Node[]): void {
46
+ const lost = new Map<string, number>();
47
+ for (const node of nodes) {
48
+ node.lost = undefined;
49
+ if (node.refusal === undefined && node.failed === undefined) {
50
+ const hit = node.reads.find((read) => lost.has(read.name));
51
+ if (hit !== undefined) {
52
+ node.lost = diagnosticFor('OS9005', ctx.span(hit.at), { name: hit.name, line: lost.get(hit.name) ?? 0 });
53
+ }
54
+ }
55
+ if (!isKept(node)) for (const one of node.declares) lost.set(one.name, node.lines.first);
56
+ }
57
+ }
58
+
59
+ /** The output text, and for each of its lines the statement that wrote it. */
60
+ function render(file: SourceFile, pieces: readonly Piece[]): { text: string; owners: (Node | undefined)[] } {
61
+ const lines: string[] = [];
62
+ const owners: (Node | undefined)[] = [];
63
+ const push = (text: string, owner: Node | undefined): void => {
64
+ if (text === '' && (lines.length === 0 || lines[lines.length - 1] === '')) return;
65
+ lines.push(text);
66
+ owners.push(owner);
67
+ };
68
+ for (const piece of pieces) {
69
+ if (piece.kind === 'blank') push('', undefined);
70
+ else if (piece.kind === 'text') push(piece.text, undefined);
71
+ else if (piece.kind === 'remark') push(`${INDENT.repeat(piece.depth)}//${piece.remark.text}`, undefined);
72
+ else if (isKept(piece.node)) {
73
+ for (const line of piece.node.output) {
74
+ push(line.text === '' ? '' : `${INDENT.repeat(line.depth)}${line.text}`, piece.node);
75
+ }
76
+ } else {
77
+ const code = reasonOf(piece.node)?.code ?? '';
78
+ for (let line = piece.node.lines.first; line <= piece.node.lines.last; line += 1) {
79
+ push(`// not translated (${code}): ${file.lineText(line)}`.trimEnd(), piece.node);
80
+ }
81
+ }
82
+ }
83
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
84
+ lines.pop();
85
+ owners.pop();
86
+ }
87
+ return { text: `${lines.join('\n')}\n`, owners };
88
+ }
89
+
90
+ /** The errors OpenScript's own compiler raises on a text. */
91
+ function compileErrors(text: string): readonly Diagnostic[] {
92
+ const file = sourceFile('translation.oscript', text);
93
+ const bag = new DiagnosticBag();
94
+ const tokens = lex(file, bag);
95
+ const script = parseTokens(file, tokens, bag);
96
+ const checked = check(file, script, bag);
97
+ if (!bag.hasErrors) emit(file, checked, bag, {});
98
+ return bag.errors;
99
+ }
100
+
101
+ /** Repeats the three passes until the output compiles or nothing more can be done. */
102
+ export function settle(ctx: Context, nodes: readonly Node[], pieces: readonly Piece[]): string {
103
+ for (let pass = 0; ; pass += 1) {
104
+ cascade(ctx, nodes);
105
+ const { text, owners } = render(ctx.facts.file, pieces);
106
+ const errors = compileErrors(text);
107
+ let changed = false;
108
+ for (const error of errors) {
109
+ const owner = owners[error.span.line - 1];
110
+ if (owner === undefined || !isKept(owner)) continue;
111
+ if (owner.declaration) {
112
+ owner.notes.push({ diagnostic: diagnosticFor('OS9012', ctx.span(owner.stmt.at), { code: error.code }), once: 'declaration' });
113
+ continue;
114
+ }
115
+ owner.failed = diagnosticFor('OS9012', ctx.span(owner.stmt.at), { code: error.code });
116
+ changed = true;
117
+ }
118
+ if (!changed || pass > nodes.length) return text;
119
+ }
120
+ }
121
+
122
+ /** Every finding, in the order a reader walks the source. */
123
+ export function findingsOf(nodes: readonly Node[]): readonly Diagnostic[] {
124
+ const bag = new DiagnosticBag();
125
+ const once = new Set<string>();
126
+ for (const node of nodes) {
127
+ const reason = reasonOf(node);
128
+ if (reason !== undefined) {
129
+ bag.add(reason);
130
+ continue;
131
+ }
132
+ for (const note of node.notes) {
133
+ if (note.once !== undefined) {
134
+ if (once.has(note.once)) continue;
135
+ once.add(note.once);
136
+ }
137
+ bag.add(note.diagnostic);
138
+ }
139
+ }
140
+ return bag.ordered();
141
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Statements, written out as OpenScript lines.
3
+ *
4
+ * Each statement is translated inside a frame that collects the lines it needs
5
+ * evaluated before it: version 5's always evaluated right operand of `and` and
6
+ * `or`, moved to a line of its own (see `values.ts`). A frame allows that only
7
+ * where the moved line runs exactly as often as the statement did, so it is
8
+ * allowed for a plain statement and for the first condition of an `if`, and
9
+ * refused for an `else if`, a `while` and a `switch` arm, whose conditions the
10
+ * source evaluates only on some passes.
11
+ */
12
+ import { diagnosticFor } from '../diagnostics/index.js';
13
+ import { canonicalNumber } from '../emit/index.js';
14
+ import { generic } from './calls.js';
15
+ import type { Context, OutLine } from './context.js';
16
+ import { binding } from './names.js';
17
+ import { fillLine, paintLine, plotCall, levelCall, shapeLines, translateInput } from './outputs.js';
18
+ import { closeLines, entryLines, exitLines } from './orders.js';
19
+ import { holdsState, mentions, neverAbsent, wholeConstant } from './presence.js';
20
+ import { pathOf } from './syntax.js';
21
+ import type { Body, CallExpr, Expr, Stmt } from './syntax.js';
22
+ import { CALLS } from './table.js';
23
+ import { translate, wrap, PREC } from './values.js';
24
+ import type { Out } from './values.js';
25
+
26
+ /** Runs `work` in a frame of its own, and puts the lines it moved before its own. */
27
+ function framed(ctx: Context, depth: number, mayHoist: boolean, work: () => OutLine[]): OutLine[] {
28
+ const held = { hoisted: ctx.hoisted, mayHoist: ctx.mayHoist, depth: ctx.depth };
29
+ ctx.hoisted = [];
30
+ ctx.mayHoist = mayHoist;
31
+ ctx.depth = depth;
32
+ try {
33
+ const lines = work();
34
+ return [...ctx.hoisted, ...lines];
35
+ } finally {
36
+ ctx.hoisted = held.hoisted;
37
+ ctx.mayHoist = held.mayHoist;
38
+ ctx.depth = held.depth;
39
+ }
40
+ }
41
+
42
+ /** A condition read where nothing may be moved out of it. */
43
+ function fixedCondition(ctx: Context, expr: Expr): string {
44
+ const may = ctx.mayHoist;
45
+ ctx.mayHoist = false;
46
+ try {
47
+ return translate(ctx, expr).text;
48
+ } finally {
49
+ ctx.mayHoist = may;
50
+ }
51
+ }
52
+
53
+ function translateBody(ctx: Context, body: Body, depth: number): OutLine[] {
54
+ const lines: OutLine[] = [];
55
+ for (const entry of body) {
56
+ if (entry.kind === 'remark') lines.push({ depth, text: `//${entry.remark.text}` });
57
+ else if (entry.kind === 'blank') lines.push({ depth, text: '' });
58
+ else lines.push(...translateStatement(ctx, entry.stmt, depth));
59
+ }
60
+ return lines;
61
+ }
62
+
63
+ function block(ctx: Context, body: Body, depth: number): OutLine[] {
64
+ return ctx.within(() => translateBody(ctx, body, depth));
65
+ }
66
+
67
+ /** Where the value is a call, its path, unless the script declares the name. */
68
+ function builtInPath(ctx: Context, expr: Expr): string | undefined {
69
+ if (expr.kind !== 'call') return undefined;
70
+ const path = pathOf(expr.callee);
71
+ if (path === undefined || ctx.scope.find(path.split('.')[0] ?? '') !== undefined) return undefined;
72
+ return path;
73
+ }
74
+
75
+ function declare(ctx: Context, stmt: Extract<Stmt, { kind: 'declare' }>, depth: number): OutLine[] {
76
+ const name = stmt.name.name;
77
+ const output = ctx.namer.declare(name, ctx.hides(name));
78
+ const bound = binding(output, 'variable', ctx.scope.isGlobal);
79
+ // The source may read the name's own history in its first value, as OpenScript may.
80
+ if (mentions(stmt.value, name)) ctx.scope.names.set(name, bound);
81
+
82
+ const path = builtInPath(ctx, stmt.value);
83
+ const special = path === undefined ? undefined : CALLS.get(path)?.special;
84
+ let value: Out;
85
+ if (special === 'input' && path !== undefined) value = translateInput(ctx, stmt.value as CallExpr, path, true);
86
+ else if (special === 'plot' && path !== undefined) value = plotCall(ctx, stmt.value as CallExpr, path);
87
+ else if (special === 'hline' && path !== undefined) value = levelCall(ctx, stmt.value as CallExpr, path);
88
+ else value = translate(ctx, stmt.value);
89
+
90
+ bound.handle = special === 'plot' ? 'plot' : special === 'hline' ? 'level' : undefined;
91
+ const reassigned = ctx.facts.reassigned.get(name);
92
+ bound.present = neverAbsent(ctx, stmt.value);
93
+ bound.whole = reassigned === undefined && stmt.mode === 'plain' && ctx.scope.isGlobal && wholeConstant(ctx, stmt.value);
94
+ ctx.scope.names.set(name, bound);
95
+ // Present only if every later value is, each read as if the name itself were.
96
+ if (bound.present && reassigned !== undefined) {
97
+ bound.present = reassigned.every((one) => one.op !== '/=' && one.op !== '%=' && neverAbsent(ctx, one.value));
98
+ }
99
+ const prefix = stmt.mode === 'var' ? 'var ' : stmt.mode === 'varip' ? 'live var ' : '';
100
+ return [{ depth, text: `${prefix}${output} = ${value.text}` }];
101
+ }
102
+
103
+ /** How each tuple built-in's elements line up with OpenScript's array. */
104
+ const ELEMENTS: Readonly<Record<string, readonly number[]>> = {
105
+ 'ta.macd': [0, 1, 2],
106
+ 'ta.bb': [0, 1, 2],
107
+ 'ta.supertrend': [0, 1],
108
+ 'ta.dmi': [1, 2, 0],
109
+ };
110
+
111
+ function tuple(ctx: Context, stmt: Extract<Stmt, { kind: 'tuple' }>, depth: number): OutLine[] {
112
+ const bindAll = (): void => {
113
+ for (const one of stmt.names) {
114
+ const out = ctx.namer.declare(one.name, ctx.hides(one.name));
115
+ ctx.scope.names.set(one.name, binding(out, 'variable', ctx.scope.isGlobal));
116
+ }
117
+ };
118
+ const path = builtInPath(ctx, stmt.value);
119
+ const row = path === undefined ? undefined : CALLS.get(path);
120
+ const order = path === undefined ? undefined : ELEMENTS[path];
121
+ if (path === undefined || row === undefined || order === undefined || order.length !== stmt.names.length) {
122
+ if (path !== undefined && row === undefined && stmt.value.kind === 'call') {
123
+ ctx.refuse(diagnosticFor('OS9003', ctx.span(stmt.value.callee.at), { name: path }));
124
+ } else ctx.refuse(diagnosticFor('OS9002', ctx.span(stmt.at), { construct: 'a tuple assignment' }));
125
+ bindAll();
126
+ return [];
127
+ }
128
+ const value = generic(ctx, stmt.value as CallExpr, row, path);
129
+ const holder = ctx.namer.fresh(`${row.target}Result`);
130
+ const lines: OutLine[] = [{ depth, text: `${holder} = ${value.text}` }];
131
+ stmt.names.forEach((one, i) => {
132
+ const out = ctx.namer.declare(one.name, ctx.hides(one.name));
133
+ ctx.scope.names.set(one.name, binding(out, 'variable', ctx.scope.isGlobal));
134
+ if (one.name !== '_') lines.push({ depth, text: `${out} = ${holder}[${canonicalNumber(order[i] ?? 0)}]` });
135
+ });
136
+ return lines;
137
+ }
138
+
139
+ function assign(ctx: Context, stmt: Extract<Stmt, { kind: 'assign' }>, depth: number): OutLine[] {
140
+ const target = ctx.lookup(stmt.name.name, stmt.name.at);
141
+ if (target === undefined) {
142
+ ctx.refuse(diagnosticFor('OS9003', ctx.span(stmt.name.at), { name: stmt.name.name }));
143
+ return [];
144
+ }
145
+ if (target.kind !== 'variable') {
146
+ ctx.refuse(diagnosticFor('OS9002', ctx.span(stmt.name.at), { construct: 'an assignment to a loop variable or a parameter' }));
147
+ return [];
148
+ }
149
+ const value = translate(ctx, stmt.value);
150
+ return [{ depth, text: `${target.output} ${stmt.op === ':=' ? '=' : stmt.op} ${value.text}` }];
151
+ }
152
+
153
+ function evaluate(ctx: Context, stmt: Extract<Stmt, { kind: 'evaluate' }>, depth: number): OutLine[] {
154
+ const path = builtInPath(ctx, stmt.expr);
155
+ const call = stmt.expr as CallExpr;
156
+ switch (path === undefined ? undefined : CALLS.get(path)?.special) {
157
+ case 'declaration':
158
+ ctx.refuse(diagnosticFor('OS9004', ctx.span(call.callee.at), { found: 'makes a second one here' }));
159
+ return [];
160
+ case 'fill':
161
+ return fillLine(ctx, call, path ?? '', depth);
162
+ case 'paint':
163
+ return paintLine(ctx, call, path ?? '', path === 'bgcolor' ? 'background' : 'barColor', depth);
164
+ case 'shape':
165
+ return shapeLines(ctx, call, path ?? '', depth);
166
+ case 'entry':
167
+ return entryLines(ctx, call, path ?? '', depth);
168
+ case 'close':
169
+ case 'closeAll':
170
+ return closeLines(ctx, call, path ?? '', depth);
171
+ case 'exit':
172
+ return exitLines(ctx, call, path ?? '', depth);
173
+ }
174
+ return [{ depth, text: translate(ctx, stmt.expr).text }];
175
+ }
176
+
177
+ function ifLines(ctx: Context, stmt: Extract<Stmt, { kind: 'if' }>, depth: number): OutLine[] {
178
+ const lines: OutLine[] = [];
179
+ stmt.branches.forEach((branch, i) => {
180
+ const condition = i === 0 ? translate(ctx, branch.condition).text : fixedCondition(ctx, branch.condition);
181
+ lines.push({ depth, text: `${i === 0 ? 'if' : 'else if'} ${condition}` }, ...block(ctx, branch.body, depth + 1));
182
+ });
183
+ if (stmt.otherwise !== undefined) lines.push({ depth, text: 'else' }, ...block(ctx, stmt.otherwise, depth + 1));
184
+ return lines;
185
+ }
186
+
187
+ /** A literal number, signed or not, as its value. */
188
+ function literalValue(expr: Expr): number | undefined {
189
+ if (expr.kind === 'number') return Number(expr.text);
190
+ if (expr.kind === 'unary' && expr.op === '-' && expr.operand.kind === 'number') return -Number(expr.operand.text);
191
+ if (expr.kind === 'group') return literalValue(expr.inner);
192
+ return undefined;
193
+ }
194
+
195
+ /** Names a loop body assigns, which a bound that is read again on each pass would see change. */
196
+ function assigned(body: Body, into: Set<string>): Set<string> {
197
+ for (const entry of body) {
198
+ if (entry.kind !== 'stmt') continue;
199
+ const stmt = entry.stmt;
200
+ if (stmt.kind === 'assign' || stmt.kind === 'declare') into.add(stmt.name.name);
201
+ if (stmt.kind === 'if') {
202
+ for (const branch of stmt.branches) assigned(branch.body, into);
203
+ if (stmt.otherwise !== undefined) assigned(stmt.otherwise, into);
204
+ }
205
+ if (stmt.kind === 'for' || stmt.kind === 'while') assigned(stmt.body, into);
206
+ if (stmt.kind === 'switch') for (const arm of stmt.arms) assigned(arm.body, into);
207
+ }
208
+ return into;
209
+ }
210
+
211
+ function forLines(ctx: Context, stmt: Extract<Stmt, { kind: 'for' }>, depth: number): OutLine[] {
212
+ const changes = assigned(stmt.body, new Set());
213
+ const bounds = [stmt.from, stmt.to, ...(stmt.by === undefined ? [] : [stmt.by])];
214
+ if (bounds.some((bound) => holdsState(ctx, bound) || [...changes].some((name) => mentions(bound, name)))) {
215
+ ctx.refuse(diagnosticFor('OS9002', ctx.span(stmt.at), { construct: 'a for loop whose bounds change as it runs' }));
216
+ return [];
217
+ }
218
+ const from = translate(ctx, stmt.from);
219
+ const to = translate(ctx, stmt.to);
220
+ const start = literalValue(stmt.from);
221
+ const end = literalValue(stmt.to);
222
+ const step = stmt.by === undefined ? undefined : literalValue(stmt.by);
223
+ let stepText = '';
224
+ if (stmt.by !== undefined) {
225
+ if (start === undefined || end === undefined || step === undefined || step <= 0 || start > end) {
226
+ ctx.refuse(diagnosticFor('OS9002', ctx.span(stmt.at), { construct: 'a for loop with a step whose direction the importer cannot settle' }));
227
+ return [];
228
+ }
229
+ stepText = ` step ${translate(ctx, stmt.by).text}`;
230
+ } else if (start !== undefined && end !== undefined) {
231
+ if (start > end) stepText = ' step -1';
232
+ } else {
233
+ stepText = ` step (${wrap(to, PREC.comparison + 1)} >= ${wrap(from, PREC.comparison + 1)} ? 1 : -1)`;
234
+ }
235
+ return ctx.within(() => {
236
+ const name = stmt.variable.name;
237
+ const output = ctx.namer.declare(name, ctx.hides(name));
238
+ ctx.scope.names.set(name, binding(output, 'loop', false));
239
+ const header = { depth, text: `for ${output} = ${from.text} to ${to.text}${stepText}` };
240
+ return [header, ...translateBody(ctx, stmt.body, depth + 1)];
241
+ });
242
+ }
243
+
244
+ function switchLines(ctx: Context, stmt: Extract<Stmt, { kind: 'switch' }>, depth: number): OutLine[] {
245
+ const subject = stmt.subject === undefined ? '' : ` ${fixedCondition(ctx, stmt.subject)}`;
246
+ const lines: OutLine[] = [{ depth, text: `switch${subject}` }];
247
+ for (const arm of stmt.arms) {
248
+ const head = arm.match === undefined ? 'default' : `case ${fixedCondition(ctx, arm.match)}`;
249
+ lines.push({ depth: depth + 1, text: head }, ...block(ctx, arm.body, depth + 2));
250
+ }
251
+ return lines;
252
+ }
253
+
254
+ function functionLines(ctx: Context, stmt: Extract<Stmt, { kind: 'function' }>, depth: number): OutLine[] {
255
+ const output = ctx.namer.declare(stmt.name.name, ctx.hides(stmt.name.name));
256
+ const fn = binding(output, 'function', ctx.scope.isGlobal);
257
+ ctx.scope.names.set(stmt.name.name, fn);
258
+ return ctx.within(() => {
259
+ const labels = new Map<string, string>();
260
+ const params = stmt.params.map((param) => {
261
+ const fallback = param.fallback === undefined ? '' : ` = ${translate(ctx, param.fallback).text}`;
262
+ const out = ctx.namer.declare(param.name, ctx.hides(param.name));
263
+ ctx.scope.names.set(param.name, binding(out, 'parameter', false));
264
+ labels.set(param.name, out);
265
+ return `${out}${fallback}`;
266
+ });
267
+ fn.labels = labels;
268
+ const header = `fn ${output}(${params.join(', ')}) =>`;
269
+ if (!Array.isArray(stmt.body)) {
270
+ const body = stmt.body as Expr;
271
+ const lines = framed(ctx, depth + 1, true, () => [{ depth: depth + 1, text: translate(ctx, body).text }]);
272
+ if (lines.length === 1) return [{ depth, text: `${header} ${lines[0]?.text ?? ''}` }];
273
+ return [{ depth, text: header }, ...lines];
274
+ }
275
+ const body = stmt.body as Body;
276
+ const lines = translateBody(ctx, body, depth + 1);
277
+ const last = [...body].reverse().find((entry) => entry.kind === 'stmt');
278
+ const tail = last?.kind === 'stmt' ? last.stmt : undefined;
279
+ if (tail !== undefined && ['if', 'for', 'while', 'switch'].includes(tail.kind)) {
280
+ ctx.refuse(diagnosticFor('OS9002', ctx.span(tail.at), { construct: 'a function whose last line is a block' }));
281
+ return [];
282
+ }
283
+ if (tail !== undefined && (tail.kind === 'declare' || tail.kind === 'assign')) {
284
+ lines.push({ depth: depth + 1, text: ctx.scope.find(tail.name.name)?.output ?? tail.name.name });
285
+ }
286
+ return [{ depth, text: header }, ...lines];
287
+ });
288
+ }
289
+
290
+ /** A statement's own lines, before anything it moved out of itself is put in front. */
291
+ function ownLines(ctx: Context, stmt: Stmt, depth: number): OutLine[] {
292
+ switch (stmt.kind) {
293
+ case 'declare':
294
+ return declare(ctx, stmt, depth);
295
+ case 'tuple':
296
+ return tuple(ctx, stmt, depth);
297
+ case 'assign':
298
+ return assign(ctx, stmt, depth);
299
+ case 'evaluate':
300
+ return evaluate(ctx, stmt, depth);
301
+ case 'if':
302
+ return ifLines(ctx, stmt, depth);
303
+ case 'for':
304
+ return forLines(ctx, stmt, depth);
305
+ case 'while':
306
+ return [{ depth, text: `while ${fixedCondition(ctx, stmt.condition)}` }, ...block(ctx, stmt.body, depth + 1)];
307
+ case 'switch':
308
+ return switchLines(ctx, stmt, depth);
309
+ case 'break':
310
+ case 'continue':
311
+ return [{ depth, text: stmt.kind }];
312
+ case 'function':
313
+ return functionLines(ctx, stmt, depth);
314
+ case 'refused':
315
+ ctx.refuse(diagnosticFor('OS9002', ctx.span(stmt.at), { construct: stmt.construct }));
316
+ return [];
317
+ }
318
+ }
319
+
320
+ /** Where the source wrote a comment after the statement, it goes after its first line. */
321
+ function remarked(stmt: Stmt, lines: OutLine[]): OutLine[] {
322
+ const first = lines[0];
323
+ if (first === undefined || stmt.remarks.length === 0) return lines;
324
+ const trailing = stmt.remarks.map((one) => `//${one.text}`).join(' ');
325
+ return [{ depth: first.depth, text: `${first.text} ${trailing}` }, ...lines.slice(1)];
326
+ }
327
+
328
+ export function translateStatement(ctx: Context, stmt: Stmt, depth: number): OutLine[] {
329
+ const hoists = stmt.kind !== 'while' && stmt.kind !== 'switch' && stmt.kind !== 'function' && stmt.kind !== 'refused';
330
+ return framed(ctx, depth, hoists, () => remarked(stmt, ownLines(ctx, stmt, depth)));
331
+ }