@slim-lang/core 1.2.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 (52) hide show
  1. package/README.md +666 -0
  2. package/package.json +55 -0
  3. package/packages/slim/.spm +7 -0
  4. package/packages/slim/converters/main.slim +106 -0
  5. package/packages/slim/helpers/array.slim +25 -0
  6. package/packages/slim/helpers/path.slim +3 -0
  7. package/packages/slim/helpers/request.slim +102 -0
  8. package/packages/slim/helpers/string.slim +27 -0
  9. package/packages/slim/main.slim +42 -0
  10. package/packages/slim/parse/main.slim +25 -0
  11. package/packages/slim/server/main.slim +423 -0
  12. package/packages/slim/time/main.slim +66 -0
  13. package/packages/slim/types/common.slim +6 -0
  14. package/packages/slim/types/formats.slim +23 -0
  15. package/packages/slim/types/hash.slim +6 -0
  16. package/packages/slim/types/mails.slim +3 -0
  17. package/packages/slim/types/numerical.slim +9 -0
  18. package/packages/slim/types/time.slim +3 -0
  19. package/run-dev-slim.js +133 -0
  20. package/run-slim.js +20 -0
  21. package/src/bin/api/github_auth.js +89 -0
  22. package/src/bin/api/github_get.js +139 -0
  23. package/src/bin/api/github_req.js +455 -0
  24. package/src/bin/api/lock.js +37 -0
  25. package/src/bin/api/spm.js +103 -0
  26. package/src/bin/api/storage.js +30 -0
  27. package/src/bin/cli.js +404 -0
  28. package/src/bin/config.default.json +5 -0
  29. package/src/bin/helpers.js +147 -0
  30. package/src/bin/parsers/spm.js +174 -0
  31. package/src/bin/spm.js +519 -0
  32. package/src/checker.js +926 -0
  33. package/src/compile.js +230 -0
  34. package/src/external/classErrors.js +202 -0
  35. package/src/external/client.js +38 -0
  36. package/src/external/core.js +861 -0
  37. package/src/external/defaults.js +25 -0
  38. package/src/external/helpers.js +541 -0
  39. package/src/external/slim-globals.d.ts +65 -0
  40. package/src/external/types.js +38 -0
  41. package/src/format.js +81 -0
  42. package/src/handlers/errorHandler.js +43 -0
  43. package/src/handlers/parser/components.js +250 -0
  44. package/src/handlers/parserHandler.js +793 -0
  45. package/src/jsdoc.js +273 -0
  46. package/src/lexer.js +174 -0
  47. package/src/modulePaths.js +74 -0
  48. package/src/parser.js +818 -0
  49. package/src/repl.js +32 -0
  50. package/src/sourcemap.js +0 -0
  51. package/src/test-runner.js +62 -0
  52. package/src/transform.js +765 -0
