@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.
- package/LICENSE +22 -0
- package/lib/ast.d.ts +334 -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 +28 -0
- package/lib/emit/ts.d.ts.map +1 -0
- package/lib/emit/ts.js +803 -0
- package/lib/emit/ts.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -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 +1133 -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/package.json +10 -3
- package/src/ast.ts +459 -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 +861 -0
- package/src/index.ts +21 -0
- package/src/lexer.ts +301 -0
- package/src/parser.ts +1251 -0
- package/src/token.ts +34 -0
- package/src/types.ts +218 -0
- package/README.md +0 -3
package/src/checker.ts
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Lang - Static checker (M2)
|
|
3
|
+
*
|
|
4
|
+
* Walks the AST and reports diagnostics from `diagnostics.ts`.
|
|
5
|
+
* Types are erased after checking; nothing here affects code generation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Assign,
|
|
10
|
+
Block,
|
|
11
|
+
ClassDecl,
|
|
12
|
+
Expr,
|
|
13
|
+
FnDecl,
|
|
14
|
+
MatchStmt,
|
|
15
|
+
Pattern,
|
|
16
|
+
Program,
|
|
17
|
+
Statement,
|
|
18
|
+
TablePattern,
|
|
19
|
+
TypeAliasDecl,
|
|
20
|
+
TypeAnn,
|
|
21
|
+
VarDecl,
|
|
22
|
+
} from './ast'
|
|
23
|
+
import { CODES, error, warning, type Diagnostic } from './diagnostics'
|
|
24
|
+
import {
|
|
25
|
+
ANY, BOOLEAN, NIL, NUMBER, STRING,
|
|
26
|
+
fromTypeAnn, isAssignable, print, union, withoutNil,
|
|
27
|
+
type Type,
|
|
28
|
+
} from './types'
|
|
29
|
+
|
|
30
|
+
export interface CheckOptions {
|
|
31
|
+
/** Report implicit `any` as an error instead of a warning. */
|
|
32
|
+
strictAny?: boolean
|
|
33
|
+
/** Emit W001 for unused locals. Default true. */
|
|
34
|
+
reportUnused?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface Scope {
|
|
38
|
+
parent?: Scope
|
|
39
|
+
vars: Map<string, { type: Type, decl: VarDecl | FnDecl | ClassDecl, used: boolean }>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface FnContext {
|
|
43
|
+
returnType: Type | null
|
|
44
|
+
name: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const GLOBAL_SCOPE: Scope = { vars: new Map() }
|
|
48
|
+
|
|
49
|
+
function newScope(parent: Scope): Scope {
|
|
50
|
+
return { parent, vars: new Map() }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function lookup(scope: Scope, name: string) {
|
|
54
|
+
let current: Scope | undefined = scope
|
|
55
|
+
while (current) {
|
|
56
|
+
const found = current.vars.get(name)
|
|
57
|
+
if (found) return found
|
|
58
|
+
current = current.parent
|
|
59
|
+
}
|
|
60
|
+
return undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function declare(scope: Scope, name: string, type: Type, decl: VarDecl | FnDecl | ClassDecl) {
|
|
64
|
+
scope.vars.set(name, { type, decl, used: false })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function check(program: Program, options: CheckOptions = {}): Diagnostic[] {
|
|
68
|
+
const diagnostics: Diagnostic[] = []
|
|
69
|
+
const aliases = new Map<string, Type>()
|
|
70
|
+
|
|
71
|
+
// Pass 1: type aliases, so later annotations can resolve them.
|
|
72
|
+
for (const statement of program.body) {
|
|
73
|
+
const decl = unwrapExport(statement)
|
|
74
|
+
if (decl.kind === 'TypeAliasDecl') aliases.set(decl.name, fromTypeAnn(decl.type))
|
|
75
|
+
}
|
|
76
|
+
for (const [name, type] of aliases) {
|
|
77
|
+
if (type.kind === 'named' && type.name === name) {
|
|
78
|
+
diagnostics.push(error('E006', `类型别名「${name}」循环引用`, program.loc))
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolveAnn = (ann: TypeAnn): Type => {
|
|
83
|
+
const type = fromTypeAnn(ann)
|
|
84
|
+
return resolveNamed(type, aliases)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const push = (diag: Diagnostic) => diagnostics.push(diag)
|
|
88
|
+
|
|
89
|
+
const checkCall = (
|
|
90
|
+
node: { loc: { line: number, column: number } },
|
|
91
|
+
argNodes: Expr[],
|
|
92
|
+
callee: Type,
|
|
93
|
+
args: Type[],
|
|
94
|
+
offset = 0,
|
|
95
|
+
): Type => {
|
|
96
|
+
if (callee.kind !== 'fn') return ANY
|
|
97
|
+
const hasSpread = argNodes.some(node => node.kind === 'Spread')
|
|
98
|
+
if (!hasSpread && args.length !== callee.params.length) {
|
|
99
|
+
push(error('E004', `需要 ${callee.params.length} 个参数,实际 ${args.length} 个`, node.loc))
|
|
100
|
+
return callee.returns
|
|
101
|
+
}
|
|
102
|
+
if (hasSpread) return callee.returns
|
|
103
|
+
callee.params.forEach((param, index) => {
|
|
104
|
+
if (!isAssignable(param, args[index])) {
|
|
105
|
+
const at = argNodes[index - offset]?.loc ?? node.loc
|
|
106
|
+
push(error('E001', `第 ${index + 1} 个参数应为 ${print(param)},得到 ${print(args[index])}`, at))
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
return callee.returns
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const checkTypeKnown = (ann: TypeAnn, loc = ann.loc) => {
|
|
113
|
+
if (ann.kind === 'named') {
|
|
114
|
+
const known = ['any', 'nil', 'number', 'string', 'boolean', 'bool']
|
|
115
|
+
if (!known.includes(ann.name) && !aliases.has(ann.name)) {
|
|
116
|
+
push(error('E003', `未知类型名「${ann.name}」`, loc))
|
|
117
|
+
}
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
if (ann.kind === 'array') checkTypeKnown(ann.element, ann.element.loc)
|
|
121
|
+
if (ann.kind === 'dict') {
|
|
122
|
+
checkTypeKnown(ann.key, ann.key.loc)
|
|
123
|
+
checkTypeKnown(ann.value, ann.value.loc)
|
|
124
|
+
}
|
|
125
|
+
if (ann.kind === 'record') for (const field of ann.fields) checkTypeKnown(field.type, field.type.loc)
|
|
126
|
+
if (ann.kind === 'union') for (const item of ann.types) checkTypeKnown(item, item.loc)
|
|
127
|
+
if (ann.kind === 'fn') {
|
|
128
|
+
for (const item of ann.params) checkTypeKnown(item, item.loc)
|
|
129
|
+
checkTypeKnown(ann.returns, ann.returns.loc)
|
|
130
|
+
}
|
|
131
|
+
if (ann.kind === 'optional') checkTypeKnown(ann.type, ann.type.loc)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const checkExpr = (expr: Expr, scope: Scope, fn: FnContext | null, narrow?: Map<string, Type>): Type => {
|
|
135
|
+
switch (expr.kind) {
|
|
136
|
+
case 'NumberLit':
|
|
137
|
+
return NUMBER
|
|
138
|
+
case 'StringLit':
|
|
139
|
+
if (expr.parts) for (const part of expr.parts) if (typeof part !== 'string') checkExpr(part, scope, fn, narrow)
|
|
140
|
+
return STRING
|
|
141
|
+
case 'BoolLit':
|
|
142
|
+
return BOOLEAN
|
|
143
|
+
case 'NilLit':
|
|
144
|
+
return NIL
|
|
145
|
+
case 'Ident': {
|
|
146
|
+
if (narrow?.has(expr.name)) return narrow.get(expr.name)!
|
|
147
|
+
const found = lookup(scope, expr.name)
|
|
148
|
+
if (!found) return ANY
|
|
149
|
+
found.used = true
|
|
150
|
+
return found.type
|
|
151
|
+
}
|
|
152
|
+
case 'SelfExpr':
|
|
153
|
+
return ANY
|
|
154
|
+
case 'Member': {
|
|
155
|
+
const object = checkExpr(expr.object, scope, fn, narrow)
|
|
156
|
+
if (object.kind === 'record') {
|
|
157
|
+
const field = object.fields.get(expr.property)
|
|
158
|
+
if (!field) {
|
|
159
|
+
push(error('E007', `记录类型没有字段「${expr.property}」`, expr.loc))
|
|
160
|
+
return ANY
|
|
161
|
+
}
|
|
162
|
+
return expr.optional ? union([field, NIL]) : field
|
|
163
|
+
}
|
|
164
|
+
return expr.optional ? ANY : ANY
|
|
165
|
+
}
|
|
166
|
+
case 'Index': {
|
|
167
|
+
const object = checkExpr(expr.object, scope, fn, narrow)
|
|
168
|
+
const index = checkExpr(expr.index, scope, fn, narrow)
|
|
169
|
+
if (object.kind === 'array') {
|
|
170
|
+
if (expr.isRange) return object
|
|
171
|
+
if (!isAssignable(NUMBER, index)) {
|
|
172
|
+
push(error('E001', `数组下标应为 number,得到 ${print(index)}`, expr.index.loc))
|
|
173
|
+
}
|
|
174
|
+
return object.element
|
|
175
|
+
}
|
|
176
|
+
if (object.kind === 'dict') {
|
|
177
|
+
if (!isAssignable(object.key, index)) {
|
|
178
|
+
push(error('E001', `字典键应为 ${print(object.key)},得到 ${print(index)}`, expr.index.loc))
|
|
179
|
+
}
|
|
180
|
+
return object.value
|
|
181
|
+
}
|
|
182
|
+
return ANY
|
|
183
|
+
}
|
|
184
|
+
case 'Call': {
|
|
185
|
+
const callee = checkExpr(expr.callee, scope, fn, narrow)
|
|
186
|
+
const args = expr.args.map(arg => checkExpr(arg, scope, fn, narrow))
|
|
187
|
+
return checkCall(expr, expr.args, callee, args)
|
|
188
|
+
}
|
|
189
|
+
case 'Binary': {
|
|
190
|
+
if (expr.op === '|>') {
|
|
191
|
+
const left = checkExpr(expr.left, scope, fn, narrow)
|
|
192
|
+
if (expr.right.kind !== 'Call') {
|
|
193
|
+
checkExpr(expr.right, scope, fn, narrow)
|
|
194
|
+
return ANY
|
|
195
|
+
}
|
|
196
|
+
const callee = checkExpr(expr.right.callee, scope, fn, narrow)
|
|
197
|
+
const args = [left, ...expr.right.args.map(arg => checkExpr(arg, scope, fn, narrow))]
|
|
198
|
+
return checkCall(expr.right, expr.right.args, callee, args, 1)
|
|
199
|
+
}
|
|
200
|
+
const left = checkExpr(expr.left, scope, fn, narrow)
|
|
201
|
+
const right = checkExpr(expr.right, scope, fn, narrow)
|
|
202
|
+
return checkBinary(expr.op, left, right, expr, push)
|
|
203
|
+
}
|
|
204
|
+
case 'Unary': {
|
|
205
|
+
const argument = checkExpr(expr.argument, scope, fn, narrow)
|
|
206
|
+
if (expr.op === '-' && !isNumeric(argument)) {
|
|
207
|
+
push(error('E001', `一元 - 只能用于 number,得到 ${print(argument)}`, expr.argument.loc))
|
|
208
|
+
}
|
|
209
|
+
if (expr.op === 'not') return BOOLEAN
|
|
210
|
+
if (expr.op === '#') return NUMBER
|
|
211
|
+
return argument
|
|
212
|
+
}
|
|
213
|
+
case 'Table': {
|
|
214
|
+
const values: Type[] = []
|
|
215
|
+
const fields = new Map<string, Type>()
|
|
216
|
+
let positional = true
|
|
217
|
+
for (const field of expr.fields) {
|
|
218
|
+
const value = checkExpr(field.value, scope, fn, narrow)
|
|
219
|
+
values.push(value)
|
|
220
|
+
if (field.key?.kind === 'Ident') {
|
|
221
|
+
positional = false
|
|
222
|
+
fields.set(field.key.name, value)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (positional) {
|
|
226
|
+
return { kind: 'array', element: values.length ? union(values) : ANY }
|
|
227
|
+
}
|
|
228
|
+
return { kind: 'record', fields }
|
|
229
|
+
}
|
|
230
|
+
case 'FunctionExpr': {
|
|
231
|
+
const inner = newScope(scope)
|
|
232
|
+
for (const param of expr.params) {
|
|
233
|
+
const type = param.type ? resolveAnn(param.type) : ANY
|
|
234
|
+
if (param.type) checkTypeKnown(param.type)
|
|
235
|
+
declare(inner, param.name, type, { kind: 'FnDecl' } as FnDecl)
|
|
236
|
+
}
|
|
237
|
+
const returns = expr.exprBody
|
|
238
|
+
? checkExpr(expr.exprBody, inner, fn, narrow)
|
|
239
|
+
: checkBlock(expr.body, inner, { returnType: null, name: 'fn' }, narrow)
|
|
240
|
+
return { kind: 'fn', params: expr.params.map(p => (p.type ? resolveAnn(p.type) : ANY)), returns }
|
|
241
|
+
}
|
|
242
|
+
case 'Range': {
|
|
243
|
+
checkExpr(expr.from, scope, fn, narrow)
|
|
244
|
+
checkExpr(expr.to, scope, fn, narrow)
|
|
245
|
+
return ANY
|
|
246
|
+
}
|
|
247
|
+
case 'AwaitExpr':
|
|
248
|
+
return checkExpr(expr.argument, scope, fn, narrow)
|
|
249
|
+
case 'Spread':
|
|
250
|
+
return expr.argument ? checkExpr(expr.argument, scope, fn, narrow) : ANY
|
|
251
|
+
default:
|
|
252
|
+
return ANY
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const checkBlock = (block: Block | undefined, scope: Scope, fn: FnContext | null, narrow?: Map<string, Type>): Type => {
|
|
257
|
+
if (!block) return NIL
|
|
258
|
+
const returns: Type[] = []
|
|
259
|
+
for (const statement of block.body) {
|
|
260
|
+
const type = checkStatement(statement, scope, fn, narrow)
|
|
261
|
+
if (statement.kind === 'ReturnStmt') returns.push(type)
|
|
262
|
+
}
|
|
263
|
+
return returns.length ? union(returns) : NIL
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const bindPattern = (pattern: Pattern, scope: Scope) => {
|
|
267
|
+
if (pattern.kind === 'TablePattern') {
|
|
268
|
+
for (const entry of pattern.entries) {
|
|
269
|
+
declare(scope, entry.target ?? entry.key, ANY, { kind: 'VarDecl' } as VarDecl)
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
for (const name of pattern.elements) {
|
|
274
|
+
declare(scope, name, ANY, { kind: 'VarDecl' } as VarDecl)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const checkAssign = (statement: Assign, scope: Scope, fn: FnContext | null, narrow?: Map<string, Type>) => {
|
|
279
|
+
const values = statement.values.map(value => checkExpr(value, scope, fn, narrow))
|
|
280
|
+
statement.targets.forEach((target, index) => {
|
|
281
|
+
const valueType = values[index] ?? ANY
|
|
282
|
+
if (isPattern(target)) {
|
|
283
|
+
bindPattern(target, scope)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
if (target.kind === 'Ident') {
|
|
287
|
+
const found = lookup(scope, target.name)
|
|
288
|
+
if (found) {
|
|
289
|
+
if (!isAssignable(found.type, valueType)) {
|
|
290
|
+
push(error('E001', `变量「${target.name}」是 ${print(found.type)},不能赋 ${print(valueType)}`, target.loc))
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
declare(scope, target.name, valueType, { kind: 'VarDecl' } as VarDecl)
|
|
294
|
+
}
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
checkExpr(target, scope, fn, narrow)
|
|
298
|
+
})
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const checkVarDecl = (statement: VarDecl, scope: Scope, narrow?: Map<string, Type>) => {
|
|
302
|
+
statement.names.forEach((entry, index) => {
|
|
303
|
+
if (entry.type) checkTypeKnown(entry.type)
|
|
304
|
+
const annotated = entry.type ? resolveAnn(entry.type) : null
|
|
305
|
+
const value = statement.values[index]
|
|
306
|
+
if (value) {
|
|
307
|
+
const valueType = checkExpr(value, scope, null, narrow)
|
|
308
|
+
if (annotated && !isAssignable(annotated, valueType)) {
|
|
309
|
+
push(error('E001', `「${entry.name}」标注为 ${print(annotated)},赋值是 ${print(valueType)}`, value.loc))
|
|
310
|
+
}
|
|
311
|
+
declare(scope, entry.name, annotated ?? valueType, statement)
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
if (annotated) {
|
|
315
|
+
declare(scope, entry.name, annotated, statement)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
declare(scope, entry.name, ANY, statement)
|
|
319
|
+
if (options.strictAny) {
|
|
320
|
+
push(error('W003', `「${entry.name}」是隐式 any`, statement.loc))
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const checkFn = (statement: FnDecl, scope: Scope) => {
|
|
326
|
+
for (const param of statement.params) if (param.type) checkTypeKnown(param.type)
|
|
327
|
+
const params = statement.params.map(param => (param.type ? resolveAnn(param.type) : ANY))
|
|
328
|
+
const declaredReturn = statement.returnType ? resolveAnn(statement.returnType) : null
|
|
329
|
+
if (statement.returnType) checkTypeKnown(statement.returnType)
|
|
330
|
+
|
|
331
|
+
const inner = newScope(scope)
|
|
332
|
+
statement.params.forEach((param, index) => declare(inner, param.name, params[index], statement))
|
|
333
|
+
|
|
334
|
+
const context: FnContext = { returnType: declaredReturn, name: statement.name }
|
|
335
|
+
if (statement.exprBody) {
|
|
336
|
+
const actual = checkExpr(statement.exprBody, inner, context)
|
|
337
|
+
if (declaredReturn && !isAssignable(declaredReturn, actual)) {
|
|
338
|
+
push(error('E005', `「${statement.name}」应返回 ${print(declaredReturn)},实际 ${print(actual)}`, statement.exprBody.loc))
|
|
339
|
+
}
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const actual = checkBlock(statement.body, inner, context)
|
|
344
|
+
if (declaredReturn && !isAssignable(declaredReturn, actual) && statement.body?.body.some(s => s.kind === 'ReturnStmt')) {
|
|
345
|
+
push(error('E005', `「${statement.name}」应返回 ${print(declaredReturn)},实际 ${print(actual)}`, statement.loc))
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const checkClass = (statement: ClassDecl, scope: Scope) => {
|
|
350
|
+
const inner = newScope(scope)
|
|
351
|
+
for (const member of statement.members) {
|
|
352
|
+
if (member.kind === 'FnDecl') {
|
|
353
|
+
const params = member.params.map(param => (param.type ? resolveAnn(param.type) : ANY))
|
|
354
|
+
declare(inner, member.name, { kind: 'fn', params, returns: member.returnType ? resolveAnn(member.returnType) : ANY }, member)
|
|
355
|
+
checkFn(member, inner)
|
|
356
|
+
} else {
|
|
357
|
+
checkVarDecl(member, inner)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
declare(scope, statement.name, { kind: 'named', name: statement.name }, statement)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const checkMatch = (statement: MatchStmt, scope: Scope, fn: FnContext | null) => {
|
|
364
|
+
checkExpr(statement.subject, scope, fn)
|
|
365
|
+
for (const arm of statement.arms) {
|
|
366
|
+
const inner = newScope(scope)
|
|
367
|
+
for (const pattern of arm.patterns) checkExpr(pattern, inner, fn)
|
|
368
|
+
if (arm.guard) checkExpr(arm.guard, inner, fn)
|
|
369
|
+
checkBlock(arm.body, inner, fn)
|
|
370
|
+
}
|
|
371
|
+
if (statement.elseBody) checkBlock(statement.elseBody, scope, fn)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const checkStatement = (statement: Statement, scope: Scope, fn: FnContext | null, narrow?: Map<string, Type>): Type => {
|
|
375
|
+
switch (statement.kind) {
|
|
376
|
+
case 'VarDecl':
|
|
377
|
+
checkVarDecl(statement, scope, narrow)
|
|
378
|
+
return NIL
|
|
379
|
+
case 'Assign':
|
|
380
|
+
checkAssign(statement, scope, fn, narrow)
|
|
381
|
+
return NIL
|
|
382
|
+
case 'FnDecl':
|
|
383
|
+
checkFn(statement, scope)
|
|
384
|
+
declare(scope, statement.name, { kind: 'fn', params: statement.params.map(p => (p.type ? resolveAnn(p.type) : ANY)), returns: statement.returnType ? resolveAnn(statement.returnType) : ANY }, statement)
|
|
385
|
+
return NIL
|
|
386
|
+
case 'ClassDecl':
|
|
387
|
+
checkClass(statement, scope)
|
|
388
|
+
return NIL
|
|
389
|
+
case 'IfStmt': {
|
|
390
|
+
const condType = checkExpr(statement.cond, scope, fn, narrow)
|
|
391
|
+
const narrowed = narrowCond(statement.cond, condType, scope, narrow, true)
|
|
392
|
+
checkBlock(statement.then, newScope(scope), fn, narrowed)
|
|
393
|
+
for (const branch of statement.elifs) {
|
|
394
|
+
checkExpr(branch.cond, scope, fn, narrow)
|
|
395
|
+
checkBlock(branch.body, newScope(scope), fn, narrowed)
|
|
396
|
+
}
|
|
397
|
+
if (statement.else) checkBlock(statement.else, newScope(scope), fn, narrow)
|
|
398
|
+
return NIL
|
|
399
|
+
}
|
|
400
|
+
case 'UnlessStmt':
|
|
401
|
+
checkExpr(statement.cond, scope, fn, narrow)
|
|
402
|
+
checkBlock(statement.body, newScope(scope), fn, narrow)
|
|
403
|
+
return NIL
|
|
404
|
+
case 'WhileStmt':
|
|
405
|
+
checkExpr(statement.cond, scope, fn, narrow)
|
|
406
|
+
checkBlock(statement.body, newScope(scope), fn, narrow)
|
|
407
|
+
return NIL
|
|
408
|
+
case 'RepeatStmt':
|
|
409
|
+
checkBlock(statement.body, newScope(scope), fn, narrow)
|
|
410
|
+
checkExpr(statement.cond, scope, fn, narrow)
|
|
411
|
+
return NIL
|
|
412
|
+
case 'ForNumeric': {
|
|
413
|
+
const inner = newScope(scope)
|
|
414
|
+
declare(inner, statement.varName, NUMBER, { kind: 'VarDecl' } as VarDecl)
|
|
415
|
+
checkExpr(statement.from, scope, fn, narrow)
|
|
416
|
+
checkExpr(statement.to, scope, fn, narrow)
|
|
417
|
+
if (statement.step) checkExpr(statement.step, scope, fn, narrow)
|
|
418
|
+
checkBlock(statement.body, inner, fn, narrow)
|
|
419
|
+
return NIL
|
|
420
|
+
}
|
|
421
|
+
case 'ForIn': {
|
|
422
|
+
const inner = newScope(scope)
|
|
423
|
+
const iter = checkExpr(statement.iter, scope, fn, narrow)
|
|
424
|
+
const element = iter.kind === 'array' ? iter.element : ANY
|
|
425
|
+
statement.vars.forEach((name, index) => declare(inner, name, index === 0 ? element : ANY, { kind: 'VarDecl' } as VarDecl))
|
|
426
|
+
checkBlock(statement.body, inner, fn, narrow)
|
|
427
|
+
return NIL
|
|
428
|
+
}
|
|
429
|
+
case 'ReturnStmt': {
|
|
430
|
+
const values = statement.values.map(value => checkExpr(value, scope, fn, narrow))
|
|
431
|
+
const type = values.length === 0 ? NIL : values.length === 1 ? values[0] : union(values)
|
|
432
|
+
if (fn?.returnType && !isAssignable(fn.returnType, type)) {
|
|
433
|
+
push(error('E005', `「${fn.name}」应返回 ${print(fn.returnType)},实际 ${print(type)}`, statement.loc))
|
|
434
|
+
}
|
|
435
|
+
return type
|
|
436
|
+
}
|
|
437
|
+
case 'ThrowStmt':
|
|
438
|
+
case 'SpawnStmt':
|
|
439
|
+
checkExpr(statement.value, scope, fn, narrow)
|
|
440
|
+
return NIL
|
|
441
|
+
case 'MatchStmt':
|
|
442
|
+
checkMatch(statement, scope, fn)
|
|
443
|
+
return NIL
|
|
444
|
+
case 'TryStmt': {
|
|
445
|
+
checkBlock(statement.body, newScope(scope), fn, narrow)
|
|
446
|
+
const catchScope = newScope(scope)
|
|
447
|
+
if (statement.catchVar) declare(catchScope, statement.catchVar, statement.catchType ? resolveAnn(statement.catchType) : ANY, { kind: 'VarDecl' } as VarDecl)
|
|
448
|
+
checkBlock(statement.catchBody, catchScope, fn, narrow)
|
|
449
|
+
checkBlock(statement.finallyBody, newScope(scope), fn, narrow)
|
|
450
|
+
return NIL
|
|
451
|
+
}
|
|
452
|
+
case 'ExportDecl':
|
|
453
|
+
return checkStatement(statement.declaration, scope, fn, narrow)
|
|
454
|
+
case 'ExportDefault':
|
|
455
|
+
checkExpr(statement.expr, scope, fn, narrow)
|
|
456
|
+
return NIL
|
|
457
|
+
case 'TypeAliasDecl':
|
|
458
|
+
checkTypeKnown(statement.type)
|
|
459
|
+
return NIL
|
|
460
|
+
case 'ImportDecl':
|
|
461
|
+
for (const clause of statement.clauses) declare(scope, clause.alias ?? clause.name, ANY, { kind: 'VarDecl' } as VarDecl)
|
|
462
|
+
return NIL
|
|
463
|
+
case 'ExprStmt':
|
|
464
|
+
checkExpr(statement.expr, scope, fn, narrow)
|
|
465
|
+
return NIL
|
|
466
|
+
case 'BreakStmt':
|
|
467
|
+
case 'ContinueStmt':
|
|
468
|
+
return NIL
|
|
469
|
+
default:
|
|
470
|
+
return NIL
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const block = newScope(GLOBAL_SCOPE)
|
|
475
|
+
for (const statement of program.body) checkStatement(statement, block, null)
|
|
476
|
+
|
|
477
|
+
if (options.reportUnused !== false) {
|
|
478
|
+
const walkUnused = (scope: Scope) => {
|
|
479
|
+
for (const [name, entry] of scope.vars) {
|
|
480
|
+
if (!entry.used && !name.startsWith('_') && entry.decl.kind !== 'FnDecl') {
|
|
481
|
+
diagnostics.push(warning('W001', `声明「${name}」未使用`, entry.decl.loc))
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
walkUnused(block)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return diagnostics
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function unwrapExport(statement: Statement): Statement {
|
|
492
|
+
return statement.kind === 'ExportDecl' ? statement.declaration : statement
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function isPattern(target: Expr | Pattern): target is Pattern {
|
|
496
|
+
return target.kind === 'TablePattern' || target.kind === 'ArrayPattern'
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function isNumeric(type: Type): boolean {
|
|
500
|
+
return type.kind === 'any' || (type.kind === 'prim' && type.name === 'number')
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function checkBinary(op: string, left: Type, right: Type, expr: Expr, push: (diag: Diagnostic) => void): Type {
|
|
504
|
+
if (op === 'and' || op === 'or') return union([left, right])
|
|
505
|
+
if (op === '==' || op === '~=' || op === '<' || op === '<=' || op === '>' || op === '>=') return BOOLEAN
|
|
506
|
+
if (op === '..') return STRING
|
|
507
|
+
if (op === '|>') return ANY
|
|
508
|
+
if (op === '??') return union([withoutNil(left), right])
|
|
509
|
+
if (op === '+' && (left.kind === 'prim' && left.name === 'string' || right.kind === 'prim' && right.name === 'string')) {
|
|
510
|
+
if (!(left.kind === 'prim' && left.name === 'string') || !(right.kind === 'prim' && right.name === 'string')) {
|
|
511
|
+
push(error('E001', `${print(left)} 与 ${print(right)} 不能相加`, expr.loc, '用 tostring(x) 或 f"{x}" 转成字符串'))
|
|
512
|
+
}
|
|
513
|
+
return STRING
|
|
514
|
+
}
|
|
515
|
+
if (!isNumeric(left) || !isNumeric(right)) {
|
|
516
|
+
push(error('E001', `${print(left)} 与 ${print(right)} 不能做 ${op} 运算`, expr.loc))
|
|
517
|
+
return ANY
|
|
518
|
+
}
|
|
519
|
+
return NUMBER
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function narrowCond(
|
|
523
|
+
cond: Expr,
|
|
524
|
+
condType: Type,
|
|
525
|
+
scope: Scope,
|
|
526
|
+
narrow: Map<string, Type> | undefined,
|
|
527
|
+
positive: boolean,
|
|
528
|
+
): Map<string, Type> | undefined {
|
|
529
|
+
if (positive && cond.kind === 'Ident') {
|
|
530
|
+
const next = new Map(narrow ?? [])
|
|
531
|
+
next.set(cond.name, withoutNil(lookup(scope, cond.name)?.type ?? condType))
|
|
532
|
+
return next
|
|
533
|
+
}
|
|
534
|
+
return narrow
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function resolveNamed(type: Type, aliases: Map<string, Type>): Type {
|
|
538
|
+
if (type.kind === 'named') {
|
|
539
|
+
const found = aliases.get(type.name)
|
|
540
|
+
if (found) return { kind: 'named', name: type.name, resolved: found }
|
|
541
|
+
}
|
|
542
|
+
if (type.kind === 'array') return { kind: 'array', element: resolveNamed(type.element, aliases) }
|
|
543
|
+
if (type.kind === 'dict') return { kind: 'dict', key: resolveNamed(type.key, aliases), value: resolveNamed(type.value, aliases) }
|
|
544
|
+
if (type.kind === 'record') {
|
|
545
|
+
const fields = new Map<string, Type>()
|
|
546
|
+
for (const [name, value] of type.fields) fields.set(name, resolveNamed(value, aliases))
|
|
547
|
+
return { kind: 'record', fields }
|
|
548
|
+
}
|
|
549
|
+
if (type.kind === 'fn') return { kind: 'fn', params: type.params.map(p => resolveNamed(p, aliases)), returns: resolveNamed(type.returns, aliases) }
|
|
550
|
+
if (type.kind === 'union') return union(type.types.map(item => resolveNamed(item, aliases)))
|
|
551
|
+
return type
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export { CODES }
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Lang - Diagnostics (M2)
|
|
3
|
+
*
|
|
4
|
+
* Codes and formatting follow `docs/syntax/typecheck.md` §4.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Position } from './ast'
|
|
8
|
+
|
|
9
|
+
export type Severity = 'error' | 'warning'
|
|
10
|
+
|
|
11
|
+
export const CODES = {
|
|
12
|
+
E001: '类型不匹配',
|
|
13
|
+
E002: '可能为 nil 的值未收窄',
|
|
14
|
+
E003: '未知类型名',
|
|
15
|
+
E004: '参数数量或类型不符',
|
|
16
|
+
E005: '返回值与注解不符',
|
|
17
|
+
E006: '重复声明或类型别名循环',
|
|
18
|
+
E007: '访问不存在的字段',
|
|
19
|
+
W001: '声明未使用',
|
|
20
|
+
W002: '分支不可达',
|
|
21
|
+
W003: '隐式 any',
|
|
22
|
+
} as const
|
|
23
|
+
|
|
24
|
+
export type DiagnosticCode = keyof typeof CODES
|
|
25
|
+
|
|
26
|
+
export interface Diagnostic {
|
|
27
|
+
code: DiagnosticCode
|
|
28
|
+
severity: Severity
|
|
29
|
+
message: string
|
|
30
|
+
loc: Position
|
|
31
|
+
/** Optional end column on the same line, for caret width. */
|
|
32
|
+
endColumn?: number
|
|
33
|
+
hint?: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function error(code: DiagnosticCode, message: string, loc: Position, hint?: string): Diagnostic {
|
|
37
|
+
return { code, severity: 'error', message, loc, hint }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function warning(code: DiagnosticCode, message: string, loc: Position, hint?: string): Diagnostic {
|
|
41
|
+
return { code, severity: 'warning', message, loc, hint }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface FormatOptions {
|
|
45
|
+
/** File name shown in the header. */
|
|
46
|
+
file?: string
|
|
47
|
+
/** Source text, needed to render the offending line. */
|
|
48
|
+
source?: string
|
|
49
|
+
/** Use ANSI colors (default: auto-detect TTY). */
|
|
50
|
+
color?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const COLOR = {
|
|
54
|
+
reset: '\u001b[0m',
|
|
55
|
+
red: '\u001b[31m',
|
|
56
|
+
yellow: '\u001b[33m',
|
|
57
|
+
dim: '\u001b[2m',
|
|
58
|
+
cyan: '\u001b[36m',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function paint(text: string, code: string, enabled: boolean): string {
|
|
62
|
+
return enabled ? `${code}${text}${COLOR.reset}` : text
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Render one diagnostic in the compiler-style layout. */
|
|
66
|
+
export function formatDiagnostic(diag: Diagnostic, options: FormatOptions = {}): string {
|
|
67
|
+
const color = options.color ?? Boolean(process.stdout.isTTY)
|
|
68
|
+
const file = options.file ?? '<input>'
|
|
69
|
+
const label = diag.severity === 'error'
|
|
70
|
+
? paint('error', COLOR.red, color)
|
|
71
|
+
: paint('warning', COLOR.yellow, color)
|
|
72
|
+
const head = `${label}${paint(`[${diag.code}]`, COLOR.dim, color)} ${diag.message}`
|
|
73
|
+
const where = ` ${paint('-->', COLOR.cyan, color)} ${file}:${diag.loc.line}:${diag.loc.column}`
|
|
74
|
+
const lines = [head, where]
|
|
75
|
+
|
|
76
|
+
if (options.source) {
|
|
77
|
+
const sourceLine = options.source.split(/\r?\n/)[diag.loc.line - 1]
|
|
78
|
+
if (sourceLine !== undefined) {
|
|
79
|
+
const number = String(diag.loc.line)
|
|
80
|
+
const gutter = ' '.repeat(number.length + 2)
|
|
81
|
+
const width = Math.max(1, (diag.endColumn ?? diag.loc.column + 1) - diag.loc.column)
|
|
82
|
+
lines.push(`${gutter}${paint('|', COLOR.dim, color)}`)
|
|
83
|
+
lines.push(` ${paint(number, COLOR.dim, color)} ${paint('|', COLOR.dim, color)} ${sourceLine}`)
|
|
84
|
+
lines.push(`${gutter}${paint('|', COLOR.dim, color)} ${' '.repeat(diag.loc.column - 1)}${paint('^'.repeat(width), diag.severity === 'error' ? COLOR.red : COLOR.yellow, color)}`)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (diag.hint) lines.push(` = 提示:${diag.hint}`)
|
|
89
|
+
return lines.join('\n')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function countBySeverity(diagnostics: Diagnostic[]): { errors: number, warnings: number } {
|
|
93
|
+
let errors = 0
|
|
94
|
+
let warnings = 0
|
|
95
|
+
for (const diag of diagnostics) {
|
|
96
|
+
if (diag.severity === 'error') errors++
|
|
97
|
+
else warnings++
|
|
98
|
+
}
|
|
99
|
+
return { errors, warnings }
|
|
100
|
+
}
|