@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,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Модель сериализации IR для JSON (RFC-IR-0.1 §8: on-disk формат не обязателен;
|
|
3
|
+
* при публикации — рекомендуется irSchemaVersion).
|
|
4
|
+
*
|
|
5
|
+
* Конверт стабилен; внутренний объект `module` — единственный источник правды по шагам
|
|
6
|
+
* и стеку (RFC-IR §3). Поля `views` / `meta` опциональны: лёгкие проекции и метаданные
|
|
7
|
+
* продьюсера. LSP/дебаг читают типы и вложенные quotation из
|
|
8
|
+
* `module` через обёртки в коде, не дублируя `preTypes`/`postTypes` во `views`.
|
|
9
|
+
*
|
|
10
|
+
* Ограничения: только JSON-safe значения; см. [assert-json-serializable.js](./assert-json-serializable.js).
|
|
11
|
+
*
|
|
12
|
+
* ---
|
|
13
|
+
* Договорённости wire (фаза 0; наполнение модуля — фазы 1+):
|
|
14
|
+
*
|
|
15
|
+
* **`nodeId`** / **`span`** на шагах тела: после фазы 2 задаются на элементах `words[].irSteps`
|
|
16
|
+
* ([lower-body-steps.js](./lower-body-steps.js)); формат id — `"{modulePath}#{wordName}#{индексы}"`
|
|
17
|
+
* (вложение quotation — индексы через `.`, напр. `…#0.1`). В `words[].stackSteps.steps[]` элемент `i`
|
|
18
|
+
* — снимок шага `i` или `null`; те же `preTypes`/`postTypes` после фазы 3 дублируются на `irSteps[i]`.
|
|
19
|
+
*
|
|
20
|
+
* **`span`** — происхождение в `.sail`, как у парсера. Для LSP позже: позиции в UTF-16 code units.
|
|
21
|
+
* @typedef {{ start: { offset: number, line: number, column: number }, end?: { offset: number, line: number, column: number } }} IrWireSpan
|
|
22
|
+
*
|
|
23
|
+
* **`preTypes` / `postTypes`** — массивы типовых слотов **дно → вершина** (в `stackSteps` и после фазы 3 на узлах тела; RFC-IR §3).
|
|
24
|
+
*
|
|
25
|
+
* **`valueBindings`** (фаза 4, опционально): `argIds` — id, снимаемые для эффекта шага (**последний — вершина** до шага);
|
|
26
|
+
* `resultIds` — id после шага (**дно → вершина**, как `postTypes`). Для `dup` два элемента `resultIds` — одна и та же строка id.
|
|
27
|
+
*
|
|
28
|
+
* **Эффекты (фаза 5, RFC-IR §6):** на записи `words[]` — булевы **`asyncDefinition`**, **`mayFail`** (явные `false`, если эффекта нет);
|
|
29
|
+
* на узлах вызова в `irSteps` (и вложенных шагах) опционально **`calleeAsync`**, **`calleeMayFail`** ([attach-call-effects.js](./attach-call-effects.js)).
|
|
30
|
+
*
|
|
31
|
+
* **Модуль (фаза 6, RFC-IR §7):** в `imports[]` опционально **`resolvedPath`** — нормализованный путь зависимости (как в `scopeByPath.importMap` после L2);
|
|
32
|
+
* на узле **`Word`** с `ref: "qualified"` опционально **`resolvedModulePath`** для того же алиаса `module` ([module-metadata.js](./module-metadata.js)).
|
|
33
|
+
*
|
|
34
|
+
* **Quotation на стеке** (типовой слот): узел с `kind: "quote" | "named_quote"`, при named —
|
|
35
|
+
* `prefix`, поле `inner` — нормализованная сигнатура (wire без AST). Полный контур шагов с
|
|
36
|
+
* `nodeId`/`span`/`preTypes`/`postTypes` на вложенных шагах — цель фаз 2–3 в `module`.
|
|
37
|
+
*
|
|
38
|
+
* @typedef {{ name: string, version?: string }} IrWireProducer
|
|
39
|
+
* @typedef {{
|
|
40
|
+
* producer?: IrWireProducer,
|
|
41
|
+
* sources?: { path?: string, uri?: string, sha256?: string }[],
|
|
42
|
+
* status?: 'ok' | 'error'
|
|
43
|
+
* }} IrWireMeta итог L3 по модулю (RFC-IR-0.1 §1.1); задаётся продьюсером IR, не выводится из `module` автоматически.
|
|
44
|
+
* @typedef {Record<string, unknown>} IrWireViews — тонкие проекции (индексы nodeId и т.д.), без дубликата стека.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { IR_SCHEMA_VERSION } from './schema-version.js'
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Record<string, unknown>} IrModulePayload — тело IR модуля; по мере фаз расширяется.
|
|
51
|
+
* `words[]`: `stackSteps.steps[i]` — снимок шага `i` или `null` (выравнивание с телом); `irSteps[i]` — узел section 5
|
|
52
|
+
* с `nodeId`/`span` и после фазы 3 теми же `preTypes`/`postTypes`, что у снимка ([stitch-types.js](./stitch-types.js));
|
|
53
|
+
* после фазы 4 опционально `valueBindings` на узле шага ([bind-values.js](./bind-values.js));
|
|
54
|
+
* после фазы 5 — `asyncDefinition`/`mayFail` на записи слова и `calleeAsync`/`calleeMayFail` на шагах вызова ([attach-call-effects.js](./attach-call-effects.js));
|
|
55
|
+
* после фазы 6 — `imports[].resolvedPath`, на квалифицированном `Word` — `resolvedModulePath` ([module-metadata.js](./module-metadata.js)).
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Корень документа при передаче/хранении в JSON.
|
|
60
|
+
*
|
|
61
|
+
* @typedef {{
|
|
62
|
+
* irSchemaVersion: string
|
|
63
|
+
* module: IrModulePayload
|
|
64
|
+
* views?: IrWireViews
|
|
65
|
+
* meta?: IrWireMeta
|
|
66
|
+
* }} SerializedIrDocument
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Результат полного разбора корня (игнорируются неизвестные дополнительные ключи).
|
|
71
|
+
*
|
|
72
|
+
* @typedef {{
|
|
73
|
+
* module: IrModulePayload
|
|
74
|
+
* views?: IrWireViews
|
|
75
|
+
* meta?: IrWireMeta
|
|
76
|
+
* }} ParsedIrDocument
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
const KEY_VERSION = 'irSchemaVersion'
|
|
80
|
+
const KEY_MODULE = 'module'
|
|
81
|
+
const KEY_VIEWS = 'views'
|
|
82
|
+
const KEY_META = 'meta'
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {unknown} parsed
|
|
86
|
+
* @returns {ParsedIrDocument}
|
|
87
|
+
*/
|
|
88
|
+
function parseDocumentRoot (parsed) {
|
|
89
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
90
|
+
throw new Error('IR JSON: ожидался объект корня документа')
|
|
91
|
+
}
|
|
92
|
+
const o = /** @type {Record<string, unknown>} */ (parsed)
|
|
93
|
+
const ver = o[KEY_VERSION]
|
|
94
|
+
if (ver !== IR_SCHEMA_VERSION) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`IR JSON: неподдерживаемая irSchemaVersion ${JSON.stringify(ver)} (ожидается ${IR_SCHEMA_VERSION})`
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
const mod = o[KEY_MODULE]
|
|
100
|
+
if (mod === null || typeof mod !== 'object' || Array.isArray(mod)) {
|
|
101
|
+
throw new Error('IR JSON: поле module должно быть объектом')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let views = undefined
|
|
105
|
+
if (Object.prototype.hasOwnProperty.call(o, KEY_VIEWS)) {
|
|
106
|
+
const v = o[KEY_VIEWS]
|
|
107
|
+
if (v != null && (typeof v !== 'object' || Array.isArray(v))) {
|
|
108
|
+
throw new Error('IR JSON: поле views должно быть объектом')
|
|
109
|
+
}
|
|
110
|
+
if (v != null) {
|
|
111
|
+
views = /** @type {IrWireViews} */ (v)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let meta = undefined
|
|
116
|
+
if (Object.prototype.hasOwnProperty.call(o, KEY_META)) {
|
|
117
|
+
const m = o[KEY_META]
|
|
118
|
+
if (m != null && (typeof m !== 'object' || Array.isArray(m))) {
|
|
119
|
+
throw new Error('IR JSON: поле meta должно быть объектом')
|
|
120
|
+
}
|
|
121
|
+
if (m != null) {
|
|
122
|
+
meta = /** @type {IrWireMeta} */ (m)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
module: /** @type {IrModulePayload} */ (mod),
|
|
128
|
+
views,
|
|
129
|
+
meta
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {IrModulePayload} modulePayload
|
|
135
|
+
* @param {{ views?: IrWireViews, meta?: IrWireMeta }} [options]
|
|
136
|
+
* @returns {SerializedIrDocument}
|
|
137
|
+
*/
|
|
138
|
+
export function createSerializedIrDocument (modulePayload, options) {
|
|
139
|
+
const doc = /** @type {SerializedIrDocument} */ ({
|
|
140
|
+
[KEY_VERSION]: IR_SCHEMA_VERSION,
|
|
141
|
+
[KEY_MODULE]: modulePayload
|
|
142
|
+
})
|
|
143
|
+
if (options?.views != null) {
|
|
144
|
+
if (typeof options.views !== 'object' || Array.isArray(options.views)) {
|
|
145
|
+
throw new Error('IR JSON: options.views должно быть объектом')
|
|
146
|
+
}
|
|
147
|
+
doc[KEY_VIEWS] = options.views
|
|
148
|
+
}
|
|
149
|
+
if (options?.meta != null) {
|
|
150
|
+
if (typeof options.meta !== 'object' || Array.isArray(options.meta)) {
|
|
151
|
+
throw new Error('IR JSON: options.meta должно быть объектом')
|
|
152
|
+
}
|
|
153
|
+
doc[KEY_META] = options.meta
|
|
154
|
+
}
|
|
155
|
+
return doc
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Достаёт `module` из уже распарсенного JSON-объекта; проверяет версию.
|
|
160
|
+
* Неизвестные ключи на корне допустимы и игнорируются.
|
|
161
|
+
*
|
|
162
|
+
* @param {unknown} parsed
|
|
163
|
+
* @returns {IrModulePayload}
|
|
164
|
+
*/
|
|
165
|
+
export function unwrapSerializedIrDocument (parsed) {
|
|
166
|
+
return parseDocumentRoot(parsed).module
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Полный разбор корня: `module`, опционально `views` и `meta`.
|
|
171
|
+
*
|
|
172
|
+
* @param {unknown} parsed
|
|
173
|
+
* @returns {ParsedIrDocument}
|
|
174
|
+
*/
|
|
175
|
+
export function unwrapSerializedIrDocumentFull (parsed) {
|
|
176
|
+
return parseDocumentRoot(parsed)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {IrModulePayload} modulePayload
|
|
181
|
+
* @param {{ views?: IrWireViews, meta?: IrWireMeta }} [options]
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
export function stringifyIrDocument (modulePayload, options) {
|
|
185
|
+
return JSON.stringify(createSerializedIrDocument(modulePayload, options))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* @param {string} jsonText
|
|
190
|
+
* @returns {IrModulePayload}
|
|
191
|
+
*/
|
|
192
|
+
export function parseIrDocument (jsonText) {
|
|
193
|
+
return unwrapSerializedIrDocument(JSON.parse(jsonText))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {string} jsonText
|
|
198
|
+
* @returns {ParsedIrDocument}
|
|
199
|
+
*/
|
|
200
|
+
export function parseIrDocumentFull (jsonText) {
|
|
201
|
+
return unwrapSerializedIrDocumentFull(JSON.parse(jsonText))
|
|
202
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L4 фаза 3: стыковка снимков L3 с `irSteps` (RFC-IR section 3).
|
|
3
|
+
* Переиспользует {@link verifySnapshotChainStitches} из typecheck без второго вывода типов.
|
|
4
|
+
*/
|
|
5
|
+
import { verifySnapshotChainStitches } from '../typecheck/stack-step-snapshots.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} v
|
|
9
|
+
* @returns {unknown}
|
|
10
|
+
*/
|
|
11
|
+
function cloneJsonSafe (v) {
|
|
12
|
+
return JSON.parse(JSON.stringify(v))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Wire `stackSteps` (как из `build-module-ir` после конвертации снимка L3) → форма `WordStackSnapshotRecord`.
|
|
17
|
+
*
|
|
18
|
+
* @param {{ steps?: unknown[], nestedByParentStep?: { parentStepIndex: number, record: object }[] }} wire
|
|
19
|
+
* @returns {{ steps: object[], nestedByParentStep: Map<number, object> }}
|
|
20
|
+
*/
|
|
21
|
+
export function wireStackSnapshotToRecord (wire) {
|
|
22
|
+
const wsteps = wire.steps ?? []
|
|
23
|
+
/** @type {object[]} */
|
|
24
|
+
const steps = []
|
|
25
|
+
for (let i = 0; i < wsteps.length; i++) {
|
|
26
|
+
const s = wsteps[i]
|
|
27
|
+
if (s == null || typeof s !== 'object') {
|
|
28
|
+
steps.push(null)
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
const o = /** @type {Record<string, unknown>} */ (s)
|
|
32
|
+
const preTypes = o.preTypes
|
|
33
|
+
const postTypes = o.postTypes
|
|
34
|
+
steps.push({
|
|
35
|
+
pre: Array.isArray(preTypes) ? preTypes : [],
|
|
36
|
+
post: postTypes !== undefined ? postTypes : undefined
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
const nested = new Map()
|
|
40
|
+
for (const e of wire.nestedByParentStep ?? []) {
|
|
41
|
+
nested.set(e.parentStepIndex, wireStackSnapshotToRecord(e.record))
|
|
42
|
+
}
|
|
43
|
+
return { steps, nestedByParentStep: nested }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @param {{ steps?: unknown[], nestedByParentStep?: unknown[] }} wire
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export function verifyWireStackSnapshot (wire) {
|
|
51
|
+
if (wire == null || typeof wire !== 'object') return false
|
|
52
|
+
return verifySnapshotChainStitches(wireStackSnapshotToRecord(wire))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Копирует `preTypes`/`postTypes` с `wireStack.steps[i]` на `irSteps[i]`; для `Quotation` — рекурсия по `nestedByParentStep`.
|
|
57
|
+
*
|
|
58
|
+
* @param {Record<string, unknown>[]} irSteps
|
|
59
|
+
* @param {{ steps?: unknown[], nestedByParentStep?: { parentStepIndex: number, record: object }[] }} wireStack
|
|
60
|
+
*/
|
|
61
|
+
export function attachStackTypesToIrSteps (irSteps, wireStack) {
|
|
62
|
+
if (!Array.isArray(irSteps) || wireStack == null || typeof wireStack !== 'object') {
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
const wsteps = wireStack.steps
|
|
66
|
+
if (!Array.isArray(wsteps)) return
|
|
67
|
+
|
|
68
|
+
const nestedArr = wireStack.nestedByParentStep ?? []
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < irSteps.length; i++) {
|
|
71
|
+
const node = irSteps[i]
|
|
72
|
+
if (node == null || typeof node !== 'object') continue
|
|
73
|
+
const w = i < wsteps.length ? wsteps[i] : undefined
|
|
74
|
+
if (w == null || typeof w !== 'object') continue
|
|
75
|
+
const slot = /** @type {Record<string, unknown>} */ (w)
|
|
76
|
+
if (Array.isArray(slot.preTypes)) {
|
|
77
|
+
node.preTypes = cloneJsonSafe(slot.preTypes)
|
|
78
|
+
}
|
|
79
|
+
if (slot.postTypes !== undefined) {
|
|
80
|
+
node.postTypes = cloneJsonSafe(slot.postTypes)
|
|
81
|
+
}
|
|
82
|
+
if (node.kind === 'Quotation' && Array.isArray(node.steps)) {
|
|
83
|
+
const nestedEntry = nestedArr.find((e) => e.parentStepIndex === i)
|
|
84
|
+
if (nestedEntry?.record != null) {
|
|
85
|
+
attachStackTypesToIrSteps(
|
|
86
|
+
/** @type {Record<string, unknown>[]} */ (node.steps),
|
|
87
|
+
nestedEntry.record
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { lowerFirst } from './lower-first.js'
|
|
2
|
+
|
|
3
|
+
/** @param {{ name: string }[]} tags */
|
|
4
|
+
export function autogenWordNamesForSumType (typeName, tags) {
|
|
5
|
+
const out = new Set()
|
|
6
|
+
for (const tag of tags) {
|
|
7
|
+
out.add(lowerFirst(tag.name))
|
|
8
|
+
}
|
|
9
|
+
out.add(lowerFirst(typeName))
|
|
10
|
+
return out
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** @param {{ name: string }[]} fields */
|
|
14
|
+
export function autogenWordNamesForProductType (typeName, fields) {
|
|
15
|
+
const out = new Set()
|
|
16
|
+
out.add(lowerFirst(typeName))
|
|
17
|
+
for (const f of fields) {
|
|
18
|
+
out.add(lowerFirst(f.name + typeName))
|
|
19
|
+
}
|
|
20
|
+
out.add(lowerFirst('with' + typeName))
|
|
21
|
+
return out
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Разрешение строки импорта в абсолютный путь (RFC-0.1 §4.2).
|
|
3
|
+
*/
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {string} importerPath
|
|
8
|
+
* @param {string} spec
|
|
9
|
+
* @param {(p: string) => string | null | undefined} readFile
|
|
10
|
+
* @param {((spec: string, fromPath: string) => string | null) | undefined} resolvePackage
|
|
11
|
+
* @returns {{ resolvedPath: string | null, missing: boolean }}
|
|
12
|
+
*/
|
|
13
|
+
export function resolveImportPath (importerPath, spec, readFile, resolvePackage) {
|
|
14
|
+
if (spec.startsWith('./') || spec.startsWith('../')) {
|
|
15
|
+
const resolved = path.normalize(path.join(path.dirname(importerPath), spec))
|
|
16
|
+
const t = readFile(resolved)
|
|
17
|
+
if (t == null || t === undefined) return { resolvedPath: null, missing: true }
|
|
18
|
+
return { resolvedPath: resolved, missing: false }
|
|
19
|
+
}
|
|
20
|
+
if (resolvePackage) {
|
|
21
|
+
const resolved = resolvePackage(spec, importerPath)
|
|
22
|
+
if (!resolved) return { resolvedPath: null, missing: true }
|
|
23
|
+
const t = readFile(resolved)
|
|
24
|
+
if (t == null || t === undefined) return { resolvedPath: null, missing: true }
|
|
25
|
+
return { resolvedPath: resolved, missing: false }
|
|
26
|
+
}
|
|
27
|
+
return { resolvedPath: null, missing: true }
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { resolveSailNames } from './resolve-sail.js'
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Локальные декларации в одном source_file: E1101–E1105, E1108, E1111; экспорт слов/типов (RFC-0.1 §4, §7).
|
|
3
|
+
*/
|
|
4
|
+
import * as diag from '../parse/diagnostics.js'
|
|
5
|
+
import { autogenWordNamesForProductType, autogenWordNamesForSumType } from './adt-autogen.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object[]} items AstTopLevel[]
|
|
9
|
+
* exportWords — только `@word`; autogen ADT не входит (RFC L2). exportTypes — имена `&`.
|
|
10
|
+
* @returns {{ diagnostics: object[], exportWords: Set<string>, exportTypes: Set<string> }}
|
|
11
|
+
*/
|
|
12
|
+
export function analyzeLocalDeclarations (items) {
|
|
13
|
+
const diagnostics = []
|
|
14
|
+
/** @type {Map<string, object>} */
|
|
15
|
+
const firstWordSpan = new Map()
|
|
16
|
+
/** @type {Map<string, object>} */
|
|
17
|
+
const firstTypeSpan = new Map()
|
|
18
|
+
/** @type {Map<string, object>} */
|
|
19
|
+
const globalTagSpan = new Map()
|
|
20
|
+
|
|
21
|
+
const exportWords = new Set()
|
|
22
|
+
const exportTypes = new Set()
|
|
23
|
+
/** Имена autogen в модуле (не входят в exportWords; нужны для E1105 между типами). */
|
|
24
|
+
const autogenUsed = new Set()
|
|
25
|
+
const seenImportModule = new Map()
|
|
26
|
+
|
|
27
|
+
function pushDiag (code, message, span) {
|
|
28
|
+
const d = { code, message }
|
|
29
|
+
if (span?.start) {
|
|
30
|
+
d.offset = span.start.offset
|
|
31
|
+
d.line = span.start.line
|
|
32
|
+
d.column = span.start.column
|
|
33
|
+
}
|
|
34
|
+
diagnostics.push(d)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const item of items) {
|
|
38
|
+
if (item.kind === 'import') {
|
|
39
|
+
if (seenImportModule.has(item.module)) {
|
|
40
|
+
pushDiag('E1111', diag.e1111DuplicateModuleImport(item.module), item.span)
|
|
41
|
+
} else {
|
|
42
|
+
seenImportModule.set(item.module, item.span)
|
|
43
|
+
}
|
|
44
|
+
if (item.bracket) {
|
|
45
|
+
const seenInBracket = new Set()
|
|
46
|
+
for (const w of item.bracket.words) {
|
|
47
|
+
if (seenInBracket.has(w)) {
|
|
48
|
+
pushDiag('E1108', diag.e1108DuplicateBracketImportName(w), item.span)
|
|
49
|
+
}
|
|
50
|
+
seenInBracket.add(w)
|
|
51
|
+
}
|
|
52
|
+
for (const ty of item.bracket.types) {
|
|
53
|
+
if (seenInBracket.has(ty)) {
|
|
54
|
+
pushDiag('E1108', diag.e1108DuplicateBracketImportName(ty), item.span)
|
|
55
|
+
}
|
|
56
|
+
seenInBracket.add(ty)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (item.kind === 'word') {
|
|
63
|
+
if (firstWordSpan.has(item.name)) {
|
|
64
|
+
pushDiag('E1101', diag.e1101DuplicateWord(item.name), item.span)
|
|
65
|
+
} else {
|
|
66
|
+
if (autogenUsed.has(item.name)) {
|
|
67
|
+
pushDiag('E1105', diag.e1105AutogenNameConflict(item.name), item.span)
|
|
68
|
+
}
|
|
69
|
+
firstWordSpan.set(item.name, item.span)
|
|
70
|
+
exportWords.add(item.name)
|
|
71
|
+
}
|
|
72
|
+
continue
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (item.kind === 'sum_type') {
|
|
76
|
+
if (firstTypeSpan.has(item.name)) {
|
|
77
|
+
pushDiag('E1102', diag.e1102DuplicateType(item.name), item.span)
|
|
78
|
+
} else {
|
|
79
|
+
firstTypeSpan.set(item.name, item.span)
|
|
80
|
+
exportTypes.add(item.name)
|
|
81
|
+
}
|
|
82
|
+
for (const tag of item.tags) {
|
|
83
|
+
if (globalTagSpan.has(tag.name)) {
|
|
84
|
+
pushDiag('E1103', diag.e1103DuplicateSumTag(tag.name), tag.span)
|
|
85
|
+
} else {
|
|
86
|
+
globalTagSpan.set(tag.name, tag.span)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const auto = autogenWordNamesForSumType(item.name, item.tags)
|
|
90
|
+
for (const w of auto) {
|
|
91
|
+
if (exportWords.has(w) || autogenUsed.has(w)) {
|
|
92
|
+
pushDiag('E1105', diag.e1105AutogenNameConflict(w), item.span)
|
|
93
|
+
} else {
|
|
94
|
+
autogenUsed.add(w)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (item.kind === 'product_type') {
|
|
101
|
+
if (firstTypeSpan.has(item.name)) {
|
|
102
|
+
pushDiag('E1102', diag.e1102DuplicateType(item.name), item.span)
|
|
103
|
+
} else {
|
|
104
|
+
firstTypeSpan.set(item.name, item.span)
|
|
105
|
+
exportTypes.add(item.name)
|
|
106
|
+
}
|
|
107
|
+
const fieldSeen = new Map()
|
|
108
|
+
for (const f of item.fields) {
|
|
109
|
+
if (fieldSeen.has(f.name)) {
|
|
110
|
+
pushDiag('E1104', diag.e1104DuplicateProductField(f.name, item.name), f.span)
|
|
111
|
+
} else {
|
|
112
|
+
fieldSeen.set(f.name, f.span)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const auto = autogenWordNamesForProductType(item.name, item.fields)
|
|
116
|
+
for (const w of auto) {
|
|
117
|
+
if (exportWords.has(w) || autogenUsed.has(w)) {
|
|
118
|
+
pushDiag('E1105', diag.e1105AutogenNameConflict(w), item.span)
|
|
119
|
+
} else {
|
|
120
|
+
autogenUsed.add(w)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { diagnostics, exportWords, exportTypes }
|
|
127
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Область импорта модуля для L2/L3: unq, importMap (RFC-0.1 §4.2, §9).
|
|
3
|
+
*/
|
|
4
|
+
import * as diag from '../parse/diagnostics.js'
|
|
5
|
+
import { autogenWordNamesForProductType, autogenWordNamesForSumType } from './adt-autogen.js'
|
|
6
|
+
import { resolveImportPath } from './import-path.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} item
|
|
10
|
+
* @returns {Set<string>}
|
|
11
|
+
*/
|
|
12
|
+
export function typeAutogenWords (item) {
|
|
13
|
+
if (item.kind === 'sum_type') {
|
|
14
|
+
return autogenWordNamesForSumType(item.name, item.tags)
|
|
15
|
+
}
|
|
16
|
+
if (item.kind === 'product_type') {
|
|
17
|
+
return autogenWordNamesForProductType(item.name, item.fields)
|
|
18
|
+
}
|
|
19
|
+
return new Set()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Слово доступно как `~Module/name`: явный экспорт или autogen экспортируемого типа. */
|
|
23
|
+
export function moduleExportsWord (dep, name) {
|
|
24
|
+
if (dep.exportWords.has(name)) return true
|
|
25
|
+
for (const ty of dep.exportTypes) {
|
|
26
|
+
const def = dep.typeIndex.get(ty)
|
|
27
|
+
if (!def) continue
|
|
28
|
+
if (typeAutogenWords(def).has(name)) return true
|
|
29
|
+
}
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} span
|
|
35
|
+
* @param {string} code
|
|
36
|
+
* @param {string} message
|
|
37
|
+
*/
|
|
38
|
+
function pushDiag (out, span, code, message) {
|
|
39
|
+
const d = { code, message }
|
|
40
|
+
if (span?.start) {
|
|
41
|
+
d.offset = span.start.offset
|
|
42
|
+
d.line = span.start.line
|
|
43
|
+
d.column = span.start.column
|
|
44
|
+
}
|
|
45
|
+
out.push(d)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {object} snap
|
|
50
|
+
* @param {{ readFile: (p: string) => string | null | undefined, resolvePackage?: (spec: string, fromPath: string) => string | null, snapshots: Map<string, object> }} ctx
|
|
51
|
+
* @returns {{ unq: Set<string>, importMap: Map<string, object>, diags: object[] }}
|
|
52
|
+
*/
|
|
53
|
+
export function buildScopeForSnapshot (snap, ctx) {
|
|
54
|
+
const diags = []
|
|
55
|
+
const unq = new Set()
|
|
56
|
+
const localTypes = new Set()
|
|
57
|
+
/** @type {Map<string, object>} */
|
|
58
|
+
const importMap = new Map()
|
|
59
|
+
|
|
60
|
+
for (const item of snap.items) {
|
|
61
|
+
if (item.kind === 'word') {
|
|
62
|
+
unq.add(item.name)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (item.kind === 'sum_type') {
|
|
66
|
+
localTypes.add(item.name)
|
|
67
|
+
for (const w of autogenWordNamesForSumType(item.name, item.tags)) unq.add(w)
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
if (item.kind === 'product_type') {
|
|
71
|
+
localTypes.add(item.name)
|
|
72
|
+
for (const w of autogenWordNamesForProductType(item.name, item.fields)) unq.add(w)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (item.kind === 'import') {
|
|
76
|
+
const r = resolveImportPath(snap.path, item.path, ctx.readFile, ctx.resolvePackage)
|
|
77
|
+
const dep = r.resolvedPath ? ctx.snapshots.get(r.resolvedPath) : null
|
|
78
|
+
const usable = dep && dep.ok === true
|
|
79
|
+
|
|
80
|
+
if (!usable) {
|
|
81
|
+
pushDiag(diags, item.span, 'E1203', diag.e1203ModuleNotFound(item.module))
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
importMap.set(item.module, dep)
|
|
86
|
+
|
|
87
|
+
if (item.bracket) {
|
|
88
|
+
for (const w of item.bracket.words) {
|
|
89
|
+
if (!dep.exportWords.has(w)) {
|
|
90
|
+
pushDiag(diags, item.span, 'E1110', diag.e1110SymbolMissingInExport(w))
|
|
91
|
+
}
|
|
92
|
+
if (unq.has(w)) {
|
|
93
|
+
pushDiag(diags, item.span, 'E1109', diag.e1109BracketImportConflicts(w))
|
|
94
|
+
}
|
|
95
|
+
unq.add(w)
|
|
96
|
+
}
|
|
97
|
+
for (const ty of item.bracket.types) {
|
|
98
|
+
if (!dep.exportTypes.has(ty)) {
|
|
99
|
+
pushDiag(diags, item.span, 'E1110', diag.e1110SymbolMissingInExport(ty))
|
|
100
|
+
}
|
|
101
|
+
if (localTypes.has(ty)) {
|
|
102
|
+
pushDiag(diags, item.span, 'E1109', diag.e1109BracketImportConflicts(ty))
|
|
103
|
+
}
|
|
104
|
+
localTypes.add(ty)
|
|
105
|
+
const def = dep.typeIndex.get(ty)
|
|
106
|
+
if (def) {
|
|
107
|
+
for (const aw of typeAutogenWords(def)) {
|
|
108
|
+
if (unq.has(aw)) {
|
|
109
|
+
pushDiag(diags, item.span, 'E1109', diag.e1109BracketImportConflicts(aw))
|
|
110
|
+
}
|
|
111
|
+
unq.add(aw)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { unq, importMap, diags }
|
|
120
|
+
}
|