Skip to content

Commit 8ec5972

Browse files
committed
Fix repl
1 parent 97e0ab3 commit 8ec5972

13 files changed

Lines changed: 1073 additions & 73 deletions

debug-bundle-node.mjs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import fs from 'fs'
2+
3+
// Get source files
4+
const constSrc = fs.readFileSync('./src/const.js', 'utf8')
5+
const parseSrc = fs.readFileSync('./src/parse.js', 'utf8')
6+
const compileSrc = fs.readFileSync('./src/compile.js', 'utf8')
7+
8+
// Replicate the bundle generation from repl.html
9+
const stripImports = code => code.replace(/^import\s+.*$/gm, '')
10+
const stripExports = code => code
11+
.replace(/^export\s+default\s+/gm, '')
12+
.replace(/^export\s+(const|let|function)\s+/gm, '$1 ')
13+
.replace(/^export\s+\{[^}]+\}.*$/gm, '')
14+
15+
let bundle = `// Bundled subscript
16+
// https://github.com/phtml/subscript
17+
18+
(function() {
19+
'use strict';
20+
21+
`
22+
23+
// 1. Constants
24+
bundle += `// === src/const.js ===\n`
25+
bundle += stripExports(constSrc) + '\n\n'
26+
27+
// 2. Parse
28+
bundle += `// === src/parse.js ===\n`
29+
let parseModule = stripImports(parseSrc)
30+
.replace(/^export\s+let\s+/gm, 'let ')
31+
.replace(/^export\s+default\s+parse\s*;?\s*$/gm, '')
32+
bundle += parseModule + '\n\n'
33+
34+
// 3. Compile
35+
bundle += `// === src/compile.js ===\n`
36+
let compileModule = stripImports(compileSrc)
37+
.replace(/^export\s+const\s+/gm, 'const ')
38+
.replace(/^export\s+default\s+compile\s*;?\s*$/gm, '')
39+
bundle += compileModule + '\n\n'
40+
41+
// Close IIFE
42+
bundle += `window.parse = parse;
43+
window.compile = compile;
44+
})();
45+
`
46+
47+
console.log('Bundle size:', bundle.length, 'bytes')
48+
49+
// Test if it's valid JavaScript
50+
try {
51+
new Function(bundle)
52+
console.log('✓ Bundle is syntactically valid!')
53+
} catch (e) {
54+
console.log('❌ Bundle has syntax error:', e.message)
55+
// Find the line with error
56+
const lines = bundle.split('\n')
57+
const match = e.message.match(/position (\d+)/)
58+
if (match) {
59+
let pos = parseInt(match[1])
60+
let lineNum = 0
61+
for (let i = 0; i < lines.length; i++) {
62+
if (pos <= lines[i].length) {
63+
console.log(`Error around line ${i}: ${lines[i].slice(0, 100)}`)
64+
break
65+
}
66+
pos -= lines[i].length + 1
67+
}
68+
}
69+
}
70+
71+
// Show compile.js section
72+
console.log('\n=== COMPILE.JS SECTION (first 30 lines) ===')
73+
const compileStart = bundle.indexOf('// === src/compile.js ===')
74+
const compileEnd = bundle.indexOf('window.parse')
75+
const compileSection = bundle.slice(compileStart, compileEnd).split('\n').slice(0, 35)
76+
compileSection.forEach((line, i) => console.log(`${i}: ${line.slice(0, 120)}`))

