@starpak/lang 0.0.0-stage → 0.1.3
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.
- package/LICENSE +22 -0
- package/lib/ast.d.ts +341 -0
- package/lib/ast.d.ts.map +1 -0
- package/lib/ast.js +9 -0
- package/lib/ast.js.map +1 -0
- package/lib/checker.d.ts +17 -0
- package/lib/checker.d.ts.map +1 -0
- package/lib/checker.js +525 -0
- package/lib/checker.js.map +1 -0
- package/lib/diagnostics.d.ts +46 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +77 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/emit/lua.d.ts +19 -0
- package/lib/emit/lua.d.ts.map +1 -0
- package/lib/emit/lua.js +451 -0
- package/lib/emit/lua.js.map +1 -0
- package/lib/emit/ts.d.ts +42 -0
- package/lib/emit/ts.d.ts.map +1 -0
- package/lib/emit/ts.js +950 -0
- package/lib/emit/ts.js.map +1 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +54 -0
- package/lib/index.js.map +1 -0
- package/lib/lexer.d.ts +10 -0
- package/lib/lexer.d.ts.map +1 -0
- package/lib/lexer.js +282 -0
- package/lib/lexer.js.map +1 -0
- package/lib/parser.d.ts +11 -0
- package/lib/parser.d.ts.map +1 -0
- package/lib/parser.js +1138 -0
- package/lib/parser.js.map +1 -0
- package/lib/token.d.ts +32 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +18 -0
- package/lib/token.js.map +1 -0
- package/lib/types.d.ts +63 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +177 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +25 -0
- package/lib/version.d.ts.map +1 -0
- package/lib/version.js +47 -0
- package/lib/version.js.map +1 -0
- package/package.json +10 -3
- package/src/ast.ts +467 -0
- package/src/checker.ts +554 -0
- package/src/diagnostics.ts +100 -0
- package/src/emit/lua.ts +484 -0
- package/src/emit/ts.ts +1024 -0
- package/src/index.ts +24 -0
- package/src/lexer.ts +301 -0
- package/src/parser.ts +1257 -0
- package/src/token.ts +34 -0
- package/src/types.ts +218 -0
- package/src/version.ts +44 -0
- package/README.md +0 -3
package/src/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
VERSION_PATTERN, isValidVersion, parseVersion, versionBanner,
|
|
18
|
+
} from './version'
|
|
19
|
+
export {
|
|
20
|
+
ANY, NIL, NUMBER, STRING, BOOLEAN,
|
|
21
|
+
fromTypeAnn, union, isNil, withoutNil, isAssignable,
|
|
22
|
+
print as printType,
|
|
23
|
+
} from './types'
|
|
24
|
+
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
|
+
}
|