package/src/checker.js ADDED
@@ -0,0 +1,926 @@
1
+ import _traverse from "@babel/traverse"
2
+ import * as t from "@babel/types"
3
+ import path from "node:path"
4
+ import { computeLineStarts, offsetToLineCol } from "./sourcemap.js"
5
+
6
+ const traverse = _traverse.default ?? _traverse
7
+
8
+ // Check annotations before lowering; report only definite type conflicts.
9
+
10
+ const NUMERIC = new Set(["int", "float", "number"])
11
+ const KNOWN = new Set([
12
+ "int", "float", "number", "string", "bool", "null", "undefined",
13
+ "object", "array", "function", "any", "element"
14
+ ])
15
+
16
+ function splitUnion(label) {
17
+ const parts = []
18
+ let depth = 0
19
+ let current = ""
20
+
21
+ for (const char of label) {
22
+ if (char === "<" || char === "[" || char === "(") depth++
23
+ else if (char === ">" || char === "]" || char === ")") depth--
24
+ else if (char === "|" && depth === 0) { parts.push(current.trim()); current = ""; continue }
25
+ current += char
26
+ }
27
+ if (current.trim()) parts.push(current.trim())
28
+ return parts
29
+ }
30
+
31
+ function expand(label) {
32
+ const trimmed = label.trim()
33
+ if (trimmed.endsWith("?")) return `${trimmed.slice(0, -1).trim()} | null | undefined`
34
+ return trimmed
35
+ }
36
+
37
+ function isIntersection(label) {
38
+ return label.includes("&") && !label.includes("|")
39
+ }
40
+
41
+ function elementOf(label) {
42
+ if (!label.endsWith("[]")) return null
43
+ const inner = label.slice(0, -2).trim()
44
+ if (inner.startsWith("(") && inner.endsWith(")")) return inner.slice(1, -1).trim()
45
+ return inner
46
+ }
47
+
48
+ function baseName(label) {
49
+ return label.replace(/(\[\])+$/, "").replace(/<[\s\S]*>$/, "").trim()
50
+ }
51
+
52
+ function splitList(text) {
53
+ const parts = []
54
+ let depth = 0
55
+ let current = ""
56
+
57
+ for (const char of text) {
58
+ if (char === "<" || char === "[" || char === "(") depth++
59
+ else if (char === ">" || char === "]" || char === ")") depth--
60
+ else if (char === "," && depth === 0) { parts.push(current.trim()); current = ""; continue }
61
+ current += char
62
+ }
63
+ if (current.trim()) parts.push(current.trim())
64
+ return parts
65
+ }
66
+
67
+ function genericOf(label) {
68
+ const open = label.indexOf("<")
69
+ if (open === -1 || !label.endsWith(">")) return null
70
+ return {
71
+ container: label.slice(0, open).trim(),
72
+ args: splitList(label.slice(open + 1, -1))
73
+ }
74
+ }
75
+
76
+ function tupleOf(label) {
77
+ if (!label.startsWith("[") || !label.endsWith("]") || label.length <= 2) return null
78
+ return splitList(label.slice(1, -1))
79
+ }
80
+
81
+ function normalize(label) {
82
+ const trimmed = label.trim()
83
+ const generic = genericOf(trimmed)
84
+ if (generic && generic.container === "Array" && generic.args.length === 1) {
85
+ return `${normalize(generic.args[0])}[]`
86
+ }
87
+ return trimmed
88
+ }
89
+
90
+ // Resolve inherited fields so structs can cross module boundaries.
91
+ function resolveStruct(name, env, seen = new Set()) {
92
+ const definition = env.structs.get(name)
93
+ if (!definition) return null
94
+ if (definition.resolved) return definition
95
+
96
+ if (!definition.parent) {
97
+ return { ...definition, ancestors: new Set(), resolved: true }
98
+ }
99
+ if (seen.has(name)) return null
100
+
101
+ seen.add(name)
102
+ const parent = resolveStruct(definition.parent, env, seen)
103
+ if (!parent) return null
104
+
105
+ const fields = new Map(parent.fields)
106
+ for (const [field, info] of definition.fields) fields.set(field, info)
107
+
108
+ return {
109
+ fields,
110
+ defaults: new Set([...parent.defaults, ...definition.defaults]),
111
+ methods: new Set([...parent.methods, ...definition.methods]),
112
+ ancestors: new Set([definition.parent, ...parent.ancestors]),
113
+ parent: null,
114
+ resolved: true
115
+ }
116
+ }
117
+
118
+ function inherits(name, ancestor, env) {
119
+ if (name === ancestor) return true
120
+ return resolveStruct(name, env)?.ancestors.has(ancestor) ?? false
121
+ }
122
+
123
+ export function createEnvironment() {
124
+ return {
125
+ structs: new Map(),
126
+ enums: new Map(),
127
+ customTypes: new Set(),
128
+ functions: new Map(),
129
+ bindings: new Map(),
130
+ classes: new Set(),
131
+ methodOwners: new Map(),
132
+ returns: new Map(),
133
+ functionsByName: new Map(),
134
+ importedFunctions: new Map()
135
+ }
136
+ }
137
+
138
+ // Module declarations keyed by absolute source path.
139
+ const moduleTypes = new Map()
140
+
141
+ function signatureFor(name, path_, env) {
142
+ const binding = path_.scope.getBinding(name)
143
+ if (binding) return env.functions.get(binding) ?? null
144
+ return env.importedFunctions.get(name) ?? null
145
+ }
146
+
147
+ const BUILTIN_CLASSES = new Set(["Map", "Set", "Date", "Promise", "RegExp", "Error"])
148
+
149
+ function isOpen(label, env) {
150
+ const base = baseName(label)
151
+ if (!base) return true
152
+ // Custom validators shadow built-ins and cannot be evaluated statically.
153
+ if (env.customTypes.has(base)) return true
154
+ if (KNOWN.has(base) || BUILTIN_CLASSES.has(base)) return false
155
+ if (env.structs.has(base) || env.enums.has(base)) return false
156
+ if (label.includes("::")) return !env.enums.has(label.split("::")[0])
157
+ return true
158
+ }
159
+
160
+ function acceptsAtom(target, source, env) {
161
+ if (target === source) return true
162
+ if (target === "any" || source === "any") return true
163
+
164
+ const targetElement = elementOf(target)
165
+ const sourceElement = elementOf(source)
166
+ if (targetElement) {
167
+ if (source === "array") return true
168
+ if (!sourceElement) return isOpen(source, env)
169
+ if (sourceElement === "null") return true
170
+ return accepts(targetElement, sourceElement, env, true)
171
+ }
172
+ if (target === "array") return !!sourceElement || source === "array" || isOpen(source, env)
173
+ if (sourceElement) return target === "object" || isOpen(target, env)
174
+
175
+ if (isOpen(target, env) || isOpen(source, env)) return true
176
+
177
+ if (NUMERIC.has(target) && NUMERIC.has(source)) {
178
+ if (target === "int") return source === "int"
179
+ return true
180
+ }
181
+
182
+ const targetTuple = tupleOf(target)
183
+ const sourceTuple = tupleOf(source)
184
+ if (targetTuple || sourceTuple) {
185
+ if (!targetTuple || !sourceTuple) return false
186
+ if (targetTuple.length !== sourceTuple.length) return false
187
+ return targetTuple.every((part, index) => accepts(part, sourceTuple[index], env, true))
188
+ }
189
+
190
+ const targetGeneric = genericOf(target)
191
+ if (targetGeneric) {
192
+ return baseName(target) === baseName(source)
193
+ }
194
+ if (genericOf(source)) return baseName(target) === baseName(source)
195
+
196
+ if (BUILTIN_CLASSES.has(target)) return target === source
197
+ if (BUILTIN_CLASSES.has(source)) return false
198
+
199
+ if (target === "object") return source === "object" || env.structs.has(source)
200
+ if (env.structs.has(target)) {
201
+ return source === "object" || inherits(source, target, env)
202
+ }
203
+
204
+ if (target.includes("::")) {
205
+ const [enumName] = target.split("::")
206
+ const definition = env.enums.get(enumName)
207
+ if (!definition) return true
208
+ if (source === target || source === enumName) return true
209
+ return definition.valueTypes.has(source)
210
+ }
211
+
212
+ if (env.enums.has(target)) {
213
+ const definition = env.enums.get(target)
214
+ return source.startsWith(`${target}::`) || definition.valueTypes.has(source)
215
+ }
216
+
217
+ return false
218
+ }
219
+
220
+ // A union needs one compatible arm; array literals need every element to fit.
221
+ export function accepts(target, source, env, requireAll = false) {
222
+ if (!target || !source) return true
223
+
224
+ const targetLabel = expand(target)
225
+ const sourceLabel = expand(source)
226
+ if (isIntersection(targetLabel) || isIntersection(sourceLabel)) return true
227
+
228
+ const targets = splitUnion(targetLabel).map(normalize)
229
+ const sources = splitUnion(sourceLabel).map(normalize)
230
+ const fits = one => targets.some(other => acceptsAtom(other, one, env))
231
+
232
+ return requireAll ? sources.every(fits) : sources.some(fits)
233
+ }
234
+
235
+ function unionOf(left, right) {
236
+ if (!left || !right) return null
237
+ if (left === right) return left
238
+ return `${left} | ${right}`
239
+ }
240
+
241
+ function typedVariableLabel(node) {
242
+ if (!t.isCallExpression(node)) return null
243
+ if (!t.isIdentifier(node.callee, { name: "__typed_variable__" })) return null
244
+ return t.isStringLiteral(node.arguments[1]) ? node.arguments[1].value : null
245
+ }
246
+
247
+ function structLiteralLabel(node, env) {
248
+ if (!t.isCallExpression(node)) return null
249
+ const callee = node.callee
250
+ if (!t.isMemberExpression(callee) || callee.computed) return null
251
+ if (!t.isIdentifier(callee.property, { name: "new" })) return null
252
+ if (!t.isIdentifier(callee.object) || !env.structs.has(callee.object.name)) return null
253
+ return callee.object.name
254
+ }
255
+
256
+ export function infer(node, path_, env) {
257
+ if (!node) return null
258
+
259
+ if (t.isNumericLiteral(node)) return Number.isInteger(node.value) ? "int" : "float"
260
+ if (t.isStringLiteral(node) || t.isTemplateLiteral(node)) return "string"
261
+ if (t.isBooleanLiteral(node)) return "bool"
262
+ if (t.isNullLiteral(node)) return "null"
263
+ if (t.isIdentifier(node, { name: "undefined" })) return "undefined"
264
+ if (t.isObjectExpression(node)) return "object"
265
+ if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) return "function"
266
+
267
+ if (t.isArrayExpression(node)) {
268
+ if (node.elements.length === 0) return "null[]"
269
+
270
+ const kinds = new Set()
271
+ for (const item of node.elements) {
272
+ if (!item || t.isSpreadElement(item)) return "array"
273
+ const kind = infer(item, path_, env)
274
+ if (!kind) return "array"
275
+ kinds.add(kind)
276
+ }
277
+
278
+ if (kinds.size === 1) return `${[...kinds][0]}[]`
279
+ return `(${[...kinds].join(" | ")})[]`
280
+ }
281
+
282
+ const structInstance = structLiteralLabel(node, env)
283
+ if (structInstance) return structInstance
284
+
285
+ if (t.isNewExpression(node) && t.isIdentifier(node.callee)) {
286
+ const name = node.callee.name
287
+ if (env.classes.has(name) || BUILTIN_CLASSES.has(name)) return name
288
+ return null
289
+ }
290
+
291
+ if (t.isIdentifier(node)) {
292
+ const binding = path_.scope.getBinding(node.name)
293
+ return binding ? env.bindings.get(binding) ?? null : null
294
+ }
295
+
296
+ if (t.isThisExpression(node)) {
297
+ const fn = path_.getFunctionParent()
298
+ return fn ? env.methodOwners.get(fn.node) ?? null : null
299
+ }
300
+
301
+ if (t.isMemberExpression(node) && node.computed) {
302
+ const objectType = normalize(infer(node.object, path_, env) ?? "")
303
+ if (!objectType) return null
304
+
305
+ const element = elementOf(objectType)
306
+ if (element) return element
307
+
308
+ const tuple = tupleOf(objectType)
309
+ if (tuple && t.isNumericLiteral(node.property)) return tuple[node.property.value] ?? null
310
+ return null
311
+ }
312
+
313
+ if (t.isMemberExpression(node) && t.isIdentifier(node.property)) {
314
+ if (t.isIdentifier(node.object) && env.enums.has(node.object.name)) {
315
+ return `${node.object.name}::${node.property.name}`
316
+ }
317
+ const objectType = infer(node.object, path_, env)
318
+ const definition = objectType ? resolveStruct(objectType, env) : null
319
+ return definition?.fields.get(node.property.name)?.type ?? null
320
+ }
321
+
322
+ if (t.isUnaryExpression(node)) {
323
+ if (node.operator === "!") return "bool"
324
+ if (node.operator === "-" || node.operator === "+") {
325
+ const operand = infer(node.argument, path_, env)
326
+ return NUMERIC.has(operand) ? operand : null
327
+ }
328
+ if (node.operator === "typeof") return "string"
329
+ return null
330
+ }
331
+
332
+ if (t.isBinaryExpression(node)) {
333
+ const operator = node.operator
334
+ if (["==", "!=", "===", "!==", "<", ">", "<=", ">=", "instanceof", "in"].includes(operator)) {
335
+ return "bool"
336
+ }
337
+
338
+ const left = infer(node.left, path_, env)
339
+ const right = infer(node.right, path_, env)
340
+
341
+ if (operator === "+") {
342
+ if (left === "string" || right === "string") return "string"
343
+ if (NUMERIC.has(left) && NUMERIC.has(right)) {
344
+ return left === "int" && right === "int" ? "int" : "float"
345
+ }
346
+ return null
347
+ }
348
+
349
+ if (["-", "*", "%", "**"].includes(operator)) {
350
+ if (NUMERIC.has(left) && NUMERIC.has(right)) {
351
+ return left === "int" && right === "int" ? "int" : "float"
352
+ }
353
+ return null
354
+ }
355
+
356
+ if (operator === "/") return NUMERIC.has(left) && NUMERIC.has(right) ? "float" : null
357
+ return null
358
+ }
359
+
360
+ if (t.isLogicalExpression(node)) {
361
+ return unionOf(infer(node.left, path_, env), infer(node.right, path_, env))
362
+ }
363
+
364
+ if (t.isConditionalExpression(node)) {
365
+ return unionOf(infer(node.consequent, path_, env), infer(node.alternate, path_, env))
366
+ }
367
+
368
+ if (t.isAwaitExpression(node)) return null
369
+
370
+ if (t.isCallExpression(node) && t.isIdentifier(node.callee)) {
371
+ const label = typedVariableLabel(node)
372
+ if (label) return label
373
+
374
+ return signatureFor(node.callee.name, path_, env)?.returns ?? null
375
+ }
376
+
377
+ return null
378
+ }
379
+
380
+ function declaredReturn(fn) {
381
+ if (!t.isBlockStatement(fn.body)) return null
382
+
383
+ for (const statement of fn.body.body) {
384
+ if (!t.isExpressionStatement(statement)) continue
385
+ const call = statement.expression
386
+ if (!t.isCallExpression(call)) continue
387
+ if (!t.isIdentifier(call.callee, { name: "__declare_return__" })) continue
388
+ if (t.isStringLiteral(call.arguments[0])) return call.arguments[0].value
389
+ }
390
+ return null
391
+ }
392
+
393
+ function collectParameters(fn) {
394
+ const declared = new Map()
395
+
396
+ if (t.isBlockStatement(fn.body)) {
397
+ for (const statement of fn.body.body) {
398
+ if (!t.isExpressionStatement(statement)) continue
399
+ const call = statement.expression
400
+ if (!t.isCallExpression(call)) continue
401
+ if (!t.isIdentifier(call.callee, { name: "__typed_parameter__" })) continue
402
+
403
+ const [value, label, , optional] = call.arguments
404
+ if (!t.isIdentifier(value) || !t.isStringLiteral(label)) continue
405
+ declared.set(value.name, {
406
+ type: label.value,
407
+ optional: t.isBooleanLiteral(optional) ? optional.value : false
408
+ })
409
+ }
410
+ }
411
+
412
+ let rest = false
413
+ const parameters = fn.params.map(param => {
414
+ if (t.isRestElement(param)) { rest = true; return { name: null, type: null, required: false } }
415
+ if (t.isAssignmentPattern(param) && t.isIdentifier(param.left)) {
416
+ return { name: param.left.name, type: declared.get(param.left.name)?.type ?? null, required: false }
417
+ }
418
+ if (t.isIdentifier(param)) {
419
+ const info = declared.get(param.name)
420
+ return { name: param.name, type: info?.type ?? null, required: !info?.optional }
421
+ }
422
+ return { name: null, type: null, required: false }
423
+ })
424
+
425
+ const returns = declaredReturn(fn)
426
+ return { parameters, rest, returns, typed: declared.size > 0 || !!returns }
427
+ }
428
+
429
+ function functionName(path_) {
430
+ if (t.isFunctionDeclaration(path_.node) && path_.node.id) return path_.node.id.name
431
+ if (path_.parentPath?.isVariableDeclarator() && t.isIdentifier(path_.parent.id)) {
432
+ return path_.parent.id.name
433
+ }
434
+ if (path_.parentPath?.isObjectProperty()) {
435
+ const key = path_.parent.key
436
+ if (t.isStringLiteral(key)) return key.value
437
+ if (t.isIdentifier(key)) return key.name
438
+ }
439
+ if ((t.isClassMethod(path_.node) || t.isObjectMethod(path_.node)) && t.isIdentifier(path_.node.key)) {
440
+ return path_.node.key.name
441
+ }
442
+ return "function"
443
+ }
444
+
445
+ function collect(ast, env) {
446
+ traverse(ast, {
447
+ ClassDeclaration(path_) {
448
+ if (path_.node.id) env.classes.add(path_.node.id.name)
449
+ },
450
+
451
+ VariableDeclarator(path_) {
452
+ const { id, init } = path_.node
453
+ if (!t.isIdentifier(id)) return
454
+
455
+ const binding = path_.scope.getBinding(id.name)
456
+
457
+ if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
458
+ const name = init.callee.name
459
+ const [nameArg, schema] = init.arguments
460
+
461
+ if (name === "__def_struct__" && t.isStringLiteral(nameArg) && t.isObjectExpression(schema)) {
462
+ const fields = new Map()
463
+ for (const property of schema.properties) {
464
+ if (!t.isObjectProperty(property) || !t.isStringLiteral(property.value)) continue
465
+ const key = t.isStringLiteral(property.key) ? property.key.value
466
+ : t.isIdentifier(property.key) ? property.key.name : null
467
+ if (key === null) continue
468
+ const optional = key.startsWith("*")
469
+ fields.set(optional ? key.slice(1).trim() : key, {
470
+ type: property.value.value,
471
+ optional
472
+ })
473
+ }
474
+
475
+ const defaults = new Set()
476
+ if (t.isObjectExpression(init.arguments[3])) {
477
+ for (const property of init.arguments[3].properties) {
478
+ if (!t.isObjectProperty(property)) continue
479
+ const key = t.isStringLiteral(property.key) ? property.key.value
480
+ : t.isIdentifier(property.key) ? property.key.name : null
481
+ if (key !== null) defaults.add(key)
482
+ }
483
+ }
484
+
485
+ const methods = new Set()
486
+ if (t.isObjectExpression(init.arguments[5])) {
487
+ for (const property of init.arguments[5].properties) {
488
+ if (!t.isObjectProperty(property)) continue
489
+ const key = t.isStringLiteral(property.key) ? property.key.value
490
+ : t.isIdentifier(property.key) ? property.key.name : null
491
+ if (key === null) continue
492
+ methods.add(key)
493
+ if (t.isFunction(property.value)) {
494
+ env.methodOwners.set(property.value, nameArg.value)
495
+ }
496
+ }
497
+ }
498
+
499
+ const parent = t.isStringLiteral(init.arguments[4]) ? init.arguments[4].value : null
500
+ env.structs.set(nameArg.value, { fields, defaults, methods, parent })
501
+ return
502
+ }
503
+
504
+ if (name === "__def_enum__" && t.isStringLiteral(nameArg) && t.isObjectExpression(schema)) {
505
+ const members = new Set()
506
+ const valueTypes = new Set()
507
+ for (const property of schema.properties) {
508
+ if (!t.isObjectProperty(property)) continue
509
+ const key = t.isStringLiteral(property.key) ? property.key.value
510
+ : t.isIdentifier(property.key) ? property.key.name : null
511
+ if (key !== null) members.add(key)
512
+ if (t.isStringLiteral(property.value)) valueTypes.add("string")
513
+ else if (t.isNumericLiteral(property.value)) {
514
+ valueTypes.add(Number.isInteger(property.value.value) ? "int" : "float")
515
+ } else valueTypes.add("any")
516
+ }
517
+ env.enums.set(nameArg.value, { members, valueTypes })
518
+ return
519
+ }
520
+
521
+ if (name === "__type_def__" && t.isStringLiteral(nameArg)) {
522
+ env.customTypes.add(nameArg.value)
523
+ return
524
+ }
525
+
526
+ const label = typedVariableLabel(init)
527
+ if (label && binding) env.bindings.set(binding, label)
528
+ }
529
+
530
+ const instance = structLiteralLabel(init, env)
531
+ if (instance && binding) env.bindings.set(binding, instance)
532
+
533
+ if ((t.isArrowFunctionExpression(init) || t.isFunctionExpression(init)) && binding) {
534
+ const signature = collectParameters(init)
535
+ env.functions.set(binding, signature)
536
+ if (binding.scope.path.isProgram()) env.functionsByName.set(id.name, signature)
537
+ }
538
+ },
539
+
540
+ FunctionDeclaration(path_) {
541
+ if (!path_.node.id) return
542
+ const name = path_.node.id.name
543
+ const binding = path_.scope.getBinding(name)
544
+ if (!binding) return
545
+
546
+ const signature = collectParameters(path_.node)
547
+ env.functions.set(binding, signature)
548
+ if (binding.scope.path.isProgram()) env.functionsByName.set(name, signature)
549
+ },
550
+
551
+ Function(path_) {
552
+ const { parameters, returns } = collectParameters(path_.node)
553
+ for (const parameter of parameters) {
554
+ if (!parameter.name || !parameter.type) continue
555
+ const binding = path_.scope.getBinding(parameter.name)
556
+ if (binding) env.bindings.set(binding, parameter.type)
557
+ }
558
+
559
+ if (returns) {
560
+ env.returns.set(path_.node, { label: returns, name: functionName(path_) })
561
+ }
562
+ }
563
+ })
564
+ }
565
+
566
+ function describe(label) {
567
+ return label.includes("|") || label.includes("&") ? `(${label})` : label
568
+ }
569
+
570
+ function checkStructLiteral(structName, node, path_, env, report) {
571
+ const definition = resolveStruct(structName, env)
572
+ if (!definition) return
573
+
574
+ const seen = new Set()
575
+ for (const property of node.properties) {
576
+ if (!t.isObjectProperty(property)) return
577
+ const key = t.isIdentifier(property.key) && !property.computed ? property.key.name
578
+ : t.isStringLiteral(property.key) ? property.key.value : null
579
+ if (key === null) return
580
+
581
+ seen.add(key)
582
+ const field = definition.fields.get(key)
583
+ if (!field) {
584
+ report(property, `"${structName}" has no field "${key}"`)
585
+ continue
586
+ }
587
+
588
+ const value = infer(property.value, path_, env)
589
+ if (!accepts(field.type, value, env)) {
590
+ report(property.value,
591
+ `"${structName}.${key}" expects ${describe(field.type)}, got ${describe(value)}`)
592
+ }
593
+ }
594
+
595
+ for (const [name, field] of definition.fields) {
596
+ if (field.optional || seen.has(name) || definition.defaults.has(name)) continue
597
+ report(node, `"${structName}" is missing field "${name}" of type ${describe(field.type)}`)
598
+ }
599
+ }
600
+
601
+ // Inspect lowered match expressions to preserve exhaustiveness checks.
602
+ function matchExpression(node) {
603
+ if (!t.isCallExpression(node) || node.arguments.length !== 1) return null
604
+
605
+ const callee = node.callee
606
+ if (!t.isArrowFunctionExpression(callee) || callee.params.length !== 1) return null
607
+ if (!t.isIdentifier(callee.params[0], { name: "__match" })) return null
608
+ if (!t.isBlockStatement(callee.body)) return null
609
+
610
+ const covered = []
611
+ let wildcard = false
612
+ let guarded = false
613
+
614
+ for (const statement of callee.body.body) {
615
+ if (t.isBlockStatement(statement)) { guarded = true; continue }
616
+
617
+ if (t.isIfStatement(statement)) {
618
+ const test = statement.test
619
+ if (t.isCallExpression(test) && t.isIdentifier(test.callee, { name: "__match_eq__" })) {
620
+ covered.push(test.arguments[1])
621
+ } else guarded = true
622
+ continue
623
+ }
624
+
625
+ if (t.isReturnStatement(statement)) {
626
+ wildcard = !t.isIdentifier(statement.argument, { name: "undefined" })
627
+ }
628
+ }
629
+
630
+ return { covered, wildcard, guarded, scrutinee: node.arguments[0] }
631
+ }
632
+
633
+ function checkExhaustive(node, match, path_, env, report) {
634
+ if (match.guarded || match.wildcard) return
635
+
636
+ const label = infer(match.scrutinee, path_, env)
637
+ if (!label) return
638
+
639
+ const enumName = label.includes("::") ? label.split("::")[0] : label
640
+ const definition = env.enums.get(enumName)
641
+ if (!definition) return
642
+
643
+ const covered = new Set()
644
+ for (const arm of match.covered) {
645
+ if (!t.isMemberExpression(arm) || arm.computed) return
646
+ if (!t.isIdentifier(arm.object, { name: enumName })) return
647
+ if (!t.isIdentifier(arm.property)) return
648
+ covered.add(arm.property.name)
649
+ }
650
+
651
+ const missing = [...definition.members].filter(member => !covered.has(member))
652
+ if (missing.length === 0) return
653
+
654
+ report(node, `match on "${enumName}" does not handle ${missing.map(member => `${enumName}.${member}`).join(", ")}` +
655
+ ` — add the missing case${missing.length === 1 ? "" : "s"} or a "_" fallback`)
656
+ }
657
+
658
+ function checkValue(label, node, path_, env, report, describeTarget, phrase = "expects") {
659
+ if (t.isObjectExpression(node) && env.structs.has(label)) {
660
+ checkStructLiteral(label, node, path_, env, report)
661
+ return
662
+ }
663
+
664
+ const actual = infer(node, path_, env)
665
+ if (!accepts(label, actual, env)) {
666
+ report(node, `${describeTarget} ${phrase} ${describe(label)}, got ${describe(actual)}`)
667
+ }
668
+ }
669
+
670
+ function verify(ast, env, report) {
671
+ traverse(ast, {
672
+ VariableDeclarator(path_) {
673
+ const { id, init } = path_.node
674
+ if (!t.isIdentifier(id) || !t.isCallExpression(init)) return
675
+
676
+ const label = typedVariableLabel(init)
677
+ if (!label) return
678
+
679
+ checkValue(label, init.arguments[0], path_, env, report, `"${id.name}"`)
680
+ },
681
+
682
+ AssignmentExpression(path_) {
683
+ const { node } = path_
684
+ if (!t.isIdentifier(node.left) || node.operator !== "=") return
685
+
686
+ const binding = path_.scope.getBinding(node.left.name)
687
+ const label = binding ? env.bindings.get(binding) : null
688
+ if (!label) return
689
+
690
+ checkValue(label, node.right, path_, env, report, `"${node.left.name}"`)
691
+ },
692
+
693
+ CallExpression(path_) {
694
+ const { node } = path_
695
+
696
+ const match = matchExpression(node)
697
+ if (match) checkExhaustive(node, match, path_, env, report)
698
+
699
+ const constructed = structLiteralLabel(node, env)
700
+ if (constructed && t.isObjectExpression(node.arguments[0])) {
701
+ const definition = resolveStruct(constructed, env)
702
+ if (!definition) return
703
+
704
+ for (const property of node.arguments[0].properties) {
705
+ if (!t.isObjectProperty(property)) return
706
+ const key = t.isIdentifier(property.key) && !property.computed ? property.key.name
707
+ : t.isStringLiteral(property.key) ? property.key.value : null
708
+ if (key === null) return
709
+
710
+ const field = definition.fields.get(key)
711
+ if (!field) {
712
+ report(property, `"${constructed}" has no field "${key}"`)
713
+ continue
714
+ }
715
+
716
+ const value = infer(property.value, path_, env)
717
+ if (!accepts(field.type, value, env)) {
718
+ report(property.value,
719
+ `"${constructed}.${key}" expects ${describe(field.type)}, got ${describe(value)}`)
720
+ }
721
+ }
722
+ return
723
+ }
724
+
725
+ if (!t.isIdentifier(node.callee)) return
726
+
727
+ const signature = signatureFor(node.callee.name, path_, env)
728
+ if (!signature || !signature.typed) return
729
+ if (node.arguments.some(argument => t.isSpreadElement(argument))) return
730
+
731
+ const name = node.callee.name
732
+ const required = signature.parameters.filter(parameter => parameter.required).length
733
+
734
+ if (node.arguments.length < required) {
735
+ report(node, `"${name}" expects ${required} argument${required === 1 ? "" : "s"}, got ${node.arguments.length}`)
736
+ return
737
+ }
738
+ if (!signature.rest && node.arguments.length > signature.parameters.length) {
739
+ report(node,
740
+ `"${name}" takes ${signature.parameters.length} argument${signature.parameters.length === 1 ? "" : "s"}, got ${node.arguments.length}`)
741
+ return
742
+ }
743
+
744
+ node.arguments.forEach((argument, index) => {
745
+ const parameter = signature.parameters[index]
746
+ if (!parameter?.type) return
747
+ checkValue(parameter.type, argument, path_, env, report,
748
+ `argument "${parameter.name}" of "${name}"`)
749
+ })
750
+ },
751
+
752
+ ReturnStatement(path_) {
753
+ const fn = path_.getFunctionParent()
754
+ const declared = fn ? env.returns.get(fn.node) : null
755
+ if (!declared) return
756
+
757
+ const value = path_.node.argument
758
+ if (!value) {
759
+ if (!accepts(declared.label, "undefined", env)) {
760
+ report(path_.node,
761
+ `"${declared.name}" must return ${describe(declared.label)}, got undefined`)
762
+ }
763
+ return
764
+ }
765
+
766
+ checkValue(declared.label, value, path_, env, report, `"${declared.name}"`, "must return")
767
+ },
768
+
769
+ MemberExpression(path_) {
770
+ const { node } = path_
771
+ if (node.computed || !t.isIdentifier(node.property)) return
772
+ if (!t.isIdentifier(node.object) && !t.isThisExpression(node.object)) return
773
+ if (t.isAssignmentExpression(path_.parent) && path_.parent.left === node) return
774
+
775
+ const label = infer(node.object, path_, env)
776
+ const definition = label ? resolveStruct(label, env) : null
777
+ if (!definition) return
778
+
779
+ const field = node.property.name
780
+ if (definition.fields.has(field) || definition.methods.has(field)) return
781
+
782
+ report(node.property, `"${label}" has no field "${field}"`)
783
+ }
784
+ })
785
+ }
786
+
787
+ function exportedNames(ast) {
788
+ const names = new Map()
789
+
790
+ traverse(ast, {
791
+ ExportNamedDeclaration(path_) {
792
+ const declaration = path_.node.declaration
793
+
794
+ if (t.isVariableDeclaration(declaration)) {
795
+ for (const declarator of declaration.declarations) {
796
+ if (t.isIdentifier(declarator.id)) names.set(declarator.id.name, declarator.id.name)
797
+ }
798
+ } else if ((t.isFunctionDeclaration(declaration) || t.isClassDeclaration(declaration)) && declaration.id) {
799
+ names.set(declaration.id.name, declaration.id.name)
800
+ }
801
+
802
+ for (const specifier of path_.node.specifiers ?? []) {
803
+ if (!t.isExportSpecifier(specifier)) continue
804
+ const exported = t.isIdentifier(specifier.exported)
805
+ ? specifier.exported.name
806
+ : specifier.exported.value
807
+ names.set(exported, specifier.local.name)
808
+ }
809
+ }
810
+ })
811
+
812
+ return names
813
+ }
814
+
815
+ function buildModuleTypes(ast, env) {
816
+ const record = {
817
+ structs: new Map(),
818
+ enums: new Map(),
819
+ customTypes: new Set(),
820
+ functions: new Map(),
821
+ ambient: { structs: new Map(), enums: new Map(), customTypes: new Set() }
822
+ }
823
+
824
+ for (const name of env.structs.keys()) {
825
+ const resolved = resolveStruct(name, env)
826
+ if (resolved) record.ambient.structs.set(name, resolved)
827
+ }
828
+ for (const [name, definition] of env.enums) record.ambient.enums.set(name, definition)
829
+ for (const name of env.customTypes) record.ambient.customTypes.add(name)
830
+
831
+ for (const [exported, local] of exportedNames(ast)) {
832
+ const struct = record.ambient.structs.get(local)
833
+ if (struct) record.structs.set(exported, struct)
834
+ if (env.enums.has(local)) record.enums.set(exported, env.enums.get(local))
835
+ if (env.customTypes.has(local)) record.customTypes.add(exported)
836
+ if (env.functionsByName.has(local)) record.functions.set(exported, env.functionsByName.get(local))
837
+ }
838
+
839
+ return record
840
+ }
841
+
842
+ // Import known declarations; unresolved modules remain unchecked.
843
+ function seedImports(env, imports, wildcards) {
844
+ const merge = (record, name, local) => {
845
+ if (record.structs.has(name)) env.structs.set(local, record.structs.get(name))
846
+ if (record.enums.has(name)) env.enums.set(local, record.enums.get(name))
847
+ if (record.customTypes.has(name)) env.customTypes.add(local)
848
+ if (record.functions.has(name)) env.importedFunctions.set(local, record.functions.get(name))
849
+ }
850
+
851
+ const sources = new Set([...wildcards, ...imports.map(entry => entry.source)])
852
+
853
+ // Local declarations override explicit and transitive imports.
854
+ for (const source of sources) {
855
+ const record = moduleTypes.get(source)
856
+ if (!record) continue
857
+
858
+ for (const [name, definition] of record.ambient.structs) {
859
+ if (!env.structs.has(name)) env.structs.set(name, definition)
860
+ }
861
+ for (const [name, definition] of record.ambient.enums) {
862
+ if (!env.enums.has(name)) env.enums.set(name, definition)
863
+ }
864
+ for (const name of record.ambient.customTypes) env.customTypes.add(name)
865
+ }
866
+
867
+ for (const source of wildcards) {
868
+ const record = moduleTypes.get(source)
869
+ if (!record) continue
870
+ for (const name of record.structs.keys()) merge(record, name, name)
871
+ for (const name of record.enums.keys()) merge(record, name, name)
872
+ for (const name of record.customTypes) merge(record, name, name)
873
+ for (const name of record.functions.keys()) merge(record, name, name)
874
+ }
875
+
876
+ for (const { local, imported, source } of imports) {
877
+ const record = moduleTypes.get(source)
878
+ if (record) merge(record, imported, local)
879
+ }
880
+ }
881
+
882
+ export function checkTypes(ast, { mapped, originalCode, sourceFile, imports = [], wildcards = [] }) {
883
+ const env = createEnvironment()
884
+ const diagnostics = []
885
+ const originalLines = computeLineStarts(originalCode)
886
+ const sourceLines = originalCode.split("\n")
887
+
888
+ const report = (node, message) => {
889
+ const offset = typeof node.start === "number" ? node.start : 0
890
+ const origin = mapped.origin[offset]
891
+ const location = origin >= 0 ? offsetToLineCol(origin, originalLines) : null
892
+
893
+ diagnostics.push({
894
+ message,
895
+ sourceFile,
896
+ line: location ? location.line + 1 : 0,
897
+ column: location ? location.column + 1 : 0,
898
+ sourceLine: location ? sourceLines[location.line] ?? "" : ""
899
+ })
900
+ }
901
+
902
+ seedImports(env, imports, wildcards)
903
+ collect(ast, env)
904
+ moduleTypes.set(path.resolve(sourceFile), buildModuleTypes(ast, env))
905
+
906
+ verify(ast, env, report)
907
+
908
+ diagnostics.sort((a, b) => a.line - b.line || a.column - b.column)
909
+ return diagnostics
910
+ }
911
+
912
+ export function formatDiagnostics(diagnostics) {
913
+ return diagnostics.map(({ message, sourceFile, line, column, sourceLine }) => {
914
+ const text = sourceLine.trim()
915
+ const indent = sourceLine.length - sourceLine.trimStart().length
916
+ const pointer = " ".repeat(Math.max(0, column - 1 - indent)) + "^"
917
+
918
+ return [
919
+ `TypeError: ${message}`,
920
+ ` at ${sourceFile}:${line}:${column}`,
921
+ "",
922
+ ` ${text}`,
923
+ ` ${pointer}`
924
+ ].join("\n")
925
+ }).join("\n\n")
926
+ }