@panagos/jojoscript 0.7.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/LICENSE +24 -0
- package/README.md +418 -0
- package/bin/jojo.js +170 -0
- package/package.json +59 -0
- package/src/compiler.js +169 -0
- package/src/index.js +19 -0
- package/src/lexer.js +178 -0
- package/src/match-parser.js +215 -0
- package/src/pipeline-parser.js +418 -0
- package/src/runtime/collections.js +327 -0
- package/src/runtime/index.js +29 -0
- package/src/transforms.js +281 -0
- package/src/type-annotations.js +191 -0
package/src/compiler.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
transformPipelines,
|
|
6
|
+
findPipelineStdlibFunctions
|
|
7
|
+
} from './pipeline-parser.js'
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
transformMutableDeclarations,
|
|
11
|
+
transformConstDeclarations,
|
|
12
|
+
transformMultiBindingDeclarations,
|
|
13
|
+
transformDestructuringDeclarations,
|
|
14
|
+
transformFunctionDeclarations,
|
|
15
|
+
transformExpressionFunctions,
|
|
16
|
+
transformLocalImports
|
|
17
|
+
} from './transforms.js'
|
|
18
|
+
|
|
19
|
+
import { transformMatchExpressions } from './match-parser.js'
|
|
20
|
+
import { transformTypeAnnotations } from './type-annotations.js'
|
|
21
|
+
|
|
22
|
+
const JOJO_RUNTIME = 'jojoscript/runtime'
|
|
23
|
+
|
|
24
|
+
export function compile(source) {
|
|
25
|
+
let result = source
|
|
26
|
+
|
|
27
|
+
/*
|
|
28
|
+
* Optional type annotations, stripped and re-expressed as JSDoc before
|
|
29
|
+
* anything else touches `fn` declarations.
|
|
30
|
+
*/
|
|
31
|
+
result = transformTypeAnnotations(result)
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* Jojo functions.
|
|
35
|
+
*/
|
|
36
|
+
result = transformExpressionFunctions(result)
|
|
37
|
+
result = transformFunctionDeclarations(result)
|
|
38
|
+
|
|
39
|
+
/*
|
|
40
|
+
* `match` expressions.
|
|
41
|
+
*/
|
|
42
|
+
result = transformMatchExpressions(result)
|
|
43
|
+
|
|
44
|
+
/*
|
|
45
|
+
* Jojo declarations.
|
|
46
|
+
*/
|
|
47
|
+
result = transformMultiBindingDeclarations(result)
|
|
48
|
+
result = transformDestructuringDeclarations(result)
|
|
49
|
+
result = transformMutableDeclarations(result)
|
|
50
|
+
result = transformConstDeclarations(result)
|
|
51
|
+
|
|
52
|
+
/*
|
|
53
|
+
* Remember which pipeline stages need the Jojo runtime before
|
|
54
|
+
* the pipeline syntax itself is removed.
|
|
55
|
+
*/
|
|
56
|
+
const pipelineStdlibFunctions =
|
|
57
|
+
findPipelineStdlibFunctions(result)
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* Pipelines.
|
|
61
|
+
*/
|
|
62
|
+
result = transformPipelines(result)
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* ES module imports.
|
|
66
|
+
*/
|
|
67
|
+
result = transformLocalImports(result)
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* Generated JavaScript imports the runtime as a normal package.
|
|
71
|
+
* This makes compiled applications portable and avoids globals. If the
|
|
72
|
+
* source already has a manual `jojoscript/runtime` import (e.g. for a
|
|
73
|
+
* stdlib function that is only ever called directly, never through a
|
|
74
|
+
* pipeline stage), the auto-imported names are merged into it instead
|
|
75
|
+
* of creating a second, colliding import declaration.
|
|
76
|
+
*/
|
|
77
|
+
if (pipelineStdlibFunctions.length) {
|
|
78
|
+
result = mergeRuntimeImport(result, pipelineStdlibFunctions)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result.endsWith('\n') ? result : result + '\n'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const RUNTIME_IMPORT_PATTERN = /import\s*\{([^}]*)\}\s*from\s*(["'])jojoscript\/runtime\2/
|
|
85
|
+
|
|
86
|
+
function mergeRuntimeImport(source, stdlibFunctions) {
|
|
87
|
+
const existing = source.match(RUNTIME_IMPORT_PATTERN)
|
|
88
|
+
|
|
89
|
+
if (!existing) {
|
|
90
|
+
return `import { ${stdlibFunctions.join(', ')} } from "${JOJO_RUNTIME}"\n` + source
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const existingNames = existing[1].split(',').map(name => name.trim()).filter(Boolean)
|
|
94
|
+
const merged = [...new Set([...existingNames, ...stdlibFunctions])]
|
|
95
|
+
|
|
96
|
+
return source.replace(
|
|
97
|
+
RUNTIME_IMPORT_PATTERN,
|
|
98
|
+
`import { ${merged.join(', ')} } from "${JOJO_RUNTIME}"`
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function compileFile(input, outDir = null) {
|
|
103
|
+
const src = path.resolve(input)
|
|
104
|
+
const dir = outDir
|
|
105
|
+
? path.resolve(outDir)
|
|
106
|
+
: path.join(path.dirname(src), 'dist')
|
|
107
|
+
|
|
108
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
109
|
+
|
|
110
|
+
const out = path.join(
|
|
111
|
+
dir,
|
|
112
|
+
path.basename(src, path.extname(src)) + '.js'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
fs.writeFileSync(
|
|
116
|
+
out,
|
|
117
|
+
compile(fs.readFileSync(src, 'utf8')),
|
|
118
|
+
'utf8'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return { input: src, output: out }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function compileDirectory(inputDir, outDir = null) {
|
|
125
|
+
const root = path.resolve(inputDir)
|
|
126
|
+
const target = outDir
|
|
127
|
+
? path.resolve(outDir)
|
|
128
|
+
: path.join(root, 'dist')
|
|
129
|
+
|
|
130
|
+
const result = []
|
|
131
|
+
|
|
132
|
+
function walk(dir) {
|
|
133
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
134
|
+
if (
|
|
135
|
+
entry.name === 'node_modules' ||
|
|
136
|
+
entry.name === 'dist' ||
|
|
137
|
+
entry.name.startsWith('.')
|
|
138
|
+
) {
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const full = path.join(dir, entry.name)
|
|
143
|
+
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
walk(full)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!entry.name.endsWith('.jojo')) continue
|
|
150
|
+
|
|
151
|
+
const rel =
|
|
152
|
+
path.relative(root, full).slice(0, -'.jojo'.length) + '.js'
|
|
153
|
+
|
|
154
|
+
const out = path.join(target, rel)
|
|
155
|
+
|
|
156
|
+
fs.mkdirSync(path.dirname(out), { recursive: true })
|
|
157
|
+
fs.writeFileSync(
|
|
158
|
+
out,
|
|
159
|
+
compile(fs.readFileSync(full, 'utf8')),
|
|
160
|
+
'utf8'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
result.push({ input: full, output: out })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
walk(root)
|
|
168
|
+
return result
|
|
169
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export {
|
|
2
|
+
compile,
|
|
3
|
+
compileFile,
|
|
4
|
+
compileDirectory
|
|
5
|
+
} from './compiler.js'
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
transformPipelines,
|
|
9
|
+
emitPipeline,
|
|
10
|
+
findPipelineStdlibFunctions
|
|
11
|
+
} from './pipeline-parser.js'
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
transformMatchExpressions
|
|
15
|
+
} from './match-parser.js'
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
transformTypeAnnotations
|
|
19
|
+
} from './type-annotations.js'
|
package/src/lexer.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
export function tokenize(source) {
|
|
2
|
+
const tokens = []
|
|
3
|
+
let i = 0
|
|
4
|
+
|
|
5
|
+
while (i < source.length) {
|
|
6
|
+
const start = i
|
|
7
|
+
const c = source[i]
|
|
8
|
+
|
|
9
|
+
if (/\s/.test(c)) {
|
|
10
|
+
i++
|
|
11
|
+
continue
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (source.startsWith('//', i)) {
|
|
15
|
+
i += 2
|
|
16
|
+
while (i < source.length && source[i] !== '\n') i++
|
|
17
|
+
continue
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (source.startsWith('/*', i)) {
|
|
21
|
+
i += 2
|
|
22
|
+
while (i < source.length && !source.startsWith('*/', i)) i++
|
|
23
|
+
if (i < source.length) i += 2
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
28
|
+
const quote = c
|
|
29
|
+
i++
|
|
30
|
+
let templateExpressionDepth = 0
|
|
31
|
+
while (i < source.length) {
|
|
32
|
+
if (source[i] === '\\') {
|
|
33
|
+
i += 2
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
if (quote === '`' && source.startsWith('${', i)) {
|
|
37
|
+
templateExpressionDepth++
|
|
38
|
+
i += 2
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
if (quote === '`' && source[i] === '}' && templateExpressionDepth > 0) {
|
|
42
|
+
templateExpressionDepth--
|
|
43
|
+
i++
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (source[i] === quote && templateExpressionDepth === 0) {
|
|
47
|
+
i++
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
i++
|
|
51
|
+
}
|
|
52
|
+
tokens.push({ type: 'string', value: source.slice(start, i), start, end: i })
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (/[A-Za-z_$]/.test(c)) {
|
|
57
|
+
i++
|
|
58
|
+
while (i < source.length && /[A-Za-z0-9_$]/.test(source[i])) i++
|
|
59
|
+
tokens.push({ type: 'identifier', value: source.slice(start, i), start, end: i })
|
|
60
|
+
continue
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (/[0-9]/.test(c)) {
|
|
64
|
+
i++
|
|
65
|
+
while (i < source.length && /[A-Za-z0-9_.$]/.test(source[i])) i++
|
|
66
|
+
tokens.push({ type: 'number', value: source.slice(start, i), start, end: i })
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const operators = [
|
|
71
|
+
'>>>=' , '===', '!==', '**=', '&&=', '||=', '??=',
|
|
72
|
+
'=>', '|>', '?.', '??', '==', '!=', '<=', '>=',
|
|
73
|
+
'&&', '||', '**', '++', '--', '+=', '-=', '*=', '/=',
|
|
74
|
+
'%=', '...', '<<', '>>', '>>>', '?.'
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
const op = operators.find(value => source.startsWith(value, i))
|
|
78
|
+
if (op) {
|
|
79
|
+
tokens.push({
|
|
80
|
+
type: op === '|>' ? 'pipeline' : 'operator',
|
|
81
|
+
value: op,
|
|
82
|
+
start,
|
|
83
|
+
end: i + op.length
|
|
84
|
+
})
|
|
85
|
+
i += op.length
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
tokens.push({ type: 'punctuation', value: c, start, end: i + 1 })
|
|
90
|
+
i++
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return tokens
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function significantTokens(tokens) {
|
|
97
|
+
return tokens
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
* Produces a same-length copy of `source` where the contents of comments
|
|
102
|
+
* and/or string/template literals are replaced with blank spaces (newlines
|
|
103
|
+
* are preserved so line-based/anchored regexes keep working).
|
|
104
|
+
*
|
|
105
|
+
* This lets regex-based transforms and character scanners check "is this
|
|
106
|
+
* position real code?" by comparing against the mask, without having to
|
|
107
|
+
* duplicate string/comment-parsing logic everywhere.
|
|
108
|
+
*/
|
|
109
|
+
export function maskNonCode(source, options = {}) {
|
|
110
|
+
const maskStrings = options.strings !== false
|
|
111
|
+
const maskComments = options.comments !== false
|
|
112
|
+
|
|
113
|
+
let mask = ''
|
|
114
|
+
let i = 0
|
|
115
|
+
|
|
116
|
+
while (i < source.length) {
|
|
117
|
+
if (source.startsWith('//', i)) {
|
|
118
|
+
const start = i
|
|
119
|
+
while (i < source.length && source[i] !== '\n') i++
|
|
120
|
+
mask += maskComments ? blank(source, start, i) : source.slice(start, i)
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (source.startsWith('/*', i)) {
|
|
125
|
+
const start = i
|
|
126
|
+
i += 2
|
|
127
|
+
while (i < source.length && !source.startsWith('*/', i)) i++
|
|
128
|
+
if (i < source.length) i += 2
|
|
129
|
+
mask += maskComments ? blank(source, start, i) : source.slice(start, i)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const c = source[i]
|
|
134
|
+
|
|
135
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
136
|
+
const quote = c
|
|
137
|
+
const start = i
|
|
138
|
+
i++
|
|
139
|
+
let templateExpressionDepth = 0
|
|
140
|
+
while (i < source.length) {
|
|
141
|
+
if (source[i] === '\\') {
|
|
142
|
+
i += 2
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
if (quote === '`' && source.startsWith('${', i)) {
|
|
146
|
+
templateExpressionDepth++
|
|
147
|
+
i += 2
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (quote === '`' && source[i] === '}' && templateExpressionDepth > 0) {
|
|
151
|
+
templateExpressionDepth--
|
|
152
|
+
i++
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (source[i] === quote && templateExpressionDepth === 0) {
|
|
156
|
+
i++
|
|
157
|
+
break
|
|
158
|
+
}
|
|
159
|
+
i++
|
|
160
|
+
}
|
|
161
|
+
mask += maskStrings ? blank(source, start, i) : source.slice(start, i)
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
mask += c
|
|
166
|
+
i++
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return mask
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function blank(source, start, end) {
|
|
173
|
+
let result = ''
|
|
174
|
+
for (let i = start; i < end; i++) {
|
|
175
|
+
result += source[i] === '\n' ? '\n' : ' '
|
|
176
|
+
}
|
|
177
|
+
return result
|
|
178
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { tokenize, significantTokens } from './lexer.js'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* `match` compiles a value-equality expression construct:
|
|
5
|
+
*
|
|
6
|
+
* match status {
|
|
7
|
+
* "ok" => handleOk(),
|
|
8
|
+
* "error" => handleError(),
|
|
9
|
+
* _ => handleUnknown()
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* into an immediately-invoked arrow function that compares the subject
|
|
13
|
+
* against each pattern with `===`, in order, falling through to the
|
|
14
|
+
* mandatory default `_` arm:
|
|
15
|
+
*
|
|
16
|
+
* (() => {
|
|
17
|
+
* const __match = status;
|
|
18
|
+
* if (__match === ("ok")) return handleOk();
|
|
19
|
+
* if (__match === ("error")) return handleError();
|
|
20
|
+
* return handleUnknown();
|
|
21
|
+
* })()
|
|
22
|
+
*
|
|
23
|
+
* This is deliberately a value-equality match, not structural pattern
|
|
24
|
+
* matching: patterns are plain expressions compared with `===`, and arms
|
|
25
|
+
* are single expressions (no statement blocks). The default `_` arm is
|
|
26
|
+
* required and must be last.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
function text(source, tokens) {
|
|
30
|
+
if (!tokens.length) return ''
|
|
31
|
+
return source.slice(tokens[0].start, tokens[tokens.length - 1].end)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitTopLevelCommas(tokens) {
|
|
35
|
+
const result = []
|
|
36
|
+
let start = 0
|
|
37
|
+
let depth = 0
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
40
|
+
const value = tokens[i].value
|
|
41
|
+
if (value === '(' || value === '[' || value === '{') depth++
|
|
42
|
+
else if (value === ')' || value === ']' || value === '}') depth--
|
|
43
|
+
else if (value === ',' && depth === 0) {
|
|
44
|
+
result.push(tokens.slice(start, i))
|
|
45
|
+
start = i + 1
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (start < tokens.length) result.push(tokens.slice(start))
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* Distinguishes the `match` keyword from ordinary identifiers named
|
|
55
|
+
* "match" (e.g. `string.match(regex)`, `const match = ...`, a `match`
|
|
56
|
+
* loop variable, or a `match:` object key).
|
|
57
|
+
*/
|
|
58
|
+
function isMatchKeyword(tokens, index) {
|
|
59
|
+
const token = tokens[index]
|
|
60
|
+
if (token.type !== 'identifier' || token.value !== 'match') return false
|
|
61
|
+
|
|
62
|
+
const prev = tokens[index - 1]
|
|
63
|
+
if (prev && (prev.value === '.' || prev.value === 'function' || prev.value === 'class')) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const next = tokens[index + 1]
|
|
68
|
+
if (!next) return false
|
|
69
|
+
if (['(', ':', '=', '.', ',', ')', ']', '}'].includes(next.value)) return false
|
|
70
|
+
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* Scans forward from a candidate `match` keyword looking for the subject
|
|
76
|
+
* expression followed by a top-level `{`. Bounded defensively: if the
|
|
77
|
+
* scan leaves the enclosing expression (unbalanced closing bracket) or
|
|
78
|
+
* runs too far without finding `{`, this is not a match construct.
|
|
79
|
+
*/
|
|
80
|
+
function findMatchBlock(source, tokens, index) {
|
|
81
|
+
let depth = 0
|
|
82
|
+
let braceOpen = -1
|
|
83
|
+
|
|
84
|
+
const limit = Math.min(tokens.length, index + 60)
|
|
85
|
+
|
|
86
|
+
for (let i = index + 1; i < limit; i++) {
|
|
87
|
+
const value = tokens[i].value
|
|
88
|
+
const prevToken = tokens[i - 1]
|
|
89
|
+
|
|
90
|
+
if (prevToken) {
|
|
91
|
+
const gap = source.slice(prevToken.end, tokens[i].start)
|
|
92
|
+
if (depth === 0 && (gap.match(/\n/g) || []).length > 1) return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (value === '(' || value === '[') { depth++; continue }
|
|
96
|
+
if (value === ')' || value === ']') {
|
|
97
|
+
depth--
|
|
98
|
+
if (depth < 0) return null
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
if (value === ';' && depth === 0) return null
|
|
102
|
+
if (value === '{' && depth === 0) { braceOpen = i; break }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (braceOpen === -1) return null
|
|
106
|
+
|
|
107
|
+
const subjectTokens = tokens.slice(index + 1, braceOpen)
|
|
108
|
+
if (!subjectTokens.length) return null
|
|
109
|
+
|
|
110
|
+
let braceDepth = 0
|
|
111
|
+
let braceClose = -1
|
|
112
|
+
|
|
113
|
+
for (let j = braceOpen; j < tokens.length; j++) {
|
|
114
|
+
if (tokens[j].value === '{') braceDepth++
|
|
115
|
+
else if (tokens[j].value === '}') {
|
|
116
|
+
braceDepth--
|
|
117
|
+
if (braceDepth === 0) { braceClose = j; break }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (braceClose === -1) {
|
|
122
|
+
throw new SyntaxError(`Unclosed match block:\n${text(source, tokens.slice(index, braceOpen + 1))}`)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { subjectTokens, braceOpen, braceClose }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function parseArms(source, tokens) {
|
|
129
|
+
const groups = splitTopLevelCommas(tokens).filter(group => group.length)
|
|
130
|
+
|
|
131
|
+
if (!groups.length) {
|
|
132
|
+
throw new SyntaxError('match requires at least one arm')
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return groups.map(group => {
|
|
136
|
+
let depth = 0
|
|
137
|
+
let arrowIndex = -1
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < group.length; i++) {
|
|
140
|
+
const value = group[i].value
|
|
141
|
+
if (value === '(' || value === '[' || value === '{') depth++
|
|
142
|
+
else if (value === ')' || value === ']' || value === '}') depth--
|
|
143
|
+
else if (value === '=>' && depth === 0) { arrowIndex = i; break }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (arrowIndex === -1) {
|
|
147
|
+
throw new SyntaxError(`Invalid match arm, expected \`pattern => expression\`:\n${text(source, group)}`)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const pattern = text(source, group.slice(0, arrowIndex)).trim()
|
|
151
|
+
const expression = text(source, group.slice(arrowIndex + 1)).trim()
|
|
152
|
+
|
|
153
|
+
if (!pattern || !expression) {
|
|
154
|
+
throw new SyntaxError(`Invalid match arm, expected \`pattern => expression\`:\n${text(source, group)}`)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { pattern, expression }
|
|
158
|
+
})
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function emitMatch(subject, arms) {
|
|
162
|
+
const defaultIndex = arms.findIndex(arm => arm.pattern === '_')
|
|
163
|
+
|
|
164
|
+
if (defaultIndex === -1) {
|
|
165
|
+
throw new SyntaxError('match requires a default `_` arm')
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (defaultIndex !== arms.length - 1) {
|
|
169
|
+
throw new SyntaxError('the default `_` arm must be the last arm in match')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const lines = arms.map(arm =>
|
|
173
|
+
arm.pattern === '_'
|
|
174
|
+
? ` return ${arm.expression};`
|
|
175
|
+
: ` if (__match === (${arm.pattern})) return ${arm.expression};`
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return `(() => {\n const __match = ${subject};\n${lines.join('\n')}\n})()`
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function transformMatchExpressions(source) {
|
|
182
|
+
let result = source
|
|
183
|
+
|
|
184
|
+
for (let iteration = 0; iteration < 1000; iteration++) {
|
|
185
|
+
const tokens = significantTokens(tokenize(result))
|
|
186
|
+
|
|
187
|
+
let matchIndex = -1
|
|
188
|
+
let block = null
|
|
189
|
+
|
|
190
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
191
|
+
if (!isMatchKeyword(tokens, i)) continue
|
|
192
|
+
|
|
193
|
+
const candidate = findMatchBlock(result, tokens, i)
|
|
194
|
+
if (candidate) {
|
|
195
|
+
matchIndex = i
|
|
196
|
+
block = candidate
|
|
197
|
+
break
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (matchIndex === -1) return result
|
|
202
|
+
|
|
203
|
+
const subject = text(result, block.subjectTokens).trim()
|
|
204
|
+
const bodyTokens = tokens.slice(block.braceOpen + 1, block.braceClose)
|
|
205
|
+
const arms = parseArms(result, bodyTokens)
|
|
206
|
+
const replacement = emitMatch(subject, arms)
|
|
207
|
+
|
|
208
|
+
const start = tokens[matchIndex].start
|
|
209
|
+
const end = tokens[block.braceClose].end
|
|
210
|
+
|
|
211
|
+
result = result.slice(0, start) + replacement + result.slice(end)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
throw new SyntaxError('match transformation exceeded 1000 iterations')
|
|
215
|
+
}
|