@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.
Files changed (145) hide show
  1. package/bin/sail.mjs +4 -0
  2. package/cli/run-cli.js +176 -0
  3. package/index.js +12 -2
  4. package/lib/codegen/README.md +230 -0
  5. package/lib/codegen/codegen-diagnostics.js +164 -0
  6. package/lib/codegen/compile-graph.js +107 -0
  7. package/lib/codegen/emit-adt.js +177 -0
  8. package/lib/codegen/emit-body.js +1265 -0
  9. package/lib/codegen/emit-builtin.js +371 -0
  10. package/lib/codegen/emit-jsdoc-sail.js +383 -0
  11. package/lib/codegen/emit-module.js +498 -0
  12. package/lib/codegen/esm-imports.js +26 -0
  13. package/lib/codegen/index.js +69 -0
  14. package/lib/codegen/out-layout.js +102 -0
  15. package/lib/ffi/extract-jsdoc-sail.js +34 -0
  16. package/lib/io-node/index.js +4 -0
  17. package/lib/io-node/package-root.js +18 -0
  18. package/lib/io-node/read-file.js +12 -0
  19. package/lib/io-node/resolve-package.js +24 -0
  20. package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
  21. package/lib/ir/assert-json-serializable.js +30 -0
  22. package/lib/ir/attach-call-effects.js +108 -0
  23. package/lib/ir/bind-values.js +594 -0
  24. package/lib/ir/build-module-ir.js +290 -0
  25. package/lib/ir/index.js +31 -0
  26. package/lib/ir/lower-body-steps.js +170 -0
  27. package/lib/ir/module-metadata.js +65 -0
  28. package/lib/ir/schema-version.js +15 -0
  29. package/lib/ir/serialize.js +202 -0
  30. package/lib/ir/stitch-types.js +92 -0
  31. package/lib/names/adt-autogen.js +22 -0
  32. package/lib/names/import-path.js +28 -0
  33. package/lib/names/index.js +1 -0
  34. package/lib/names/local-declarations.js +127 -0
  35. package/lib/names/lower-first.js +6 -0
  36. package/lib/names/module-scope.js +120 -0
  37. package/lib/names/resolve-sail.js +365 -0
  38. package/lib/names/walk-ast-refs.js +91 -0
  39. package/lib/parse/ast-build.js +51 -0
  40. package/lib/parse/ast-spec.js +212 -0
  41. package/lib/parse/builtins-set.js +12 -0
  42. package/lib/parse/diagnostics.js +180 -0
  43. package/lib/parse/index.js +46 -0
  44. package/lib/parse/lexer.js +390 -0
  45. package/lib/parse/parse-source.js +912 -0
  46. package/lib/typecheck/adt-autogen-sigs.js +345 -0
  47. package/lib/typecheck/build-type-env.js +148 -0
  48. package/lib/typecheck/builtin-signatures.js +183 -0
  49. package/lib/typecheck/check-word-body.js +1021 -0
  50. package/lib/typecheck/effect-decl.js +124 -0
  51. package/lib/typecheck/index.js +55 -0
  52. package/lib/typecheck/normalize-sig.js +369 -0
  53. package/lib/typecheck/stack-step-snapshots.js +56 -0
  54. package/lib/typecheck/unify-type.js +665 -0
  55. package/lib/typecheck/validate-adt.js +201 -0
  56. package/package.json +4 -9
  57. package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
  58. package/test/cli/sail-cli.test.js +64 -0
  59. package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
  60. package/test/codegen/compile-stage0.test.js +128 -0
  61. package/test/codegen/compile-stage4-layout.test.js +124 -0
  62. package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
  63. package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +34 -0
  64. package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
  65. package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
  66. package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
  67. package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
  68. package/test/codegen/emit-adt-stage6.test.js +168 -0
  69. package/test/codegen/emit-async-stage5.test.js +164 -0
  70. package/test/codegen/emit-body-stage2.test.js +139 -0
  71. package/test/codegen/emit-body.test.js +163 -0
  72. package/test/codegen/emit-builtins-stage7.test.js +258 -0
  73. package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
  74. package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
  75. package/test/codegen/emit-module-stage3.test.js +78 -0
  76. package/test/conformance/conformance-ir-l4.test.js +38 -0
  77. package/test/conformance/conformance-l5-codegen.test.js +111 -0
  78. package/test/conformance/conformance-runner.js +91 -0
  79. package/test/conformance/conformance-suite-l3.test.js +32 -0
  80. package/test/ffi/prelude-jsdoc.test.js +49 -0
  81. package/test/fixtures/demo-full-syntax.ast.json +1471 -0
  82. package/test/fixtures/demo-full-syntax.sail +35 -0
  83. package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
  84. package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
  85. package/test/fixtures/io-node-mini/dep.sail +2 -0
  86. package/test/fixtures/io-node-mini/entry.sail +4 -0
  87. package/test/fixtures/io-node-prelude/entry.sail +4 -0
  88. package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
  89. package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
  90. package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
  91. package/test/io-node/resolve-disk.test.js +59 -0
  92. package/test/ir/bind-values.test.js +84 -0
  93. package/test/ir/build-module-ir.test.js +100 -0
  94. package/test/ir/call-effects.test.js +97 -0
  95. package/test/ir/ffi-bracket-ir.test.js +59 -0
  96. package/test/ir/full-ir-document.test.js +51 -0
  97. package/test/ir/ir-document-assert.js +67 -0
  98. package/test/ir/lower-body-steps.test.js +90 -0
  99. package/test/ir/module-metadata.test.js +42 -0
  100. package/test/ir/serialization-model.test.js +172 -0
  101. package/test/ir/stitch-types.test.js +74 -0
  102. package/test/names/l2-resolve-adt-autogen.test.js +155 -0
  103. package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
  104. package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
  105. package/test/names/l2-resolve-graph.test.js +105 -0
  106. package/test/names/l2-resolve-single-file.test.js +79 -0
  107. package/test/parse/ast-spec.test.js +56 -0
  108. package/test/parse/ast.test.js +476 -0
  109. package/test/parse/contract.test.js +37 -0
  110. package/test/parse/fixtures-full-syntax.test.js +24 -0
  111. package/test/parse/helpers.js +27 -0
  112. package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
  113. package/test/parse/l0-lex.test.js +40 -0
  114. package/test/parse/l1-diagnostics.test.js +77 -0
  115. package/test/parse/l1-import.test.js +28 -0
  116. package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
  117. package/test/parse/l1-top-level.test.js +47 -0
  118. package/test/parse/l1-types.test.js +31 -0
  119. package/test/parse/l1-words.test.js +49 -0
  120. package/test/parse/l2-diagnostics-contract.test.js +67 -0
  121. package/test/parse/l3-diagnostics-contract.test.js +66 -0
  122. package/test/typecheck/adt-decl-stage2.test.js +83 -0
  123. package/test/typecheck/container-contract-e1309.test.js +258 -0
  124. package/test/typecheck/ffi-bracket-l3.test.js +61 -0
  125. package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
  126. package/test/typecheck/l3-partial-pipeline.test.js +74 -0
  127. package/test/typecheck/opaque-ffi-type.test.js +78 -0
  128. package/test/typecheck/sig-type-stage3.test.js +190 -0
  129. package/test/typecheck/stack-check-stage4.test.js +149 -0
  130. package/test/typecheck/stack-check-stage5.test.js +74 -0
  131. package/test/typecheck/stack-check-stage6.test.js +56 -0
  132. package/test/typecheck/stack-check-stage7.test.js +160 -0
  133. package/test/typecheck/stack-check-stage8.test.js +146 -0
  134. package/test/typecheck/stack-check-stage9.test.js +105 -0
  135. package/test/typecheck/typecheck-env.test.js +53 -0
  136. package/test/typecheck/typecheck-pipeline.test.js +37 -0
  137. package/README.md +0 -37
  138. package/cli/sail.js +0 -151
  139. package/cli/typecheck.js +0 -39
  140. package/docs/ARCHITECTURE.md +0 -50
  141. package/docs/CHANGELOG.md +0 -18
  142. package/docs/FFI-GUIDE.md +0 -65
  143. package/docs/RELEASE.md +0 -36
  144. package/docs/TESTING.md +0 -86
  145. 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,6 @@
1
+ /** RFC-0.1 §7.1.2 / §7.2.1 — lower-first для автоген-имён. */
2
+
3
+ export function lowerFirst (s) {
4
+ if (s == null || s.length === 0) return s
5
+ return s[0].toLowerCase() + s.slice(1)
6
+ }
@@ -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
+ }