@algosail/lang 0.2.12 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/sail.mjs +4 -0
- package/cli/run-cli.js +176 -0
- package/index.js +12 -2
- package/lib/codegen/README.md +230 -0
- package/lib/codegen/codegen-diagnostics.js +164 -0
- package/lib/codegen/compile-graph.js +107 -0
- package/lib/codegen/emit-adt.js +177 -0
- package/lib/codegen/emit-body.js +1265 -0
- package/lib/codegen/emit-builtin.js +371 -0
- package/lib/codegen/emit-jsdoc-sail.js +383 -0
- package/lib/codegen/emit-module.js +498 -0
- package/lib/codegen/esm-imports.js +26 -0
- package/lib/codegen/index.js +69 -0
- package/lib/codegen/out-layout.js +102 -0
- package/lib/ffi/extract-jsdoc-sail.js +34 -0
- package/lib/io-node/index.js +4 -0
- package/lib/io-node/package-root.js +18 -0
- package/lib/io-node/read-file.js +12 -0
- package/lib/io-node/resolve-package.js +24 -0
- package/lib/io-node/resolve-sail-names-from-disk.js +21 -0
- package/lib/ir/assert-json-serializable.js +30 -0
- package/lib/ir/attach-call-effects.js +108 -0
- package/lib/ir/bind-values.js +594 -0
- package/lib/ir/build-module-ir.js +290 -0
- package/lib/ir/index.js +31 -0
- package/lib/ir/lower-body-steps.js +170 -0
- package/lib/ir/module-metadata.js +65 -0
- package/lib/ir/schema-version.js +15 -0
- package/lib/ir/serialize.js +202 -0
- package/lib/ir/stitch-types.js +92 -0
- package/lib/names/adt-autogen.js +22 -0
- package/lib/names/import-path.js +28 -0
- package/lib/names/index.js +1 -0
- package/lib/names/local-declarations.js +127 -0
- package/lib/names/lower-first.js +6 -0
- package/lib/names/module-scope.js +120 -0
- package/lib/names/resolve-sail.js +365 -0
- package/lib/names/walk-ast-refs.js +91 -0
- package/lib/parse/ast-build.js +51 -0
- package/lib/parse/ast-spec.js +212 -0
- package/lib/parse/builtins-set.js +12 -0
- package/lib/parse/diagnostics.js +180 -0
- package/lib/parse/index.js +46 -0
- package/lib/parse/lexer.js +390 -0
- package/lib/parse/parse-source.js +912 -0
- package/lib/typecheck/adt-autogen-sigs.js +345 -0
- package/lib/typecheck/build-type-env.js +148 -0
- package/lib/typecheck/builtin-signatures.js +183 -0
- package/lib/typecheck/check-word-body.js +1021 -0
- package/lib/typecheck/effect-decl.js +124 -0
- package/lib/typecheck/index.js +55 -0
- package/lib/typecheck/normalize-sig.js +369 -0
- package/lib/typecheck/stack-step-snapshots.js +56 -0
- package/lib/typecheck/unify-type.js +665 -0
- package/lib/typecheck/validate-adt.js +201 -0
- package/package.json +4 -9
- package/scripts/regen-demo-full-syntax-ast.mjs +22 -0
- package/test/cli/sail-cli.test.js +64 -0
- package/test/codegen/compile-bracket-ffi-e2e.test.js +64 -0
- package/test/codegen/compile-stage0.test.js +128 -0
- package/test/codegen/compile-stage4-layout.test.js +124 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/extra.sail +6 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/lib.sail +34 -0
- package/test/codegen/e2e-prelude-ffi-adt/app/main.sail +28 -0
- package/test/codegen/e2e-prelude-ffi-adt/artifacts/.gitignore +2 -0
- package/test/codegen/e2e-prelude-ffi-adt/ffi/helpers.js +27 -0
- package/test/codegen/e2e-prelude-ffi-adt.test.js +100 -0
- package/test/codegen/emit-adt-stage6.test.js +168 -0
- package/test/codegen/emit-async-stage5.test.js +164 -0
- package/test/codegen/emit-body-stage2.test.js +139 -0
- package/test/codegen/emit-body.test.js +163 -0
- package/test/codegen/emit-builtins-stage7.test.js +258 -0
- package/test/codegen/emit-diagnostics-stage9.test.js +90 -0
- package/test/codegen/emit-jsdoc-stage8.test.js +113 -0
- package/test/codegen/emit-module-stage3.test.js +78 -0
- package/test/conformance/conformance-ir-l4.test.js +38 -0
- package/test/conformance/conformance-l5-codegen.test.js +111 -0
- package/test/conformance/conformance-runner.js +91 -0
- package/test/conformance/conformance-suite-l3.test.js +32 -0
- package/test/ffi/prelude-jsdoc.test.js +49 -0
- package/test/fixtures/demo-full-syntax.ast.json +1471 -0
- package/test/fixtures/demo-full-syntax.sail +35 -0
- package/test/fixtures/io-node-ffi-adt/ffi.js +7 -0
- package/test/fixtures/io-node-ffi-adt/use.sail +4 -0
- package/test/fixtures/io-node-mini/dep.sail +2 -0
- package/test/fixtures/io-node-mini/entry.sail +4 -0
- package/test/fixtures/io-node-prelude/entry.sail +4 -0
- package/test/fixtures/io-node-reexport-chain/a.sail +4 -0
- package/test/fixtures/io-node-reexport-chain/b.sail +2 -0
- package/test/fixtures/io-node-reexport-chain/c.sail +2 -0
- package/test/io-node/resolve-disk.test.js +59 -0
- package/test/ir/bind-values.test.js +84 -0
- package/test/ir/build-module-ir.test.js +100 -0
- package/test/ir/call-effects.test.js +97 -0
- package/test/ir/ffi-bracket-ir.test.js +59 -0
- package/test/ir/full-ir-document.test.js +51 -0
- package/test/ir/ir-document-assert.js +67 -0
- package/test/ir/lower-body-steps.test.js +90 -0
- package/test/ir/module-metadata.test.js +42 -0
- package/test/ir/serialization-model.test.js +172 -0
- package/test/ir/stitch-types.test.js +74 -0
- package/test/names/l2-resolve-adt-autogen.test.js +155 -0
- package/test/names/l2-resolve-bracket-ffi.test.js +108 -0
- package/test/names/l2-resolve-declaration-and-bracket-errors.test.js +276 -0
- package/test/names/l2-resolve-graph.test.js +105 -0
- package/test/names/l2-resolve-single-file.test.js +79 -0
- package/test/parse/ast-spec.test.js +56 -0
- package/test/parse/ast.test.js +476 -0
- package/test/parse/contract.test.js +37 -0
- package/test/parse/fixtures-full-syntax.test.js +24 -0
- package/test/parse/helpers.js +27 -0
- package/test/parse/l0-lex-diagnostics-matrix.test.js +59 -0
- package/test/parse/l0-lex.test.js +40 -0
- package/test/parse/l1-diagnostics.test.js +77 -0
- package/test/parse/l1-import.test.js +28 -0
- package/test/parse/l1-parse-diagnostics-matrix.test.js +32 -0
- package/test/parse/l1-top-level.test.js +47 -0
- package/test/parse/l1-types.test.js +31 -0
- package/test/parse/l1-words.test.js +49 -0
- package/test/parse/l2-diagnostics-contract.test.js +67 -0
- package/test/parse/l3-diagnostics-contract.test.js +66 -0
- package/test/typecheck/adt-decl-stage2.test.js +83 -0
- package/test/typecheck/container-contract-e1309.test.js +258 -0
- package/test/typecheck/ffi-bracket-l3.test.js +61 -0
- package/test/typecheck/l3-diagnostics-matrix.test.js +248 -0
- package/test/typecheck/l3-partial-pipeline.test.js +74 -0
- package/test/typecheck/opaque-ffi-type.test.js +78 -0
- package/test/typecheck/sig-type-stage3.test.js +190 -0
- package/test/typecheck/stack-check-stage4.test.js +149 -0
- package/test/typecheck/stack-check-stage5.test.js +74 -0
- package/test/typecheck/stack-check-stage6.test.js +56 -0
- package/test/typecheck/stack-check-stage7.test.js +160 -0
- package/test/typecheck/stack-check-stage8.test.js +146 -0
- package/test/typecheck/stack-check-stage9.test.js +105 -0
- package/test/typecheck/typecheck-env.test.js +53 -0
- package/test/typecheck/typecheck-pipeline.test.js +37 -0
- package/README.md +0 -37
- package/cli/sail.js +0 -151
- package/cli/typecheck.js +0 -39
- package/docs/ARCHITECTURE.md +0 -50
- package/docs/CHANGELOG.md +0 -18
- package/docs/FFI-GUIDE.md +0 -65
- package/docs/RELEASE.md +0 -36
- package/docs/TESTING.md +0 -86
- package/test/integration.test.js +0 -61
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 1: emitWordBodyIr (irSteps + valueBindings).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import { buildModuleIr } from '../../lib/ir/index.js'
|
|
8
|
+
import { emitWordBodyIr } from '../../lib/codegen/emit-body.js'
|
|
9
|
+
|
|
10
|
+
const root = path.resolve('/virtual/sail-codegen-emit-body')
|
|
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
|
+
/** Эвристика: не моделируем стек массивом с push/pop (см. план этапа 1). */
|
|
19
|
+
function assertNoStackArrayModel (t, source) {
|
|
20
|
+
t.ok(
|
|
21
|
+
!/\.push\s*\(/.test(source) && !/\.pop\s*\(/.test(source),
|
|
22
|
+
'ожидался код без .push( / .pop( как у массива-стека'
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('emit-body: dup — без stack-array, два одинаковых значения на выходе', function (t) {
|
|
27
|
+
const main = path.join(root, 'dup.sail')
|
|
28
|
+
const src = ['@w ( -> Num Num )', '', ' 1', '', ' dup', ''].join('\n')
|
|
29
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
30
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
31
|
+
const norm = path.normalize(main)
|
|
32
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
33
|
+
moduleStatus: 'ok',
|
|
34
|
+
valueBindings: true
|
|
35
|
+
})
|
|
36
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
37
|
+
const sig = w.normalizedSig
|
|
38
|
+
const { source } = emitWordBodyIr(w.irSteps, {
|
|
39
|
+
normalizedSig: sig,
|
|
40
|
+
wordName: 'w'
|
|
41
|
+
})
|
|
42
|
+
assertNoStackArrayModel(t, source)
|
|
43
|
+
t.ok(/const v\d+ = 1/.test(source), 'литерал 1')
|
|
44
|
+
t.ok(/return \[v\d+, v\d+\]/.test(source), 'return двух значений')
|
|
45
|
+
t.end()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('emit-body: swap', function (t) {
|
|
49
|
+
const main = path.join(root, 'swap.sail')
|
|
50
|
+
const src = ['@w ( -> Num Num )', '', ' 1', '', ' 2', '', ' swap', ''].join(
|
|
51
|
+
'\n'
|
|
52
|
+
)
|
|
53
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
54
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
55
|
+
const norm = path.normalize(main)
|
|
56
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
57
|
+
moduleStatus: 'ok',
|
|
58
|
+
valueBindings: true
|
|
59
|
+
})
|
|
60
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
61
|
+
const { source } = emitWordBodyIr(w.irSteps, {
|
|
62
|
+
normalizedSig: w.normalizedSig,
|
|
63
|
+
wordName: 'w'
|
|
64
|
+
})
|
|
65
|
+
assertNoStackArrayModel(t, source)
|
|
66
|
+
t.ok(source.includes('1') && source.includes('2'), 'литералы присутствуют')
|
|
67
|
+
t.ok(
|
|
68
|
+
!/const\s+v\d+\s*=\s+v\d+\s*;/.test(source),
|
|
69
|
+
'swap — без лишних const (только перестановка ссылок на уже существующие имена)'
|
|
70
|
+
)
|
|
71
|
+
t.end()
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('emit-body: drop', function (t) {
|
|
75
|
+
const main = path.join(root, 'drop.sail')
|
|
76
|
+
const src = ['@w ( -> )', '', ' 1', '', ' drop', ''].join('\n')
|
|
77
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
78
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
79
|
+
const norm = path.normalize(main)
|
|
80
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
81
|
+
moduleStatus: 'ok',
|
|
82
|
+
valueBindings: true
|
|
83
|
+
})
|
|
84
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
85
|
+
const { source } = emitWordBodyIr(w.irSteps, {
|
|
86
|
+
normalizedSig: w.normalizedSig,
|
|
87
|
+
wordName: 'w'
|
|
88
|
+
})
|
|
89
|
+
assertNoStackArrayModel(t, source)
|
|
90
|
+
t.ok(/\breturn;\s*$/.test(source.trim()), 'пустой результат — return;')
|
|
91
|
+
t.end()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('emit-body: локальный вызов Word (вершина — последний аргумент)', function (t) {
|
|
95
|
+
const main = path.join(root, 'local.sail')
|
|
96
|
+
const src = [
|
|
97
|
+
'@idem ( Num -> Num )',
|
|
98
|
+
'',
|
|
99
|
+
' dup drop',
|
|
100
|
+
'',
|
|
101
|
+
'@caller ( Num -> Num )',
|
|
102
|
+
'',
|
|
103
|
+
' /idem',
|
|
104
|
+
''
|
|
105
|
+
].join('\n')
|
|
106
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
107
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
108
|
+
const norm = path.normalize(main)
|
|
109
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
110
|
+
moduleStatus: 'ok',
|
|
111
|
+
valueBindings: true
|
|
112
|
+
})
|
|
113
|
+
const caller = payload.words.find((x) => x.name === 'caller')
|
|
114
|
+
const { source } = emitWordBodyIr(caller.irSteps, {
|
|
115
|
+
normalizedSig: caller.normalizedSig,
|
|
116
|
+
wordName: 'caller'
|
|
117
|
+
})
|
|
118
|
+
assertNoStackArrayModel(t, source)
|
|
119
|
+
t.ok(/idem\s*\(\s*p0\s*\)/.test(source), 'вызов idem(p0) — вершина последний аргумент')
|
|
120
|
+
t.end()
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
test('emit-body: strict — шаг без valueBindings', function (t) {
|
|
124
|
+
const main = path.join(root, 'strict.sail')
|
|
125
|
+
const src = ['@w ( -> Num )', '', ' 1', ''].join('\n')
|
|
126
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
127
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
128
|
+
const norm = path.normalize(main)
|
|
129
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
130
|
+
moduleStatus: 'ok',
|
|
131
|
+
valueBindings: true
|
|
132
|
+
})
|
|
133
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
134
|
+
const steps = structuredClone(w.irSteps)
|
|
135
|
+
delete steps[0].valueBindings
|
|
136
|
+
t.exception(
|
|
137
|
+
() => {
|
|
138
|
+
emitWordBodyIr(steps, { strict: true, wordName: 'w' })
|
|
139
|
+
},
|
|
140
|
+
/без valueBindings/
|
|
141
|
+
)
|
|
142
|
+
t.end()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('emit-body: исполнение числового сценария через new Function', function (t) {
|
|
146
|
+
const main = path.join(root, 'num.sail')
|
|
147
|
+
const src = ['@w ( -> Num )', '', ' 7', ''].join('\n')
|
|
148
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
149
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
150
|
+
const norm = path.normalize(main)
|
|
151
|
+
const payload = buildModuleIr(r.env, norm, {
|
|
152
|
+
moduleStatus: 'ok',
|
|
153
|
+
valueBindings: true
|
|
154
|
+
})
|
|
155
|
+
const w = payload.words.find((x) => x.name === 'w')
|
|
156
|
+
const { source } = emitWordBodyIr(w.irSteps, {
|
|
157
|
+
normalizedSig: w.normalizedSig,
|
|
158
|
+
wordName: 'w'
|
|
159
|
+
})
|
|
160
|
+
const fn = new Function(`${source}`)
|
|
161
|
+
t.is(fn(), 7)
|
|
162
|
+
t.end()
|
|
163
|
+
})
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 7: комбинаторы dip, keep, bi, tri, spread, both (RFC-builtins §4.4–4.6).
|
|
3
|
+
* Литеральные quotation с ожидаемой сигнатурой задаются через вспомогательные слова `( -> (...) )`.
|
|
4
|
+
*/
|
|
5
|
+
import test from 'brittle'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import { typecheckSail } from '../../index.js'
|
|
8
|
+
import {
|
|
9
|
+
emitModuleEsmSource,
|
|
10
|
+
resolveCompileLayout
|
|
11
|
+
} from '../../lib/codegen/index.js'
|
|
12
|
+
|
|
13
|
+
const root = path.resolve('/virtual/sail-codegen-builtins-7')
|
|
14
|
+
|
|
15
|
+
function esmSourceForNewFunction (source) {
|
|
16
|
+
return source.replace(/^export function /gm, 'function ')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function vfs (files) {
|
|
20
|
+
const norm = Object.fromEntries(
|
|
21
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
22
|
+
)
|
|
23
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sharedPrelude = [
|
|
27
|
+
'@id ( Num -> Num )',
|
|
28
|
+
'',
|
|
29
|
+
' dup',
|
|
30
|
+
'',
|
|
31
|
+
' drop',
|
|
32
|
+
'',
|
|
33
|
+
'',
|
|
34
|
+
'@innerQ ( -> ( Num -> Num ) )',
|
|
35
|
+
'',
|
|
36
|
+
' ( /id )',
|
|
37
|
+
'',
|
|
38
|
+
'',
|
|
39
|
+
'@innerK ( -> ( Num Num -> Num ) )',
|
|
40
|
+
'',
|
|
41
|
+
' ( drop )',
|
|
42
|
+
''
|
|
43
|
+
].join('\n')
|
|
44
|
+
|
|
45
|
+
test('stage7: dip', function (t) {
|
|
46
|
+
const sourceRoot = path.join(root, 'dip')
|
|
47
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
48
|
+
const src =
|
|
49
|
+
sharedPrelude +
|
|
50
|
+
[
|
|
51
|
+
'@dipCase ( -> Num Num )',
|
|
52
|
+
'',
|
|
53
|
+
' 1',
|
|
54
|
+
'',
|
|
55
|
+
' 2',
|
|
56
|
+
'',
|
|
57
|
+
' /innerQ',
|
|
58
|
+
'',
|
|
59
|
+
' dip',
|
|
60
|
+
''
|
|
61
|
+
].join('\n')
|
|
62
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
63
|
+
t.ok(r.ok)
|
|
64
|
+
const layout = resolveCompileLayout({
|
|
65
|
+
entryPath: main,
|
|
66
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
67
|
+
sourceRoot
|
|
68
|
+
})
|
|
69
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
70
|
+
t.ok(g.ok)
|
|
71
|
+
const mod = new Function(
|
|
72
|
+
`${esmSourceForNewFunction(g.source)}; return { dipCase }`
|
|
73
|
+
)()
|
|
74
|
+
const out = mod.dipCase()
|
|
75
|
+
t.ok(Array.isArray(out))
|
|
76
|
+
t.is(out.length, 2)
|
|
77
|
+
t.is(out[0], 1)
|
|
78
|
+
t.is(out[1], 2)
|
|
79
|
+
t.end()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('stage7: keep', function (t) {
|
|
83
|
+
const sourceRoot = path.join(root, 'keep')
|
|
84
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
85
|
+
const src =
|
|
86
|
+
sharedPrelude +
|
|
87
|
+
[
|
|
88
|
+
'@keepCase ( -> Num Num )',
|
|
89
|
+
'',
|
|
90
|
+
' 1',
|
|
91
|
+
'',
|
|
92
|
+
' 2',
|
|
93
|
+
'',
|
|
94
|
+
' /innerK',
|
|
95
|
+
'',
|
|
96
|
+
' keep',
|
|
97
|
+
''
|
|
98
|
+
].join('\n')
|
|
99
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
100
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
101
|
+
const layout = resolveCompileLayout({
|
|
102
|
+
entryPath: main,
|
|
103
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
104
|
+
sourceRoot
|
|
105
|
+
})
|
|
106
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
107
|
+
t.ok(g.ok)
|
|
108
|
+
const mod = new Function(
|
|
109
|
+
`${esmSourceForNewFunction(g.source)}; return { keepCase }`
|
|
110
|
+
)()
|
|
111
|
+
const out = mod.keepCase()
|
|
112
|
+
t.alike(out, [1, 2])
|
|
113
|
+
t.end()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('stage7: bi', function (t) {
|
|
117
|
+
const sourceRoot = path.join(root, 'bi')
|
|
118
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
119
|
+
const src =
|
|
120
|
+
sharedPrelude +
|
|
121
|
+
[
|
|
122
|
+
'@biCase ( -> Num Num Num )',
|
|
123
|
+
'',
|
|
124
|
+
' 0',
|
|
125
|
+
'',
|
|
126
|
+
' 1',
|
|
127
|
+
'',
|
|
128
|
+
' /innerQ',
|
|
129
|
+
'',
|
|
130
|
+
' /innerQ',
|
|
131
|
+
'',
|
|
132
|
+
' bi',
|
|
133
|
+
''
|
|
134
|
+
].join('\n')
|
|
135
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
136
|
+
t.ok(r.ok)
|
|
137
|
+
const layout = resolveCompileLayout({
|
|
138
|
+
entryPath: main,
|
|
139
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
140
|
+
sourceRoot
|
|
141
|
+
})
|
|
142
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
143
|
+
t.ok(g.ok)
|
|
144
|
+
const mod = new Function(
|
|
145
|
+
`${esmSourceForNewFunction(g.source)}; return { biCase }`
|
|
146
|
+
)()
|
|
147
|
+
t.alike(mod.biCase(), [0, 1, 1])
|
|
148
|
+
t.end()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
test('stage7: tri', function (t) {
|
|
152
|
+
const sourceRoot = path.join(root, 'tri')
|
|
153
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
154
|
+
const src =
|
|
155
|
+
sharedPrelude +
|
|
156
|
+
[
|
|
157
|
+
'@triCase ( -> Num Num Num Num )',
|
|
158
|
+
'',
|
|
159
|
+
' 0',
|
|
160
|
+
'',
|
|
161
|
+
' 1',
|
|
162
|
+
'',
|
|
163
|
+
' /innerQ',
|
|
164
|
+
'',
|
|
165
|
+
' /innerQ',
|
|
166
|
+
'',
|
|
167
|
+
' /innerQ',
|
|
168
|
+
'',
|
|
169
|
+
' tri',
|
|
170
|
+
''
|
|
171
|
+
].join('\n')
|
|
172
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
173
|
+
t.ok(r.ok)
|
|
174
|
+
const layout = resolveCompileLayout({
|
|
175
|
+
entryPath: main,
|
|
176
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
177
|
+
sourceRoot
|
|
178
|
+
})
|
|
179
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
180
|
+
t.ok(g.ok)
|
|
181
|
+
const mod = new Function(
|
|
182
|
+
`${esmSourceForNewFunction(g.source)}; return { triCase }`
|
|
183
|
+
)()
|
|
184
|
+
t.alike(mod.triCase(), [0, 1, 1, 1])
|
|
185
|
+
t.end()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('stage7: spread', function (t) {
|
|
189
|
+
const sourceRoot = path.join(root, 'spread')
|
|
190
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
191
|
+
const src =
|
|
192
|
+
sharedPrelude +
|
|
193
|
+
[
|
|
194
|
+
'@spreadCase ( -> Num Num Num )',
|
|
195
|
+
'',
|
|
196
|
+
' 0',
|
|
197
|
+
'',
|
|
198
|
+
' 1',
|
|
199
|
+
'',
|
|
200
|
+
' 2',
|
|
201
|
+
'',
|
|
202
|
+
' /innerQ',
|
|
203
|
+
'',
|
|
204
|
+
' /innerQ',
|
|
205
|
+
'',
|
|
206
|
+
' spread',
|
|
207
|
+
''
|
|
208
|
+
].join('\n')
|
|
209
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
210
|
+
t.ok(r.ok)
|
|
211
|
+
const layout = resolveCompileLayout({
|
|
212
|
+
entryPath: main,
|
|
213
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
214
|
+
sourceRoot
|
|
215
|
+
})
|
|
216
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
217
|
+
t.ok(g.ok)
|
|
218
|
+
const mod = new Function(
|
|
219
|
+
`${esmSourceForNewFunction(g.source)}; return { spreadCase }`
|
|
220
|
+
)()
|
|
221
|
+
t.alike(mod.spreadCase(), [0, 1, 2])
|
|
222
|
+
t.end()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
test('stage7: both', function (t) {
|
|
226
|
+
const sourceRoot = path.join(root, 'both')
|
|
227
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
228
|
+
const src =
|
|
229
|
+
sharedPrelude +
|
|
230
|
+
[
|
|
231
|
+
'@bothCase ( -> Num Num Num )',
|
|
232
|
+
'',
|
|
233
|
+
' 0',
|
|
234
|
+
'',
|
|
235
|
+
' 5',
|
|
236
|
+
'',
|
|
237
|
+
' 7',
|
|
238
|
+
'',
|
|
239
|
+
' /innerQ',
|
|
240
|
+
'',
|
|
241
|
+
' both',
|
|
242
|
+
''
|
|
243
|
+
].join('\n')
|
|
244
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
245
|
+
t.ok(r.ok)
|
|
246
|
+
const layout = resolveCompileLayout({
|
|
247
|
+
entryPath: main,
|
|
248
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
249
|
+
sourceRoot
|
|
250
|
+
})
|
|
251
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
252
|
+
t.ok(g.ok)
|
|
253
|
+
const mod = new Function(
|
|
254
|
+
`${esmSourceForNewFunction(g.source)}; return { bothCase }`
|
|
255
|
+
)()
|
|
256
|
+
t.alike(mod.bothCase(), [0, 5, 7])
|
|
257
|
+
t.end()
|
|
258
|
+
})
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 9: диагностики codegen (E51xx–E53xx), поле path, валидация layout.
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import { emitCodegenStage0Stub } from '../../lib/codegen/compile-graph.js'
|
|
8
|
+
import {
|
|
9
|
+
compileSailToOutDir,
|
|
10
|
+
resolveCompileLayout,
|
|
11
|
+
sailModuleToOutputJsPath
|
|
12
|
+
} from '../../lib/codegen/index.js'
|
|
13
|
+
|
|
14
|
+
const root = path.resolve('/virtual/sail-codegen-diag9')
|
|
15
|
+
|
|
16
|
+
function vfs (files) {
|
|
17
|
+
const norm = Object.fromEntries(
|
|
18
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
19
|
+
)
|
|
20
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test('stage9: E5105 — entryPath вне явного sourceRoot', async function (t) {
|
|
24
|
+
const sourceRoot = path.join(root, 'sr')
|
|
25
|
+
const entryOutside = path.join(root, 'other', 'main.sail')
|
|
26
|
+
const files = {
|
|
27
|
+
[entryOutside]: ['@m ( -> )', '', ''].join('\n')
|
|
28
|
+
}
|
|
29
|
+
const r = await compileSailToOutDir({
|
|
30
|
+
entryPath: entryOutside,
|
|
31
|
+
outDir: path.join(root, 'out'),
|
|
32
|
+
sourceRoot,
|
|
33
|
+
readFile: vfs(files)
|
|
34
|
+
})
|
|
35
|
+
t.ok(r.ok === false)
|
|
36
|
+
t.is(r.diagnostics[0].code, 'E5105')
|
|
37
|
+
t.ok(typeof r.diagnostics[0].path === 'string')
|
|
38
|
+
t.end()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('stage9: E5101 + path — sailModuleToOutputJsPath вне source-root', function (t) {
|
|
42
|
+
const layout = resolveCompileLayout({
|
|
43
|
+
entryPath: path.join(root, 'e.sail'),
|
|
44
|
+
outDir: path.join(root, 'o'),
|
|
45
|
+
sourceRoot: path.join(root, 'src')
|
|
46
|
+
})
|
|
47
|
+
const modOutside = path.join(root, 'external', 'm.sail')
|
|
48
|
+
const mapped = sailModuleToOutputJsPath(layout, modOutside)
|
|
49
|
+
t.ok(mapped.ok === false)
|
|
50
|
+
t.is(mapped.diagnostic.code, 'E5101')
|
|
51
|
+
t.is(path.normalize(mapped.diagnostic.path), path.normalize(modOutside))
|
|
52
|
+
t.end()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('stage9: E5301 — FFI в замыкании без readFile у emitCodegenStage0Stub', async function (t) {
|
|
56
|
+
const sourceRoot = path.join(root, 'ffi-noread')
|
|
57
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
58
|
+
const helper = path.join(sourceRoot, 'helper.js')
|
|
59
|
+
const files = {
|
|
60
|
+
[main]: [
|
|
61
|
+
'+H ( @add ) ./helper.js',
|
|
62
|
+
'@main ( Num Num -> Num )',
|
|
63
|
+
'',
|
|
64
|
+
' /add'
|
|
65
|
+
].join('\n'),
|
|
66
|
+
[helper]: [
|
|
67
|
+
'/**',
|
|
68
|
+
' * @sail',
|
|
69
|
+
' * @add ( Num Num -> Num )',
|
|
70
|
+
' */',
|
|
71
|
+
'export function add (a, b) { return a + b }'
|
|
72
|
+
].join('\n')
|
|
73
|
+
}
|
|
74
|
+
const read = vfs(files)
|
|
75
|
+
const tc = typecheckSail({ entryPath: main, readFile: read })
|
|
76
|
+
t.ok(tc.ok, tc.diagnostics?.map((d) => d.message).join('; ') ?? '')
|
|
77
|
+
const layout = resolveCompileLayout({
|
|
78
|
+
entryPath: main,
|
|
79
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
80
|
+
sourceRoot
|
|
81
|
+
})
|
|
82
|
+
const sink = {
|
|
83
|
+
mkdir: async () => {},
|
|
84
|
+
writeFile: async () => {}
|
|
85
|
+
}
|
|
86
|
+
const gen = await emitCodegenStage0Stub(tc.env, layout, sink, undefined)
|
|
87
|
+
t.ok(gen.ok === false)
|
|
88
|
+
t.is(gen.diagnostics[0].code, 'E5301')
|
|
89
|
+
t.end()
|
|
90
|
+
})
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 8: JSDoc `@sail` в сгенерированном ESM (RFC-compile §11, RFC-0.1 §10.1).
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { parseSource, typecheckSail } from '../../index.js'
|
|
7
|
+
import {
|
|
8
|
+
emitModuleEsmSource,
|
|
9
|
+
resolveCompileLayout
|
|
10
|
+
} from '../../lib/codegen/index.js'
|
|
11
|
+
import { extractSailFragmentsFromJs } from '../../lib/ffi/extract-jsdoc-sail.js'
|
|
12
|
+
|
|
13
|
+
const root = path.resolve('/virtual/sail-codegen-jsdoc8')
|
|
14
|
+
|
|
15
|
+
function vfs (files) {
|
|
16
|
+
const norm = Object.fromEntries(
|
|
17
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
18
|
+
)
|
|
19
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('stage8: extractSailFragmentsFromJs — & в первом фрагменте, @word во втором', function (t) {
|
|
23
|
+
const main = path.join(root, 'twoblock.sail')
|
|
24
|
+
const src = [
|
|
25
|
+
'& Color',
|
|
26
|
+
'-- цветовой тип --',
|
|
27
|
+
'| Red',
|
|
28
|
+
'| Green',
|
|
29
|
+
'',
|
|
30
|
+
'@label ( -> Str )',
|
|
31
|
+
'-- текст для слова --',
|
|
32
|
+
' "ok"',
|
|
33
|
+
''
|
|
34
|
+
].join('\n')
|
|
35
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
36
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
37
|
+
const layout = resolveCompileLayout({
|
|
38
|
+
entryPath: main,
|
|
39
|
+
outDir: path.join(root, '_out'),
|
|
40
|
+
sourceRoot: root
|
|
41
|
+
})
|
|
42
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
43
|
+
t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
|
|
44
|
+
const frags = extractSailFragmentsFromJs(g.source)
|
|
45
|
+
t.ok(frags.length >= 2, 'минимум: заголовок + слово')
|
|
46
|
+
t.ok(frags[0].includes('& Color'), 'первый фрагмент: ADT')
|
|
47
|
+
t.ok(frags[0].includes('| Red'), 'теги sum в заголовке')
|
|
48
|
+
const wordFrag = frags.find((f) => f.includes('@label'))
|
|
49
|
+
t.ok(wordFrag, 'фрагмент с @label')
|
|
50
|
+
t.ok(wordFrag.includes('@label'), '@label в извлечённом')
|
|
51
|
+
for (const f of frags) {
|
|
52
|
+
const pr = parseSource(f)
|
|
53
|
+
t.ok(pr.ok, `фрагмент парсится как Sail: ${pr.diagnostics?.[0]?.message ?? ''}`)
|
|
54
|
+
}
|
|
55
|
+
t.ok(g.source.includes('текст для слова'), 'doc слова в префиксе JSDoc у export')
|
|
56
|
+
t.end()
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('stage8: текст из doc AST — в префиксе JSDoc до @sail и валидный Sail после', function (t) {
|
|
60
|
+
const main = path.join(root, 'prose.sail')
|
|
61
|
+
const typeNote = 'заметка к типу Bit'
|
|
62
|
+
const src = [
|
|
63
|
+
'& Bit',
|
|
64
|
+
`-- ${typeNote} --`,
|
|
65
|
+
'| B0',
|
|
66
|
+
'| B1',
|
|
67
|
+
'',
|
|
68
|
+
'@flip ( Num -> )',
|
|
69
|
+
' drop',
|
|
70
|
+
''
|
|
71
|
+
].join('\n')
|
|
72
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
73
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
74
|
+
const layout = resolveCompileLayout({
|
|
75
|
+
entryPath: main,
|
|
76
|
+
outDir: path.join(root, '_out2'),
|
|
77
|
+
sourceRoot: root
|
|
78
|
+
})
|
|
79
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
80
|
+
t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
|
|
81
|
+
const idx = g.source.indexOf('@sail')
|
|
82
|
+
t.ok(idx >= 0)
|
|
83
|
+
const beforeFirstTag = g.source.slice(0, idx)
|
|
84
|
+
t.ok(
|
|
85
|
+
beforeFirstTag.includes(typeNote),
|
|
86
|
+
'doc типа продублирован строками ` * …` до первого @sail'
|
|
87
|
+
)
|
|
88
|
+
const frags = extractSailFragmentsFromJs(g.source)
|
|
89
|
+
const head = frags[0]
|
|
90
|
+
t.ok(head.includes('--'), 'в извлечённом заголовке есть sail doc_block')
|
|
91
|
+
const pr = parseSource(head)
|
|
92
|
+
t.ok(pr.ok, pr.diagnostics?.[0]?.message ?? '')
|
|
93
|
+
t.end()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('stage8: exportAsync — +Async в фрагменте @sail у экспорта', function (t) {
|
|
97
|
+
const main = path.join(root, 'asyncfrag.sail')
|
|
98
|
+
const src = ['@wake ( -> +Async )', '', ''].join('\n')
|
|
99
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs({ [main]: src }) })
|
|
100
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
101
|
+
const layout = resolveCompileLayout({
|
|
102
|
+
entryPath: main,
|
|
103
|
+
outDir: path.join(root, '_out3'),
|
|
104
|
+
sourceRoot: root
|
|
105
|
+
})
|
|
106
|
+
const g = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
107
|
+
t.ok(g.ok, g.ok === false ? g.diagnostics?.[0]?.message : '')
|
|
108
|
+
t.ok(/\bexport async function wake\b/.test(g.source), 'async export')
|
|
109
|
+
const frags = extractSailFragmentsFromJs(g.source)
|
|
110
|
+
const wFrag = frags.find((f) => f.includes('@wake'))
|
|
111
|
+
t.ok(wFrag && /\+Async/.test(wFrag), '+Async в контракте wake')
|
|
112
|
+
t.end()
|
|
113
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L5 этап 3: один модуль → ESM (импорты + export function), без записи на диск.
|
|
3
|
+
*/
|
|
4
|
+
import test from 'brittle'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { typecheckSail } from '../../index.js'
|
|
7
|
+
import {
|
|
8
|
+
emitModuleEsmSource,
|
|
9
|
+
resolveCompileLayout
|
|
10
|
+
} from '../../lib/codegen/index.js'
|
|
11
|
+
|
|
12
|
+
const root = path.resolve('/virtual/sail-codegen-stage3')
|
|
13
|
+
|
|
14
|
+
function vfs (files) {
|
|
15
|
+
const norm = Object.fromEntries(
|
|
16
|
+
Object.entries(files).map(([k, v]) => [path.normalize(k), v])
|
|
17
|
+
)
|
|
18
|
+
return (p) => norm[path.normalize(p)] ?? null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('emit-module stage3: два .sail — import * as Lib и export function', function (t) {
|
|
22
|
+
const sourceRoot = path.join(root, 'repo')
|
|
23
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
24
|
+
const lib = path.join(sourceRoot, 'lib.sail')
|
|
25
|
+
const files = {
|
|
26
|
+
[main]: ['+Lib ./lib.sail', '@main ( -> )', '', ' ~Lib/hello'].join('\n'),
|
|
27
|
+
[lib]: ['@hello ( -> )', '', ''].join('\n')
|
|
28
|
+
}
|
|
29
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
|
|
30
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
31
|
+
const layout = resolveCompileLayout({
|
|
32
|
+
entryPath: main,
|
|
33
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
34
|
+
sourceRoot
|
|
35
|
+
})
|
|
36
|
+
const libGen = emitModuleEsmSource({ env: r.env, modulePath: lib, layout })
|
|
37
|
+
t.ok(libGen.ok, libGen.ok === false ? libGen.diagnostics?.[0]?.message : '')
|
|
38
|
+
t.ok(
|
|
39
|
+
/\bexport function hello\s*\(/.test(libGen.source),
|
|
40
|
+
'lib: export function hello'
|
|
41
|
+
)
|
|
42
|
+
const mainGen = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
43
|
+
t.ok(mainGen.ok, mainGen.ok === false ? mainGen.diagnostics?.[0]?.message : '')
|
|
44
|
+
t.ok(
|
|
45
|
+
mainGen.source.includes('import * as Lib from "./lib.js"'),
|
|
46
|
+
'main: статический import относительного .js'
|
|
47
|
+
)
|
|
48
|
+
t.ok(/\bLib\.hello\s*\(/.test(mainGen.source), 'main: вызов Lib.hello()')
|
|
49
|
+
t.ok(!/\bimport\s*\(/.test(mainGen.source), 'без import() в сгенерированном тексте')
|
|
50
|
+
t.end()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('emit-module stage3: скобочный импорт — import { hello } и вызов hello()', function (t) {
|
|
54
|
+
const sourceRoot = path.join(root, 'bracket')
|
|
55
|
+
const main = path.join(sourceRoot, 'main.sail')
|
|
56
|
+
const lib = path.join(sourceRoot, 'lib.sail')
|
|
57
|
+
const files = {
|
|
58
|
+
[main]: ['+Lib ( @hello ) ./lib.sail', '@main ( -> )', '', ' ~Lib/hello'].join(
|
|
59
|
+
'\n'
|
|
60
|
+
),
|
|
61
|
+
[lib]: ['@hello ( -> )', '', ''].join('\n')
|
|
62
|
+
}
|
|
63
|
+
const r = typecheckSail({ entryPath: main, readFile: vfs(files) })
|
|
64
|
+
t.ok(r.ok, r.diagnostics?.map((d) => d.message).join('; '))
|
|
65
|
+
const layout = resolveCompileLayout({
|
|
66
|
+
entryPath: main,
|
|
67
|
+
outDir: path.join(sourceRoot, '_out'),
|
|
68
|
+
sourceRoot
|
|
69
|
+
})
|
|
70
|
+
const mainGen = emitModuleEsmSource({ env: r.env, modulePath: main, layout })
|
|
71
|
+
t.ok(mainGen.ok, mainGen.ok === false ? mainGen.diagnostics?.[0]?.message : '')
|
|
72
|
+
t.ok(
|
|
73
|
+
/import \{\s*hello\s*\} from/.test(mainGen.source),
|
|
74
|
+
'именованный import hello'
|
|
75
|
+
)
|
|
76
|
+
t.ok(/\bhello\s*\(\s*\)/.test(mainGen.source), 'вызов hello(), не Lib.hello')
|
|
77
|
+
t.end()
|
|
78
|
+
})
|