debug-bundle.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { chromium } from 'playwright'
2+
3+
const browser = await chromium.launch({ headless: true })
4+
const page = await browser.newPage()
5+
6+
// Capture console messages
7+
const consoleLogs = []
8+
page.on('console', msg => consoleLogs.push(`[${msg.type()}] ${msg.text()}`))
9+
10+
// Navigate to REPL
11+
await page.goto('http://localhost:8765/repl.html', { waitUntil: 'domcontentloaded' })
12+
await page.waitForTimeout(3000) // Wait for init
13+
14+
// Enable bundle toggle and click get bundle
15+
try {
16+
const toggle = await page.$('#bundleToggle')
17+
if (toggle) {
18+
await toggle.check()
19+
await page.waitForTimeout(500)
20+
}
21+
22+
const btn = await page.$('#getBundleBtn')
23+
if (btn) {
24+
await btn.click()
25+
await page.waitForTimeout(2000)
26+
}
27+
} catch (e) {
28+
console.log('Error interacting with page:', e.message)
29+
}
30+
31+
// Print all console logs
32+
console.log('\n=== CONSOLE OUTPUT ===')
33+
consoleLogs.forEach(log => console.log(log))
34+
35+
// Get the modal code content (what bundle was generated)
36+
try {
37+
const modalText = await page.textContent('#modalCode')
38+
if (modalText && modalText.length > 0) {
39+
// Just show first 3000 chars to see structure
40+
console.log('\n=== BUNDLE (first 3000 chars) ===')
41+
console.log(modalText.slice(0, 3000))
42+
43+
// Find where "parse" appears
44+
console.log('\n=== PARSE DECLARATIONS ===')
45+
const lines = modalText.split('\n')
46+
let parseCount = 0
47+
lines.forEach((line, i) => {
48+
if (/\bparse\s*[=\(]|^parse|function parse|const parse|let parse|var parse/.test(line)) {
49+
parseCount++
50+
console.log(`Line ${i}: ${line.trim().slice(0, 100)}`)
51+
if (parseCount > 10) console.log('... (more matches)')
52+
}
53+
})
54+
}
55+
} catch (e) {
56+
console.log('Error getting modal text:', e.message)
57+
}
58+
59+
await browser.close()
60+
console.log('\n✓ Debug complete')
61+

feature/asi.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// ASI (Automatic Semicolon Insertion) preprocessor
2+
// Wraps input to insert semicolons where JS would infer them
3+
// Not a full JS spec implementation - covers common practical cases
4+
5+
// Tokens that REQUIRE continuation (don't insert ; before these)
6+
// Note: [ and { can START statements (array literal, block), so not included
7+
const CONT_STARTS = /^[\.\(\+\-\*\/\%\&\|\^\~\?\:\,\<\>]|^&&|^\|\||^\.\.\.$/
8+
9+
// Lines that can't be statements by themselves (incomplete)
10+
const INCOMPLETE = /[\+\-\*\/\%\&\|\^\~\?\:\,\=\<\>\(\{]$/
11+
12+
export function asi(src, { keepNewlines = false } = {}) {
13+
const lines = src.split('\n')
14+
const result = []
15+
16+
for (let i = 0; i < lines.length; i++) {
17+
const line = lines[i].trim()
18+
19+
// Skip empty lines
20+
if (!line) {
21+
if (keepNewlines) result.push(lines[i])
22+
continue
23+
}
24+
25+
// Skip comment lines
26+
if (line.startsWith('//') || line.startsWith('/*')) {
27+
if (keepNewlines) result.push(lines[i])
28+
continue
29+
}
30+
31+
result.push(lines[i])
32+
33+
// Already ends with semicolon, opening brace, comma, or closing brace - no insert
34+
if (line.endsWith(';') || line.endsWith('{') || line.endsWith(',') || line.endsWith('}')) continue
35+
36+
// Line is incomplete (ends with operator/opening paren) - no insert
37+
if (INCOMPLETE.test(line)) continue
38+
39+
// Find next non-empty, non-comment line
40+
let nextLine = ''
41+
for (let j = i + 1; j < lines.length; j++) {
42+
const nl = lines[j].trim()
43+
if (nl && !nl.startsWith('//') && !nl.startsWith('/*')) {
44+
nextLine = nl
45+
break
46+
}
47+
}
48+
49+
// Next line starts with continuation token - no insert
50+
const nextToken = nextLine.match(/^[^\s\w]*|^\w+/)?.[0] || ''
51+
if (nextLine && CONT_STARTS.test(nextToken)) continue
52+
53+
// If this is NOT the last code line, add semicolon
54+
// (Don't add trailing semicolon - it makes result undefined)
55+
if (nextLine) {
56+
result[result.length - 1] = result[result.length - 1] + ';'
57+
}
58+
}
59+
60+
// Join with space instead of newline for parser compatibility
61+
return result.join(keepNewlines ? '\n' : ' ')
62+
}
63+
64+
// Wrap parse function with ASI preprocessing
65+
export function withASI(parse) {
66+
return (src, ...args) => parse(asi(src), ...args)
67+
}
68+
69+
export default asi

feature/loop.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { PREC_STATEMENT, OPAREN, CPAREN, CBRACE, PREC_SEQ, PREC_TOKEN } from '..
1313
import { parseBody, loop, BREAK, CONTINUE, Return_ as Return } from './block.js'
1414
export { BREAK, CONTINUE } from './block.js'
1515

16-
const { token, expr, skip, space, err } = P
16+
const { token, expr, skip, space, err, next, parse } = P
1717
const SEMI = 59
1818

1919
// while (cond) body
@@ -40,11 +40,35 @@ operator('while', (cond, body) => {
4040
})
4141

4242
// for (init; cond; step) body
43+
// Note: init supports both expressions AND let/const declarations
4344
token('for', PREC_STATEMENT, a => {
4445
if (a) return
4546
space() === OPAREN || err('Expected (')
4647
skip()
47-
const init = space() === SEMI ? null : expr(PREC_SEQ)
48+
// Parse init: can be expression (PREC_SEQ) or let/const declaration (PREC_STATEMENT)
49+
// Using expr(PREC_SEQ) excludes statement-level tokens like let/const
50+
// So we detect let/const keywords and parse the declaration inline
51+
let init
52+
const cc = space()
53+
if (cc === SEMI) init = null
54+
else if (cc === 108 && P.cur.substr(P.idx, 3) === 'let' && !parse.id(P.cur.charCodeAt(P.idx + 3))) {
55+
skip(); skip(); skip() // skip 'let'
56+
space()
57+
const name = next(parse.id)
58+
if (!name) err('Expected identifier')
59+
space()
60+
if (P.cur.charCodeAt(P.idx) === 61 && P.cur.charCodeAt(P.idx + 1) !== 61) {
61+
skip(); init = ['let', name, expr(PREC_SEQ)]
62+
} else init = ['let', name]
63+
} else if (cc === 99 && P.cur.substr(P.idx, 5) === 'const' && !parse.id(P.cur.charCodeAt(P.idx + 5))) {
64+
skip(); skip(); skip(); skip(); skip() // skip 'const'
65+
space()
66+
const name = next(parse.id)
67+
if (!name) err('Expected identifier')
68+
space()
69+
P.cur.charCodeAt(P.idx) === 61 && P.cur.charCodeAt(P.idx + 1) !== 61 || err('Expected =')
70+
skip(); init = ['const', name, expr(PREC_SEQ)]
71+
} else init = expr(PREC_SEQ)
4872
space() === SEMI ? skip() : err('Expected ;')
4973
const cond = space() === SEMI ? null : expr(PREC_SEQ)
5074
space() === SEMI ? skip() : err('Expected ;')

feature/unit.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ const wrapHandler = (charCode) => {
5353
}
5454

5555
// Wrap all number entry points (0-9 and .)
56-
const PERIOD = 46, _0 = 48, _9 = 57
56+
// PERIOD, _0, _9 are from src/const.js
57+
import { PERIOD, _0, _9 } from '../src/const.js'
5758
wrapHandler(PERIOD)
5859
for (let i = _0; i <= _9; i++) wrapHandler(i)

0 commit comments

Comments
 (0)