@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.
Files changed (58) hide show
  1. package/LICENSE +22 -0
  2. package/lib/ast.d.ts +341 -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 +42 -0
  19. package/lib/emit/ts.d.ts.map +1 -0
  20. package/lib/emit/ts.js +950 -0
  21. package/lib/emit/ts.js.map +1 -0
  22. package/lib/index.d.ts +18 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +54 -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 +1138 -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/lib/version.d.ts +25 -0
  43. package/lib/version.d.ts.map +1 -0
  44. package/lib/version.js +47 -0
  45. package/lib/version.js.map +1 -0
  46. package/package.json +10 -3
  47. package/src/ast.ts +467 -0
  48. package/src/checker.ts +554 -0
  49. package/src/diagnostics.ts +100 -0
  50. package/src/emit/lua.ts +484 -0
  51. package/src/emit/ts.ts +1024 -0
  52. package/src/index.ts +24 -0
  53. package/src/lexer.ts +301 -0
  54. package/src/parser.ts +1257 -0
  55. package/src/token.ts +34 -0
  56. package/src/types.ts +218 -0
  57. package/src/version.ts +44 -0
  58. 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
+ }