openalgo-script 0.1.0-alpha.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 (266) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +12 -0
  3. package/README.md +255 -0
  4. package/dist/core/ast/annotations.d.ts +56 -0
  5. package/dist/core/ast/annotations.d.ts.map +1 -0
  6. package/dist/core/ast/annotations.js +27 -0
  7. package/dist/core/ast/annotations.js.map +1 -0
  8. package/dist/core/ast/build.d.ts +25 -0
  9. package/dist/core/ast/build.d.ts.map +1 -0
  10. package/dist/core/ast/build.js +26 -0
  11. package/dist/core/ast/build.js.map +1 -0
  12. package/dist/core/ast/children.d.ts +15 -0
  13. package/dist/core/ast/children.d.ts.map +1 -0
  14. package/dist/core/ast/children.js +103 -0
  15. package/dist/core/ast/children.js.map +1 -0
  16. package/dist/core/ast/expressions.d.ts +178 -0
  17. package/dist/core/ast/expressions.d.ts.map +1 -0
  18. package/dist/core/ast/expressions.js +50 -0
  19. package/dist/core/ast/expressions.js.map +1 -0
  20. package/dist/core/ast/header.d.ts +50 -0
  21. package/dist/core/ast/header.d.ts.map +1 -0
  22. package/dist/core/ast/header.js +2 -0
  23. package/dist/core/ast/header.js.map +1 -0
  24. package/dist/core/ast/index.d.ts +30 -0
  25. package/dist/core/ast/index.d.ts.map +1 -0
  26. package/dist/core/ast/index.js +21 -0
  27. package/dist/core/ast/index.js.map +1 -0
  28. package/dist/core/ast/name.d.ts +24 -0
  29. package/dist/core/ast/name.d.ts.map +1 -0
  30. package/dist/core/ast/name.js +2 -0
  31. package/dist/core/ast/name.js.map +1 -0
  32. package/dist/core/ast/node.d.ts +34 -0
  33. package/dist/core/ast/node.d.ts.map +1 -0
  34. package/dist/core/ast/node.js +70 -0
  35. package/dist/core/ast/node.js.map +1 -0
  36. package/dist/core/ast/script.d.ts +57 -0
  37. package/dist/core/ast/script.d.ts.map +1 -0
  38. package/dist/core/ast/script.js +2 -0
  39. package/dist/core/ast/script.js.map +1 -0
  40. package/dist/core/ast/statements.d.ts +161 -0
  41. package/dist/core/ast/statements.d.ts.map +1 -0
  42. package/dist/core/ast/statements.js +10 -0
  43. package/dist/core/ast/statements.js.map +1 -0
  44. package/dist/core/ast/walk.d.ts +39 -0
  45. package/dist/core/ast/walk.d.ts.map +1 -0
  46. package/dist/core/ast/walk.js +54 -0
  47. package/dist/core/ast/walk.js.map +1 -0
  48. package/dist/core/catalogue/catalogue.generated.d.ts +1590 -0
  49. package/dist/core/catalogue/catalogue.generated.d.ts.map +1 -0
  50. package/dist/core/catalogue/catalogue.generated.js +162 -0
  51. package/dist/core/catalogue/catalogue.generated.js.map +1 -0
  52. package/dist/core/catalogue/index.d.ts +15 -0
  53. package/dist/core/catalogue/index.d.ts.map +1 -0
  54. package/dist/core/catalogue/index.js +4 -0
  55. package/dist/core/catalogue/index.js.map +1 -0
  56. package/dist/core/catalogue/lookup.d.ts +14 -0
  57. package/dist/core/catalogue/lookup.d.ts.map +1 -0
  58. package/dist/core/catalogue/lookup.js +19 -0
  59. package/dist/core/catalogue/lookup.js.map +1 -0
  60. package/dist/core/catalogue/template.d.ts +15 -0
  61. package/dist/core/catalogue/template.d.ts.map +1 -0
  62. package/dist/core/catalogue/template.js +33 -0
  63. package/dist/core/catalogue/template.js.map +1 -0
  64. package/dist/core/catalogue/types.d.ts +34 -0
  65. package/dist/core/catalogue/types.d.ts.map +1 -0
  66. package/dist/core/catalogue/types.js +6 -0
  67. package/dist/core/catalogue/types.js.map +1 -0
  68. package/dist/core/catalogue/values.generated.d.ts +565 -0
  69. package/dist/core/catalogue/values.generated.d.ts.map +1 -0
  70. package/dist/core/catalogue/values.generated.js +6 -0
  71. package/dist/core/catalogue/values.generated.js.map +1 -0
  72. package/dist/core/diagnostics/collector.d.ts +46 -0
  73. package/dist/core/diagnostics/collector.d.ts.map +1 -0
  74. package/dist/core/diagnostics/collector.js +55 -0
  75. package/dist/core/diagnostics/collector.js.map +1 -0
  76. package/dist/core/diagnostics/diagnostic.d.ts +40 -0
  77. package/dist/core/diagnostics/diagnostic.d.ts.map +1 -0
  78. package/dist/core/diagnostics/diagnostic.js +29 -0
  79. package/dist/core/diagnostics/diagnostic.js.map +1 -0
  80. package/dist/core/diagnostics/index.d.ts +12 -0
  81. package/dist/core/diagnostics/index.d.ts.map +1 -0
  82. package/dist/core/diagnostics/index.js +3 -0
  83. package/dist/core/diagnostics/index.js.map +1 -0
  84. package/dist/core/index.d.ts +28 -0
  85. package/dist/core/index.d.ts.map +1 -0
  86. package/dist/core/index.js +22 -0
  87. package/dist/core/index.js.map +1 -0
  88. package/dist/core/lex/blocks.d.ts +51 -0
  89. package/dist/core/lex/blocks.d.ts.map +1 -0
  90. package/dist/core/lex/blocks.js +56 -0
  91. package/dist/core/lex/blocks.js.map +1 -0
  92. package/dist/core/lex/characters.d.ts +38 -0
  93. package/dist/core/lex/characters.d.ts.map +1 -0
  94. package/dist/core/lex/characters.js +50 -0
  95. package/dist/core/lex/characters.js.map +1 -0
  96. package/dist/core/lex/comments.d.ts +50 -0
  97. package/dist/core/lex/comments.d.ts.map +1 -0
  98. package/dist/core/lex/comments.js +64 -0
  99. package/dist/core/lex/comments.js.map +1 -0
  100. package/dist/core/lex/index.d.ts +12 -0
  101. package/dist/core/lex/index.d.ts.map +1 -0
  102. package/dist/core/lex/index.js +12 -0
  103. package/dist/core/lex/index.js.map +1 -0
  104. package/dist/core/lex/layout.d.ts +40 -0
  105. package/dist/core/lex/layout.d.ts.map +1 -0
  106. package/dist/core/lex/layout.js +143 -0
  107. package/dist/core/lex/layout.js.map +1 -0
  108. package/dist/core/lex/lexer.d.ts +13 -0
  109. package/dist/core/lex/lexer.d.ts.map +1 -0
  110. package/dist/core/lex/lexer.js +419 -0
  111. package/dist/core/lex/lexer.js.map +1 -0
  112. package/dist/core/lex/numbers.d.ts +22 -0
  113. package/dist/core/lex/numbers.d.ts.map +1 -0
  114. package/dist/core/lex/numbers.js +58 -0
  115. package/dist/core/lex/numbers.js.map +1 -0
  116. package/dist/core/lex/statements.d.ts +24 -0
  117. package/dist/core/lex/statements.d.ts.map +1 -0
  118. package/dist/core/lex/statements.js +90 -0
  119. package/dist/core/lex/statements.js.map +1 -0
  120. package/dist/core/lex/strings.d.ts +27 -0
  121. package/dist/core/lex/strings.d.ts.map +1 -0
  122. package/dist/core/lex/strings.js +92 -0
  123. package/dist/core/lex/strings.js.map +1 -0
  124. package/dist/core/lex/unexpected.d.ts +31 -0
  125. package/dist/core/lex/unexpected.d.ts.map +1 -0
  126. package/dist/core/lex/unexpected.js +114 -0
  127. package/dist/core/lex/unexpected.js.map +1 -0
  128. package/dist/core/parse/annotations.d.ts +4 -0
  129. package/dist/core/parse/annotations.d.ts.map +1 -0
  130. package/dist/core/parse/annotations.js +49 -0
  131. package/dist/core/parse/annotations.js.map +1 -0
  132. package/dist/core/parse/blocks.d.ts +18 -0
  133. package/dist/core/parse/blocks.d.ts.map +1 -0
  134. package/dist/core/parse/blocks.js +47 -0
  135. package/dist/core/parse/blocks.js.map +1 -0
  136. package/dist/core/parse/branches.d.ts +17 -0
  137. package/dist/core/parse/branches.d.ts.map +1 -0
  138. package/dist/core/parse/branches.js +66 -0
  139. package/dist/core/parse/branches.js.map +1 -0
  140. package/dist/core/parse/cursor.d.ts +119 -0
  141. package/dist/core/parse/cursor.d.ts.map +1 -0
  142. package/dist/core/parse/cursor.js +305 -0
  143. package/dist/core/parse/cursor.js.map +1 -0
  144. package/dist/core/parse/expressions.d.ts +57 -0
  145. package/dist/core/parse/expressions.d.ts.map +1 -0
  146. package/dist/core/parse/expressions.js +373 -0
  147. package/dist/core/parse/expressions.js.map +1 -0
  148. package/dist/core/parse/functions.d.ts +16 -0
  149. package/dist/core/parse/functions.d.ts.map +1 -0
  150. package/dist/core/parse/functions.js +92 -0
  151. package/dist/core/parse/functions.js.map +1 -0
  152. package/dist/core/parse/index.d.ts +13 -0
  153. package/dist/core/parse/index.d.ts.map +1 -0
  154. package/dist/core/parse/index.js +13 -0
  155. package/dist/core/parse/index.js.map +1 -0
  156. package/dist/core/parse/loops.d.ts +13 -0
  157. package/dist/core/parse/loops.d.ts.map +1 -0
  158. package/dist/core/parse/loops.js +78 -0
  159. package/dist/core/parse/loops.js.map +1 -0
  160. package/dist/core/parse/names.d.ts +34 -0
  161. package/dist/core/parse/names.d.ts.map +1 -0
  162. package/dist/core/parse/names.js +122 -0
  163. package/dist/core/parse/names.js.map +1 -0
  164. package/dist/core/parse/script.d.ts +23 -0
  165. package/dist/core/parse/script.d.ts.map +1 -0
  166. package/dist/core/parse/script.js +55 -0
  167. package/dist/core/parse/script.js.map +1 -0
  168. package/dist/core/parse/statements.d.ts +14 -0
  169. package/dist/core/parse/statements.d.ts.map +1 -0
  170. package/dist/core/parse/statements.js +292 -0
  171. package/dist/core/parse/statements.js.map +1 -0
  172. package/dist/core/parse/switches.d.ts +16 -0
  173. package/dist/core/parse/switches.d.ts.map +1 -0
  174. package/dist/core/parse/switches.js +98 -0
  175. package/dist/core/parse/switches.js.map +1 -0
  176. package/dist/core/render/index.d.ts +10 -0
  177. package/dist/core/render/index.d.ts.map +1 -0
  178. package/dist/core/render/index.js +10 -0
  179. package/dist/core/render/index.js.map +1 -0
  180. package/dist/core/render/terminal.d.ts +11 -0
  181. package/dist/core/render/terminal.d.ts.map +1 -0
  182. package/dist/core/render/terminal.js +51 -0
  183. package/dist/core/render/terminal.js.map +1 -0
  184. package/dist/core/source/index.d.ts +7 -0
  185. package/dist/core/source/index.d.ts.map +1 -0
  186. package/dist/core/source/index.js +2 -0
  187. package/dist/core/source/index.js.map +1 -0
  188. package/dist/core/source/source.d.ts +41 -0
  189. package/dist/core/source/source.d.ts.map +1 -0
  190. package/dist/core/source/source.js +67 -0
  191. package/dist/core/source/source.js.map +1 -0
  192. package/dist/core/span/index.d.ts +3 -0
  193. package/dist/core/span/index.d.ts.map +1 -0
  194. package/dist/core/span/index.js +2 -0
  195. package/dist/core/span/index.js.map +1 -0
  196. package/dist/core/span/span.d.ts +67 -0
  197. package/dist/core/span/span.d.ts.map +1 -0
  198. package/dist/core/span/span.js +27 -0
  199. package/dist/core/span/span.js.map +1 -0
  200. package/dist/core/tokens/index.d.ts +8 -0
  201. package/dist/core/tokens/index.d.ts.map +1 -0
  202. package/dist/core/tokens/index.js +2 -0
  203. package/dist/core/tokens/index.js.map +1 -0
  204. package/dist/core/tokens/kind.d.ts +53 -0
  205. package/dist/core/tokens/kind.d.ts.map +1 -0
  206. package/dist/core/tokens/kind.js +94 -0
  207. package/dist/core/tokens/kind.js.map +1 -0
  208. package/dist/core/tokens/token.d.ts +36 -0
  209. package/dist/core/tokens/token.d.ts.map +1 -0
  210. package/dist/core/tokens/token.js +2 -0
  211. package/dist/core/tokens/token.js.map +1 -0
  212. package/package.json +52 -0
  213. package/spec/README.md +48 -0
  214. package/spec/errors.json +3204 -0
  215. package/src/core/ast/annotations.ts +68 -0
  216. package/src/core/ast/build.ts +35 -0
  217. package/src/core/ast/children.ts +110 -0
  218. package/src/core/ast/expressions.ts +254 -0
  219. package/src/core/ast/header.ts +53 -0
  220. package/src/core/ast/index.ts +102 -0
  221. package/src/core/ast/name.ts +24 -0
  222. package/src/core/ast/node.ts +120 -0
  223. package/src/core/ast/script.ts +60 -0
  224. package/src/core/ast/statements.ts +193 -0
  225. package/src/core/ast/walk.ts +67 -0
  226. package/src/core/catalogue/catalogue.generated.ts +171 -0
  227. package/src/core/catalogue/index.ts +20 -0
  228. package/src/core/catalogue/lookup.ts +23 -0
  229. package/src/core/catalogue/template.ts +38 -0
  230. package/src/core/catalogue/types.ts +37 -0
  231. package/src/core/catalogue/values.generated.ts +160 -0
  232. package/src/core/diagnostics/collector.ts +87 -0
  233. package/src/core/diagnostics/diagnostic.ts +72 -0
  234. package/src/core/diagnostics/index.ts +12 -0
  235. package/src/core/index.ts +146 -0
  236. package/src/core/lex/blocks.ts +92 -0
  237. package/src/core/lex/characters.ts +56 -0
  238. package/src/core/lex/comments.ts +75 -0
  239. package/src/core/lex/index.ts +11 -0
  240. package/src/core/lex/layout.ts +188 -0
  241. package/src/core/lex/lexer.ts +486 -0
  242. package/src/core/lex/numbers.ts +77 -0
  243. package/src/core/lex/statements.ts +94 -0
  244. package/src/core/lex/strings.ts +119 -0
  245. package/src/core/lex/unexpected.ts +125 -0
  246. package/src/core/parse/annotations.ts +60 -0
  247. package/src/core/parse/blocks.ts +52 -0
  248. package/src/core/parse/branches.ts +77 -0
  249. package/src/core/parse/cursor.ts +348 -0
  250. package/src/core/parse/expressions.ts +441 -0
  251. package/src/core/parse/functions.ts +109 -0
  252. package/src/core/parse/index.ts +12 -0
  253. package/src/core/parse/loops.ts +97 -0
  254. package/src/core/parse/names.ts +135 -0
  255. package/src/core/parse/script.ts +64 -0
  256. package/src/core/parse/statements.ts +329 -0
  257. package/src/core/parse/switches.ts +105 -0
  258. package/src/core/render/index.ts +9 -0
  259. package/src/core/render/terminal.ts +62 -0
  260. package/src/core/source/index.ts +6 -0
  261. package/src/core/source/source.ts +102 -0
  262. package/src/core/span/index.ts +2 -0
  263. package/src/core/span/span.ts +84 -0
  264. package/src/core/tokens/index.ts +15 -0
  265. package/src/core/tokens/kind.ts +123 -0
  266. package/src/core/tokens/token.ts +39 -0
