@algosail/lang 0.2.12 → 0.5.1
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/bin/sail.mjs +4 -0
- package/cli/run-cli.js +176 -0
- package/index.js +12 -2
- package/lib/codegen/README.md +230 -0
- package/lib/codegen/codegen-diagnostics.js +164 -0
- package/lib/codegen/compile-graph.js +107 -0
- package/lib/codegen/emit-adt.js +177 -0
- package/lib/codegen/emit-body.js +1265 -0
- package/lib/codegen/emit-builtin.js +371 -0
- package/lib/codegen/emit-jsdoc-sail.js +383 -0
- package/lib/codegen/emit-module.js +498 -0
- package/lib/codegen/esm-imports.js +26 -0
- package/lib/codegen/index.js +69 -0
- package/lib/codegen/out-layout.js +102 -0
- package/lib/ffi/extract-jsdoc-sail.js +34 -0
- package/lib/io-node/index.js +4 -0
- package/lib/io-node/package-root.js +18 -0
- package/lib/io-node/read-file.js +12 -0
- package/lib/io-node/resolve-package.js +24 -0
- package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
- package/lib/ir/assert-json-serializable.js +30 -0
- package/lib/ir/attach-call-effects.js +108 -0
- package/lib/ir/bind-values.js +594 -0
- package/lib/ir/build-module-ir.js +290 -0
- package/lib/ir/index.js +31 -0
- package/lib/ir/lower-body-steps.js +170 -0
- package/lib/ir/module-metadata.js +65 -0
- package/lib/ir/schema-version.js +15 -0
- package/lib/ir/serialize.js +202 -0
- package/lib/ir/stitch-types.js +92 -0
- package/lib/names/adt-autogen.js +22 -0
- package/lib/names/import-path.js +28 -0
- package/lib/names/index.js +1 -0
- package/lib/names/local-declarations.js +127 -0
- package/lib/names/lower-first.js +6 -0
- package/lib/names/module-scope.js +120 -0
- package/lib/names/resolve-sail.js +365 -0
- package/lib/names/walk-ast-refs.js +91 -0
- package/lib/parse/ast-build.js +51 -0
- package/lib/parse/ast-spec.js +212 -0
- package/lib/parse/builtins-set.js +12 -0
- package/lib/parse/diagnostics.js +180 -0
- package/lib/parse/index.js +46 -0
- package/lib/parse/lexer.js +390 -0
- package/lib/parse/parse-source.js +912 -0
- package/lib/typecheck/adt-autogen-sigs.js +345 -0
- package/lib/typecheck/build-type-env.js +148 -0
- package/lib/typecheck/builtin-signatures.js +183 -0
- package/lib/typecheck/check-word-body.js +1021 -0
- package/lib/typecheck/effect-decl.js +124 -0
- package/lib/typecheck/index.js +55 -0
- package/lib/typecheck/normalize-sig.js +369 -0
- package/lib/typecheck/stack-step-snapshots.js +56 -0
- package/lib/typecheck/unify-type.js +665 -0
- package/lib/typecheck/validate-adt.js +201 -0
- package/package.json +4 -9
- package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
- package/test/cli/sail-cli.test.js +64 -0
- package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
- package/test/codegen/compile-stage0.test.js +128 -0
- package/test/codegen/compile-stage4-layout.test.js +124 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +34 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
- package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
- package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
- package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
- package/test/codegen/emit-adt-stage6.test.js +168 -0
- package/test/codegen/emit-async-stage5.test.js +164 -0
- package/test/codegen/emit-body-stage2.test.js +139 -0
- package/test/codegen/emit-body.test.js +163 -0
- package/test/codegen/emit-builtins-stage7.test.js +258 -0
- package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
- package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
- package/test/codegen/emit-module-stage3.test.js +78 -0
- package/test/conformance/conformance-ir-l4.test.js +38 -0
- package/test/conformance/conformance-l5-codegen.test.js +111 -0
- package/test/conformance/conformance-runner.js +91 -0
- package/test/conformance/conformance-suite-l3.test.js +32 -0
- package/test/ffi/prelude-jsdoc.test.js +49 -0
- package/test/fixtures/demo-full-syntax.ast.json +1471 -0
- package/test/fixtures/demo-full-syntax.sail +35 -0
- package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
- package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
- package/test/fixtures/io-node-mini/dep.sail +2 -0
- package/test/fixtures/io-node-mini/entry.sail +4 -0
- package/test/fixtures/io-node-prelude/entry.sail +4 -0
- package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
- package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
- package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
- package/test/io-node/resolve-disk.test.js +59 -0
- package/test/ir/bind-values.test.js +84 -0
- package/test/ir/build-module-ir.test.js +100 -0
- package/test/ir/call-effects.test.js +97 -0
- package/test/ir/ffi-bracket-ir.test.js +59 -0
- package/test/ir/full-ir-document.test.js +51 -0
- package/test/ir/ir-document-assert.js +67 -0
- package/test/ir/lower-body-steps.test.js +90 -0
- package/test/ir/module-metadata.test.js +42 -0
- package/test/ir/serialization-model.test.js +172 -0
- package/test/ir/stitch-types.test.js +74 -0
- package/test/names/l2-resolve-adt-autogen.test.js +155 -0
- package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
- package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
- package/test/names/l2-resolve-graph.test.js +105 -0
- package/test/names/l2-resolve-single-file.test.js +79 -0
- package/test/parse/ast-spec.test.js +56 -0
- package/test/parse/ast.test.js +476 -0
- package/test/parse/contract.test.js +37 -0
- package/test/parse/fixtures-full-syntax.test.js +24 -0
- package/test/parse/helpers.js +27 -0
- package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
- package/test/parse/l0-lex.test.js +40 -0
- package/test/parse/l1-diagnostics.test.js +77 -0
- package/test/parse/l1-import.test.js +28 -0
- package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
- package/test/parse/l1-top-level.test.js +47 -0
- package/test/parse/l1-types.test.js +31 -0
- package/test/parse/l1-words.test.js +49 -0
- package/test/parse/l2-diagnostics-contract.test.js +67 -0
- package/test/parse/l3-diagnostics-contract.test.js +66 -0
- package/test/typecheck/adt-decl-stage2.test.js +83 -0
- package/test/typecheck/container-contract-e1309.test.js +258 -0
- package/test/typecheck/ffi-bracket-l3.test.js +61 -0
- package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
- package/test/typecheck/l3-partial-pipeline.test.js +74 -0
- package/test/typecheck/opaque-ffi-type.test.js +78 -0
- package/test/typecheck/sig-type-stage3.test.js +190 -0
- package/test/typecheck/stack-check-stage4.test.js +149 -0
- package/test/typecheck/stack-check-stage5.test.js +74 -0
- package/test/typecheck/stack-check-stage6.test.js +56 -0
- package/test/typecheck/stack-check-stage7.test.js +160 -0
- package/test/typecheck/stack-check-stage8.test.js +146 -0
- package/test/typecheck/stack-check-stage9.test.js +105 -0
- package/test/typecheck/typecheck-env.test.js +53 -0
- package/test/typecheck/typecheck-pipeline.test.js +37 -0
- package/README.md +0 -37
- package/cli/sail.js +0 -151
- package/cli/typecheck.js +0 -39
- package/docs/ARCHITECTURE.md +0 -50
- package/docs/CHANGELOG.md +0 -18
- package/docs/FFI-GUIDE.md +0 -65
- package/docs/RELEASE.md +0 -36
- package/docs/TESTING.md +0 -86
- package/test/integration.test.js +0 -61
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3 этап 7: объявленные эффекты сигнатуры (RFC-0.1 §5.7, RFC-IR-0.1 §6).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ plusAsync: boolean, minusAsync: boolean, plusFail: boolean, minusFail: boolean }} EffectFlags
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {import('./normalize-sig.js').NormalizedSignature | null | undefined} sig
|
|
11
|
+
* @returns {EffectFlags}
|
|
12
|
+
*/
|
|
13
|
+
export function declaredEffectFlags (sig) {
|
|
14
|
+
const f = {
|
|
15
|
+
plusAsync: false,
|
|
16
|
+
minusAsync: false,
|
|
17
|
+
plusFail: false,
|
|
18
|
+
minusFail: false
|
|
19
|
+
}
|
|
20
|
+
if (!sig) return f
|
|
21
|
+
for (const e of sig.effectsAdd ?? []) {
|
|
22
|
+
if (e.name === 'Async') f.plusAsync = true
|
|
23
|
+
if (e.name === 'Fail') f.plusFail = true
|
|
24
|
+
}
|
|
25
|
+
for (const e of sig.effectsRemove ?? []) {
|
|
26
|
+
if (e.name === 'Async') f.minusAsync = true
|
|
27
|
+
if (e.name === 'Fail') f.minusFail = true
|
|
28
|
+
}
|
|
29
|
+
return f
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Контекст (слово или quotation inner) может содержать вызов callee, у которого всплывает +Async/+Fail к нему.
|
|
34
|
+
* Разрешено при +Name или при исходящем -Name (обёртка, §5.7).
|
|
35
|
+
*
|
|
36
|
+
* @param {EffectFlags} ctx
|
|
37
|
+
*/
|
|
38
|
+
export function contextAllowsAsyncCallFromCallee (ctx) {
|
|
39
|
+
return ctx.plusAsync || ctx.minusAsync
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {EffectFlags} ctx
|
|
44
|
+
*/
|
|
45
|
+
export function contextAllowsFailCallFromCallee (ctx) {
|
|
46
|
+
return ctx.plusFail || ctx.minusFail
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Вызов callee обязывает непосредственного вызывающего учитывать +Async (E1310), если у callee объявлен +Async
|
|
51
|
+
* и не снят исходящим -Async у callee (§5.7.1).
|
|
52
|
+
*
|
|
53
|
+
* @param {EffectFlags} callee
|
|
54
|
+
*/
|
|
55
|
+
export function calleeAsyncPropagatesToCaller (callee) {
|
|
56
|
+
return callee.plusAsync && !callee.minusAsync
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {EffectFlags} callee
|
|
61
|
+
*/
|
|
62
|
+
export function calleeFailPropagatesToCaller (callee) {
|
|
63
|
+
return callee.plusFail && !callee.minusFail
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {object | null} span
|
|
68
|
+
* @returns {{ effectsAdd: object[], effectsRemove: object[] }}
|
|
69
|
+
*/
|
|
70
|
+
export function effectArraysFromFlags (flags, span) {
|
|
71
|
+
const effectsAdd = []
|
|
72
|
+
const effectsRemove = []
|
|
73
|
+
const side = 'right'
|
|
74
|
+
if (flags.plusAsync) {
|
|
75
|
+
effectsAdd.push({
|
|
76
|
+
kind: 'sig_effect_add',
|
|
77
|
+
span: span ?? null,
|
|
78
|
+
name: 'Async',
|
|
79
|
+
side
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
if (flags.plusFail) {
|
|
83
|
+
effectsAdd.push({
|
|
84
|
+
kind: 'sig_effect_add',
|
|
85
|
+
span: span ?? null,
|
|
86
|
+
name: 'Fail',
|
|
87
|
+
side
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
if (flags.minusAsync) {
|
|
91
|
+
effectsRemove.push({
|
|
92
|
+
kind: 'sig_effect_remove',
|
|
93
|
+
span: span ?? null,
|
|
94
|
+
name: 'Async',
|
|
95
|
+
side
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
if (flags.minusFail) {
|
|
99
|
+
effectsRemove.push({
|
|
100
|
+
kind: 'sig_effect_remove',
|
|
101
|
+
span: span ?? null,
|
|
102
|
+
name: 'Fail',
|
|
103
|
+
side
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
return { effectsAdd, effectsRemove }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Сравнение маркеров исходящей части вложенной quotation_sig при унификации Quote (этап 7).
|
|
111
|
+
*
|
|
112
|
+
* @param {import('./normalize-sig.js').NormalizedSignature} ia
|
|
113
|
+
* @param {import('./normalize-sig.js').NormalizedSignature} ib
|
|
114
|
+
*/
|
|
115
|
+
export function quotationOutgoingEffectsEqual (ia, ib) {
|
|
116
|
+
const a = declaredEffectFlags(ia)
|
|
117
|
+
const b = declaredEffectFlags(ib)
|
|
118
|
+
return (
|
|
119
|
+
a.plusAsync === b.plusAsync &&
|
|
120
|
+
a.minusAsync === b.minusAsync &&
|
|
121
|
+
a.plusFail === b.plusFail &&
|
|
122
|
+
a.minusFail === b.minusFail
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { resolveSailNames } from '../names/resolve-sail.js'
|
|
2
|
+
import { buildTypecheckEnv } from './build-type-env.js'
|
|
3
|
+
import { validateAdtDeclarations } from './validate-adt.js'
|
|
4
|
+
import { normalizeAllSignatures } from './normalize-sig.js'
|
|
5
|
+
import { mergeAdtAutogenIntoSigIrByPath } from './adt-autogen-sigs.js'
|
|
6
|
+
import { checkAllWordBodies } from './check-word-body.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* L3: типизация и проверка стека после успешного Parse и Name resolution (RFC-0.1 §11 п.1–2).
|
|
10
|
+
* Диагностики этой фазы — коды E13xx (RFC-0.1 §13). Алгоритм — RFC-typecheck-0.1.
|
|
11
|
+
*
|
|
12
|
+
* Вход совпадает с {@link resolveSailNames}: один обход графа модулей; при `ok === false`
|
|
13
|
+
* возвращаются диагностики L0/L1/L2 без запуска тела L3.
|
|
14
|
+
*
|
|
15
|
+
* **Порядок фаз `typecheckSail`** (имена шагов, не «этап N» в сообщениях об ошибках):
|
|
16
|
+
*
|
|
17
|
+
* | Шаг | Действие | Тесты (brittle) |
|
|
18
|
+
* |-----|----------|-----------------|
|
|
19
|
+
* | L2 + среда | `resolveSailNames` → `buildTypecheckEnv` | `typecheck-env.test.js`, `typecheck-pipeline.test.js` |
|
|
20
|
+
* | ADT | `validateAdtDeclarations` | `adt-decl-stage2.test.js` |
|
|
21
|
+
* | Сигнатуры | `normalizeAllSignatures` (+ автоген сигнатур ADT) | `sig-type-stage3.test.js` |
|
|
22
|
+
* | Тела слов | `checkAllWordBodies` (стек, унификация, эффекты) | `stack-check-stage4.test.js` … `stack-check-stage9.test.js` |
|
|
23
|
+
*
|
|
24
|
+
* При успехе L2 поле `env` — среда резолва и импортов. `env.adtByPath` — только **валидные** `&` (частично, если часть объявлений с ошибками; RFC-typecheck §5.1).
|
|
25
|
+
* `env.sigIrByPath` — **частичная** карта после нормализации + `mergeAdtAutogenIntoSigIrByPath` (автоген только для ADT из `adtByPath`).
|
|
26
|
+
* Итог: `ok === false`, если есть **хоть одна** диагностика ADT, сигнатур или тел; `diagnostics` — склейка **ADT → сигнатуры → тела** (полный отчёт, пайплайн не обрывается до проверки тел).
|
|
27
|
+
* `env.stackSnapshotsByPath` — успешные слова и **частичные** цепочки по RFC-typecheck §3 / RFC-IR §1.1 при ошибке в теле.
|
|
28
|
+
*
|
|
29
|
+
* **E1302** (некорректная сигнатура quotation, RFC-0.1 §13): код и шаблон `e1302QuotationSig` зарезервированы; эмиссия из
|
|
30
|
+
* нормализации сигнатур не подключена до уточнения RFC, какие сбои `quotation_sig` / `quotation_type` отличать от прочих E1304.
|
|
31
|
+
*
|
|
32
|
+
* @param {{ entryPath: string, readFile: (p: string) => string | null | undefined, resolvePackage?: (spec: string, fromPath: string) => string | null }} opts
|
|
33
|
+
* @returns {{ ok: boolean, diagnostics: object[], snapshots?: Map<string, object>, env?: object }}
|
|
34
|
+
*/
|
|
35
|
+
export function typecheckSail (opts) {
|
|
36
|
+
const r = resolveSailNames({ ...opts, withSnapshots: true })
|
|
37
|
+
if (!r.ok) {
|
|
38
|
+
return { ok: false, diagnostics: r.diagnostics }
|
|
39
|
+
}
|
|
40
|
+
const env = buildTypecheckEnv({
|
|
41
|
+
snapshots: r.snapshots,
|
|
42
|
+
entryPath: opts.entryPath,
|
|
43
|
+
readFile: opts.readFile,
|
|
44
|
+
resolvePackage: opts.resolvePackage
|
|
45
|
+
})
|
|
46
|
+
const { diagnostics: adtDiags, adtByPath } = validateAdtDeclarations(env)
|
|
47
|
+
env.adtByPath = adtByPath
|
|
48
|
+
const { diagnostics: sigDiags, sigIrByPath } = normalizeAllSignatures(env)
|
|
49
|
+
mergeAdtAutogenIntoSigIrByPath(env, sigIrByPath)
|
|
50
|
+
env.sigIrByPath = sigIrByPath
|
|
51
|
+
const bodyDiags = checkAllWordBodies(env)
|
|
52
|
+
const diagnostics = [...adtDiags, ...sigDiags, ...bodyDiags]
|
|
53
|
+
const ok = diagnostics.length === 0
|
|
54
|
+
return { ok, diagnostics, snapshots: r.snapshots, env }
|
|
55
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3 этап 3: нормализация типовых выражений в сигнатурах (IR + source/decl).
|
|
3
|
+
*/
|
|
4
|
+
import * as diag from '../parse/diagnostics.js'
|
|
5
|
+
|
|
6
|
+
/** @type {ReadonlySet<string>} */
|
|
7
|
+
export const PRIMITIVE_TYPE_NAMES = new Set([
|
|
8
|
+
'Str',
|
|
9
|
+
'String',
|
|
10
|
+
'Num',
|
|
11
|
+
'Int',
|
|
12
|
+
'Bool',
|
|
13
|
+
'Float',
|
|
14
|
+
'Double',
|
|
15
|
+
'Char',
|
|
16
|
+
'Unit',
|
|
17
|
+
'Nil'
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
/** @type {ReadonlySet<string>} */
|
|
21
|
+
const BUILTIN_TYPE_CTORS = new Set(['List', 'Dict', 'Map'])
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} span
|
|
25
|
+
* @param {string} code
|
|
26
|
+
* @param {string} message
|
|
27
|
+
*/
|
|
28
|
+
function pushDiag (out, span, code, message) {
|
|
29
|
+
const d = { code, message }
|
|
30
|
+
if (span?.start) {
|
|
31
|
+
d.offset = span.start.offset
|
|
32
|
+
d.line = span.start.line
|
|
33
|
+
d.column = span.start.column
|
|
34
|
+
}
|
|
35
|
+
out.push(d)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Модуль, в `items` которого лежит данный узел верхнего уровня (sum/product/word/import).
|
|
40
|
+
*
|
|
41
|
+
* @param {Map<string, object>} snapshots
|
|
42
|
+
* @param {object} node
|
|
43
|
+
* @returns {string | null}
|
|
44
|
+
*/
|
|
45
|
+
export function modulePathForAstNode (snapshots, node) {
|
|
46
|
+
for (const [p, snap] of snapshots) {
|
|
47
|
+
if (!snap?.ok) continue
|
|
48
|
+
for (const it of snap.items) {
|
|
49
|
+
if (it === node) return p
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @typedef {{ path: string, node: object }} TypeDeclRef
|
|
57
|
+
*
|
|
58
|
+
* @typedef {{ kind: 'prim', name: string, source: object, decl: null }} IrPrim
|
|
59
|
+
* @typedef {{ kind: 'tvar', name: string, source: object, decl?: TypeDeclRef | null }} IrTvar
|
|
60
|
+
* @typedef {{ kind: 'app', ctor: string, args: object[], source: object, decl: TypeDeclRef | null }} IrApp
|
|
61
|
+
* @typedef {{ kind: 'mod_adt', module: string, type: string, source: object, decl: TypeDeclRef }} IrModAdt
|
|
62
|
+
* @typedef {{ kind: 'adt', path: string, type: string, source: object, decl: TypeDeclRef }} IrAdt
|
|
63
|
+
* @typedef {{ kind: 'opaque', name: string, source: object, decl: null }} IrOpaque — номинальный opaque FFI (RFC-0.1 §5.4), без параметров.
|
|
64
|
+
* @typedef {{ kind: 'stack_label', name: string, source: object }} IrStackLabel
|
|
65
|
+
* @typedef {{ kind: 'quote', source: object, inner: NormalizedSignature }} IrQuote
|
|
66
|
+
* @typedef {{ kind: 'named_quote', prefix: string, source: object, inner: NormalizedSignature }} IrNamedQuote
|
|
67
|
+
*
|
|
68
|
+
* @typedef {{
|
|
69
|
+
* left: object[],
|
|
70
|
+
* right: object[],
|
|
71
|
+
* effectsAdd: object[],
|
|
72
|
+
* effectsRemove: object[]
|
|
73
|
+
* }} NormalizedSignature
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {import('./build-type-env.js').TypecheckEnv} env
|
|
78
|
+
* @param {string} modulePath
|
|
79
|
+
* @param {{ importMap: Map<string, object>, typeIndex: Map<string, object> }} scopeInfo
|
|
80
|
+
*/
|
|
81
|
+
function makeCtx (env, modulePath, scopeInfo) {
|
|
82
|
+
return {
|
|
83
|
+
env,
|
|
84
|
+
modulePath,
|
|
85
|
+
scopeInfo,
|
|
86
|
+
/** @type {object[]} */
|
|
87
|
+
diagnostics: []
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {object} expr
|
|
93
|
+
* @param {ReturnType<typeof makeCtx>} ctx
|
|
94
|
+
* @returns {object | null}
|
|
95
|
+
*/
|
|
96
|
+
export function normalizeTypeExpr (expr, ctx) {
|
|
97
|
+
if (!expr) return null
|
|
98
|
+
switch (expr.kind) {
|
|
99
|
+
case 'type_name': {
|
|
100
|
+
const nm = expr.name
|
|
101
|
+
if (PRIMITIVE_TYPE_NAMES.has(nm)) {
|
|
102
|
+
return { kind: 'prim', name: nm, source: expr, decl: null }
|
|
103
|
+
}
|
|
104
|
+
const def = ctx.scopeInfo.typeIndex.get(nm)
|
|
105
|
+
if (def && (def.kind === 'sum_type' || def.kind === 'product_type')) {
|
|
106
|
+
const npar = def.typeParams?.length ?? 0
|
|
107
|
+
if (npar > 0) {
|
|
108
|
+
pushDiag(
|
|
109
|
+
ctx.diagnostics,
|
|
110
|
+
expr.span,
|
|
111
|
+
'E1311',
|
|
112
|
+
diag.e1311ParameterizedTypeNeedsParens(nm)
|
|
113
|
+
)
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
const defPath =
|
|
117
|
+
modulePathForAstNode(ctx.env.snapshots, def) ?? ctx.modulePath
|
|
118
|
+
return {
|
|
119
|
+
kind: 'adt',
|
|
120
|
+
path: defPath,
|
|
121
|
+
type: nm,
|
|
122
|
+
source: expr,
|
|
123
|
+
decl: { path: defPath, node: def }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { kind: 'opaque', name: nm, source: expr, decl: null }
|
|
127
|
+
}
|
|
128
|
+
case 'type_var':
|
|
129
|
+
return { kind: 'tvar', name: expr.name, source: expr, decl: null }
|
|
130
|
+
case 'module_type_ref': {
|
|
131
|
+
const { module: mod, type: ty } = expr
|
|
132
|
+
const dep = ctx.scopeInfo.importMap.get(mod)
|
|
133
|
+
if (!dep?.ok) {
|
|
134
|
+
pushDiag(ctx.diagnostics, expr.span, 'E1203', diag.e1203ModuleNotFound(mod))
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
if (!dep.exportTypes.has(ty)) {
|
|
138
|
+
pushDiag(
|
|
139
|
+
ctx.diagnostics,
|
|
140
|
+
expr.span,
|
|
141
|
+
'E1204',
|
|
142
|
+
diag.e1204MissingMember(mod, ty)
|
|
143
|
+
)
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
const node = dep.typeIndex.get(ty)
|
|
147
|
+
if (!node) {
|
|
148
|
+
pushDiag(
|
|
149
|
+
ctx.diagnostics,
|
|
150
|
+
expr.span,
|
|
151
|
+
'E1204',
|
|
152
|
+
diag.e1204MissingMember(mod, ty)
|
|
153
|
+
)
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
const npar = node.typeParams?.length ?? 0
|
|
157
|
+
if (
|
|
158
|
+
npar > 0 &&
|
|
159
|
+
(node.kind === 'sum_type' || node.kind === 'product_type')
|
|
160
|
+
) {
|
|
161
|
+
pushDiag(
|
|
162
|
+
ctx.diagnostics,
|
|
163
|
+
expr.span,
|
|
164
|
+
'E1311',
|
|
165
|
+
diag.e1311ParameterizedTypeNeedsParens(ty)
|
|
166
|
+
)
|
|
167
|
+
return null
|
|
168
|
+
}
|
|
169
|
+
const declPath = dep.path
|
|
170
|
+
return {
|
|
171
|
+
kind: 'mod_adt',
|
|
172
|
+
module: mod,
|
|
173
|
+
type: ty,
|
|
174
|
+
source: expr,
|
|
175
|
+
decl: { path: declPath, node }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
case 'paren_type':
|
|
179
|
+
return normalizeTypeExpr(expr.inner, ctx)
|
|
180
|
+
case 'quotation_type': {
|
|
181
|
+
const inner = normalizeSignature(expr.inner, ctx)
|
|
182
|
+
if (!inner) return null
|
|
183
|
+
return { kind: 'quote', source: expr, inner }
|
|
184
|
+
}
|
|
185
|
+
case 'type_app': {
|
|
186
|
+
const ctor = expr.ctor
|
|
187
|
+
const args = []
|
|
188
|
+
for (const a of expr.args) {
|
|
189
|
+
const na = normalizeTypeExpr(a, ctx)
|
|
190
|
+
if (!na) return null
|
|
191
|
+
args.push(na)
|
|
192
|
+
}
|
|
193
|
+
if (BUILTIN_TYPE_CTORS.has(ctor)) {
|
|
194
|
+
return { kind: 'app', ctor, args, source: expr, decl: null }
|
|
195
|
+
}
|
|
196
|
+
const def = ctx.scopeInfo.typeIndex.get(ctor)
|
|
197
|
+
if (def && (def.kind === 'sum_type' || def.kind === 'product_type')) {
|
|
198
|
+
const defPath =
|
|
199
|
+
modulePathForAstNode(ctx.env.snapshots, def) ?? ctx.modulePath
|
|
200
|
+
return {
|
|
201
|
+
kind: 'app',
|
|
202
|
+
ctor,
|
|
203
|
+
args,
|
|
204
|
+
source: expr,
|
|
205
|
+
decl: { path: defPath, node: def }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
pushDiag(
|
|
209
|
+
ctx.diagnostics,
|
|
210
|
+
expr.span,
|
|
211
|
+
'E1304',
|
|
212
|
+
diag.e1304UnknownTypeInSignature(ctor)
|
|
213
|
+
)
|
|
214
|
+
return null
|
|
215
|
+
}
|
|
216
|
+
default:
|
|
217
|
+
pushDiag(
|
|
218
|
+
ctx.diagnostics,
|
|
219
|
+
expr.span,
|
|
220
|
+
'E1304',
|
|
221
|
+
diag.e1304TypeMismatch('известное типовое выражение', expr.kind)
|
|
222
|
+
)
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {object} sig
|
|
229
|
+
* @param {ReturnType<typeof makeCtx>} ctx
|
|
230
|
+
* @returns {NormalizedSignature | null}
|
|
231
|
+
*/
|
|
232
|
+
export function normalizeSignature (sig, ctx) {
|
|
233
|
+
/** @type {object[]} */
|
|
234
|
+
const left = []
|
|
235
|
+
for (const item of sig.left) {
|
|
236
|
+
const s = normalizeSigItem(item, ctx)
|
|
237
|
+
if (!s) return null
|
|
238
|
+
left.push(s)
|
|
239
|
+
}
|
|
240
|
+
/** @type {object[]} */
|
|
241
|
+
const right = []
|
|
242
|
+
for (const item of sig.right) {
|
|
243
|
+
const s = normalizeSigItem(item, ctx)
|
|
244
|
+
if (!s) return null
|
|
245
|
+
right.push(s)
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
left,
|
|
249
|
+
right,
|
|
250
|
+
effectsAdd: [...sig.effectsAdd],
|
|
251
|
+
effectsRemove: [...sig.effectsRemove]
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* @param {object} item
|
|
257
|
+
* @param {ReturnType<typeof makeCtx>} ctx
|
|
258
|
+
* @returns {object | null}
|
|
259
|
+
*/
|
|
260
|
+
/**
|
|
261
|
+
* @param {object} item
|
|
262
|
+
* @param {import('./build-type-env.js').TypecheckEnv} env
|
|
263
|
+
* @param {string} modulePath
|
|
264
|
+
* @returns {{ ir: object | null, diagnostics: object[] }}
|
|
265
|
+
*/
|
|
266
|
+
export function normalizeSigItemWithEnv (item, env, modulePath) {
|
|
267
|
+
const scopeInfo = env.scopeByPath.get(modulePath)
|
|
268
|
+
if (!scopeInfo) {
|
|
269
|
+
return { ir: null, diagnostics: [] }
|
|
270
|
+
}
|
|
271
|
+
const ctx = makeCtx(env, modulePath, scopeInfo)
|
|
272
|
+
const ir = normalizeSigItem(item, ctx)
|
|
273
|
+
return { ir, diagnostics: ctx.diagnostics }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function normalizeSigItem (item, ctx) {
|
|
277
|
+
switch (item.kind) {
|
|
278
|
+
case 'sig_type_expr': {
|
|
279
|
+
return normalizeTypeExpr(item.type, ctx)
|
|
280
|
+
}
|
|
281
|
+
case 'stack_var':
|
|
282
|
+
return { kind: 'stack_label', name: item.name, source: item }
|
|
283
|
+
case 'quotation_sig': {
|
|
284
|
+
const inner = normalizeSignature(item.inner, ctx)
|
|
285
|
+
if (!inner) return null
|
|
286
|
+
return { kind: 'quote', source: item, inner }
|
|
287
|
+
}
|
|
288
|
+
case 'named_quotation_sig': {
|
|
289
|
+
const inner = normalizeSignature(item.quotation.inner, ctx)
|
|
290
|
+
if (!inner) return null
|
|
291
|
+
return {
|
|
292
|
+
kind: 'named_quote',
|
|
293
|
+
prefix: item.prefix,
|
|
294
|
+
source: item,
|
|
295
|
+
inner
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
default:
|
|
299
|
+
pushDiag(
|
|
300
|
+
ctx.diagnostics,
|
|
301
|
+
item.span,
|
|
302
|
+
'E1304',
|
|
303
|
+
diag.e1304TypeMismatch('элемент сигнатуры', item.kind)
|
|
304
|
+
)
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* @param {object} word
|
|
311
|
+
* @param {ReturnType<typeof makeCtx>} ctx
|
|
312
|
+
* @returns {NormalizedSignature | null}
|
|
313
|
+
*/
|
|
314
|
+
export function normalizeWordSignature (word, ctx) {
|
|
315
|
+
if (!word.signature) {
|
|
316
|
+
pushDiag(
|
|
317
|
+
ctx.diagnostics,
|
|
318
|
+
word.span,
|
|
319
|
+
'E1301',
|
|
320
|
+
diag.e1301WordSignature(word.name)
|
|
321
|
+
)
|
|
322
|
+
return null
|
|
323
|
+
}
|
|
324
|
+
const ir = normalizeSignature(word.signature, ctx)
|
|
325
|
+
if (!ir && ctx.diagnostics.length === 0) {
|
|
326
|
+
pushDiag(
|
|
327
|
+
ctx.diagnostics,
|
|
328
|
+
word.span,
|
|
329
|
+
'E1301',
|
|
330
|
+
diag.e1301WordSignature(word.name)
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
return ir
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @param {import('./build-type-env.js').TypecheckEnv} env
|
|
338
|
+
* @returns {{ diagnostics: object[], sigIrByPath: Map<string, Map<string, NormalizedSignature>> }}
|
|
339
|
+
*/
|
|
340
|
+
export function normalizeAllSignatures (env) {
|
|
341
|
+
/** @type {object[]} */
|
|
342
|
+
const allDiags = []
|
|
343
|
+
/** @type {Map<string, Map<string, NormalizedSignature>>} */
|
|
344
|
+
const sigIrByPath = new Map()
|
|
345
|
+
|
|
346
|
+
for (const p of env.modulePathsOrdered) {
|
|
347
|
+
const snap = env.snapshots.get(p)
|
|
348
|
+
const scopeInfo = env.scopeByPath.get(p)
|
|
349
|
+
const words = env.wordDeclByPath.get(p)
|
|
350
|
+
if (!snap?.ok || !scopeInfo || !words) continue
|
|
351
|
+
|
|
352
|
+
/** @type {Map<string, NormalizedSignature>} */
|
|
353
|
+
const perPath = new Map()
|
|
354
|
+
|
|
355
|
+
for (const [name, word] of words) {
|
|
356
|
+
const ctx = makeCtx(env, p, scopeInfo)
|
|
357
|
+
const ir = normalizeWordSignature(word, ctx)
|
|
358
|
+
if (ctx.diagnostics.length > 0) {
|
|
359
|
+
allDiags.push(...ctx.diagnostics)
|
|
360
|
+
}
|
|
361
|
+
if (ir) {
|
|
362
|
+
perPath.set(name, ir)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
sigIrByPath.set(p, perPath)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return { diagnostics: allDiags, sigIrByPath }
|
|
369
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3 этап 9: снимки стека по шагам для RFC-IR-0.1 §3, RFC-typecheck-0.1 §3 и §6.
|
|
3
|
+
* Полная цепочка на каждый шаг (сжатие до базовых блоков не делаем — см. RFC-IR §3).
|
|
4
|
+
*/
|
|
5
|
+
import { applySubstDeep, unifyTypes } from './unify-type.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{ pre: object[], post: object[] }} StackStepSnapshot
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} WordStackSnapshotRecord
|
|
13
|
+
* @property {StackStepSnapshot[]} steps индекс = индекс шага в `word.body` (дно→вершина в pre/post)
|
|
14
|
+
* @property {Map<number, WordStackSnapshotRecord>} nestedByParentStep вложенные цитаты по индексу шага `( … )`
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Копия стека для снимка: слоты после текущей подстановки (читаемо снаружи без повторного вывода).
|
|
19
|
+
*
|
|
20
|
+
* @param {object[]} stack
|
|
21
|
+
* @param {WeakMap<object, object>} subst
|
|
22
|
+
* @returns {object[]}
|
|
23
|
+
*/
|
|
24
|
+
export function snapshotStackSlots (stack, subst) {
|
|
25
|
+
return stack.map((t) => applySubstDeep(t, subst))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* RFC-IR-0.1 §3 п.2: для валидной цепочки post[i] унифицируется с pre[i+1].
|
|
30
|
+
*
|
|
31
|
+
* @param {WordStackSnapshotRecord} record
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function verifySnapshotChainStitches (record) {
|
|
35
|
+
const steps = record.steps
|
|
36
|
+
for (let i = 0; i < steps.length - 1; i++) {
|
|
37
|
+
const cur = steps[i]
|
|
38
|
+
const next = steps[i + 1]
|
|
39
|
+
if (cur == null || next == null) continue
|
|
40
|
+
const a = cur.post
|
|
41
|
+
const b = next.pre
|
|
42
|
+
if (a == null || b == null || !Array.isArray(a) || !Array.isArray(b)) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (a.length !== b.length) return false
|
|
46
|
+
const subst = new WeakMap()
|
|
47
|
+
const diags = []
|
|
48
|
+
for (let j = 0; j < a.length; j++) {
|
|
49
|
+
if (!unifyTypes(a[j], b[j], subst, diags, null)) return false
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const nested of record.nestedByParentStep.values()) {
|
|
53
|
+
if (!verifySnapshotChainStitches(nested)) return false
|
|
54
|
+
}
|
|
55
|
+
return true
|
|
56
|
+
}
|