@starpak/lang 0.0.0-stage → 0.1.2

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 (53) hide show
  1. package/LICENSE +22 -0
  2. package/lib/ast.d.ts +334 -0
  3. package/lib/ast.d.ts.map +1 -0
  4. package/lib/ast.js +9 -0
  5. package/lib/ast.js.map +1 -0
  6. package/lib/checker.d.ts +17 -0
  7. package/lib/checker.d.ts.map +1 -0
  8. package/lib/checker.js +525 -0
  9. package/lib/checker.js.map +1 -0
  10. package/lib/diagnostics.d.ts +46 -0
  11. package/lib/diagnostics.d.ts.map +1 -0
  12. package/lib/diagnostics.js +77 -0
  13. package/lib/diagnostics.js.map +1 -0
  14. package/lib/emit/lua.d.ts +19 -0
  15. package/lib/emit/lua.d.ts.map +1 -0
  16. package/lib/emit/lua.js +451 -0
  17. package/lib/emit/lua.js.map +1 -0
  18. package/lib/emit/ts.d.ts +28 -0
  19. package/lib/emit/ts.d.ts.map +1 -0
  20. package/lib/emit/ts.js +803 -0
  21. package/lib/emit/ts.js.map +1 -0
  22. package/lib/index.d.ts +17 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +49 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/lexer.d.ts +10 -0
  27. package/lib/lexer.d.ts.map +1 -0
  28. package/lib/lexer.js +282 -0
  29. package/lib/lexer.js.map +1 -0
  30. package/lib/parser.d.ts +11 -0
  31. package/lib/parser.d.ts.map +1 -0
  32. package/lib/parser.js +1133 -0
  33. package/lib/parser.js.map +1 -0
  34. package/lib/token.d.ts +32 -0
  35. package/lib/token.d.ts.map +1 -0
  36. package/lib/token.js +18 -0
  37. package/lib/token.js.map +1 -0
  38. package/lib/types.d.ts +63 -0
  39. package/lib/types.d.ts.map +1 -0
  40. package/lib/types.js +177 -0
  41. package/lib/types.js.map +1 -0
  42. package/package.json +10 -3
  43. package/src/ast.ts +459 -0
  44. package/src/checker.ts +554 -0
  45. package/src/diagnostics.ts +100 -0
  46. package/src/emit/lua.ts +484 -0
  47. package/src/emit/ts.ts +861 -0
  48. package/src/index.ts +21 -0
  49. package/src/lexer.ts +301 -0
  50. package/src/parser.ts +1251 -0
  51. package/src/token.ts +34 -0
  52. package/src/types.ts +218 -0
  53. package/README.md +0 -3
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Spak Lang - Public API
3
+ */
4
+
5
+ export * from './token'
6
+ export * from './ast'
7
+ export * from './lexer'
8
+ export * from './parser'
9
+ export * from './checker'
10
+ export { CODES, error, warning, countBySeverity, formatDiagnostic } from './diagnostics'
11
+ export type { Diagnostic, DiagnosticCode, Severity } from './diagnostics'
12
+ export { emitTypeScript, typeAnn } from './emit/ts'
13
+ export type { EmitOptions } from './emit/ts'
14
+ export { emitLua } from './emit/lua'
15
+ export type { EmitLuaOptions } from './emit/lua'
16
+ export {
17
+ ANY, NIL, NUMBER, STRING, BOOLEAN,
18
+ fromTypeAnn, union, isNil, withoutNil, isAssignable,
19
+ print as printType,
20
+ } from './types'
21
+ export type { Type, AnyType, NilType, PrimType } from './types'
package/src/lexer.ts ADDED
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Spak Lang - Lexer
3
+ *
4
+ * Converts source text into a flat token stream. Newlines are significant
5
+ * (statement separators) and consecutive blank lines are collapsed into a
6
+ * single NEWLINE token.
7
+ */
8
+
9
+ import { Token, TokenType } from './token'
10
+
11
+ const KEYWORDS = new Set([
12
+ 'and', 'break', 'class', 'const', 'continue', 'do', 'else', 'elseif',
13
+ 'end', 'export', 'false', 'fn', 'for', 'from', 'global', 'if', 'import',
14
+ 'in', 'match', 'nil', 'not', 'or', 'repeat', 'return', 'spawn', 'then',
15
+ 'throw', 'true', 'try', 'catch', 'finally', 'type', 'unless', 'until',
16
+ 'when', 'while', 'async', 'await', 'extends', 'self', 'as', 'default',
17
+ ])
18
+
19
+ /** Longest match wins, so multi-character operators come first. */
20
+ const OPERATORS = [
21
+ '...', '..<', '..', '|>', '?.', '??', '->', '==', '~=', '<=', '>=', '//',
22
+ '+', '-', '*', '/', '%', '^', '#', '<', '>', '=', '(', ')', '{', '}', '[',
23
+ ']', ',', ':', '.', '@', '|', '?',
24
+ ]
25
+
26
+ const ESCAPES: Record<string, string> = {
27
+ n: '\n',
28
+ t: '\t',
29
+ r: '\r',
30
+ '0': '\0',
31
+ '\\': '\\',
32
+ '"': '"',
33
+ "'": "'",
34
+ }
35
+
36
+ function isDigit(ch: string | undefined): boolean {
37
+ return ch !== undefined && ch >= '0' && ch <= '9'
38
+ }
39
+
40
+ function isHexDigit(ch: string | undefined): boolean {
41
+ return ch !== undefined && (
42
+ (ch >= '0' && ch <= '9') ||
43
+ (ch >= 'a' && ch <= 'f') ||
44
+ (ch >= 'A' && ch <= 'F')
45
+ )
46
+ }
47
+
48
+ function isIdentStart(ch: string | undefined): boolean {
49
+ return ch !== undefined && (
50
+ (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_'
51
+ )
52
+ }
53
+
54
+ function isIdentPart(ch: string | undefined): boolean {
55
+ return isIdentStart(ch) || isDigit(ch)
56
+ }
57
+
58
+ function syntaxError(message: string, line: number, column: number): SyntaxError {
59
+ return new SyntaxError(`${message} (line ${line}, column ${column})`)
60
+ }
61
+
62
+ function decodeEscapes(raw: string): string {
63
+ return raw.replace(/\\(.)/g, (match, ch: string) => ESCAPES[ch] ?? ch)
64
+ }
65
+
66
+ export function tokenize(source: string): Token[] {
67
+ const tokens: Token[] = []
68
+ let index = 0
69
+ let line = 1
70
+ let column = 1
71
+
72
+ /** Pending `---` doc lines, attached to the next non-newline token. */
73
+ let pendingDoc: string[] | undefined
74
+ let pendingDocLine = 0
75
+
76
+ function advance(count = 1): string {
77
+ let consumed = ''
78
+ for (let i = 0; i < count && index < source.length; i++) {
79
+ const ch = source[index++]
80
+ consumed += ch
81
+ if (ch === '\n') {
82
+ line++
83
+ column = 1
84
+ } else {
85
+ column++
86
+ }
87
+ }
88
+ return consumed
89
+ }
90
+
91
+ function push(
92
+ type: TokenType,
93
+ value: string,
94
+ start: number,
95
+ startLine: number,
96
+ startColumn: number,
97
+ fstring?: boolean,
98
+ ): void {
99
+ const token: Token = {
100
+ type,
101
+ value,
102
+ line: startLine,
103
+ column: startColumn,
104
+ start,
105
+ end: index,
106
+ }
107
+ if (fstring) token.fstring = true
108
+ if (type !== TokenType.Newline && pendingDoc) {
109
+ token.doc = pendingDoc.join('\n')
110
+ pendingDoc = undefined
111
+ }
112
+ tokens.push(token)
113
+ }
114
+
115
+ function pushNewline(start: number, startLine: number, startColumn: number): void {
116
+ if (!tokens.length) return
117
+ if (tokens[tokens.length - 1].type === TokenType.Newline) return
118
+ push(TokenType.Newline, '\n', start, startLine, startColumn)
119
+ }
120
+
121
+ function scanNumber(): void {
122
+ const start = index
123
+ const startLine = line
124
+ const startColumn = column
125
+
126
+ if (source[index] === '0' && (source[index + 1] === 'x' || source[index + 1] === 'X')) {
127
+ advance(2)
128
+ let digits = 0
129
+ while (isHexDigit(source[index]) || source[index] === '_') {
130
+ if (source[index] !== '_') digits++
131
+ advance()
132
+ }
133
+ if (!digits) throw syntaxError('Invalid hexadecimal number', startLine, startColumn)
134
+ } else if (source[index] === '0' && (source[index + 1] === 'b' || source[index + 1] === 'B')) {
135
+ advance(2)
136
+ let digits = 0
137
+ while (source[index] === '0' || source[index] === '1' || source[index] === '_') {
138
+ if (source[index] !== '_') digits++
139
+ advance()
140
+ }
141
+ if (!digits) throw syntaxError('Invalid binary number', startLine, startColumn)
142
+ } else {
143
+ let digits = 0
144
+ while (isDigit(source[index]) || source[index] === '_') {
145
+ if (source[index] !== '_') digits++
146
+ advance()
147
+ }
148
+ if (source[index] === '.' && source[index + 1] !== '.') {
149
+ if (isDigit(source[index + 1]) || source[index + 1] === undefined) {
150
+ advance()
151
+ while (isDigit(source[index]) || source[index] === '_') {
152
+ if (source[index] !== '_') digits++
153
+ advance()
154
+ }
155
+ }
156
+ }
157
+ if (source[index] === 'e' || source[index] === 'E') {
158
+ let lookahead = index + 1
159
+ if (source[lookahead] === '+' || source[lookahead] === '-') lookahead++
160
+ if (isDigit(source[lookahead])) {
161
+ advance()
162
+ if (source[index] === '+' || source[index] === '-') advance()
163
+ while (isDigit(source[index]) || source[index] === '_') advance()
164
+ }
165
+ }
166
+ if (!digits) throw syntaxError('Invalid number', startLine, startColumn)
167
+ }
168
+
169
+ push(TokenType.Number, source.slice(start, index), start, startLine, startColumn)
170
+ }
171
+
172
+ function scanString(quote: string, fstring: boolean): void {
173
+ const start = index
174
+ const startLine = line
175
+ const startColumn = column
176
+ const multiline = source.startsWith('"""', index)
177
+ advance(multiline ? 3 : 1)
178
+ const closing = multiline ? '"""' : quote
179
+ let raw = ''
180
+
181
+ while (index < source.length) {
182
+ if (source.startsWith(closing, index)) {
183
+ advance(closing.length)
184
+ const value = fstring || multiline ? raw : decodeEscapes(raw)
185
+ push(fstring ? TokenType.FString : TokenType.String, value, start, startLine, startColumn, fstring)
186
+ return
187
+ }
188
+ const ch = source[index]
189
+ if (ch === '\\' && !multiline) {
190
+ raw += ch
191
+ advance()
192
+ if (index < source.length) {
193
+ raw += source[index]
194
+ advance()
195
+ }
196
+ continue
197
+ }
198
+ raw += ch
199
+ advance()
200
+ }
201
+
202
+ throw syntaxError('Unterminated string literal', startLine, startColumn)
203
+ }
204
+
205
+ while (index < source.length) {
206
+ const ch = source[index]
207
+ const start = index
208
+ const startLine = line
209
+ const startColumn = column
210
+
211
+ if (ch === ' ' || ch === '\t' || ch === '\r') {
212
+ advance()
213
+ continue
214
+ }
215
+
216
+ if (ch === '\n') {
217
+ // A blank line breaks a doc comment run.
218
+ if (pendingDoc && pendingDocLine !== startLine) pendingDoc = undefined
219
+ advance()
220
+ pushNewline(start, startLine, startColumn)
221
+ continue
222
+ }
223
+
224
+ if (ch === '-' && source[index + 1] === '-') {
225
+ // Block comment: --[[ ... ]]
226
+ if (source[index + 2] === '[' && source[index + 3] === '[') {
227
+ pendingDoc = undefined
228
+ advance(4)
229
+ while (index < source.length && !(source[index] === ']' && source[index + 1] === ']')) {
230
+ advance()
231
+ }
232
+ if (index >= source.length) {
233
+ throw syntaxError('Unterminated block comment', startLine, startColumn)
234
+ }
235
+ advance(2)
236
+ continue
237
+ }
238
+ // Line comment, including doc comments (--- ...)
239
+ const isDoc = source[index + 2] === '-'
240
+ while (index < source.length && source[index] !== '\n') advance()
241
+ if (isDoc) {
242
+ if (pendingDoc && pendingDocLine !== startLine - 1) pendingDoc = undefined
243
+ if (!pendingDoc) pendingDoc = []
244
+ pendingDoc.push(source.slice(start, index).replace(/^---\s?/, '').trimEnd())
245
+ pendingDocLine = startLine
246
+ } else {
247
+ pendingDoc = undefined
248
+ }
249
+ continue
250
+ }
251
+
252
+ if (ch === ';') {
253
+ advance()
254
+ push(TokenType.Semicolon, ';', start, startLine, startColumn)
255
+ continue
256
+ }
257
+
258
+ if (ch === '"' || ch === "'") {
259
+ scanString(ch, false)
260
+ continue
261
+ }
262
+
263
+ if (ch === 'f' && (source[index + 1] === '"' || source[index + 1] === "'")) {
264
+ advance()
265
+ scanString(source[index], true)
266
+ continue
267
+ }
268
+
269
+ if (isDigit(ch) || (ch === '.' && isDigit(source[index + 1]))) {
270
+ scanNumber()
271
+ continue
272
+ }
273
+
274
+ if (isIdentStart(ch)) {
275
+ while (isIdentPart(source[index])) advance()
276
+ const value = source.slice(start, index)
277
+ push(KEYWORDS.has(value) ? TokenType.Keyword : TokenType.Identifier, value, start, startLine, startColumn)
278
+ continue
279
+ }
280
+
281
+ const operator = OPERATORS.find((candidate) => source.startsWith(candidate, index))
282
+ if (operator) {
283
+ advance(operator.length)
284
+ push(TokenType.Operator, operator, start, startLine, startColumn)
285
+ continue
286
+ }
287
+
288
+ throw syntaxError(`Unexpected character "${ch}"`, line, column)
289
+ }
290
+
291
+ tokens.push({
292
+ type: TokenType.EOF,
293
+ value: '',
294
+ line,
295
+ column,
296
+ start: index,
297
+ end: index,
298
+ })
299
+
300
+ return tokens
301
+ }