@@ -0,0 +1,135 @@
1
+ import type { Name } from '../ast/index.js';
2
+ import { makeNode } from '../ast/index.js';
3
+ import type { TokenKind } from '../tokens/index.js';
4
+ import { RESERVED_WORDS } from '../tokens/index.js';
5
+ import type { Cursor } from './cursor.js';
6
+ import { describeToken } from './cursor.js';
7
+
8
+ /**
9
+ * Reading the places a name is written, which is where OS1019 lives.
10
+ *
11
+ * The lexer cannot make this call. A reserved word before an `=` is a legal
12
+ * label inside a call and is OS1019 inside a parameter list (language.md 3.4),
13
+ * and only the parser knows which of the two lists it is reading. So a reserved
14
+ * word arrives carrying its keyword kind, and the readers below are the
15
+ * answers: `takeName` refuses one, `takeLabel` and `takeMember` accept one,
16
+ * because a label and a member are matched against a published list rather than
17
+ * looked up in a scope.
18
+ */
19
+
20
+ const RESERVED: ReadonlySet<TokenKind> = new Set<TokenKind>(RESERVED_WORDS);
21
+
22
+ /** Whether a token may stand where a word is expected. */
23
+ export function isWordKind(kind: TokenKind): boolean {
24
+ return kind === 'identifier' || RESERVED.has(kind);
25
+ }
26
+
27
+ /**
28
+ * The reserved words a reader can only have meant as a name.
29
+ *
30
+ * A reserved word written where a value belongs is one of two things, and which
31
+ * one depends on the word. Six of them are read by a rule standing next to the
32
+ * expression: `and` and `or` join two operands and `not` takes one, and `in`,
33
+ * `to` and `step` are read by a `for` header the moment the expression before
34
+ * them ends. A word that opens a statement is the same answer again, because an
35
+ * unclosed bracket carries the statement onto the lines below it and the `if`
36
+ * down there belongs to its own line rather than to this expression. Taking any
37
+ * of those as a name would swallow a token the rule around it is waiting for,
38
+ * and the second diagnostic would then be about the rule rather than about the
39
+ * mistake.
40
+ *
41
+ * What is left is the four value type names, the two words that build a type,
42
+ * and the six the language reserves and version 1 does not implement. No rule
43
+ * of the grammar reads one of them anywhere near an expression, so a reader who
44
+ * wrote one where a value belongs wrote a name, and the answer is OS1019: the
45
+ * same answer `takeName` gives for the same word written as a target.
46
+ */
47
+ const MEANT_AS_A_NAME: ReadonlySet<TokenKind> = new Set<TokenKind>([
48
+ 'array',
49
+ 'as',
50
+ 'bool',
51
+ 'color',
52
+ 'import',
53
+ 'is',
54
+ 'map',
55
+ 'matrix',
56
+ 'number',
57
+ 'series',
58
+ 'string',
59
+ 'type',
60
+ ]);
61
+
62
+ /** Whether a word standing where a value belongs is a name the language has taken. */
63
+ export function isMeantAsAName(kind: TokenKind): boolean {
64
+ return MEANT_AS_A_NAME.has(kind);
65
+ }
66
+
67
+ /**
68
+ * A name that is not reserved, derived from one that is.
69
+ *
70
+ * OS1019 promises a near name that keeps the meaning, and the derivation has to
71
+ * work for all thirty six words rather than for the five anyone remembers, so
72
+ * it is mechanical: the word is what the reader meant, and the suffix is what
73
+ * makes it theirs to use.
74
+ */
75
+ function unreservedNear(word: string): string {
76
+ return `${word}Value`;
77
+ }
78
+
79
+ /** No word where one was required, which leaves the statement without its subject. */
80
+ function missingWord(cursor: Cursor): Name {
81
+ const hole = cursor.holeSpan();
82
+ cursor.report('OS1022', hole, {
83
+ token: describeToken(cursor.previousMeaningful() ?? cursor.token),
84
+ });
85
+ return makeNode('name', hole, { text: '' });
86
+ }
87
+
88
+ /**
89
+ * The name at the cursor: an identifier, or a reserved word that is OS1019.
90
+ *
91
+ * The word is taken either way. A script that named something `type` still
92
+ * meant to declare it, and every later diagnostic about that name is more use
93
+ * to the reader than a hole where the declaration should have been.
94
+ */
95
+ export function takeName(cursor: Cursor): Name {
96
+ const token = cursor.token;
97
+ if (!isWordKind(token.kind)) return missingWord(cursor);
98
+
99
+ cursor.advance();
100
+ if (token.kind !== 'identifier') {
101
+ cursor.report('OS1019', token.span, {
102
+ word: token.text,
103
+ suggestion: unreservedNear(token.text),
104
+ });
105
+ }
106
+ return makeNode('name', token.span, { text: token.text });
107
+ }
108
+
109
+ /**
110
+ * The label of a named argument, where a reserved word is legal.
111
+ *
112
+ * A label is matched against the callee's parameter list and is never looked up
113
+ * in a scope, so `plot(v, "V", color = aqua)` is correct and is not OS1019.
114
+ */
115
+ export function takeLabel(cursor: Cursor): Name {
116
+ const token = cursor.advance();
117
+ return makeNode('name', token.span, { text: token.text });
118
+ }
119
+
120
+ /**
121
+ * The word after a dot.
122
+ *
123
+ * A member is matched against the namespace or the object type it was read
124
+ * from, which is the same kind of published list a label is matched against, so
125
+ * a reserved word is not OS1019 here either. Whether the member exists at all
126
+ * is OS2009, and the checker owns it.
127
+ */
128
+ export function takeMember(cursor: Cursor): Name {
129
+ return isWordKind(cursor.kind) ? takeLabel(cursor) : missingWord(cursor);
130
+ }
131
+
132
+ /** Whether the cursor is on a named argument's label: a word, then a bare `=`. */
133
+ export function atLabel(cursor: Cursor): boolean {
134
+ return isWordKind(cursor.kind) && cursor.peek().kind === '=';
135
+ }
@@ -0,0 +1,64 @@
1
+ import type { Script, TopLevelItem } from '../ast/index.js';
2
+ import { makeNode } from '../ast/index.js';
3
+ import type { DiagnosticSink } from '../diagnostics/index.js';
4
+ import { lex } from '../lex/index.js';
5
+ import type { SourceFile } from '../source/index.js';
6
+ import type { Token } from '../tokens/index.js';
7
+ import { Cursor } from './cursor.js';
8
+ import { parseFunctionDeclaration } from './functions.js';
9
+ import { parseStatement } from './statements.js';
10
+
11
+ /**
12
+ * A file, which is a list of statements and function declarations.
13
+ *
14
+ * Section 19 writes a file as a version line, a declaration and a limits line
15
+ * followed by the rest. The prose wins: the declaration being missing is
16
+ * OS2007, two of them is OS2008, and a version line that is not first is
17
+ * OS1021, all of which name a line and suggest a move. So every line here is an
18
+ * item in source order, a file with no declaration still has a tree, and the
19
+ * checker is left holding the questions that are its to answer.
20
+ *
21
+ * Nothing in this file throws and nothing stops early. A statement that could
22
+ * not be read costs its own line and, where it opened one, its block; the next
23
+ * line is read as though nothing had happened, which is what lets a file with
24
+ * three mistakes in it produce three diagnostics from one compile.
25
+ */
26
+ export function parse(file: SourceFile, diagnostics: DiagnosticSink): Script {
27
+ return parseTokens(file, lex(file, diagnostics), diagnostics);
28
+ }
29
+
30
+ /** The same, for a host that has already lexed the file and kept the tokens. */
31
+ export function parseTokens(
32
+ file: SourceFile,
33
+ tokens: readonly Token[],
34
+ diagnostics: DiagnosticSink,
35
+ ): Script {
36
+ const cursor = new Cursor(file, tokens, diagnostics);
37
+ const items: TopLevelItem[] = [];
38
+
39
+ while (!cursor.at('endOfFile')) {
40
+ const before = cursor.position;
41
+
42
+ if (cursor.at('newline') || cursor.at('dedent') || cursor.at('indent')) {
43
+ // Layout with no statement to belong to, which a recovery can leave
44
+ // behind. Taking it here keeps the loop below reading statements only.
45
+ if (cursor.at('indent')) cursor.skipBlockBody();
46
+ else cursor.advance();
47
+ continue;
48
+ }
49
+
50
+ if (cursor.at('fn')) {
51
+ cursor.beginStatement();
52
+ const declaration = parseFunctionDeclaration(cursor);
53
+ cursor.noteStatement(declaration.span.line);
54
+ items.push(declaration);
55
+ } else {
56
+ const statement = parseStatement(cursor);
57
+ if (statement !== undefined) items.push(statement);
58
+ }
59
+
60
+ if (cursor.position === before) cursor.skipStatement();
61
+ }
62
+
63
+ return makeNode('script', file.spanAt(0, file.text.length), { items });
64
+ }
@@ -0,0 +1,329 @@
1
+ import type {
2
+ AssignmentOperator,
3
+ Expression,
4
+ FunctionDeclaration,
5
+ Statement,
6
+ TypeAnnotation,
7
+ } from '../ast/index.js';
8
+ import { ASSIGNMENT_OPERATORS, makeNode, withoutGrouping } from '../ast/index.js';
9
+ import { spanning } from '../span/index.js';
10
+ import type { NumberToken, TokenKind } from '../tokens/index.js';
11
+ import { parseTypeAnnotation } from './annotations.js';
12
+ import { parseIfStatement } from './branches.js';
13
+ import type { Cursor } from './cursor.js';
14
+ import { missingExpression, parseArgumentList, parseExpression } from './expressions.js';
15
+ import { parseFunctionDeclaration } from './functions.js';
16
+ import { parseForStatement, parseWhileStatement } from './loops.js';
17
+ import { isWordKind, takeName } from './names.js';
18
+ import { parseSwitchStatement } from './switches.js';
19
+
20
+ /**
21
+ * One statement, and the choice of which one it is.
22
+ *
23
+ * The choice is made from the first token and at most one more, which is what
24
+ * keeps the language readable: a line that begins with `if` is an `if` and
25
+ * nothing else, and the only lookahead anywhere is the token after a word,
26
+ * which says whether the word is being assigned to.
27
+ *
28
+ * Three of the forms here describe the file rather than compute anything. They
29
+ * are statements like the rest and may be written anywhere a statement may be,
30
+ * because the errors for putting one in the wrong place name a line and suggest
31
+ * a move (OS1021, OS2007, OS2008, OS3014), and a tree that refused to hold them
32
+ * could not carry a line number for any of those to report.
33
+ */
34
+
35
+ const ASSIGNMENT: ReadonlySet<TokenKind> = new Set<TokenKind>(ASSIGNMENT_OPERATORS);
36
+
37
+ export function parseStatement(
38
+ cursor: Cursor,
39
+ ): Statement | FunctionDeclaration | undefined {
40
+ cursor.beginStatement();
41
+ const statement = dispatch(cursor);
42
+ if (statement !== undefined) cursor.noteStatement(statement.span.line);
43
+ return statement;
44
+ }
45
+
46
+ /**
47
+ * The end of a statement: a newline, and OS1018 for anything before it.
48
+ *
49
+ * A line holds at most one statement, so whatever is left over is either a
50
+ * missing operator or a second statement that lost its newline. It is only
51
+ * reported when the statement has nothing else wrong with it, because leftovers
52
+ * are what every other mistake on the line produces and the reader should be
53
+ * told about the mistake rather than about its wreckage.
54
+ */
55
+ export function finishStatement(cursor: Cursor): void {
56
+ if (cursor.take('newline') !== undefined) return;
57
+ if (cursor.atStatementEnd()) return;
58
+ if (!cursor.reportedHere) cursor.unexpected(cursor.token);
59
+ cursor.skipToEndOfLine();
60
+ }
61
+
62
+ function dispatch(cursor: Cursor): Statement | FunctionDeclaration | undefined {
63
+ switch (cursor.kind) {
64
+ case 'if':
65
+ return parseIfStatement(cursor);
66
+ case 'for':
67
+ return parseForStatement(cursor);
68
+ case 'while':
69
+ return parseWhileStatement(cursor);
70
+ case 'switch':
71
+ return parseSwitchStatement(cursor);
72
+ case 'break':
73
+ case 'continue':
74
+ return parseJump(cursor);
75
+ case 'return':
76
+ return parseReturn(cursor);
77
+ case 'var':
78
+ return parseVarDeclaration(cursor, false);
79
+ case 'live':
80
+ return cursor.peek().kind === 'var'
81
+ ? parseVarDeclaration(cursor, true)
82
+ : parseSimpleStatement(cursor);
83
+ case 'study':
84
+ case 'strategy':
85
+ return parseScriptDeclaration(cursor);
86
+ case 'else':
87
+ return strayElse(cursor);
88
+ case 'case':
89
+ case 'default':
90
+ return strayArm(cursor);
91
+ case 'fn':
92
+ return nestedFunction(cursor);
93
+ default:
94
+ return parseSimpleStatement(cursor);
95
+ }
96
+ }
97
+
98
+ function parseSimpleStatement(cursor: Cursor): Statement | undefined {
99
+ const token = cursor.token;
100
+
101
+ // A block whose header was not understood, so nothing is left that could own
102
+ // it. Skipping it whole reports the header once instead of every line of it.
103
+ if (token.kind === 'indent') {
104
+ cursor.skipBlockBody();
105
+ return undefined;
106
+ }
107
+
108
+ if (token.kind === 'identifier') {
109
+ const next = cursor.peek();
110
+ // `version` and `limits` are not reserved words: a script written before
111
+ // either existed keeps compiling, so both are recognised by position.
112
+ if (token.text === 'version' && next.kind === 'numberLiteral') {
113
+ return parseVersionLine(cursor, next);
114
+ }
115
+ if (token.text === 'limits' && next.kind === '(') return parseLimitsLine(cursor);
116
+ }
117
+
118
+ if (isWordKind(token.kind) && ASSIGNMENT.has(cursor.peek().kind)) {
119
+ return parseAssignment(cursor);
120
+ }
121
+
122
+ if (cursor.atStatementEnd()) {
123
+ cursor.take('newline');
124
+ return undefined;
125
+ }
126
+
127
+ const expression = parseExpression(cursor);
128
+ if (ASSIGNMENT.has(cursor.kind)) {
129
+ const refused = assignmentToSomethingOtherThanAName(cursor, expression);
130
+ if (refused !== undefined) return refused;
131
+ }
132
+
133
+ finishStatement(cursor);
134
+ return makeNode('expressionStatement', expression.span, { expression });
135
+ }
136
+
137
+ /**
138
+ * An assignment whose target is an index or a member: OS1024 and OS1025.
139
+ *
140
+ * The grammar of section 19 has one assignment target and it is an identifier,
141
+ * so what is wrong on this line is the target and not the operator after it.
142
+ * That is why neither of these is OS1018, whose message would say the statement
143
+ * had ended before an operator the reader wrote in the middle of it. Two codes
144
+ * rather than one because the two have different fixes: an array element is
145
+ * written with a call, and a member is not written at all.
146
+ *
147
+ * Anything else the grammar cannot assign to, a literal or a call, is left to
148
+ * the caller and reported as OS1018, which is the nearest true thing the
149
+ * catalogue has for it.
150
+ *
151
+ * The value is read rather than skipped, so a mistake in it is reported on the
152
+ * same compile, and the statement reaches the tree as the target that was
153
+ * written. There is no node for an assignment whose target is not a name, and
154
+ * building one over a name taken out of the target, `prices` out of
155
+ * `prices[0]`, would put a different program in the tree from the one on the
156
+ * page.
157
+ */
158
+ function assignmentToSomethingOtherThanAName(
159
+ cursor: Cursor,
160
+ target: Expression,
161
+ ): Statement | undefined {
162
+ const written = withoutGrouping(target);
163
+
164
+ if (written.kind === 'index') {
165
+ cursor.report('OS1024', target.span, { name: cursor.textOf(written.target.span) });
166
+ } else if (written.kind === 'member') {
167
+ cursor.report('OS1025', target.span, {
168
+ name: cursor.textOf(written.object.span),
169
+ member: written.member.text,
170
+ });
171
+ } else {
172
+ return undefined;
173
+ }
174
+
175
+ // The operator, and then the value, which is read so that a mistake inside it
176
+ // is reported on this compile rather than on the one after the target is
177
+ // fixed.
178
+ cursor.advance();
179
+ const value = parseExpression(cursor);
180
+ finishStatement(cursor);
181
+ return makeNode('expressionStatement', spanning(target.span, value.span), {
182
+ expression: target,
183
+ });
184
+ }
185
+
186
+ function parseAssignment(cursor: Cursor): Statement {
187
+ const target = takeName(cursor);
188
+ const operator = cursor.advance().kind as AssignmentOperator;
189
+ const value = parseExpression(cursor);
190
+ finishStatement(cursor);
191
+ return makeNode('assignment', spanning(target.span, value.span), { target, operator, value });
192
+ }
193
+
194
+ function parseVarDeclaration(cursor: Cursor, live: boolean): Statement {
195
+ const start = cursor.advance();
196
+ if (live) cursor.advance();
197
+
198
+ const name = takeName(cursor);
199
+ const annotation: TypeAnnotation | undefined =
200
+ cursor.take(':') !== undefined ? parseTypeAnnotation(cursor) : undefined;
201
+
202
+ let initialiser: Expression;
203
+ if (cursor.take('=') !== undefined) {
204
+ initialiser = parseExpression(cursor);
205
+ } else {
206
+ // The declaration and the first assignment are one statement, so a
207
+ // persistent value always has something in it and no bar can read it
208
+ // before it exists.
209
+ cursor.report('OS1011', spanning(start.span, name.span), { name: name.text });
210
+ initialiser = missingExpression(cursor);
211
+ }
212
+
213
+ finishStatement(cursor);
214
+ return makeNode('varDeclaration', spanning(start.span, initialiser.span), {
215
+ live,
216
+ name,
217
+ annotation,
218
+ initialiser,
219
+ });
220
+ }
221
+
222
+ function parseScriptDeclaration(cursor: Cursor): Statement {
223
+ const keyword = cursor.advance();
224
+ const open = cursor.take('(');
225
+ const list = open === undefined ? undefined : parseArgumentList(cursor, open);
226
+ finishStatement(cursor);
227
+
228
+ return makeNode('scriptDeclaration', spanning(keyword.span, list?.end ?? keyword.span), {
229
+ form: keyword.kind === 'study' ? 'study' : 'strategy',
230
+ args: list?.items ?? [],
231
+ });
232
+ }
233
+
234
+ function parseVersionLine(cursor: Cursor, number: NumberToken): Statement {
235
+ const word = cursor.advance();
236
+ cursor.advance();
237
+ const span = spanning(word.span, number.span);
238
+
239
+ const before = cursor.firstStatementLine;
240
+ if (before !== undefined) cursor.report('OS1021', span, { line: before });
241
+
242
+ finishStatement(cursor);
243
+ return makeNode('versionLine', span, {
244
+ version: makeNode('numberLiteral', number.span, { value: number.value }),
245
+ });
246
+ }
247
+
248
+ function parseLimitsLine(cursor: Cursor): Statement {
249
+ const word = cursor.advance();
250
+ const list = parseArgumentList(cursor, cursor.advance());
251
+ finishStatement(cursor);
252
+ return makeNode('limitsLine', spanning(word.span, list.end), { args: list.items });
253
+ }
254
+
255
+ function parseJump(cursor: Cursor): Statement {
256
+ const keyword = cursor.advance();
257
+ if (!cursor.insideLoop) cursor.report('OS1009', keyword.span, { word: keyword.text });
258
+ finishStatement(cursor);
259
+ return keyword.kind === 'break'
260
+ ? makeNode('breakStatement', keyword.span, {})
261
+ : makeNode('continueStatement', keyword.span, {});
262
+ }
263
+
264
+ function parseReturn(cursor: Cursor): Statement {
265
+ const keyword = cursor.advance();
266
+ // A bare `return` exits with `none`, so the value is genuinely absent here
267
+ // rather than a hole something went missing from.
268
+ const value = cursor.atStatementEnd() ? undefined : parseExpression(cursor);
269
+ finishStatement(cursor);
270
+ return makeNode(
271
+ 'returnStatement',
272
+ value === undefined ? keyword.span : spanning(keyword.span, value.span),
273
+ { value },
274
+ );
275
+ }
276
+
277
+ /**
278
+ * An `else` that no `if` took.
279
+ *
280
+ * `parseIfStatement` takes the `else` that belongs to it, so one that reaches
281
+ * here has no `if` at its own indentation: either the statement above it is
282
+ * something else, or the lexer has already put the two in different blocks.
283
+ */
284
+ function strayElse(cursor: Cursor): undefined {
285
+ const token = cursor.token;
286
+ cursor.report('OS1016', token.span, {
287
+ found: token.span.column - 1,
288
+ expected: cursor.nearestIfColumn - 1,
289
+ });
290
+ cursor.skipStatement();
291
+ return undefined;
292
+ }
293
+
294
+ function strayArm(cursor: Cursor): undefined {
295
+ const token = cursor.token;
296
+ cursor.report('OS1017', token.span, { word: token.text });
297
+ cursor.skipStatement();
298
+ return undefined;
299
+ }
300
+
301
+ /**
302
+ * A `fn` inside a block, which language.md 11.1 forbids: OS1023.
303
+ *
304
+ * It is read and it is kept. Reading it keeps the lines under it from being
305
+ * taken for statements of the block around it. Keeping it is what lets anything
306
+ * after the parser see the function at all: a declaration dropped here is gone
307
+ * from the tree, so a call to it could only ever be reported as a name nobody
308
+ * declared, and a nested function nobody calls would go unreported for good. It
309
+ * stays where the reader wrote it rather than being lifted to the top level,
310
+ * which would compile a file the language refuses.
311
+ *
312
+ * The caret covers `fn` and the name, which is the part of the declaration the
313
+ * reader has to move, and the same span OS1011 puts under `var name`.
314
+ *
315
+ * A header with no name at all has already been told that a name was expected,
316
+ * and the message here has nothing to name, so it waits: the node is kept
317
+ * either way, and the declaration is reported as nested once it has a name for
318
+ * the sentence to use and for the fix to tell the reader to call.
319
+ */
320
+ function nestedFunction(cursor: Cursor): FunctionDeclaration {
321
+ const keyword = cursor.token;
322
+ const declaration = parseFunctionDeclaration(cursor);
323
+ if (declaration.name.text !== '') {
324
+ cursor.report('OS1023', spanning(keyword.span, declaration.name.span), {
325
+ name: declaration.name.text,
326
+ });
327
+ }
328
+ return declaration;
329
+ }
@@ -0,0 +1,105 @@
1
+ import type { Expression, SwitchCase, SwitchDefault, SwitchStatement } from '../ast/index.js';
2
+ import { makeNode } from '../ast/index.js';
3
+ import type { Span } from '../span/index.js';
4
+ import { spanning } from '../span/index.js';
5
+ import { parseBlock } from './blocks.js';
6
+ import type { Cursor } from './cursor.js';
7
+ import { parseCondition } from './expressions.js';
8
+ import { finishStatement } from './statements.js';
9
+
10
+ /**
11
+ * `switch`, in both forms of language.md 10.6.
12
+ *
13
+ * With a subject each case value is compared against it, and without one each
14
+ * case value is a condition. The difference is the absent subject and needs no
15
+ * flag beside it.
16
+ *
17
+ * The arms are the whole of a switch body. A `default` that is not last would
18
+ * leave the arms after it unreachable, so the arm that follows one is OS1017,
19
+ * and the arm is kept in the tree all the same: a reader who moves the default
20
+ * should not then be told about the arms one at a time.
21
+ */
22
+ export function parseSwitchStatement(cursor: Cursor): SwitchStatement {
23
+ const keyword = cursor.advance();
24
+ const subject = cursor.atStatementEnd() ? undefined : parseCondition(cursor);
25
+ finishStatement(cursor);
26
+
27
+ const cases: SwitchCase[] = [];
28
+ let defaultCase: SwitchDefault | undefined;
29
+ let end: Span = keyword.span;
30
+
31
+ if (!cursor.at('indent')) {
32
+ if (!cursor.reportedHere) cursor.report('OS1010', keyword.span, { header: 'switch' });
33
+ return makeNode('switchStatement', spanning(keyword.span, end), {
34
+ subject,
35
+ cases,
36
+ defaultCase,
37
+ });
38
+ }
39
+
40
+ if (!cursor.openBlock(cursor.token.span)) {
41
+ cursor.skipBlockBody();
42
+ return makeNode('switchStatement', spanning(keyword.span, end), {
43
+ subject,
44
+ cases,
45
+ defaultCase,
46
+ });
47
+ }
48
+ cursor.advance();
49
+
50
+ while (!cursor.at('dedent') && !cursor.at('endOfFile')) {
51
+ const before = cursor.position;
52
+ const word = cursor.token;
53
+
54
+ if (cursor.at('case')) {
55
+ if (defaultCase !== undefined) cursor.report('OS1017', word.span, { word: word.text });
56
+ const arm = parseCase(cursor);
57
+ cases.push(arm);
58
+ end = arm.span;
59
+ } else if (cursor.at('default')) {
60
+ const arm = parseDefault(cursor);
61
+ if (defaultCase === undefined) defaultCase = arm;
62
+ else cursor.report('OS1017', word.span, { word: word.text });
63
+ end = arm.span;
64
+ } else {
65
+ // A line in a switch body that is not an arm. The catalogue has no code
66
+ // for it, and OS1017 carries the rule it breaks: the arms are the whole
67
+ // of the body, so this line belongs inside one of them or above the
68
+ // switch.
69
+ cursor.report('OS1017', word.span, { word: 'case' });
70
+ cursor.skipStatement();
71
+ }
72
+
73
+ if (cursor.position === before) cursor.skipStatement();
74
+ }
75
+
76
+ cursor.closeBlock();
77
+ cursor.take('dedent');
78
+
79
+ return makeNode('switchStatement', spanning(keyword.span, end), { subject, cases, defaultCase });
80
+ }
81
+
82
+ /** One arm. Several values separated by commas are one arm, not several. */
83
+ function parseCase(cursor: Cursor): SwitchCase {
84
+ cursor.beginStatement();
85
+ const keyword = cursor.advance();
86
+
87
+ const values: Expression[] = [parseCondition(cursor)];
88
+ while (cursor.take(',') !== undefined) {
89
+ const before = cursor.position;
90
+ values.push(parseCondition(cursor));
91
+ if (cursor.position === before) break;
92
+ }
93
+
94
+ finishStatement(cursor);
95
+ const body = parseBlock(cursor, 'case', keyword.span);
96
+ return makeNode('switchCase', spanning(keyword.span, body.span), { values, body });
97
+ }
98
+
99
+ function parseDefault(cursor: Cursor): SwitchDefault {
100
+ cursor.beginStatement();
101
+ const keyword = cursor.advance();
102
+ finishStatement(cursor);
103
+ const body = parseBlock(cursor, 'default', keyword.span);
104
+ return makeNode('switchDefault', spanning(keyword.span, body.span), { body });
105
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Turning a diagnostic into something a reader sees.
3
+ *
4
+ * Only the terminal shape lives here. An editor puts the same diagnostic in a
5
+ * gutter and a chart puts it over a pane, and both take the Diagnostic and the
6
+ * span rather than this text, which is why rendering is a module of its own
7
+ * rather than a method on the diagnostic.
8
+ */
9
+ export { renderDiagnostic, renderDiagnostics } from './terminal.js';