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,348 @@
1
+ import type { DiagnosticCode, DiagnosticValues } from '../catalogue/index.js';
2
+ import type { DiagnosticSink } from '../diagnostics/index.js';
3
+ import type { SourceFile } from '../source/index.js';
4
+ import type { Span } from '../span/index.js';
5
+ import { endOffset } from '../span/index.js';
6
+ import type { Token, TokenKind } from '../tokens/index.js';
7
+
8
+ /**
9
+ * The parser's place in the token stream, and the three things every rule of
10
+ * the grammar needs that are not grammar: where it is, how it reports, and how
11
+ * it gets out of trouble.
12
+ *
13
+ * Recovery lives here rather than in each rule because it has to be the same
14
+ * everywhere. A trader with three mistakes in a file wants three diagnostics,
15
+ * so no rule throws and no rule gives up on the file: it reports, skips to the
16
+ * next boundary the language guarantees, and carries on. The boundaries are the
17
+ * lexer's own layout tokens, which is what makes the skip reliable rather than
18
+ * a guess: a statement ends at a `newline`, and a block is bracketed by an
19
+ * `indent` and a `dedent`.
20
+ */
21
+
22
+ /**
23
+ * The ceiling on source nesting, OS5005.
24
+ *
25
+ * Its purpose is a bounded stack: the parser descends one frame per level of
26
+ * brackets, and generated source is the usual way a file reaches a depth no
27
+ * person writes. The specification names no number, so this one is the
28
+ * compiler's, and it is far above anything written by hand: the deepest
29
+ * expression in the example scripts nests four.
30
+ */
31
+ const MAX_NESTING = 128;
32
+
33
+ /** The tokens that end a statement. None of them is ever part of one. */
34
+ const STATEMENT_END: ReadonlySet<TokenKind> = new Set<TokenKind>([
35
+ 'newline',
36
+ 'dedent',
37
+ 'endOfFile',
38
+ ]);
39
+
40
+ const LAYOUT: ReadonlySet<TokenKind> = new Set<TokenKind>([
41
+ 'newline',
42
+ 'indent',
43
+ 'dedent',
44
+ 'endOfFile',
45
+ ]);
46
+
47
+ /**
48
+ * What a diagnostic calls a token when it has to quote one.
49
+ *
50
+ * A layout token carries no text, and a message that quoted one would quote
51
+ * nothing at all, so each is named by what a reader would call it.
52
+ */
53
+ export function describeToken(token: Token): string {
54
+ switch (token.kind) {
55
+ case 'newline':
56
+ return 'the end of the line';
57
+ case 'endOfFile':
58
+ return 'the end of the file';
59
+ case 'indent':
60
+ case 'dedent':
61
+ return 'the change of indentation';
62
+ default:
63
+ return token.text;
64
+ }
65
+ }
66
+
67
+ export class Cursor {
68
+ readonly file: SourceFile;
69
+ readonly #tokens: readonly Token[];
70
+ readonly #sink: DiagnosticSink;
71
+
72
+ #at = 0;
73
+ /** Diagnostics reported since this statement began. See `reportedHere`. */
74
+ #reported = 0;
75
+ #expressions = 0;
76
+ #blocks = 0;
77
+ #abandoned = false;
78
+ #firstStatementLine: number | undefined;
79
+ #ifColumn: number | undefined;
80
+ #loops = 0;
81
+
82
+ constructor(file: SourceFile, tokens: readonly Token[], sink: DiagnosticSink) {
83
+ this.file = file;
84
+ this.#tokens = tokens;
85
+ this.#sink = sink;
86
+ }
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // Where it is
90
+ // ---------------------------------------------------------------------------
91
+
92
+ /** An index into the stream, which a loop compares to prove it made progress. */
93
+ get position(): number {
94
+ return this.#at;
95
+ }
96
+
97
+ get token(): Token {
98
+ return this.#tokenAt(this.#at);
99
+ }
100
+
101
+ get kind(): TokenKind {
102
+ return this.token.kind;
103
+ }
104
+
105
+ peek(ahead = 1): Token {
106
+ return this.#tokenAt(this.#at + ahead);
107
+ }
108
+
109
+ at(kind: TokenKind): boolean {
110
+ return this.token.kind === kind;
111
+ }
112
+
113
+ atStatementEnd(): boolean {
114
+ return STATEMENT_END.has(this.token.kind);
115
+ }
116
+
117
+ advance(): Token {
118
+ const token = this.token;
119
+ if (this.#at < this.#tokens.length - 1) this.#at += 1;
120
+ return token;
121
+ }
122
+
123
+ take(kind: TokenKind): Token | undefined {
124
+ return this.at(kind) ? this.advance() : undefined;
125
+ }
126
+
127
+ /** The last token consumed, for a node that has to end where one ended. */
128
+ previous(): Token {
129
+ return this.#tokenAt(this.#at - 1);
130
+ }
131
+
132
+ /**
133
+ * The token before this one, when it is one a reader wrote.
134
+ *
135
+ * OS1022 names the token the statement ran out after, which is the one to the
136
+ * left of the hole. At the start of a line there is no such token, and the
137
+ * caller names the one it is looking at instead.
138
+ */
139
+ previousMeaningful(): Token | undefined {
140
+ const before = this.#tokenAt(this.#at - 1);
141
+ return this.#at > 0 && !LAYOUT.has(before.kind) ? before : undefined;
142
+ }
143
+
144
+ /**
145
+ * What the reader wrote across a span.
146
+ *
147
+ * A diagnostic that has to quote a piece of a program quotes the source
148
+ * rather than rebuilding it from the tree. `a.b` put back together out of a
149
+ * member node is a reconstruction, and one that differs from the page by so
150
+ * much as a space sends the reader looking for something they did not write.
151
+ */
152
+ textOf(span: Span): string {
153
+ return this.file.text.slice(span.offset, endOffset(span));
154
+ }
155
+
156
+ /** An empty span where something should have been written and was not. */
157
+ holeSpan(): Span {
158
+ return this.file.spanAt(this.token.span.offset, 0);
159
+ }
160
+
161
+ #tokenAt(index: number): Token {
162
+ const clamped = Math.min(Math.max(index, 0), this.#tokens.length - 1);
163
+ // The stream always ends with an end of file token, so there is no empty
164
+ // case here to invent a token for.
165
+ return this.#tokens[clamped] as Token;
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // How it reports
170
+ // ---------------------------------------------------------------------------
171
+
172
+ report<Code extends DiagnosticCode>(
173
+ code: Code,
174
+ span: Span,
175
+ values: DiagnosticValues[Code],
176
+ ): void {
177
+ // An abandoned statement has said what it has to say. Everything an
178
+ // unwinding parser would report after it is about the same mistake, and a
179
+ // file that reports two hundred unclosed brackets for one expression that
180
+ // was too deep has told the reader nothing.
181
+ if (this.#abandoned) return;
182
+ this.#reported += 1;
183
+ this.#sink.report(code, span, values);
184
+ }
185
+
186
+ /**
187
+ * Whether this statement has already been reported on.
188
+ *
189
+ * One mistake is one diagnostic. A statement that has been reported on has
190
+ * nothing more to say about its own leftovers, so the second message about
191
+ * the same mistake is suppressed rather than the reader being left to work
192
+ * out which of the two to believe.
193
+ */
194
+ get reportedHere(): boolean {
195
+ return this.#reported > 0;
196
+ }
197
+
198
+ /**
199
+ * A token where the grammar had one exact thing in mind, OS1018.
200
+ *
201
+ * The catalogue has no code for "a closing bracket belongs here", so the one
202
+ * it does have is used for all of them: OS1018 states what is true, that the
203
+ * token was not expected where it sits, and its fix names the two things that
204
+ * are usually wrong, a missing operator or a line that lost its newline.
205
+ */
206
+ unexpected(token: Token): void {
207
+ this.report('OS1018', token.span, { token: describeToken(token) });
208
+ }
209
+
210
+ /**
211
+ * Whether one more level of expression nesting is within the ceiling.
212
+ *
213
+ * Refusing abandons the statement rather than only this level, because a
214
+ * parser that unwound from here would report the brackets it never reached
215
+ * the closers of, one per level, all of them about the expression that was
216
+ * already too deep.
217
+ */
218
+ deeper(span: Span): boolean {
219
+ if (this.#expressions >= MAX_NESTING) {
220
+ this.report('OS5005', span, {
221
+ construct: 'An expression',
222
+ found: this.#expressions + 1,
223
+ max: MAX_NESTING,
224
+ });
225
+ this.#abandoned = true;
226
+ return false;
227
+ }
228
+ this.#expressions += 1;
229
+ return true;
230
+ }
231
+
232
+ shallower(): void {
233
+ this.#expressions -= 1;
234
+ }
235
+
236
+ /**
237
+ * The same ceiling for blocks, counted across statements rather than within
238
+ * one, because a block's depth is the file's shape and not a line's.
239
+ *
240
+ * A refusal here costs the block and not the statement: the caller skips the
241
+ * whole of it, so there is nothing left inside to report on twice.
242
+ */
243
+ openBlock(span: Span): boolean {
244
+ if (this.#blocks >= MAX_NESTING) {
245
+ this.report('OS5005', span, {
246
+ construct: 'A block',
247
+ found: this.#blocks + 1,
248
+ max: MAX_NESTING,
249
+ });
250
+ return false;
251
+ }
252
+ this.#blocks += 1;
253
+ return true;
254
+ }
255
+
256
+ closeBlock(): void {
257
+ this.#blocks -= 1;
258
+ }
259
+
260
+ // ---------------------------------------------------------------------------
261
+ // The file as a whole
262
+ // ---------------------------------------------------------------------------
263
+
264
+ beginStatement(): void {
265
+ this.#reported = 0;
266
+ this.#expressions = 0;
267
+ this.#abandoned = false;
268
+ }
269
+
270
+ /**
271
+ * The first statement in the file, which is the line OS1021 names when a
272
+ * version declaration turns up below it.
273
+ */
274
+ noteStatement(line: number): void {
275
+ this.#firstStatementLine ??= line;
276
+ }
277
+
278
+ get firstStatementLine(): number | undefined {
279
+ return this.#firstStatementLine;
280
+ }
281
+
282
+ /** Where the most recent `if` sat, which is what OS1016 pairs an `else` against. */
283
+ noteIf(column: number): void {
284
+ this.#ifColumn = column;
285
+ }
286
+
287
+ get nearestIfColumn(): number {
288
+ return this.#ifColumn ?? 1;
289
+ }
290
+
291
+ /**
292
+ * Whether a loop body is being read, which is the whole of OS1009.
293
+ *
294
+ * It is a count on the cursor rather than an argument threaded through every
295
+ * rule, because the one question `break` asks is whether any loop is open,
296
+ * and a function body cannot be inside one: functions are top level only.
297
+ */
298
+ enterLoop(): void {
299
+ this.#loops += 1;
300
+ }
301
+
302
+ leaveLoop(): void {
303
+ this.#loops -= 1;
304
+ }
305
+
306
+ get insideLoop(): boolean {
307
+ return this.#loops > 0;
308
+ }
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // How it gets out of trouble
312
+ // ---------------------------------------------------------------------------
313
+
314
+ /** To the end of this line, taking the newline, and no further. */
315
+ skipToEndOfLine(): void {
316
+ const before = this.#at;
317
+ while (!this.atStatementEnd()) this.advance();
318
+ if (this.take('newline') !== undefined) return;
319
+ // A skip that consumed nothing leaves the loop that called it where it
320
+ // began, and a compiler that hangs on a malformed file is worse than one
321
+ // that gives up on it.
322
+ if (this.#at === before && !this.at('endOfFile')) this.advance();
323
+ }
324
+
325
+ /**
326
+ * The whole statement, including a block written under it.
327
+ *
328
+ * This is the boundary to recover at when the line itself was not understood.
329
+ * A header the parser could not read still has its body indented beneath it,
330
+ * and reading that body as though it stood at the outer level would report
331
+ * every line of it.
332
+ */
333
+ skipStatement(): void {
334
+ this.skipToEndOfLine();
335
+ this.skipBlockBody();
336
+ }
337
+
338
+ /** A block, and the blocks inside it, when no statement can hold it. */
339
+ skipBlockBody(): void {
340
+ if (!this.at('indent')) return;
341
+ let depth = 0;
342
+ do {
343
+ if (this.at('indent')) depth += 1;
344
+ else if (this.at('dedent')) depth -= 1;
345
+ this.advance();
346
+ } while (depth > 0 && !this.at('endOfFile'));
347
+ }
348
+ }