@algosail/lang 0.2.12 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sail.mjs +4 -0
- package/cli/run-cli.js +176 -0
- package/index.js +11 -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 +33 -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,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E1309: встроенные контейнеры List / Dict / Map — сходимость параметров (RFC-0.1 §13, unify-type.js).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import * as d from '../../lib/parse/diagnostics.js'
|
|
7
|
+
import { typecheckSail } from '../../index.js'
|
|
8
|
+
import { unifyTypes } from '../../lib/typecheck/unify-type.js'
|
|
9
|
+
|
|
10
|
+
const root = path.resolve('/virtual/sail-e1309-containers')
|
|
11
|
+
function vfs (files) {
|
|
12
|
+
const norm = Object.fromEntries(
|
|
13
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
14
|
+
)
|
|
15
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function prim (name) {
|
|
19
|
+
return { kind: 'prim', name, source: null, decl: null }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function app (ctor, args) {
|
|
23
|
+
return { kind: 'app', ctor, args, source: null, decl: null }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('E1309: unifyTypes List Num vs List Str', function (t) {
|
|
27
|
+
const subst = new WeakMap()
|
|
28
|
+
const diags = []
|
|
29
|
+
const ok = unifyTypes(
|
|
30
|
+
app('List', [prim('Num')]),
|
|
31
|
+
app('List', [prim('Str')]),
|
|
32
|
+
subst,
|
|
33
|
+
diags,
|
|
34
|
+
null
|
|
35
|
+
)
|
|
36
|
+
t.ok(ok === false)
|
|
37
|
+
t.is(diags.length, 1)
|
|
38
|
+
t.is(diags[0].code, 'E1309')
|
|
39
|
+
t.is(
|
|
40
|
+
diags[0].message,
|
|
41
|
+
d.e1309ContainerContract('List', 'Num', 'Str')
|
|
42
|
+
)
|
|
43
|
+
t.end()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('E1309: unifyTypes Dict — несходимость ключа', function (t) {
|
|
47
|
+
const subst = new WeakMap()
|
|
48
|
+
const diags = []
|
|
49
|
+
const ok = unifyTypes(
|
|
50
|
+
app('Dict', [prim('Bool'), prim('Num')]),
|
|
51
|
+
app('Dict', [prim('Str'), prim('Num')]),
|
|
52
|
+
subst,
|
|
53
|
+
diags,
|
|
54
|
+
null
|
|
55
|
+
)
|
|
56
|
+
t.ok(ok === false)
|
|
57
|
+
t.is(diags[0].code, 'E1309')
|
|
58
|
+
t.is(
|
|
59
|
+
diags[0].message,
|
|
60
|
+
d.e1309ContainerContract('Dict', 'Bool', 'Str')
|
|
61
|
+
)
|
|
62
|
+
t.end()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('E1309: unifyTypes Dict — несходимость значения', function (t) {
|
|
66
|
+
const subst = new WeakMap()
|
|
67
|
+
const diags = []
|
|
68
|
+
const ok = unifyTypes(
|
|
69
|
+
app('Dict', [prim('Str'), prim('Bool')]),
|
|
70
|
+
app('Dict', [prim('Str'), prim('Num')]),
|
|
71
|
+
subst,
|
|
72
|
+
diags,
|
|
73
|
+
null
|
|
74
|
+
)
|
|
75
|
+
t.ok(ok === false)
|
|
76
|
+
t.is(diags[0].code, 'E1309')
|
|
77
|
+
t.is(
|
|
78
|
+
diags[0].message,
|
|
79
|
+
d.e1309ContainerContract('Dict', 'Bool', 'Num')
|
|
80
|
+
)
|
|
81
|
+
t.end()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('E1309: unifyTypes Map — только первая позиция (одна диагностика)', function (t) {
|
|
85
|
+
const subst = new WeakMap()
|
|
86
|
+
const diags = []
|
|
87
|
+
const ok = unifyTypes(
|
|
88
|
+
app('Map', [prim('Bool'), prim('Bool')]),
|
|
89
|
+
app('Map', [prim('Str'), prim('Num')]),
|
|
90
|
+
subst,
|
|
91
|
+
diags,
|
|
92
|
+
null
|
|
93
|
+
)
|
|
94
|
+
t.ok(ok === false)
|
|
95
|
+
t.is(diags.filter((x) => x.code === 'E1309').length, 1)
|
|
96
|
+
t.is(
|
|
97
|
+
diags[0].message,
|
|
98
|
+
d.e1309ContainerContract('Map', 'Bool', 'Str')
|
|
99
|
+
)
|
|
100
|
+
t.end()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('E1309: unifyTypes Map — вторая позиция', function (t) {
|
|
104
|
+
const subst = new WeakMap()
|
|
105
|
+
const diags = []
|
|
106
|
+
const ok = unifyTypes(
|
|
107
|
+
app('Map', [prim('Str'), prim('Bool')]),
|
|
108
|
+
app('Map', [prim('Str'), prim('Num')]),
|
|
109
|
+
subst,
|
|
110
|
+
diags,
|
|
111
|
+
null
|
|
112
|
+
)
|
|
113
|
+
t.ok(ok === false)
|
|
114
|
+
t.is(diags[0].code, 'E1309')
|
|
115
|
+
t.is(
|
|
116
|
+
diags[0].message,
|
|
117
|
+
d.e1309ContainerContract('Map', 'Bool', 'Num')
|
|
118
|
+
)
|
|
119
|
+
t.end()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('E1305 внутри List: не подменять на E1309', function (t) {
|
|
123
|
+
const subst = new WeakMap()
|
|
124
|
+
const diags = []
|
|
125
|
+
const v = { kind: 'tvar', name: 'a', source: null, decl: null }
|
|
126
|
+
const inner = app('List', [v])
|
|
127
|
+
const ok = unifyTypes(v, inner, subst, diags, null)
|
|
128
|
+
t.ok(ok === false)
|
|
129
|
+
t.is(diags[0].code, 'E1305')
|
|
130
|
+
t.end()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('E1304: List vs Map (разные конструкторы)', function (t) {
|
|
134
|
+
const subst = new WeakMap()
|
|
135
|
+
const diags = []
|
|
136
|
+
const ok = unifyTypes(
|
|
137
|
+
app('List', [prim('Num')]),
|
|
138
|
+
app('Map', [prim('Str'), prim('Num')]),
|
|
139
|
+
subst,
|
|
140
|
+
diags,
|
|
141
|
+
null
|
|
142
|
+
)
|
|
143
|
+
t.ok(ok === false)
|
|
144
|
+
t.is(diags[0].code, 'E1304')
|
|
145
|
+
t.ok(diags[0].message.includes('List'))
|
|
146
|
+
t.ok(diags[0].message.includes('Map'))
|
|
147
|
+
t.end()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('интеграция: List Num vs List Str при вызове — E1309', function (t) {
|
|
151
|
+
const main = path.join(root, 'list-call.sail')
|
|
152
|
+
const src = [
|
|
153
|
+
'@need ( List Str -> )',
|
|
154
|
+
'',
|
|
155
|
+
' drop',
|
|
156
|
+
'',
|
|
157
|
+
'',
|
|
158
|
+
'@w ( List Num -> )',
|
|
159
|
+
'',
|
|
160
|
+
' /need',
|
|
161
|
+
''
|
|
162
|
+
].join('\n')
|
|
163
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
164
|
+
t.ok(r.ok === false)
|
|
165
|
+
const e1309 = r.diagnostics.filter((x) => x.code === 'E1309')
|
|
166
|
+
t.is(e1309.length, 1)
|
|
167
|
+
t.is(e1309[0].message, d.e1309ContainerContract('List', 'Num', 'Str'))
|
|
168
|
+
t.end()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
test('интеграция: Map ключ — E1309', function (t) {
|
|
172
|
+
const main = path.join(root, 'map-key.sail')
|
|
173
|
+
const src = [
|
|
174
|
+
'@need ( (Map Str Num) -> )',
|
|
175
|
+
'',
|
|
176
|
+
' drop',
|
|
177
|
+
'',
|
|
178
|
+
'',
|
|
179
|
+
'@w ( (Map Bool Num) -> )',
|
|
180
|
+
'',
|
|
181
|
+
' /need',
|
|
182
|
+
''
|
|
183
|
+
].join('\n')
|
|
184
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
185
|
+
t.ok(r.ok === false)
|
|
186
|
+
t.is(r.diagnostics.filter((x) => x.code === 'E1309').length, 1)
|
|
187
|
+
t.ok(
|
|
188
|
+
r.diagnostics.some(
|
|
189
|
+
(x) =>
|
|
190
|
+
x.code === 'E1309' &&
|
|
191
|
+
x.message === d.e1309ContainerContract('Map', 'Bool', 'Str')
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
t.end()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test('интеграция: Map значение — E1309', function (t) {
|
|
198
|
+
const main = path.join(root, 'map-val.sail')
|
|
199
|
+
const src = [
|
|
200
|
+
'@need ( (Map Str Num) -> )',
|
|
201
|
+
'',
|
|
202
|
+
' drop',
|
|
203
|
+
'',
|
|
204
|
+
'',
|
|
205
|
+
'@w ( (Map Str Bool) -> )',
|
|
206
|
+
'',
|
|
207
|
+
' /need',
|
|
208
|
+
''
|
|
209
|
+
].join('\n')
|
|
210
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
211
|
+
t.ok(r.ok === false)
|
|
212
|
+
t.is(r.diagnostics.filter((x) => x.code === 'E1309').length, 1)
|
|
213
|
+
t.ok(
|
|
214
|
+
r.diagnostics.some(
|
|
215
|
+
(x) =>
|
|
216
|
+
x.code === 'E1309' &&
|
|
217
|
+
x.message === d.e1309ContainerContract('Map', 'Bool', 'Num')
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
t.end()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('интеграция: List vs Map — E1304', function (t) {
|
|
224
|
+
const main = path.join(root, 'list-vs-map.sail')
|
|
225
|
+
const src = [
|
|
226
|
+
'@need ( List Num -> )',
|
|
227
|
+
'',
|
|
228
|
+
' drop',
|
|
229
|
+
'',
|
|
230
|
+
'',
|
|
231
|
+
'@w ( (Map Str Num) -> )',
|
|
232
|
+
'',
|
|
233
|
+
' /need',
|
|
234
|
+
''
|
|
235
|
+
].join('\n')
|
|
236
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
237
|
+
t.ok(r.ok === false)
|
|
238
|
+
t.ok(r.diagnostics.some((x) => x.code === 'E1304'))
|
|
239
|
+
t.ok(!r.diagnostics.some((x) => x.code === 'E1309'))
|
|
240
|
+
t.end()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
test('вложенный контейнер: внутреннее расхождение — E1309 по внутреннему List', function (t) {
|
|
244
|
+
const subst = new WeakMap()
|
|
245
|
+
const diags = []
|
|
246
|
+
const ok = unifyTypes(
|
|
247
|
+
app('List', [app('List', [prim('Num')])]),
|
|
248
|
+
app('List', [app('List', [prim('Str')])]),
|
|
249
|
+
subst,
|
|
250
|
+
diags,
|
|
251
|
+
null
|
|
252
|
+
)
|
|
253
|
+
t.ok(ok === false)
|
|
254
|
+
const e1309 = diags.filter((x) => x.code === 'E1309')
|
|
255
|
+
t.ok(e1309.length >= 1)
|
|
256
|
+
t.is(e1309[0].message, d.e1309ContainerContract('List', 'Num', 'Str'))
|
|
257
|
+
t.end()
|
|
258
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3: FFI `.js` — контракт из JSDoc без проверки тела; скобочный импорт и вызов /word.
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
|
|
8
|
+
const root = path.resolve('/virtual/sail-l3-ffi-bracket')
|
|
9
|
+
|
|
10
|
+
function vfs (files) {
|
|
11
|
+
const norm = Object.fromEntries(
|
|
12
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
13
|
+
)
|
|
14
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('L3: скобочный FFI + /add — ok', function (t) {
|
|
18
|
+
const main = path.join(root, 'main.sail')
|
|
19
|
+
const num = path.join(root, 'num.js')
|
|
20
|
+
const files = {
|
|
21
|
+
[main]: [
|
|
22
|
+
'+Num ( @add ) ./num.js',
|
|
23
|
+
'@main ( Num Num -> Num )',
|
|
24
|
+
'',
|
|
25
|
+
' /add'
|
|
26
|
+
].join('\n'),
|
|
27
|
+
[num]: [
|
|
28
|
+
'/**',
|
|
29
|
+
' * @sail',
|
|
30
|
+
' * @add ( Num Num -> Num )',
|
|
31
|
+
' */',
|
|
32
|
+
'export function add (a, b) { return a + b }'
|
|
33
|
+
].join('\n')
|
|
34
|
+
}
|
|
35
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
|
|
36
|
+
t.ok(r.ok, r.ok === false ? r.diagnostics.map((d) => d.message).join('; ') : '')
|
|
37
|
+
t.end()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('L3: полный импорт FFI + ~Num/add — ok', function (t) {
|
|
41
|
+
const main = path.join(root, 'main-full.sail')
|
|
42
|
+
const num = path.join(root, 'num-full.js')
|
|
43
|
+
const files = {
|
|
44
|
+
[main]: [
|
|
45
|
+
'+Num ./num-full.js',
|
|
46
|
+
'@main ( Num Num -> Num )',
|
|
47
|
+
'',
|
|
48
|
+
' ~Num/add'
|
|
49
|
+
].join('\n'),
|
|
50
|
+
[num]: [
|
|
51
|
+
'/**',
|
|
52
|
+
' * @sail',
|
|
53
|
+
' * @add ( Num Num -> Num )',
|
|
54
|
+
' */',
|
|
55
|
+
'export function add (a, b) { return a + b }'
|
|
56
|
+
].join('\n')
|
|
57
|
+
}
|
|
58
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
|
|
59
|
+
t.ok(r.ok, r.ok === false ? r.diagnostics.map((d) => d.message).join('; ') : '')
|
|
60
|
+
t.end()
|
|
61
|
+
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Матрица L3 (RFC-0.1 §13 E1301–E1314, RFC-typecheck-0.1): диагностики typecheck.
|
|
3
|
+
* Шаблоны сообщений дублируются здесь для навигации рядом со сценариями; канон — [l3-diagnostics-contract.test.js](../parse/l3-diagnostics-contract.test.js).
|
|
4
|
+
* Эталонные многофайловые сценарии — [conformance/suite.json](../../../conformance/suite.json), прогон L2/L3: [conformance-suite-l3.test.js](../conformance/conformance-suite-l3.test.js).
|
|
5
|
+
*
|
|
6
|
+
* | Код | Источник (lang/lib/typecheck/ и др.) | Тест(ы) | conformance `id` (если есть) |
|
|
7
|
+
* |-------|--------------------------------------|---------|------------------------------|
|
|
8
|
+
* | E1301 | normalize-sig.js (normalizeWordSignature) | **ниже** | — |
|
|
9
|
+
* | E1302 | шаблон RFC; эмиссия из normalize-sig **не подключена** (зарезервировано до уточнения RFC, см. JSDoc `typecheckSail`) | **ниже** (шаблон) | — |
|
|
10
|
+
* | E1303 | check-word-body.js, unify-type.js | **ниже** | reject-e1303-stack-underflow; reject-e1307 (`codes`) |
|
|
11
|
+
* | E1304 | normalize-sig.js (`type_app` без ADT), check-word-body.js, unify-type.js | **ниже** | reject-e1304-unknown-type-app-ctor; reject-e1307 (`codes`). Свободный `type_name` без ADT → **opaque** (RFC-0.1 §5.4), не E1304. |
|
|
12
|
+
* | E1305 | unify-type.js (occurs check) | **ниже** (unifyTypes) | — |
|
|
13
|
+
* | E1306 | check-word-body.js `applyCallStack` | **ниже**; `call` без quotation на вершине | — |
|
|
14
|
+
* | E1307 | check-word-body.js (eliminator) | **ниже** | reject-e1307-eliminator-branches |
|
|
15
|
+
* | E1308 | check-word-body.js (eliminator) | **ниже** | reject-e1308-eliminator-missing-quotes |
|
|
16
|
+
* | E1309 | unify-type.js (встроенные List/Dict/Map, несходимость параметров) | [container-contract-e1309.test.js](container-contract-e1309.test.js), **ниже** (шаблон) | — |
|
|
17
|
+
* | E1310 | check-word-body.js (+Async) | **ниже** | reject-e1310-async-not-propagated |
|
|
18
|
+
* | E1311 | normalize-sig.js | **ниже** | reject-e1311-parameterized-slot |
|
|
19
|
+
* | E1312 | check-word-body.js (+Fail) | **ниже** | reject-e1312-fail-not-propagated |
|
|
20
|
+
* | E1313 | check-word-body.js (:slot) | **ниже** | reject-e1313-slot-redefine |
|
|
21
|
+
* | E1314 | check-word-body.js (;slot) | **ниже** | reject-e1314-slot-read-before-write |
|
|
22
|
+
*
|
|
23
|
+
* L2 (не E13xx, но тот же раннер conformance): E1110–E1112, E1201, E1206 — names/resolve и см. conformance `reject-e*`.
|
|
24
|
+
*/
|
|
25
|
+
import test from 'brittle'
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
import * as d from '../../lib/parse/diagnostics.js'
|
|
28
|
+
import { typecheckSail } from '../../index.js'
|
|
29
|
+
import { normalizeWordSignature } from '../../lib/typecheck/normalize-sig.js'
|
|
30
|
+
import { unifyTypes } from '../../lib/typecheck/unify-type.js'
|
|
31
|
+
|
|
32
|
+
const root = path.resolve('/virtual/sail-l3-matrix')
|
|
33
|
+
function vfs (files) {
|
|
34
|
+
const norm = Object.fromEntries(
|
|
35
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
36
|
+
)
|
|
37
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const mockSpan = {
|
|
41
|
+
start: { offset: 0, line: 1, column: 1 },
|
|
42
|
+
end: { offset: 1, line: 1, column: 2 }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('E1301: нет сигнатуры у слова — normalizeWordSignature', function (t) {
|
|
46
|
+
const ctx = {
|
|
47
|
+
env: { snapshots: new Map() },
|
|
48
|
+
modulePath: root,
|
|
49
|
+
scopeInfo: { typeIndex: new Map(), importMap: new Map() },
|
|
50
|
+
diagnostics: []
|
|
51
|
+
}
|
|
52
|
+
const word = { name: 'w', signature: null, span: mockSpan }
|
|
53
|
+
const ir = normalizeWordSignature(word, ctx)
|
|
54
|
+
t.is(ir, null)
|
|
55
|
+
const diag0 = ctx.diagnostics[0]
|
|
56
|
+
t.is(diag0.code, 'E1301')
|
|
57
|
+
t.is(diag0.message, d.e1301WordSignature('w'))
|
|
58
|
+
t.end()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('E1302: шаблон RFC (эмиссия E1302 из typecheck пока не подключена)', function (t) {
|
|
62
|
+
t.is(
|
|
63
|
+
d.e1302QuotationSig('oops'),
|
|
64
|
+
"Некорректная сигнатура quotation: 'oops'."
|
|
65
|
+
)
|
|
66
|
+
t.end()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('E1303: несогласованный стек (drop при Int -> Int)', function (t) {
|
|
70
|
+
const main = path.join(root, 'e1303-drop.sail')
|
|
71
|
+
const src = ['@main ( Int -> Int )', '', ' drop', ''].join('\n')
|
|
72
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
73
|
+
t.ok(r.ok === false)
|
|
74
|
+
t.ok(r.diagnostics.some((x) => x.code === 'E1303'), 'expected E1303')
|
|
75
|
+
t.end()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('E1304: неизвестный ctor в type_app (opaque только как голый TypeName)', function (t) {
|
|
79
|
+
const main = path.join(root, 'e1304-unknown-ctor.sail')
|
|
80
|
+
const src = ['@m ( UnknownCtor Num -> )', '', ' drop', ''].join('\n')
|
|
81
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
82
|
+
t.ok(r.ok === false)
|
|
83
|
+
const di = r.diagnostics.find((x) => x.code === 'E1304')
|
|
84
|
+
t.ok(di, 'E1304')
|
|
85
|
+
t.ok(di.message.includes('UnknownCtor'))
|
|
86
|
+
t.end()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('E1305: occurs-check при унификации (tvar внутри себя)', function (t) {
|
|
90
|
+
const subst = new WeakMap()
|
|
91
|
+
const diags = []
|
|
92
|
+
const a = { kind: 'tvar', name: 'a', source: null }
|
|
93
|
+
const listA = {
|
|
94
|
+
kind: 'app',
|
|
95
|
+
ctor: 'List',
|
|
96
|
+
args: [a],
|
|
97
|
+
source: null,
|
|
98
|
+
decl: null
|
|
99
|
+
}
|
|
100
|
+
const ok = unifyTypes(a, listA, subst, diags, null)
|
|
101
|
+
t.ok(ok === false)
|
|
102
|
+
const di = diags.find((x) => x.code === 'E1305')
|
|
103
|
+
t.ok(di, 'E1305')
|
|
104
|
+
t.is(di.message, d.e1305OccursCheck('a'))
|
|
105
|
+
t.end()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('E1306: шаблон RFC; call без quotation на вершине — E1306', function (t) {
|
|
109
|
+
t.is(
|
|
110
|
+
d.e1306CallNeedsQuote('Num'),
|
|
111
|
+
'Нельзя применить CALL: ожидается Quote (I -> O), получено Num.'
|
|
112
|
+
)
|
|
113
|
+
const main = path.join(root, 'e1306-call-num.sail')
|
|
114
|
+
const src = ['@w ( Num -> )', '', ' 1', '', ' call', ''].join('\n')
|
|
115
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
116
|
+
t.ok(r.ok === false)
|
|
117
|
+
const hit = r.diagnostics.find((x) => x.code === 'E1306')
|
|
118
|
+
t.ok(hit, 'call с Num на вершине → E1306')
|
|
119
|
+
t.is(hit.message, d.e1306CallNeedsQuote('Num'))
|
|
120
|
+
t.end()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('E1306: call при вершине Bool — E1306', function (t) {
|
|
124
|
+
const main = path.join(root, 'e1306-call-bool.sail')
|
|
125
|
+
const src = ['@w ( Bool -> )', '', ' true', '', ' call', ''].join('\n')
|
|
126
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
127
|
+
t.ok(r.ok === false)
|
|
128
|
+
const hit = r.diagnostics.find((x) => x.code === 'E1306')
|
|
129
|
+
t.ok(hit)
|
|
130
|
+
t.is(hit.message, d.e1306CallNeedsQuote('Bool'))
|
|
131
|
+
t.end()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('E1307: ветки /maybe несовместимы', function (t) {
|
|
135
|
+
const main = path.join(root, 'e1307-maybe.sail')
|
|
136
|
+
const src = [
|
|
137
|
+
'&Maybe a',
|
|
138
|
+
'| Nothing',
|
|
139
|
+
'| Just a',
|
|
140
|
+
'',
|
|
141
|
+
'@w ( ~s (Maybe a) -> ~s Num )',
|
|
142
|
+
' ( )',
|
|
143
|
+
' ( )',
|
|
144
|
+
' /maybe',
|
|
145
|
+
''
|
|
146
|
+
].join('\n')
|
|
147
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
148
|
+
t.ok(r.ok === false)
|
|
149
|
+
t.ok(r.diagnostics.some((x) => x.code === 'E1307'), 'E1307')
|
|
150
|
+
t.end()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
test('E1308: eliminator /maybe без quotations', function (t) {
|
|
154
|
+
const main = path.join(root, 'e1308-maybe.sail')
|
|
155
|
+
const src = [
|
|
156
|
+
'&Maybe a',
|
|
157
|
+
'| Nothing',
|
|
158
|
+
'| Just a',
|
|
159
|
+
'',
|
|
160
|
+
'@w ( ~s (Maybe a) -> ~s )',
|
|
161
|
+
' /maybe',
|
|
162
|
+
''
|
|
163
|
+
].join('\n')
|
|
164
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
165
|
+
t.ok(r.ok === false)
|
|
166
|
+
t.ok(r.diagnostics.some((x) => x.code === 'E1308'), 'E1308')
|
|
167
|
+
t.end()
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('E1309: шаблон RFC (сценарии — container-contract-e1309.test.js)', function (t) {
|
|
171
|
+
t.is(
|
|
172
|
+
d.e1309ContainerContract('List', 'T', 'Str'),
|
|
173
|
+
"Нарушение ограничений контейнера 'List': expected T, got Str."
|
|
174
|
+
)
|
|
175
|
+
t.end()
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('E1310: +Async не всплывает к вызывающему', function (t) {
|
|
179
|
+
const main = path.join(root, 'e1310-async.sail')
|
|
180
|
+
const src = [
|
|
181
|
+
'@run ( -> +Async )',
|
|
182
|
+
'',
|
|
183
|
+
'@main ( -> )',
|
|
184
|
+
'',
|
|
185
|
+
' /run',
|
|
186
|
+
''
|
|
187
|
+
].join('\n')
|
|
188
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
189
|
+
t.ok(r.ok === false)
|
|
190
|
+
t.ok(r.diagnostics.some((x) => x.code === 'E1310'), 'E1310')
|
|
191
|
+
t.end()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('E1311: параметризованный тип без скобок в слоте', function (t) {
|
|
195
|
+
const main = path.join(root, 'e1311-maybe.sail')
|
|
196
|
+
const src = [
|
|
197
|
+
'&Maybe a',
|
|
198
|
+
'| Nothing',
|
|
199
|
+
'| Just a',
|
|
200
|
+
'',
|
|
201
|
+
'@f ( Maybe -> )',
|
|
202
|
+
'',
|
|
203
|
+
' drop',
|
|
204
|
+
''
|
|
205
|
+
].join('\n')
|
|
206
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
207
|
+
t.ok(r.ok === false)
|
|
208
|
+
t.ok(r.diagnostics.some((x) => x.code === 'E1311'), 'E1311')
|
|
209
|
+
t.end()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
test('E1312: +Fail не всплывает к вызывающему', function (t) {
|
|
213
|
+
const main = path.join(root, 'e1312-fail.sail')
|
|
214
|
+
const src = [
|
|
215
|
+
'@risky ( -> +Fail )',
|
|
216
|
+
'',
|
|
217
|
+
'@main ( -> )',
|
|
218
|
+
'',
|
|
219
|
+
' /risky',
|
|
220
|
+
''
|
|
221
|
+
].join('\n')
|
|
222
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
223
|
+
t.ok(r.ok === false)
|
|
224
|
+
t.ok(r.diagnostics.some((x) => x.code === 'E1312'), 'E1312')
|
|
225
|
+
t.end()
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('E1313: повторная :x', function (t) {
|
|
229
|
+
const main = path.join(root, 'e1313-slot.sail')
|
|
230
|
+
const src = ['@w ( Num -> Num )', '', ' dup', '', ' :x', '', ' :x', ''].join('\n')
|
|
231
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
232
|
+
t.ok(r.ok === false)
|
|
233
|
+
const di = r.diagnostics.find((x) => x.code === 'E1313')
|
|
234
|
+
t.ok(di, 'E1313')
|
|
235
|
+
t.is(di.message, d.e1313DuplicateSlotWrite('x'))
|
|
236
|
+
t.end()
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
test('E1314: ;x до :x', function (t) {
|
|
240
|
+
const main = path.join(root, 'e1314-slot.sail')
|
|
241
|
+
const src = ['@w ( Num -> Num )', '', ' ;x', ''].join('\n')
|
|
242
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
243
|
+
t.ok(r.ok === false)
|
|
244
|
+
const di = r.diagnostics.find((x) => x.code === 'E1314')
|
|
245
|
+
t.ok(di, 'E1314')
|
|
246
|
+
t.is(di.message, d.e1314SlotReadBeforeWrite('x'))
|
|
247
|
+
t.end()
|
|
248
|
+
})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L3: частичный env и полный отчёт диагностик (RFC-typecheck §5.1, §3).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import { verifySnapshotChainStitches } from '../../lib/typecheck/stack-step-snapshots.js'
|
|
8
|
+
|
|
9
|
+
const root = path.resolve('/virtual/sail-l3-partial')
|
|
10
|
+
function vfs (files) {
|
|
11
|
+
const norm = Object.fromEntries(
|
|
12
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
13
|
+
)
|
|
14
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('L3: два ADT — сломанный не в adtByPath, валидный остаётся', function (t) {
|
|
18
|
+
const main = path.join(root, 'two-adt.sail')
|
|
19
|
+
const lib = path.join(root, 'libpt.sail')
|
|
20
|
+
const files = {
|
|
21
|
+
[main]: [
|
|
22
|
+
'+Lib ./libpt.sail',
|
|
23
|
+
'&Bad',
|
|
24
|
+
' |X ~Lib/Ghost',
|
|
25
|
+
'&Good',
|
|
26
|
+
' |Y Num',
|
|
27
|
+
'@main ( -> )',
|
|
28
|
+
''
|
|
29
|
+
].join('\n'),
|
|
30
|
+
[lib]: ['&Point', ' :x Num', ' :y Num', ''].join('\n')
|
|
31
|
+
}
|
|
32
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
|
|
33
|
+
t.ok(r.ok === false)
|
|
34
|
+
t.ok(r.diagnostics.some((d) => d.code === 'E1204'), 'ошибка по Bad')
|
|
35
|
+
const good = r.env.adtByPath?.get(main)?.get('Good')
|
|
36
|
+
t.ok(good && good.name === 'Good')
|
|
37
|
+
t.ok(!r.env.adtByPath?.get(main)?.get('Bad'))
|
|
38
|
+
t.end()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('L3: две сигнатуры — одна с E1304 (неизвестный тип), вторая нормализуется и тело проверяется', function (t) {
|
|
42
|
+
const main = path.join(root, 'two-sig.sail')
|
|
43
|
+
const src = [
|
|
44
|
+
'@broken ( UnknownCtor Num -> )',
|
|
45
|
+
'',
|
|
46
|
+
' drop',
|
|
47
|
+
'',
|
|
48
|
+
'@fine ( -> )',
|
|
49
|
+
'',
|
|
50
|
+
''
|
|
51
|
+
].join('\n')
|
|
52
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
53
|
+
t.ok(r.ok === false)
|
|
54
|
+
t.ok(r.diagnostics.some((d) => d.code === 'E1304'), 'ошибка нормализации broken')
|
|
55
|
+
const sig = r.env?.sigIrByPath?.get(main)?.get('fine')
|
|
56
|
+
t.ok(sig, 'частичный sigIrByPath для успешного слова')
|
|
57
|
+
const per = r.env?.stackSnapshotsByPath?.get(main)
|
|
58
|
+
t.ok(per?.has('fine'))
|
|
59
|
+
t.is(per.get('fine').steps.length, 0)
|
|
60
|
+
t.end()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('L3: ошибка выхода слова — полная цепь pre/post по шагам в снимке', function (t) {
|
|
64
|
+
const main = path.join(root, 'out-mismatch.sail')
|
|
65
|
+
const src = ['@w ( Num -> Num )', '', ' drop', ''].join('\n')
|
|
66
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
67
|
+
t.ok(r.ok === false)
|
|
68
|
+
const rec = r.env.stackSnapshotsByPath.get(main).get('w')
|
|
69
|
+
t.ok(rec)
|
|
70
|
+
t.is(rec.steps.length, 1)
|
|
71
|
+
t.ok(rec.steps[0].pre && rec.steps[0].post)
|
|
72
|
+
t.ok(verifySnapshotChainStitches(rec))
|
|
73
|
+
t.end()
|
|
74
|
+
})
|