@algosail/lang 0.2.11 → 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,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Контракт AST для публичного `parseSource` (RFC-0.1 п.12.2, `lang/lib/parse/ast-spec.js`).
|
|
3
|
+
*
|
|
4
|
+
* Ожидается: при успехе всегда есть объект `ast` (в т.ч. для пустого/пробельного файла);
|
|
5
|
+
* при ошибке `ast === undefined`. Следующий этап — реализация в `parse-source.js` / `index.js`.
|
|
6
|
+
*/
|
|
7
|
+
import test from 'brittle'
|
|
8
|
+
import { parseSource } from '../../index.js'
|
|
9
|
+
|
|
10
|
+
/** @param {import('brittle').Test} t */
|
|
11
|
+
function assertPos (t, p, label) {
|
|
12
|
+
t.ok(p !== null && typeof p === 'object', `${label} is an object`)
|
|
13
|
+
t.ok(Number.isFinite(p.offset) && p.offset >= 0, `${label}.offset`)
|
|
14
|
+
t.ok(Number.isFinite(p.line) && p.line >= 1, `${label}.line`)
|
|
15
|
+
t.ok(Number.isFinite(p.column) && p.column >= 1, `${label}.column`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @param {import('brittle').Test} t */
|
|
19
|
+
function assertSpan (t, span) {
|
|
20
|
+
t.ok(span != null && typeof span === 'object', 'span is an object')
|
|
21
|
+
if (span == null || typeof span !== 'object') return
|
|
22
|
+
assertPos(t, span.start, 'span.start')
|
|
23
|
+
assertPos(t, span.end, 'span.end')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import('brittle').Test} t
|
|
28
|
+
* @param {unknown} ast
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
function assertSourceFileShell (t, ast) {
|
|
32
|
+
t.ok(ast !== null && ast !== undefined && typeof ast === 'object', 'ast is an object')
|
|
33
|
+
if (ast == null || typeof ast !== 'object') return false
|
|
34
|
+
t.is(ast.kind, 'source_file')
|
|
35
|
+
t.ok(Array.isArray(ast.items), 'ast.items is an array')
|
|
36
|
+
if (!Array.isArray(ast.items)) return false
|
|
37
|
+
assertSpan(t, ast.span)
|
|
38
|
+
return ast.span != null && ast.span.start != null && ast.span.end != null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('parseSource: при ошибке ast отсутствует (undefined)', function (t) {
|
|
42
|
+
const r = parseSource('not_a_marker')
|
|
43
|
+
t.ok(r.ok === false)
|
|
44
|
+
t.is(r.ast, undefined)
|
|
45
|
+
t.end()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('parseSource: при успехе ast — объект source_file с span и items', function (t) {
|
|
49
|
+
const r = parseSource('+Math ./math.sail')
|
|
50
|
+
t.ok(r.ok === true)
|
|
51
|
+
t.ok(r.ast !== undefined && r.ast !== null)
|
|
52
|
+
assertSourceFileShell(t, r.ast)
|
|
53
|
+
t.end()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('AST: пустой файл и только пробелы дают source_file с пустыми items', function (t) {
|
|
57
|
+
for (const src of ['', ' \n\t \n']) {
|
|
58
|
+
const r = parseSource(src)
|
|
59
|
+
t.ok(r.ok === true, `ok for ${JSON.stringify(src)}`)
|
|
60
|
+
t.ok(r.ast !== undefined && r.ast !== null, `ast present for ${JSON.stringify(src)}`)
|
|
61
|
+
if (!assertSourceFileShell(t, r.ast)) continue
|
|
62
|
+
t.alike(r.ast.items, [])
|
|
63
|
+
}
|
|
64
|
+
t.end()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('AST: простой импорт +Module path', function (t) {
|
|
68
|
+
const r = parseSource('+Math ./math.sail\n')
|
|
69
|
+
t.ok(r.ok)
|
|
70
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
71
|
+
t.is(r.ast.items.length, 1)
|
|
72
|
+
const imp = r.ast.items[0]
|
|
73
|
+
t.is(imp.kind, 'import')
|
|
74
|
+
assertSpan(t, imp.span)
|
|
75
|
+
t.is(imp.module, 'Math')
|
|
76
|
+
t.is(imp.path, './math.sail')
|
|
77
|
+
t.alike(imp.doc, [])
|
|
78
|
+
t.is(imp.bracket, null)
|
|
79
|
+
t.end()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('AST: скобочный импорт — bracket.words и bracket.types', function (t) {
|
|
83
|
+
const r = parseSource('+Lib ( @add &Point ) ./lib.sail')
|
|
84
|
+
t.ok(r.ok)
|
|
85
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
86
|
+
const imp = r.ast.items[0]
|
|
87
|
+
t.is(imp.kind, 'import')
|
|
88
|
+
t.ok(imp.bracket !== null)
|
|
89
|
+
t.alike(imp.bracket.words, ['add'])
|
|
90
|
+
t.alike(imp.bracket.types, ['Point'])
|
|
91
|
+
t.end()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('AST: doc перед импортом попадает в import.doc', function (t) {
|
|
95
|
+
const r = parseSource('-- hello --\n+Mod ./p.sail')
|
|
96
|
+
t.ok(r.ok)
|
|
97
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
98
|
+
const imp = r.ast.items[0]
|
|
99
|
+
t.alike(imp.doc, [' hello '])
|
|
100
|
+
t.end()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('AST: sum-тип с тегами и пустыми doc', function (t) {
|
|
104
|
+
const src = '&Bool\n| True\n| False'
|
|
105
|
+
const r = parseSource(src)
|
|
106
|
+
t.ok(r.ok)
|
|
107
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
108
|
+
const sum = r.ast.items[0]
|
|
109
|
+
t.is(sum.kind, 'sum_type')
|
|
110
|
+
assertSpan(t, sum.span)
|
|
111
|
+
t.is(sum.name, 'Bool')
|
|
112
|
+
t.alike(sum.typeParams, [])
|
|
113
|
+
t.alike(sum.doc, [])
|
|
114
|
+
t.is(sum.tags.length, 2)
|
|
115
|
+
t.is(sum.tags[0].name, 'True')
|
|
116
|
+
t.is(sum.tags[0].payload, null)
|
|
117
|
+
t.alike(sum.tags[0].doc, [])
|
|
118
|
+
assertSpan(t, sum.tags[0].span)
|
|
119
|
+
t.is(sum.tags[1].name, 'False')
|
|
120
|
+
t.end()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('AST: sum-тип с параметром типа и полезной нагрузкой у тега', function (t) {
|
|
124
|
+
const src = '&Maybe a\n| None\n| Some a'
|
|
125
|
+
const r = parseSource(src)
|
|
126
|
+
t.ok(r.ok)
|
|
127
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
128
|
+
const sum = r.ast.items[0]
|
|
129
|
+
t.is(sum.name, 'Maybe')
|
|
130
|
+
t.alike(sum.typeParams, ['a'])
|
|
131
|
+
t.is(sum.tags[0].name, 'None')
|
|
132
|
+
t.is(sum.tags[0].payload, null)
|
|
133
|
+
t.is(sum.tags[1].name, 'Some')
|
|
134
|
+
t.ok(sum.tags[1].payload !== null)
|
|
135
|
+
t.is(sum.tags[1].payload.kind, 'type_var')
|
|
136
|
+
t.is(sum.tags[1].payload.name, 'a')
|
|
137
|
+
t.end()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('AST: product-тип с полями', function (t) {
|
|
141
|
+
const src = '&Point\n:x Num\n:y Num'
|
|
142
|
+
const r = parseSource(src)
|
|
143
|
+
t.ok(r.ok)
|
|
144
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
145
|
+
const prod = r.ast.items[0]
|
|
146
|
+
t.is(prod.kind, 'product_type')
|
|
147
|
+
t.is(prod.name, 'Point')
|
|
148
|
+
t.alike(prod.typeParams, [])
|
|
149
|
+
t.alike(prod.doc, [])
|
|
150
|
+
t.is(prod.fields.length, 2)
|
|
151
|
+
t.is(prod.fields[0].name, 'x')
|
|
152
|
+
t.is(prod.fields[0].type.kind, 'type_name')
|
|
153
|
+
t.is(prod.fields[0].type.name, 'Num')
|
|
154
|
+
t.alike(prod.fields[0].doc, [])
|
|
155
|
+
assertSpan(t, prod.fields[0].span)
|
|
156
|
+
t.is(prod.fields[1].name, 'y')
|
|
157
|
+
t.end()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('AST: слово с пустой сигнатурой ( -> ) и пустым телом', function (t) {
|
|
161
|
+
const r = parseSource('@nop ( -> )\n\n')
|
|
162
|
+
t.ok(r.ok)
|
|
163
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
164
|
+
t.is(r.ast.items.length, 1)
|
|
165
|
+
const w = r.ast.items[0]
|
|
166
|
+
t.is(w.kind, 'word')
|
|
167
|
+
t.is(w.name, 'nop')
|
|
168
|
+
assertSpan(t, w.span)
|
|
169
|
+
t.alike(w.doc, [])
|
|
170
|
+
const sig = w.signature
|
|
171
|
+
t.is(sig.kind, 'signature')
|
|
172
|
+
assertSpan(t, sig.span)
|
|
173
|
+
t.alike(sig.left, [])
|
|
174
|
+
t.alike(sig.right, [])
|
|
175
|
+
t.alike(sig.effectsAdd, [])
|
|
176
|
+
t.alike(sig.effectsRemove, [])
|
|
177
|
+
t.alike(w.body, [])
|
|
178
|
+
t.end()
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('AST: сигнатура со stack_var ~s и типом Num с обеих сторон стрелки', function (t) {
|
|
182
|
+
const r = parseSource('@id ( ~s Num -> ~s Num )\n\n')
|
|
183
|
+
t.ok(r.ok)
|
|
184
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
185
|
+
const w0 = r.ast.items[0]
|
|
186
|
+
t.ok(w0 && w0.signature, 'word has signature')
|
|
187
|
+
if (!w0 || !w0.signature) return t.end()
|
|
188
|
+
const sig = w0.signature
|
|
189
|
+
t.is(sig.left.length, 2)
|
|
190
|
+
t.is(sig.left[0].kind, 'stack_var')
|
|
191
|
+
t.is(sig.left[0].name, 's')
|
|
192
|
+
assertSpan(t, sig.left[0].span)
|
|
193
|
+
t.is(sig.left[1].kind, 'sig_type_expr')
|
|
194
|
+
t.is(sig.left[1].type.kind, 'type_name')
|
|
195
|
+
t.is(sig.left[1].type.name, 'Num')
|
|
196
|
+
assertSpan(t, sig.left[1].span)
|
|
197
|
+
|
|
198
|
+
t.is(sig.right.length, 2)
|
|
199
|
+
t.is(sig.right[0].kind, 'stack_var')
|
|
200
|
+
t.is(sig.right[0].name, 's')
|
|
201
|
+
t.is(sig.right[1].kind, 'sig_type_expr')
|
|
202
|
+
t.is(sig.right[1].type.kind, 'type_name')
|
|
203
|
+
t.is(sig.right[1].type.name, 'Num')
|
|
204
|
+
t.alike(sig.effectsAdd, [])
|
|
205
|
+
t.alike(sig.effectsRemove, [])
|
|
206
|
+
t.end()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('AST: маркеры эффектов только в конце правой части (effectsAdd / effectsRemove, side right)', function (t) {
|
|
210
|
+
const r = parseSource('@run ( Num -> Str +Async +Fail -Async )\n\n')
|
|
211
|
+
t.ok(r.ok)
|
|
212
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
213
|
+
const w0 = r.ast.items[0]
|
|
214
|
+
t.ok(w0 && w0.signature, 'word has signature')
|
|
215
|
+
if (!w0 || !w0.signature) return t.end()
|
|
216
|
+
const sig = w0.signature
|
|
217
|
+
t.is(sig.left.length, 1)
|
|
218
|
+
t.is(sig.left[0].kind, 'sig_type_expr')
|
|
219
|
+
t.is(sig.left[0].type.kind, 'type_name')
|
|
220
|
+
t.is(sig.left[0].type.name, 'Num')
|
|
221
|
+
t.is(sig.right.length, 1)
|
|
222
|
+
t.is(sig.right[0].kind, 'sig_type_expr')
|
|
223
|
+
t.is(sig.right[0].type.kind, 'type_name')
|
|
224
|
+
t.is(sig.right[0].type.name, 'Str')
|
|
225
|
+
t.is(sig.effectsAdd.length, 2)
|
|
226
|
+
t.is(sig.effectsAdd[0].kind, 'sig_effect_add')
|
|
227
|
+
t.is(sig.effectsAdd[0].name, 'Async')
|
|
228
|
+
t.is(sig.effectsAdd[0].side, 'right')
|
|
229
|
+
assertSpan(t, sig.effectsAdd[0].span)
|
|
230
|
+
t.is(sig.effectsAdd[1].kind, 'sig_effect_add')
|
|
231
|
+
t.is(sig.effectsAdd[1].name, 'Fail')
|
|
232
|
+
t.is(sig.effectsAdd[1].side, 'right')
|
|
233
|
+
t.is(sig.effectsRemove.length, 1)
|
|
234
|
+
t.is(sig.effectsRemove[0].kind, 'sig_effect_remove')
|
|
235
|
+
t.is(sig.effectsRemove[0].name, 'Async')
|
|
236
|
+
t.is(sig.effectsRemove[0].side, 'right')
|
|
237
|
+
t.end()
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
test('AST: сигнатура — type_app List Num на слоте', function (t) {
|
|
241
|
+
const r = parseSource('@f ( List Num -> )\n\n')
|
|
242
|
+
t.ok(r.ok)
|
|
243
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
244
|
+
const sig = r.ast.items[0].signature
|
|
245
|
+
t.is(sig.left.length, 1)
|
|
246
|
+
t.is(sig.left[0].kind, 'sig_type_expr')
|
|
247
|
+
t.is(sig.left[0].type.kind, 'type_app')
|
|
248
|
+
t.is(sig.left[0].type.ctor, 'List')
|
|
249
|
+
t.is(sig.left[0].type.args.length, 1)
|
|
250
|
+
t.is(sig.left[0].type.args[0].kind, 'type_name')
|
|
251
|
+
t.is(sig.left[0].type.args[0].name, 'Num')
|
|
252
|
+
t.alike(sig.right, [])
|
|
253
|
+
t.end()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
test('AST: сигнатура — paren_type вокруг Maybe Num', function (t) {
|
|
257
|
+
const r = parseSource('@f ( ( Maybe Num ) -> )\n\n')
|
|
258
|
+
t.ok(r.ok)
|
|
259
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
260
|
+
const ty = r.ast.items[0].signature.left[0].type
|
|
261
|
+
t.is(ty.kind, 'paren_type')
|
|
262
|
+
t.is(ty.inner.kind, 'type_app')
|
|
263
|
+
t.is(ty.inner.ctor, 'Maybe')
|
|
264
|
+
t.is(ty.inner.args.length, 1)
|
|
265
|
+
t.is(ty.inner.args[0].kind, 'type_name')
|
|
266
|
+
t.is(ty.inner.args[0].name, 'Num')
|
|
267
|
+
t.end()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
test('AST: сигнатура — module_type_ref ~Lib/Point (RFC-0.1 lit_type / module_type_ref)', function (t) {
|
|
271
|
+
const r = parseSource('@usePoint ( ~Lib/Point -> )\n\n')
|
|
272
|
+
t.ok(r.ok, 'парсер должен принимать ~Module/Type в sig_item (parseSigItem → parseSigTypeExpr)')
|
|
273
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
274
|
+
const ty = r.ast.items[0].signature.left[0].type
|
|
275
|
+
t.is(ty.kind, 'module_type_ref')
|
|
276
|
+
t.is(ty.module, 'Lib')
|
|
277
|
+
t.is(ty.type, 'Point')
|
|
278
|
+
t.end()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
test('AST: сигнатура — Quote ( Num -> Str ) как quotation_sig (RFC-0.1 §6.1)', function (t) {
|
|
282
|
+
const r = parseSource('@w ( Quote ( Num -> Str ) -> )\n\n')
|
|
283
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
284
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
285
|
+
const sig = r.ast.items[0].signature
|
|
286
|
+
t.is(sig.left.length, 1)
|
|
287
|
+
t.is(sig.left[0].kind, 'quotation_sig')
|
|
288
|
+
const inner = sig.left[0].inner
|
|
289
|
+
t.is(inner.left.length, 1)
|
|
290
|
+
t.is(inner.left[0].type.kind, 'type_name')
|
|
291
|
+
t.is(inner.left[0].type.name, 'Num')
|
|
292
|
+
t.is(inner.right.length, 1)
|
|
293
|
+
t.is(inner.right[0].type.name, 'Str')
|
|
294
|
+
t.end()
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test('AST: сигнатура — sugar ( ( Num -> Str ) -> ) вложенная цитата', function (t) {
|
|
298
|
+
const r = parseSource('@w ( ( Num -> Str ) -> )\n\n')
|
|
299
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
300
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
301
|
+
const sig = r.ast.items[0].signature
|
|
302
|
+
t.is(sig.left.length, 1)
|
|
303
|
+
t.is(sig.left[0].kind, 'quotation_sig')
|
|
304
|
+
const inner = sig.left[0].inner
|
|
305
|
+
t.is(inner.left[0].type.name, 'Num')
|
|
306
|
+
t.is(inner.right[0].type.name, 'Str')
|
|
307
|
+
t.end()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test('AST: сигнатура — ( ( Num -> Str ) ) без внешней -> — ошибка', function (t) {
|
|
311
|
+
const r = parseSource('@bad ( ( Num -> Str ) )\n\n')
|
|
312
|
+
t.ok(r.ok === false)
|
|
313
|
+
t.is(r.diagnostics[0].code, 'E1001')
|
|
314
|
+
t.end()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
test('AST: сигнатура — Maybe ( Num -> Str ) как type_app + quotation_type', function (t) {
|
|
318
|
+
const r = parseSource('@f ( Maybe ( Num -> Str ) -> )\n\n')
|
|
319
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
320
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
321
|
+
const ty = r.ast.items[0].signature.left[0].type
|
|
322
|
+
t.is(ty.kind, 'type_app')
|
|
323
|
+
t.is(ty.ctor, 'Maybe')
|
|
324
|
+
t.is(ty.args.length, 1)
|
|
325
|
+
t.is(ty.args[0].kind, 'quotation_type')
|
|
326
|
+
t.is(ty.args[0].inner.left[0].type.name, 'Num')
|
|
327
|
+
t.is(ty.args[0].inner.right[0].type.name, 'Str')
|
|
328
|
+
t.end()
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('AST: сигнатура — named_quotation_sig (just: ( ~s a -> ~s a )) — stack_var и type_var', function (t) {
|
|
332
|
+
const r = parseSource('@withJust ( just: ( ~s a -> ~s a ) -> )\n\n /id\n')
|
|
333
|
+
t.ok(r.ok)
|
|
334
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
335
|
+
const sig = r.ast.items[0].signature
|
|
336
|
+
t.is(sig.left.length, 1)
|
|
337
|
+
const nq = sig.left[0]
|
|
338
|
+
t.is(nq.kind, 'named_quotation_sig')
|
|
339
|
+
t.is(nq.prefix, 'just')
|
|
340
|
+
assertSpan(t, nq.span)
|
|
341
|
+
t.is(nq.quotation.kind, 'quotation_sig')
|
|
342
|
+
assertSpan(t, nq.quotation.span)
|
|
343
|
+
const inner = nq.quotation.inner
|
|
344
|
+
t.is(inner.kind, 'signature')
|
|
345
|
+
t.is(inner.left.length, 2)
|
|
346
|
+
t.is(inner.left[0].kind, 'stack_var')
|
|
347
|
+
t.is(inner.left[0].name, 's')
|
|
348
|
+
t.is(inner.left[1].kind, 'sig_type_expr')
|
|
349
|
+
t.is(inner.left[1].type.kind, 'type_var')
|
|
350
|
+
t.is(inner.left[1].type.name, 'a')
|
|
351
|
+
t.is(inner.right.length, 2)
|
|
352
|
+
t.is(inner.right[0].kind, 'stack_var')
|
|
353
|
+
t.is(inner.right[0].name, 's')
|
|
354
|
+
t.is(inner.right[1].kind, 'sig_type_expr')
|
|
355
|
+
t.is(inner.right[1].type.kind, 'type_var')
|
|
356
|
+
t.is(inner.right[1].type.name, 'a')
|
|
357
|
+
t.alike(inner.effectsAdd, [])
|
|
358
|
+
t.alike(inner.effectsRemove, [])
|
|
359
|
+
t.end()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
test('AST: тело слова — string и number литералы', function (t) {
|
|
363
|
+
const r = parseSource('@w ( -> )\n\n "hi"\n 42\n')
|
|
364
|
+
t.ok(r.ok)
|
|
365
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
366
|
+
const body = r.ast.items[0].body
|
|
367
|
+
t.is(body.length, 2)
|
|
368
|
+
t.is(body[0].kind, 'literal')
|
|
369
|
+
t.is(body[0].litKind, 'string')
|
|
370
|
+
t.is(body[0].raw, '"hi"')
|
|
371
|
+
assertSpan(t, body[0].span)
|
|
372
|
+
t.is(body[1].kind, 'literal')
|
|
373
|
+
t.is(body[1].litKind, 'number')
|
|
374
|
+
t.is(body[1].raw, '42')
|
|
375
|
+
t.end()
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
test('AST: тело слова — bool, nil', function (t) {
|
|
379
|
+
const r = parseSource('@w ( -> )\n\n true\n false\n nil\n')
|
|
380
|
+
t.ok(r.ok)
|
|
381
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
382
|
+
const b = r.ast.items[0].body
|
|
383
|
+
t.is(b.length, 3)
|
|
384
|
+
t.is(b[0].litKind, 'bool')
|
|
385
|
+
t.is(b[0].raw, 'true')
|
|
386
|
+
t.is(b[1].litKind, 'bool')
|
|
387
|
+
t.is(b[1].raw, 'false')
|
|
388
|
+
t.is(b[2].litKind, 'nil')
|
|
389
|
+
t.is(b[2].raw, 'nil')
|
|
390
|
+
t.end()
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
test('AST: тело слова — word_ref, builtin, module_word_ref', function (t) {
|
|
394
|
+
const r = parseSource('@w ( -> )\n\n /swap\n dup\n ~Math/add\n')
|
|
395
|
+
t.ok(r.ok)
|
|
396
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
397
|
+
const b = r.ast.items[0].body
|
|
398
|
+
t.is(b.length, 3)
|
|
399
|
+
t.is(b[0].kind, 'word_ref')
|
|
400
|
+
t.is(b[0].name, 'swap')
|
|
401
|
+
assertSpan(t, b[0].span)
|
|
402
|
+
t.is(b[1].kind, 'builtin')
|
|
403
|
+
t.is(b[1].name, 'dup')
|
|
404
|
+
t.is(b[2].kind, 'module_word_ref')
|
|
405
|
+
t.is(b[2].module, 'Math')
|
|
406
|
+
t.is(b[2].word, 'add')
|
|
407
|
+
t.end()
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
test('AST: неизвестное lower в теле — узел builtin (RFC-0.1 §13 E1206 на L2)', function (t) {
|
|
411
|
+
const r = parseSource('@w ( -> )\n\n not_a_real_builtin_xyz\n')
|
|
412
|
+
t.ok(r.ok)
|
|
413
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
414
|
+
const b = r.ast.items[0].body
|
|
415
|
+
t.is(b.length, 1)
|
|
416
|
+
t.is(b[0].kind, 'builtin')
|
|
417
|
+
t.is(b[0].name, 'not_a_real_builtin_xyz')
|
|
418
|
+
t.end()
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
test('AST: тело слова — list_literal и quotation', function (t) {
|
|
422
|
+
const r = parseSource('@w ( -> )\n\n [ 1 2 ]\n ( /drop )\n')
|
|
423
|
+
t.ok(r.ok)
|
|
424
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
425
|
+
const b = r.ast.items[0].body
|
|
426
|
+
t.is(b.length, 2)
|
|
427
|
+
t.is(b[0].kind, 'list_literal')
|
|
428
|
+
assertSpan(t, b[0].span)
|
|
429
|
+
t.is(b[0].elements.length, 2)
|
|
430
|
+
t.is(b[0].elements[0].litKind, 'number')
|
|
431
|
+
t.is(b[0].elements[0].raw, '1')
|
|
432
|
+
t.is(b[0].elements[1].raw, '2')
|
|
433
|
+
t.is(b[1].kind, 'quotation')
|
|
434
|
+
assertSpan(t, b[1].span)
|
|
435
|
+
t.is(b[1].body.length, 1)
|
|
436
|
+
t.is(b[1].body[0].kind, 'word_ref')
|
|
437
|
+
t.is(b[1].body[0].name, 'drop')
|
|
438
|
+
t.end()
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
test('AST: тело слова — slot_write и slot_read', function (t) {
|
|
442
|
+
const r = parseSource('@w ( -> )\n\n :acc\n ;acc\n')
|
|
443
|
+
t.ok(r.ok)
|
|
444
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
445
|
+
const b = r.ast.items[0].body
|
|
446
|
+
t.is(b.length, 2)
|
|
447
|
+
t.is(b[0].kind, 'slot_write')
|
|
448
|
+
t.is(b[0].name, 'acc')
|
|
449
|
+
t.is(b[1].kind, 'slot_read')
|
|
450
|
+
t.is(b[1].name, 'acc')
|
|
451
|
+
t.end()
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
test('AST: doc между сигнатурой и телом слова (word.doc)', function (t) {
|
|
455
|
+
const r = parseSource('@w ( -> )\n-- about --\n\n /id\n')
|
|
456
|
+
t.ok(r.ok)
|
|
457
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
458
|
+
const w = r.ast.items[0]
|
|
459
|
+
t.alike(w.doc, [' about '])
|
|
460
|
+
t.is(w.body.length, 1)
|
|
461
|
+
t.is(w.body[0].kind, 'word_ref')
|
|
462
|
+
t.is(w.body[0].name, 'id')
|
|
463
|
+
t.end()
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
test('AST: несколько top-level подряд сохраняют порядок в items', function (t) {
|
|
467
|
+
const src = '+M ./a.sail\n&Bool\n| True\n@nop ( -> )\n'
|
|
468
|
+
const r = parseSource(src)
|
|
469
|
+
t.ok(r.ok)
|
|
470
|
+
if (!assertSourceFileShell(t, r.ast)) return t.end()
|
|
471
|
+
t.is(r.ast.items.length, 3)
|
|
472
|
+
t.is(r.ast.items[0].kind, 'import')
|
|
473
|
+
t.is(r.ast.items[1].kind, 'sum_type')
|
|
474
|
+
t.is(r.ast.items[2].kind, 'word')
|
|
475
|
+
t.end()
|
|
476
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public parse API shape — plan + RFC-0.1 §11 (Parse step).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import { parseSource } from '../../index.js'
|
|
6
|
+
import { expectAccept } from './helpers.js'
|
|
7
|
+
|
|
8
|
+
test('parseSource returns { ok, diagnostics }', function (t) {
|
|
9
|
+
const r = parseSource('')
|
|
10
|
+
t.ok(typeof r === 'object' && r !== null)
|
|
11
|
+
t.ok('ok' in r && typeof r.ok === 'boolean')
|
|
12
|
+
t.ok(Array.isArray(r.diagnostics))
|
|
13
|
+
t.end()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('parseSource: success has empty diagnostics', function (t) {
|
|
17
|
+
expectAccept(t, '')
|
|
18
|
+
t.end()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('parseSource: each diagnostic has string code and message', function (t) {
|
|
22
|
+
const r = parseSource('@x')
|
|
23
|
+
t.ok(r.ok === false)
|
|
24
|
+
t.ok(r.diagnostics.length >= 1)
|
|
25
|
+
for (const d of r.diagnostics) {
|
|
26
|
+
t.is(typeof d.code, 'string')
|
|
27
|
+
t.is(typeof d.message, 'string')
|
|
28
|
+
}
|
|
29
|
+
t.end()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('parseSource: non-string input is rejected', function (t) {
|
|
33
|
+
const r = parseSource(null)
|
|
34
|
+
t.ok(r.ok === false)
|
|
35
|
+
t.ok(r.diagnostics.some(d => d.code === 'E1001'))
|
|
36
|
+
t.end()
|
|
37
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Золотой эталон: один большой .sail и ожидаемое AST в JSON (test/fixtures/).
|
|
3
|
+
* Обновить JSON после намеренного изменения парсера: `npm run regen:fixture-ast` (из каталога lang).
|
|
4
|
+
*/
|
|
5
|
+
import test from 'brittle'
|
|
6
|
+
import { readFileSync } from 'fs'
|
|
7
|
+
import path from 'path'
|
|
8
|
+
import { fileURLToPath } from 'url'
|
|
9
|
+
import { parseSource } from '../../index.js'
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
+
const fixtureDir = path.join(__dirname, '..', 'fixtures')
|
|
13
|
+
const fixtureSailPath = path.join(fixtureDir, 'demo-full-syntax.sail')
|
|
14
|
+
const fixtureAstPath = path.join(fixtureDir, 'demo-full-syntax.ast.json')
|
|
15
|
+
|
|
16
|
+
test('fixture demo-full-syntax: parse matches demo-full-syntax.ast.json', function (t) {
|
|
17
|
+
const sourceText = readFileSync(fixtureSailPath, 'utf8')
|
|
18
|
+
const expected = JSON.parse(readFileSync(fixtureAstPath, 'utf8'))
|
|
19
|
+
const r = parseSource(sourceText)
|
|
20
|
+
t.ok(r.ok, 'fixture must parse without diagnostics')
|
|
21
|
+
t.is(r.diagnostics.length, 0)
|
|
22
|
+
t.alike(r.ast, expected)
|
|
23
|
+
t.end()
|
|
24
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { parseSource } from '../../lib/parse/index.js'
|
|
2
|
+
|
|
3
|
+
export { parseSource }
|
|
4
|
+
|
|
5
|
+
export function expectAccept (t, sourceText) {
|
|
6
|
+
const r = parseSource(sourceText)
|
|
7
|
+
t.ok(r.ok === true, 'expected ok')
|
|
8
|
+
t.ok(Array.isArray(r.diagnostics), 'diagnostics must be an array')
|
|
9
|
+
t.is(r.diagnostics.length, 0, 'expected no diagnostics on success')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function expectReject (t, sourceText, code, messageIncludes) {
|
|
13
|
+
const r = parseSource(sourceText)
|
|
14
|
+
t.ok(r.ok === false, 'expected failure')
|
|
15
|
+
t.ok(Array.isArray(r.diagnostics), 'diagnostics must be an array')
|
|
16
|
+
const d = r.diagnostics.find(x => x.code === code)
|
|
17
|
+
t.ok(
|
|
18
|
+
d,
|
|
19
|
+
`expected a diagnostic with code ${code}, got ${r.diagnostics.map(x => x.code).join(', ')}`
|
|
20
|
+
)
|
|
21
|
+
if (d !== undefined && messageIncludes !== undefined) {
|
|
22
|
+
t.ok(
|
|
23
|
+
typeof d.message === 'string' && d.message.includes(messageIncludes),
|
|
24
|
+
`message should include ${JSON.stringify(messageIncludes)}, got ${JSON.stringify(d.message)}`
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Матрица L0 (лексер RFC-lex-0.1, RFC-conformance-0.1 §3): диагностики из [lexer.js](lang/lib/parse/lexer.js).
|
|
3
|
+
* Парсер (L1) добавляет E1003–E1007; см. l1-parse-diagnostics-matrix.test.js.
|
|
4
|
+
*
|
|
5
|
+
* | Код | Место в lexer.js | Смысл | Проверка |
|
|
6
|
+
* |-------|-------------------------|-------|----------|
|
|
7
|
+
* | E1002 | readCommentToken | Незакрытый `-- … --` | **ниже** (nextToken), l0-lex.test.js (parseSource) |
|
|
8
|
+
* | E1001 | readString | Незакрытая строка | **ниже** (nextToken), l1-parse-diagnostics-matrix (parseSource) |
|
|
9
|
+
* | E1001 | `/` без regexp/word_ref | Недопустимый `/` | l1-parse-diagnostics-matrix (parseSource) |
|
|
10
|
+
* | E1001 | fallback символ | Неизвестный символ | **ниже** (nextToken) |
|
|
11
|
+
*
|
|
12
|
+
* Путь импорта читается `readPathToken`, не как отдельный шаг `nextToken` для `./…` (см. l0-lex «import path» через parseSource).
|
|
13
|
+
* Интеграция через `parseSource` для путей, тел слов и т.д. — в l0-lex.test.js и l1-*.
|
|
14
|
+
*/
|
|
15
|
+
import test from 'brittle'
|
|
16
|
+
import { createLexerState, nextToken } from '../../lib/parse/lexer.js'
|
|
17
|
+
|
|
18
|
+
function firstTokenDiagnostics (sourceText, regexpOk = false) {
|
|
19
|
+
const state = createLexerState(sourceText)
|
|
20
|
+
const ctx = { diagnostics: [], regexpOk }
|
|
21
|
+
const tok = nextToken(state, ctx)
|
|
22
|
+
return { tok, diagnostics: ctx.diagnostics }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('L0 E1002: незакрытый блочный комментарий (только лексер)', function (t) {
|
|
26
|
+
const { tok, diagnostics } = firstTokenDiagnostics('-- без закрытия')
|
|
27
|
+
t.is(tok.type, 'EOF')
|
|
28
|
+
const d = diagnostics.find(x => x.code === 'E1002')
|
|
29
|
+
t.ok(d, 'E1002')
|
|
30
|
+
t.ok(d.message.includes('комментарий'))
|
|
31
|
+
t.end()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('L0 E1001: неизвестный символ (fallback nextToken)', function (t) {
|
|
35
|
+
const { tok, diagnostics } = firstTokenDiagnostics('§')
|
|
36
|
+
t.is(tok.type, 'EOF')
|
|
37
|
+
const d = diagnostics.find(x => x.code === 'E1001')
|
|
38
|
+
t.ok(d && d.message.includes('§'))
|
|
39
|
+
t.end()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('L0 E1001: незакрытая строка (только лексер)', function (t) {
|
|
43
|
+
const { tok, diagnostics } = firstTokenDiagnostics('"unclosed')
|
|
44
|
+
t.is(tok.type, 'EOF')
|
|
45
|
+
const d = diagnostics.find(x => x.code === 'E1001')
|
|
46
|
+
t.ok(d && d.message.includes('"'))
|
|
47
|
+
t.end()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('L0: базовая токенизация MODULE и число (без диагностик)', function (t) {
|
|
51
|
+
const state = createLexerState('+Lib 42')
|
|
52
|
+
const ctx = { diagnostics: [], regexpOk: false }
|
|
53
|
+
const a = nextToken(state, ctx)
|
|
54
|
+
const b = nextToken(state, ctx)
|
|
55
|
+
t.is(a.type, 'MODULE')
|
|
56
|
+
t.is(b.type, 'NUMBER')
|
|
57
|
+
t.is(ctx.diagnostics.length, 0)
|
|
58
|
+
t.end()
|
|
59
|
+
})
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L0 Lex — RFC-conformance-0.1 §3, RFC-lex-0.1 (ws, comments §3, literals §6, path §7, order §8).
|
|
3
|
+
* Diagnostics for lex/parse — RFC-0.1 §13 (E1001–E1007).
|
|
4
|
+
* Матрица диагностик лексера (E1001/E1002 из lexer.js): l0-lex-diagnostics-matrix.test.js
|
|
5
|
+
*/
|
|
6
|
+
import test from 'brittle'
|
|
7
|
+
import { expectAccept, expectReject } from './helpers.js'
|
|
8
|
+
|
|
9
|
+
test('L0: whitespace-only file is valid (RFC-0.1 §12.2 source_file)', function (t) {
|
|
10
|
+
expectAccept(t, '')
|
|
11
|
+
expectAccept(t, ' \n\t \n')
|
|
12
|
+
t.end()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('L0: unclosed Sail block comment → E1002 (RFC-0.1 §13, RFC-lex §3)', function (t) {
|
|
16
|
+
expectReject(
|
|
17
|
+
t,
|
|
18
|
+
'-- opens but never closes',
|
|
19
|
+
'E1002',
|
|
20
|
+
"Незакрытый комментарий"
|
|
21
|
+
)
|
|
22
|
+
t.end()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('L0: `--` inside string literal is not comment (RFC-lex §3, §6)', function (t) {
|
|
26
|
+
const src = '@w ( -> )\n\n "-- not a comment --"'
|
|
27
|
+
expectAccept(t, src)
|
|
28
|
+
t.end()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('L0: import path is single token without internal whitespace (RFC-lex §7)', function (t) {
|
|
32
|
+
expectAccept(t, '+Lib ./relative/path.sail')
|
|
33
|
+
expectAccept(t, '+Npm @scope/pkg-name')
|
|
34
|
+
t.end()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('L0: regexp vs division — regexp after token that allows InputElementRegExp (RFC-lex §6)', function (t) {
|
|
38
|
+
expectAccept(t, '@w ( -> )\n\n /just[^/]*/ dup')
|
|
39
|
+
t.end()
|
|
40
|
+
})
|