@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.
@@ -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 }