@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { tokenize, significantTokens } from './lexer.js'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Optional, opt-in type annotations for `fn` parameters and return values:
|
|
5
|
+
*
|
|
6
|
+
* fn add(a: number, b: number): number { return a + b }
|
|
7
|
+
* fn double(value: number): number = value * 2
|
|
8
|
+
*
|
|
9
|
+
* Annotations are never checked. They are stripped from the emitted
|
|
10
|
+
* JavaScript and re-expressed as a JSDoc block immediately above the
|
|
11
|
+
* function, so editors/tools that understand JSDoc (including plain
|
|
12
|
+
* Node/TypeScript-in-JS tooling) get parameter and return type
|
|
13
|
+
* information "for free" without JojoScript needing its own type checker:
|
|
14
|
+
*
|
|
15
|
+
* /**
|
|
16
|
+
* * @param {number} a
|
|
17
|
+
* * @param {number} b
|
|
18
|
+
* * @returns {number}
|
|
19
|
+
* * /
|
|
20
|
+
* function add(a, b) { return a + b }
|
|
21
|
+
*
|
|
22
|
+
* Functions without any annotations are left completely untouched.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
function text(source, tokens) {
|
|
26
|
+
if (!tokens.length) return ''
|
|
27
|
+
return source.slice(tokens[0].start, tokens[tokens.length - 1].end)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function splitTopLevelCommas(tokens) {
|
|
31
|
+
const result = []
|
|
32
|
+
let start = 0
|
|
33
|
+
let depth = 0
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
36
|
+
const value = tokens[i].value
|
|
37
|
+
if (value === '(' || value === '[' || value === '{') depth++
|
|
38
|
+
else if (value === ')' || value === ']' || value === '}') depth--
|
|
39
|
+
else if (value === ',' && depth === 0) {
|
|
40
|
+
result.push(tokens.slice(start, i))
|
|
41
|
+
start = i + 1
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (start < tokens.length) result.push(tokens.slice(start))
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findTopLevel(tokens, value) {
|
|
50
|
+
let depth = 0
|
|
51
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
52
|
+
const v = tokens[i].value
|
|
53
|
+
if (v === '(' || v === '[' || v === '{') depth++
|
|
54
|
+
else if (v === ')' || v === ']' || v === '}') depth--
|
|
55
|
+
else if (v === value && depth === 0) return i
|
|
56
|
+
}
|
|
57
|
+
return -1
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseParameter(source, tokens) {
|
|
61
|
+
const colonIndex = findTopLevel(tokens, ':')
|
|
62
|
+
|
|
63
|
+
if (colonIndex === -1) {
|
|
64
|
+
const code = text(source, tokens).trim()
|
|
65
|
+
return { code, type: null }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const nameTokens = tokens.slice(0, colonIndex)
|
|
69
|
+
const afterColon = tokens.slice(colonIndex + 1)
|
|
70
|
+
const equalsIndex = findTopLevel(afterColon, '=')
|
|
71
|
+
|
|
72
|
+
const typeTokens = equalsIndex === -1 ? afterColon : afterColon.slice(0, equalsIndex)
|
|
73
|
+
const defaultTokens = equalsIndex === -1 ? [] : afterColon.slice(equalsIndex)
|
|
74
|
+
|
|
75
|
+
const name = text(source, nameTokens).trim()
|
|
76
|
+
const type = text(source, typeTokens).trim()
|
|
77
|
+
const code = defaultTokens.length
|
|
78
|
+
? `${name} ${text(source, defaultTokens).trim()}`
|
|
79
|
+
: name
|
|
80
|
+
|
|
81
|
+
return { code, name, type: type || null }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function jsdocBlock(params, returnType, indent) {
|
|
85
|
+
const lines = ['/**']
|
|
86
|
+
|
|
87
|
+
for (const param of params) {
|
|
88
|
+
if (param.type) {
|
|
89
|
+
lines.push(` * @param {${param.type}} ${param.name}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (returnType) {
|
|
94
|
+
lines.push(` * @returns {${returnType}}`)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
lines.push(' */')
|
|
98
|
+
return lines.map(line => `${indent}${line}`).join('\n')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findFnCandidates(tokens) {
|
|
102
|
+
const candidates = []
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
105
|
+
if (tokens[i].type !== 'identifier' || tokens[i].value !== 'fn') continue
|
|
106
|
+
|
|
107
|
+
const prev = tokens[i - 1]
|
|
108
|
+
if (prev && prev.value === '.') continue
|
|
109
|
+
|
|
110
|
+
const nameToken = tokens[i + 1]
|
|
111
|
+
const parenToken = tokens[i + 2]
|
|
112
|
+
if (!nameToken || nameToken.type !== 'identifier') continue
|
|
113
|
+
if (!parenToken || parenToken.value !== '(') continue
|
|
114
|
+
|
|
115
|
+
candidates.push({ fnIndex: i, parenIndex: i + 2 })
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return candidates
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function transformTypeAnnotations(source) {
|
|
122
|
+
const tokens = significantTokens(tokenize(source))
|
|
123
|
+
const candidates = findFnCandidates(tokens)
|
|
124
|
+
const replacements = []
|
|
125
|
+
|
|
126
|
+
for (const candidate of candidates) {
|
|
127
|
+
let depth = 0
|
|
128
|
+
let closeIndex = -1
|
|
129
|
+
|
|
130
|
+
for (let i = candidate.parenIndex; i < tokens.length; i++) {
|
|
131
|
+
const value = tokens[i].value
|
|
132
|
+
if (value === '(') depth++
|
|
133
|
+
else if (value === ')') {
|
|
134
|
+
depth--
|
|
135
|
+
if (depth === 0) { closeIndex = i; break }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (closeIndex === -1) continue
|
|
140
|
+
|
|
141
|
+
const paramTokens = tokens.slice(candidate.parenIndex + 1, closeIndex)
|
|
142
|
+
const hasParamTypes = findTopLevel(paramTokens, ':') !== -1
|
|
143
|
+
|
|
144
|
+
let returnType = null
|
|
145
|
+
let end = tokens[closeIndex].end
|
|
146
|
+
|
|
147
|
+
const afterClose = tokens[closeIndex + 1]
|
|
148
|
+
if (afterClose && afterClose.value === ':') {
|
|
149
|
+
let j = closeIndex + 2
|
|
150
|
+
while (j < tokens.length && tokens[j].value !== '{' && tokens[j].value !== '=') j++
|
|
151
|
+
if (j < tokens.length) {
|
|
152
|
+
returnType = text(source, tokens.slice(closeIndex + 2, j)).trim()
|
|
153
|
+
end = tokens[j].start
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!hasParamTypes && !returnType) continue
|
|
158
|
+
|
|
159
|
+
const params = splitTopLevelCommas(paramTokens)
|
|
160
|
+
.filter(group => group.length)
|
|
161
|
+
.map(group => parseParameter(source, group))
|
|
162
|
+
|
|
163
|
+
const paramList = params.map(param => param.code).join(', ')
|
|
164
|
+
|
|
165
|
+
const fnStart = tokens[candidate.fnIndex].start
|
|
166
|
+
const lineStart = source.lastIndexOf('\n', fnStart - 1) + 1
|
|
167
|
+
const indentMatch = source.slice(lineStart, fnStart).match(/^[ \t]*/)
|
|
168
|
+
const indent = indentMatch ? indentMatch[0] : ''
|
|
169
|
+
|
|
170
|
+
const doc = jsdocBlock(params, returnType, indent)
|
|
171
|
+
const headerPrefix = source.slice(lineStart, tokens[candidate.parenIndex].start + 1)
|
|
172
|
+
const tail = returnType ? ') ' : ')'
|
|
173
|
+
|
|
174
|
+
replacements.push({
|
|
175
|
+
start: lineStart,
|
|
176
|
+
end,
|
|
177
|
+
text: `${doc}\n${headerPrefix}${paramList}${tail}`
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!replacements.length) return source
|
|
182
|
+
|
|
183
|
+
replacements.sort((a, b) => b.start - a.start)
|
|
184
|
+
|
|
185
|
+
let result = source
|
|
186
|
+
for (const replacement of replacements) {
|
|
187
|
+
result = result.slice(0, replacement.start) + replacement.text + result.slice(replacement.end)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result
|
|
191
|
+
}
|