@dramxx/brolang 0.1.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 +21 -0
- package/README.md +934 -0
- package/bin/bro.js +164 -0
- package/examples/classes.bro +43 -0
- package/examples/hello.bro +46 -0
- package/examples/promise.bro +6 -0
- package/package.json +38 -0
- package/src/keywords.js +67 -0
- package/src/runtime.js +169 -0
- package/src/transpiler.js +261 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { KEYWORD_MAP, MULTI_WORD_MAP } = require('./keywords')
|
|
4
|
+
const { getRuntimeCode } = require('./runtime')
|
|
5
|
+
|
|
6
|
+
// JS value-keywords — after these `/` is division, not a regex literal
|
|
7
|
+
const EXPR_VALUE_JS = new Set(['true', 'false', 'null', 'undefined', 'this'])
|
|
8
|
+
|
|
9
|
+
function transpile(source) {
|
|
10
|
+
return getRuntimeCode() + '\n' + substituteKeywords(source)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function substituteKeywords(source) {
|
|
14
|
+
let result = ''
|
|
15
|
+
let i = 0
|
|
16
|
+
const len = source.length
|
|
17
|
+
let prevIsExpr = false // tracks whether `/` should be division or regex start
|
|
18
|
+
|
|
19
|
+
while (i < len) {
|
|
20
|
+
const ch = source[i]
|
|
21
|
+
|
|
22
|
+
// Single-quoted string
|
|
23
|
+
if (ch === "'") {
|
|
24
|
+
let j = i + 1
|
|
25
|
+
while (j < len) {
|
|
26
|
+
if (source[j] === '\\') { j += 2; continue }
|
|
27
|
+
if (source[j] === "'") { j++; break }
|
|
28
|
+
j++
|
|
29
|
+
}
|
|
30
|
+
result += source.slice(i, j)
|
|
31
|
+
i = j
|
|
32
|
+
prevIsExpr = true
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Double-quoted string
|
|
37
|
+
if (ch === '"') {
|
|
38
|
+
let j = i + 1
|
|
39
|
+
while (j < len) {
|
|
40
|
+
if (source[j] === '\\') { j += 2; continue }
|
|
41
|
+
if (source[j] === '"') { j++; break }
|
|
42
|
+
j++
|
|
43
|
+
}
|
|
44
|
+
result += source.slice(i, j)
|
|
45
|
+
i = j
|
|
46
|
+
prevIsExpr = true
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Template literal — recurse into ${...} expressions so keywords are substituted there too
|
|
51
|
+
if (ch === '`') {
|
|
52
|
+
result += '`'
|
|
53
|
+
i++
|
|
54
|
+
while (i < len) {
|
|
55
|
+
const c = source[i]
|
|
56
|
+
if (c === '\\') {
|
|
57
|
+
result += source.slice(i, i + 2)
|
|
58
|
+
i += 2
|
|
59
|
+
} else if (c === '`') {
|
|
60
|
+
result += '`'
|
|
61
|
+
i++
|
|
62
|
+
break
|
|
63
|
+
} else if (c === '$' && i + 1 < len && source[i + 1] === '{') {
|
|
64
|
+
result += '${'
|
|
65
|
+
i += 2
|
|
66
|
+
const [inner, endIdx] = extractBraced(source, i, len)
|
|
67
|
+
result += substituteKeywords(inner) + '}'
|
|
68
|
+
i = endIdx
|
|
69
|
+
} else {
|
|
70
|
+
result += c
|
|
71
|
+
i++
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
prevIsExpr = true
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Line comment
|
|
79
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
80
|
+
let j = i + 2
|
|
81
|
+
while (j < len && source[j] !== '\n') j++
|
|
82
|
+
result += source.slice(i, j)
|
|
83
|
+
i = j
|
|
84
|
+
// prevIsExpr unchanged — comment is invisible to the grammar
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Block comment
|
|
89
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
90
|
+
let j = i + 2
|
|
91
|
+
while (j < len && !(source[j] === '*' && source[j + 1] === '/')) j++
|
|
92
|
+
j += 2
|
|
93
|
+
result += source.slice(i, j)
|
|
94
|
+
i = j
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Regex literal vs division operator
|
|
99
|
+
// Heuristic: if the previous expression-ending token makes `/` a division,
|
|
100
|
+
// treat as division; otherwise treat as the start of a regex literal.
|
|
101
|
+
if (ch === '/') {
|
|
102
|
+
if (!prevIsExpr) {
|
|
103
|
+
// regex literal: /pattern/flags
|
|
104
|
+
let j = i + 1
|
|
105
|
+
while (j < len) {
|
|
106
|
+
if (source[j] === '\\') { j += 2; continue }
|
|
107
|
+
if (source[j] === '[') {
|
|
108
|
+
// character class [...] — `]` doesn't end the regex inside here
|
|
109
|
+
j++
|
|
110
|
+
while (j < len && source[j] !== ']') {
|
|
111
|
+
if (source[j] === '\\') j++
|
|
112
|
+
j++
|
|
113
|
+
}
|
|
114
|
+
if (j < len) j++ // skip ]
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
if (source[j] === '/') { j++; break }
|
|
118
|
+
j++
|
|
119
|
+
}
|
|
120
|
+
// flags: g i m s u y
|
|
121
|
+
while (j < len && /[gimsuy]/.test(source[j])) j++
|
|
122
|
+
result += source.slice(i, j)
|
|
123
|
+
i = j
|
|
124
|
+
prevIsExpr = true
|
|
125
|
+
} else {
|
|
126
|
+
result += ch
|
|
127
|
+
i++
|
|
128
|
+
prevIsExpr = false
|
|
129
|
+
}
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Identifier or keyword
|
|
134
|
+
if (/[a-zA-Z_$]/.test(ch)) {
|
|
135
|
+
let j = i + 1
|
|
136
|
+
while (j < len && /[a-zA-Z0-9_$]/.test(source[j])) j++
|
|
137
|
+
const word = source.slice(i, j)
|
|
138
|
+
|
|
139
|
+
// Check multi-word patterns first (no newline-crossing)
|
|
140
|
+
let matched = false
|
|
141
|
+
for (const [from, to] of MULTI_WORD_MAP) {
|
|
142
|
+
const parts = from.split(' ')
|
|
143
|
+
if (parts[0] !== word) continue
|
|
144
|
+
|
|
145
|
+
let k = j
|
|
146
|
+
let ok = true
|
|
147
|
+
for (let p = 1; p < parts.length; p++) {
|
|
148
|
+
while (k < len && (source[k] === ' ' || source[k] === '\t')) k++
|
|
149
|
+
const ws = k
|
|
150
|
+
while (k < len && /[a-zA-Z0-9_$]/.test(source[k])) k++
|
|
151
|
+
if (source.slice(ws, k) !== parts[p]) { ok = false; break }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (ok) {
|
|
155
|
+
result += to
|
|
156
|
+
i = k
|
|
157
|
+
matched = true
|
|
158
|
+
prevIsExpr = false // multi-word patterns are all statement constructs
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!matched) {
|
|
164
|
+
if (word === 'yeet') {
|
|
165
|
+
result += resolveYeet(source, j)
|
|
166
|
+
prevIsExpr = false
|
|
167
|
+
} else {
|
|
168
|
+
const entry = KEYWORD_MAP.find(([from]) => from === word)
|
|
169
|
+
if (entry) {
|
|
170
|
+
result += entry[1]
|
|
171
|
+
prevIsExpr = EXPR_VALUE_JS.has(entry[1])
|
|
172
|
+
} else {
|
|
173
|
+
result += word
|
|
174
|
+
prevIsExpr = true // plain identifier ends an expression
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
i = j
|
|
178
|
+
}
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Default: single character — update prevIsExpr based on its role
|
|
183
|
+
result += ch
|
|
184
|
+
i++
|
|
185
|
+
if (ch === ')' || ch === ']' || ch === '}') {
|
|
186
|
+
prevIsExpr = true
|
|
187
|
+
} else if (/[0-9]/.test(ch)) {
|
|
188
|
+
prevIsExpr = true // digit — part of a number literal
|
|
189
|
+
} else if (/\s/.test(ch)) {
|
|
190
|
+
// whitespace: no change (invisible to grammar)
|
|
191
|
+
} else {
|
|
192
|
+
prevIsExpr = false // operator or punctuation
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Extract the content inside a matched `{...}` block, handling nested strings,
|
|
200
|
+
// template literals, and comments so brace-counting is accurate.
|
|
201
|
+
// `start` = index right after the opening `{`.
|
|
202
|
+
// Returns [content, indexAfterClosingBrace].
|
|
203
|
+
function extractBraced(source, start, len) {
|
|
204
|
+
let depth = 1
|
|
205
|
+
let j = start
|
|
206
|
+
|
|
207
|
+
while (j < len) {
|
|
208
|
+
const c = source[j]
|
|
209
|
+
|
|
210
|
+
if (c === '{') {
|
|
211
|
+
depth++
|
|
212
|
+
j++
|
|
213
|
+
} else if (c === '}') {
|
|
214
|
+
depth--
|
|
215
|
+
if (depth === 0) break // j is now at the closing }
|
|
216
|
+
j++
|
|
217
|
+
} else if (c === '"' || c === "'") {
|
|
218
|
+
const q = c
|
|
219
|
+
j++
|
|
220
|
+
while (j < len && source[j] !== q) {
|
|
221
|
+
if (source[j] === '\\') j++
|
|
222
|
+
j++
|
|
223
|
+
}
|
|
224
|
+
if (j < len) j++ // skip closing quote
|
|
225
|
+
} else if (c === '`') {
|
|
226
|
+
// skip template literal body (simplified: doesn't re-count ${} inside)
|
|
227
|
+
j++
|
|
228
|
+
while (j < len && source[j] !== '`') {
|
|
229
|
+
if (source[j] === '\\') { j += 2; continue }
|
|
230
|
+
j++
|
|
231
|
+
}
|
|
232
|
+
if (j < len) j++ // skip closing `
|
|
233
|
+
} else if (c === '/' && j + 1 < len && source[j + 1] === '/') {
|
|
234
|
+
while (j < len && source[j] !== '\n') j++
|
|
235
|
+
} else if (c === '/' && j + 1 < len && source[j + 1] === '*') {
|
|
236
|
+
j += 2
|
|
237
|
+
while (j + 1 < len && !(source[j] === '*' && source[j + 1] === '/')) j++
|
|
238
|
+
if (j + 1 < len) j += 2
|
|
239
|
+
} else {
|
|
240
|
+
j++
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// j is at the closing `}` (depth===0) or at len (unterminated)
|
|
245
|
+
return [source.slice(start, j), j + 1]
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Heuristic: `yeet identifier.prop` → `delete`, everything else → `throw`
|
|
249
|
+
function resolveYeet(source, afterYeet) {
|
|
250
|
+
let j = afterYeet
|
|
251
|
+
const len = source.length
|
|
252
|
+
while (j < len && (source[j] === ' ' || source[j] === '\t')) j++
|
|
253
|
+
const start = j
|
|
254
|
+
while (j < len && /[a-zA-Z0-9_$]/.test(source[j])) j++
|
|
255
|
+
const nextWord = source.slice(start, j)
|
|
256
|
+
while (j < len && (source[j] === ' ' || source[j] === '\t')) j++
|
|
257
|
+
if (nextWord && source[j] === '.') return 'delete'
|
|
258
|
+
return 'throw'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = { transpile, substituteKeywords }
|