This commit is contained in:
Corey Johnson 2025-10-22 11:23:11 -07:00
parent 8da3c1674e
commit 82cd199ed8
19 changed files with 314 additions and 74 deletions

BIN
assets/C64_Pro-STYLE.woff2 Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/PixeloidMono.ttf Normal file

Binary file not shown.

BIN
assets/PixeloidSans.ttf Normal file

Binary file not shown.

BIN
assets/PixeloidSansBold.ttf Normal file

Binary file not shown.

@ -1 +1 @@
Subproject commit 1a18a713d7ae86b03a6bef38cc53d12ecfbf9627
Subproject commit 47f829fcada71655f0d40ec363b5bcc844af8856

View File

@ -3,7 +3,7 @@ import { parser } from '#parser/shrimp.ts'
import * as terms from '#parser/shrimp.terms'
import type { SyntaxNode, Tree } from '@lezer/common'
import { assert, errorMessage } from '#utils/utils'
import { toBytecode, type Bytecode, type ProgramItem } from 'reefvm'
import { toBytecode, type Bytecode, type ProgramItem, bytecodeToString } from 'reefvm'
import {
checkTreeForErrors,
getAllChildren,
@ -72,9 +72,12 @@ export class Compiler {
this.instructions.push(['RETURN'])
}
if (DEBUG) logInstructions(this.instructions)
this.bytecode = toBytecode(this.instructions)
if (DEBUG) {
const bytecodeString = bytecodeToString(this.bytecode)
console.log(`\n🤖 bytecode:\n----------------\n${bytecodeString}\n\n`)
}
} catch (error) {
if (error instanceof CompilerError) {
throw new Error(error.toReadableString(input))
@ -475,19 +478,3 @@ export class Compiler {
}
}
}
const logInstructions = (instructions: ProgramItem[]) => {
const instructionsString = instructions
.map((parts) => {
const isPush = parts[0] === 'PUSH'
return parts
.map((part, i) => {
const partAsString = typeof part == 'string' && isPush ? `'${part}'` : part!.toString()
return i > 0 ? partAsString : part
})
.join(' ')
})
.join('\n')
console.log(`\n🤖 instructions:\n----------------\n${instructionsString}\n\n`)
}

View File

@ -213,7 +213,7 @@ describe('Regex', () => {
})
})
describe.skip('native functions', () => {
describe('native functions', () => {
test('print function', () => {
const add = (x: number, y: number) => x + y
expect(`add 5 9`).toEvaluateTo(14, { add })

View File

@ -4,7 +4,7 @@ import { shrimpTheme } from '#editor/plugins/theme'
import { shrimpLanguage } from '#/editor/plugins/shrimpLanguage'
import { shrimpHighlighting } from '#editor/plugins/theme'
import { shrimpKeymap } from '#editor/plugins/keymap'
import { asciiEscapeToHtml, log, toElement } from '#utils/utils'
import { asciiEscapeToHtml, assert, assertNever, log, toElement } from '#utils/utils'
import { Signal } from '#utils/signal'
import { shrimpErrors } from '#editor/plugins/errors'
import { debugTags } from '#editor/plugins/debugTags'
@ -12,6 +12,11 @@ import { getContent, persistencePlugin } from '#editor/plugins/persistence'
import '#editor/editor.css'
import type { HtmlEscapedString } from 'hono/utils/html'
import { catchErrors } from '#editor/plugins/catchErrors'
import { connectToNose, noseSignals } from '#editor/noseClient'
import type { Value } from 'reefvm'
connectToNose()
export const Editor = () => {
return (
@ -23,6 +28,7 @@ export const Editor = () => {
parent: ref,
doc: getContent(),
extensions: [
catchErrors,
shrimpKeymap,
basicSetup,
shrimpTheme,
@ -30,7 +36,7 @@ export const Editor = () => {
shrimpHighlighting,
shrimpErrors,
persistencePlugin,
debugTags,
// debugTags,
],
})
@ -47,23 +53,31 @@ export const Editor = () => {
)
}
export const outputSignal = new Signal<{ output: string } | { error: string }>()
noseSignals.connect((message) => {
if (message.type === 'error') {
log.error(`Nose error: ${message.data}`)
errorSignal.emit(`Nose error: ${message.data}`)
} else if (message.type === 'reef-output') {
const x = outputSignal.emit(message.data)
} else if (message.type === 'connected') {
outputSignal.emit(`╞ Connected to Nose VM`)
}
})
let outputTimeout: ReturnType<typeof setTimeout>
export const outputSignal = new Signal<Value | string>()
export const errorSignal = new Signal<string>()
outputSignal.connect((output) => {
outputSignal.connect((value) => {
const el = document.querySelector('#output')!
el.innerHTML = ''
let content
if ('error' in output) {
el.classList.add('error')
content = output.error
} else {
el.classList.remove('error')
content = output.output
}
el.innerHTML = asciiEscapeToHtml(valueToString(value))
})
el.innerHTML = asciiEscapeToHtml(content)
errorSignal.connect((error) => {
const el = document.querySelector('#output')!
el.innerHTML = ''
el.classList.add('error')
el.innerHTML = asciiEscapeToHtml(error)
})
type StatusBarMessage = {
@ -96,3 +110,37 @@ statusBarSignal.connect(async ({ side, message, className, order }) => {
sideEl.insertBefore(toElement(messageEl), nodes[index]!)
}
})
const valueToString = (value: Value | string): string => {
if (typeof value === 'string') {
return value
}
switch (value.type) {
case 'null':
return 'null'
case 'boolean':
return value.value ? 'true' : 'false'
case 'number':
return value.value.toString()
case 'string':
return value.value
case 'array':
return `${value.value.map(valueToString).join('\n')}`
case 'dict': {
const entries = Array.from(value.value.entries()).map(
([key, val]) => `"${key}": ${valueToString(val)}`
)
return `{${entries.join(', ')}}`
}
case 'regex':
return `/${value.value.source}/`
case 'function':
return `<function>`
case 'native':
return `<function ${value.fn.name}>`
default:
assertNever(value)
return `<unknown value type: ${(value as any).type}>`
}
}

59
src/editor/noseClient.ts Normal file
View File

@ -0,0 +1,59 @@
import { Signal } from '#utils/signal'
import type { Bytecode, Value } from 'reefvm'
let ws: WebSocket
type IncomingMessage =
| { type: 'connected' }
| { type: 'ping'; data: number }
| { type: 'commands'; data: number }
| {
type: 'apps'
data: {
name: string
type: 'browser' | 'server'
}[]
}
| {
type: 'session:start'
data: {
NOSE_DIR: string
cwd: string
hostname: string
mode: string
project: string
}
}
| { type: 'reef-output'; data: Value }
| { type: 'error'; data: string }
export const noseSignals = new Signal<IncomingMessage>()
export const connectToNose = (url: string = 'ws://localhost:3000/ws') => {
ws = new WebSocket(url)
ws.onopen = () => noseSignals.emit({ type: 'connected' })
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
noseSignals.emit(message)
}
ws.onerror = (event) => {
console.error(`💥WebSocket error:`, event)
}
ws.onclose = () => {
console.log(`🚪 Connection closed`)
}
}
let id = 0
export const sendToNose = (code: Bytecode) => {
if (!ws) {
throw new Error('WebSocket is not connected.')
} else if (ws.readyState !== WebSocket.OPEN) {
throw new Error(`WebSocket is not open, current status is ${ws.readyState}.`)
}
id += 1
ws.send(JSON.stringify({ type: 'reef-bytecode', data: code, id }))
}

View File

@ -0,0 +1,9 @@
import { errorSignal } from '#editor/editor'
import { EditorView } from '@codemirror/view'
export const catchErrors = EditorView.exceptionSink.of((exception) => {
console.error('CodeMirror error:', exception)
errorSignal.emit(
`Editor error: ${exception instanceof Error ? exception.message : String(exception)}`
)
})

View File

@ -1,9 +1,9 @@
import { statusBarSignal } from '#editor/editor'
import { run } from '#editor/runCode'
import { printBytecodeOutput, printParserOutput, runCode } from '#editor/runCode'
import { EditorState } from '@codemirror/state'
import { keymap } from '@codemirror/view'
let multilineMode = false
const customKeymap = keymap.of([
{
key: 'Enter',
@ -11,17 +11,22 @@ const customKeymap = keymap.of([
if (multilineMode) return false
const input = view.state.doc.toString()
run(input)
history.push(input)
runCode(input)
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: '' },
selection: { anchor: 0 },
})
return true
},
},
{
key: 'Alt-Enter',
key: 'Shift-Enter',
run: (view) => {
if (multilineMode) {
const input = view.state.doc.toString()
run(input)
runCode(input)
return true
}
@ -31,7 +36,62 @@ const customKeymap = keymap.of([
selection: { anchor: view.state.doc.length + 1 },
})
updateStatusMessage()
return true
},
},
{
key: 'Tab',
preventDefault: true,
run: (view) => {
return true
},
},
{
key: 'ArrowUp',
run: (view) => {
const command = history.previous()
if (command === undefined) return false
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: command },
selection: { anchor: command.length },
})
return true
},
},
{
key: 'ArrowDown',
run: (view) => {
const command = history.next()
if (command === undefined) return false
view.dispatch({
changes: { from: 0, to: view.state.doc.length, insert: command },
selection: { anchor: command.length },
})
return true
},
},
{
key: 'Mod-k 1',
preventDefault: true,
run: (view) => {
const input = view.state.doc.toString()
printParserOutput(input)
return true
},
},
{
key: 'Mod-k 2',
preventDefault: true,
run: (view) => {
const input = view.state.doc.toString()
printBytecodeOutput(input)
return true
},
},
@ -45,7 +105,6 @@ const singleLineFilter = EditorState.transactionFilter.of((transaction) => {
firstTime = false
if (transaction.newDoc.toString().includes('\n')) {
multilineMode = true
updateStatusMessage()
return transaction
}
}
@ -53,7 +112,6 @@ const singleLineFilter = EditorState.transactionFilter.of((transaction) => {
transaction.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
if (inserted.toString().includes('\n')) {
multilineMode = true
updateStatusMessage()
return
}
})
@ -63,22 +121,38 @@ const singleLineFilter = EditorState.transactionFilter.of((transaction) => {
export const shrimpKeymap = [customKeymap, singleLineFilter]
const updateStatusMessage = () => {
statusBarSignal.emit({
side: 'left',
message: multilineMode ? 'Press Alt-Enter run' : 'Alt-Enter will enter multiline mode',
className: 'status',
})
class History {
private commands: string[] = []
private index: number | undefined
statusBarSignal.emit({
side: 'right',
message: (
<div className="multiline">
<span className={multilineMode ? 'dot active' : 'dot inactive'}></span> multiline
</div>
),
className: 'multiline-status',
})
push(command: string) {
this.commands.push(command)
this.index = undefined
}
previous(): string | undefined {
if (this.commands.length === 0) return
if (this.index === undefined) {
this.index = this.commands.length - 1
} else if (this.index > 0) {
this.index -= 1
}
return this.commands[this.index]
}
next(): string | undefined {
if (this.commands.length === 0 || this.index === undefined) return
if (this.index < this.commands.length - 1) {
this.index += 1
return this.commands[this.index]
} else {
this.index = undefined
return ''
}
}
}
requestAnimationFrame(() => updateStatusMessage())
const history = new History()

View File

@ -1,16 +1,36 @@
import { outputSignal } from '#editor/editor'
import { outputSignal, errorSignal } from '#editor/editor'
import { Compiler } from '#compiler/compiler'
import { errorMessage, log } from '#utils/utils'
import { VM } from 'reefvm'
import { bytecodeToString, run } from 'reefvm'
import { parser } from '#parser/shrimp'
import { sendToNose } from '#editor/noseClient'
export const run = async (input: string) => {
export const runCode = async (input: string) => {
try {
const compiler = new Compiler(input)
const vm = new VM(compiler.bytecode)
const output = await vm.run()
outputSignal.emit({ output: String(output.value) })
sendToNose(compiler.bytecode)
} catch (error) {
log.error(error)
outputSignal.emit({ error: `${errorMessage(error)}` })
errorSignal.emit(`${errorMessage(error)}`)
}
}
export const printParserOutput = (input: string) => {
try {
const cst = parser.parse(input)
outputSignal.emit(cst.toString())
} catch (error) {
log.error(error)
errorSignal.emit(`${errorMessage(error)}`)
}
}
export const printBytecodeOutput = (input: string) => {
try {
const compiler = new Compiler(input)
outputSignal.emit(bytecodeToString(compiler.bytecode))
} catch (error) {
log.error(error)
errorSignal.emit(`${errorMessage(error)}`)
}
}

View File

@ -6,6 +6,8 @@
@top Program { item* }
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot }
@tokens {
@precedence { Number "-" Regex "/"}
@ -43,8 +45,6 @@
}
@external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot }
@precedence {
pipe @left,
multiplicative @left,

View File

@ -5,7 +5,7 @@ import {trackScope} from "./scopeTracker"
import {highlighting} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: ".jQVQaOOO#XQbO'#CfO$RQPO'#CgO$aQPO'#DmO$xQaO'#CeO%gOSO'#CuOOQ`'#Dq'#DqO%uOPO'#C}O%zQPO'#DpO&cQaO'#D|OOQ`'#DO'#DOOOQO'#Dn'#DnO&kQPO'#DmO&yQaO'#EQOOQO'#DX'#DXO'hQPO'#DaOOQO'#Dm'#DmO'mQPO'#DlOOQ`'#Dl'#DlOOQ`'#Db'#DbQVQaOOOOQ`'#Dp'#DpOOQ`'#Cd'#CdO'uQaO'#DUOOQ`'#Do'#DoOOQ`'#Dc'#DcO(PQbO,58}O&yQaO,59RO&yQaO,59RO)XQPO'#CgO)iQPO,59PO)zQPO,59PO)uQPO,59PO*uQPO,59PO*}QaO'#CwO+VQWO'#CxOOOO'#Du'#DuOOOO'#Dd'#DdO+kOSO,59aOOQ`,59a,59aO+yO`O,59iOOQ`'#De'#DeO,OQaO'#DQO,WQPO,5:hO,]QaO'#DgO,bQPO,58|O,sQPO,5:lO,zQPO,5:lO-PQaO,59{OOQ`,5:W,5:WOOQ`-E7`-E7`OOQ`,59p,59pOOQ`-E7a-E7aOOQO1G.m1G.mO-^QPO1G.mO&yQaO,59WO&yQaO,59WOOQ`1G.k1G.kOOOO,59c,59cOOOO,59d,59dOOOO-E7b-E7bOOQ`1G.{1G.{OOQ`1G/T1G/TOOQ`-E7c-E7cO-xQaO1G0SO!QQbO'#CfOOQO,5:R,5:ROOQO-E7e-E7eO.YQaO1G0WOOQO1G/g1G/gOOQO1G.r1G.rO.jQPO1G.rO.tQPO7+%nO.yQaO7+%oOOQO'#DZ'#DZOOQO7+%r7+%rO/ZQaO7+%sOOQ`<<IY<<IYO/qQPO'#DfO/vQaO'#EPO0^QPO<<IZOOQO'#D['#D[O0cQPO<<I_OOQ`,5:Q,5:QOOQ`-E7d-E7dOOQ`AN>uAN>uO&yQaO'#D]OOQO'#Dh'#DhO0nQPOAN>yO0yQPO'#D_OOQOAN>yAN>yO1OQPOAN>yO1TQPO,59wO1[QPO,59wOOQO-E7f-E7fOOQOG24eG24eO1aQPOG24eO1fQPO,59yO1kQPO1G/cOOQOLD*PLD*PO.yQaO1G/eO/ZQaO7+$}OOQO7+%P7+%POOQO<<Hi<<Hi",
states: ".jQVQROOO#XQTO'#CfO$RQQO'#CgO$aQQO'#DmO$xQRO'#CeO%gOWO'#CuOOQP'#Dq'#DqO%uOQO'#C}O%zQQO'#DpO&cQRO'#D|OOQP'#DO'#DOOOQO'#Dn'#DnO&kQQO'#DmO&yQRO'#EQOOQO'#DX'#DXO'hQQO'#DaOOQO'#Dm'#DmO'mQQO'#DlOOQP'#Dl'#DlOOQP'#Db'#DbQVQROOOOQP'#Dp'#DpOOQP'#Cd'#CdO'uQRO'#DUOOQP'#Do'#DoOOQP'#Dc'#DcO(PQTO,58}O&yQRO,59RO&yQRO,59RO)XQQO'#CgO)iQQO,59PO)zQQO,59PO)uQQO,59PO*uQQO,59PO*}QRO'#CwO+VQ`O'#CxOOOO'#Du'#DuOOOO'#Dd'#DdO+kOWO,59aOOQP,59a,59aO+yOPO,59iOOQP'#De'#DeO,OQRO'#DQO,WQQO,5:hO,]QRO'#DgO,bQQO,58|O,sQQO,5:lO,zQQO,5:lO-PQRO,59{OOQP,5:W,5:WOOQP-E7`-E7`OOQP,59p,59pOOQP-E7a-E7aOOQO1G.m1G.mO-^QQO1G.mO&yQRO,59WO&yQRO,59WOOQP1G.k1G.kOOOO,59c,59cOOOO,59d,59dOOOO-E7b-E7bOOQP1G.{1G.{OOQP1G/T1G/TOOQP-E7c-E7cO-xQRO1G0SO!QQTO'#CfOOQO,5:R,5:ROOQO-E7e-E7eO.YQRO1G0WOOQO1G/g1G/gOOQO1G.r1G.rO.jQQO1G.rO.tQQO7+%nO.yQRO7+%oOOQO'#DZ'#DZOOQO7+%r7+%rO/ZQRO7+%sOOQP<<IY<<IYO/qQQO'#DfO/vQRO'#EPO0^QQO<<IZOOQO'#D['#D[O0cQQO<<I_OOQP,5:Q,5:QOOQP-E7d-E7dOOQPAN>uAN>uO&yQRO'#D]OOQO'#Dh'#DhO0nQQOAN>yO0yQQO'#D_OOQOAN>yAN>yO1OQQOAN>yO1TQQO,59wO1[QQO,59wOOQO-E7f-E7fOOQOG24eG24eO1aQQOG24eO1fQQO,59yO1kQQO1G/cOOQOLD*PLD*PO.yQRO1G/eO/ZQRO7+$}OOQO7+%P7+%POOQO<<Hi<<Hi",
stateData: "1v~O!_OS~OPPOQ_ORUOSVOmUOnUOoUOpUOsXO|]O!fSO!hTO!rbO~OPeORUOSVOmUOnUOoUOpUOsXOwfOygO!fSO!hTOzYX!rYX!vYX!gYXvYX~O[!dX]!dX^!dX_!dXa!dXb!dXc!dXd!dXe!dXf!dXg!dXh!dX~P!QO[kO]kO^lO_lO~O[kO]kO^lO_lO!r!aX!v!aXv!aX~OPPORUOSVOmUOnUOoUOpUO!fSO!hTO~OjtO!hwO!jrO!ksO~O!oxO~O[!dX]!dX^!dX_!dX!r!aX!v!aXv!aX~OQyOutP~Oz|O!r!aX!v!aXv!aX~OPeORUOSVOmUOnUOoUOpUO!fSO!hTO~Oa!QO~O!r!RO!v!RO~OsXOw!TO~P&yOsXOwfOygOzVa!rVa!vVa!gVavVa~P&yOa!XOb!XOc!XOd!XOe!XOf!XOg!YOh!YO~O[kO]kO^lO_lO~P(mO[kO]kO^lO_lO!g!ZO~O!g!ZO[!dX]!dX^!dX_!dXa!dXb!dXc!dXd!dXe!dXf!dXg!dXh!dX~Oz|O!g!ZO~OP![O!fSO~O!h!]O!j!]O!k!]O!l!]O!m!]O!n!]O~OjtO!h!_O!jrO!ksO~OP!`O~OQyOutX~Ou!bO~OP!cO~Oz|O!rUa!vUa!gUavUa~Ou!fO~P(mOu!fO~OQ_OsXO|]O~P$xO[kO]kO^Zi_Zi!rZi!vZi!gZivZi~OQ_OsXO|]O!r!kO~P$xOQ_OsXO|]O!r!nO~P$xO!g`iu`i~P(mOv!oO~OQ_OsXO|]Ov!sP~P$xOQ_OsXO|]Ov!sP!Q!sP!S!sP~P$xO!r!uO~OQ_OsXO|]Ov!sX!Q!sX!S!sX~P$xOv!wO~Ov!|O!Q!xO!S!{O~Ov#RO!Q!xO!S!{O~Ou#TO~Ov#RO~Ou#UO~P(mOu#UO~Ov#VO~O!r#WO~O!r#XO~Om_o]o~",
goto: "+m!vPPPPPP!w#W#f#k#W$VPPPP$lPPPPPPPP$xP%a%aPPPP%e&OP&dPPP#fPP&gP&s&v'PP'TP&g'Z'a'h'n't'}(UPPP([(`(t)W)]*WPPP*sPPPPPP*w*wP+X+a+ad`Od!Q!b!f!k!n!q#W#XRpSiZOSd|!Q!b!f!k!n!q#W#XVhPj!czUOPS]dgjkl!Q!X!Y!b!c!f!k!n!q!x#W#XR![rdROd!Q!b!f!k!n!q#W#XQnSQ!VkR!WlQpSQ!P]Q!h!YR#P!x{UOPS]dgjkl!Q!X!Y!b!c!f!k!n!q!x#W#XTtTvdWOd!Q!b!f!k!n!q#W#XgePS]gjkl!X!Y!c!xd`Od!Q!b!f!k!n!q#W#XUfPj!cR!TgR{Xe`Od!Q!b!f!k!n!q#W#XR!m!fQ!t!nQ#Y#WR#Z#XT!y!t!zQ!}!tR#S!zQdOR!SdSjP!cR!UjQvTR!^vQzXR!azW!q!k!n#W#XR!v!qS}[qR!e}Q!z!tR#Q!zTcOdSaOdQ!g!QQ!j!bQ!l!fZ!p!k!n!q#W#Xd[Od!Q!b!f!k!n!q#W#XQqSR!d|ViPj!cdQOd!Q!b!f!k!n!q#W#XUfPj!cQmSQ!O]Q!TgQ!VkQ!WlQ!h!XQ!i!YR#O!xdWOd!Q!b!f!k!n!q#W#XdeP]gjkl!X!Y!c!xRoSTuTvmYOPdgj!Q!b!c!f!k!n!q#W#XQ!r!kV!s!n#W#Xe^Od!Q!b!f!k!n!q#W#X",
nodeNames: "⚠ Identifier AssignableIdentifier Word IdentifierBeforeDot Program PipeExpr FunctionCall PositionalArg ParenExpr FunctionCallOrIdentifier BinOp operator operator operator operator ConditionalOp operator operator operator operator operator operator operator operator String StringFragment Interpolation EscapeSeq Number Boolean Regex Null DotGet FunctionDef keyword Params colon end Underscore NamedArg NamedArgPrefix operator IfExpr keyword ThenBlock ThenBlock ElsifExpr keyword ElseExpr keyword Assign",
@ -19,7 +19,7 @@ export const parser = LRParser.deserialize({
skippedNodes: [0],
repeatNodeCount: 7,
tokenData: "!&X~R!SOX$_XY$|YZ%gZp$_pq$|qr&Qrt$_tu'Yuw$_wx'_xy'dyz'}z{(h{|)R|}$_}!O)l!O!P,b!P!Q,{!Q![*]![!]5j!]!^%g!^!_6T!_!`7_!`!a7x!a#O$_#O#P9S#P#R$_#R#S9X#S#T$_#T#U9r#U#X;W#X#Y=m#Y#ZDs#Z#];W#]#^JO#^#b;W#b#cKp#c#d! Y#d#f;W#f#g!!z#g#h;W#h#i!#q#i#o;W#o#p$_#p#q!%i#q;'S$_;'S;=`$v<%l~$_~O$_~~!&SS$dUjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_S$yP;=`<%l$__%TUjS!_ZOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V%nUjS!rROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V&VWjSOt$_uw$_x!_$_!_!`&o!`#O$_#P;'S$_;'S;=`$v<%lO$_V&vUbRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~'_O!j~~'dO!h~V'kUjS!fROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V(UUjS!gROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V(oU[RjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V)YU^RjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V)sWjS_ROt$_uw$_x!Q$_!Q![*]![#O$_#P;'S$_;'S;=`$v<%lO$_V*dYjSmROt$_uw$_x!O$_!O!P+S!P!Q$_!Q![*]![#O$_#P;'S$_;'S;=`$v<%lO$_V+XWjSOt$_uw$_x!Q$_!Q![+q![#O$_#P;'S$_;'S;=`$v<%lO$_V+xWjSmROt$_uw$_x!Q$_!Q![+q![#O$_#P;'S$_;'S;=`$v<%lO$_T,iU!oPjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V-SWjS]ROt$_uw$_x!P$_!P!Q-l!Q#O$_#P;'S$_;'S;=`$v<%lO$_V-q^jSOY.mYZ$_Zt.mtu/puw.mwx/px!P.m!P!Q$_!Q!}.m!}#O4c#O#P2O#P;'S.m;'S;=`5d<%lO.mV.t^jSoROY.mYZ$_Zt.mtu/puw.mwx/px!P.m!P!Q2e!Q!}.m!}#O4c#O#P2O#P;'S.m;'S;=`5d<%lO.mR/uXoROY/pZ!P/p!P!Q0b!Q!}/p!}#O1P#O#P2O#P;'S/p;'S;=`2_<%lO/pR0eP!P!Q0hR0mUoR#Z#[0h#]#^0h#a#b0h#g#h0h#i#j0h#m#n0hR1SVOY1PZ#O1P#O#P1i#P#Q/p#Q;'S1P;'S;=`1x<%lO1PR1lSOY1PZ;'S1P;'S;=`1x<%lO1PR1{P;=`<%l1PR2RSOY/pZ;'S/p;'S;=`2_<%lO/pR2bP;=`<%l/pV2jWjSOt$_uw$_x!P$_!P!Q3S!Q#O$_#P;'S$_;'S;=`$v<%lO$_V3ZbjSoROt$_uw$_x#O$_#P#Z$_#Z#[3S#[#]$_#]#^3S#^#a$_#a#b3S#b#g$_#g#h3S#h#i$_#i#j3S#j#m$_#m#n3S#n;'S$_;'S;=`$v<%lO$_V4h[jSOY4cYZ$_Zt4ctu1Puw4cwx1Px#O4c#O#P1i#P#Q.m#Q;'S4c;'S;=`5^<%lO4cV5aP;=`<%l4cV5gP;=`<%l.mT5qUjSuPOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V6[WcRjSOt$_uw$_x!_$_!_!`6t!`#O$_#P;'S$_;'S;=`$v<%lO$_V6{UdRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V7fUaRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V8PWeRjSOt$_uw$_x!_$_!_!`8i!`#O$_#P;'S$_;'S;=`$v<%lO$_V8pUfRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~9XO!k~V9`UjSwROt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_V9w[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#b;W#b#c;{#c#o;W#o;'S$_;'S;=`$v<%lO$_U:tUyQjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_U;]YjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_V<Q[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#W;W#W#X<v#X#o;W#o;'S$_;'S;=`$v<%lO$_V<}YgRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_V=r^jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#a>n#a#b;W#b#cCR#c#o;W#o;'S$_;'S;=`$v<%lO$_V>s[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#g;W#g#h?i#h#o;W#o;'S$_;'S;=`$v<%lO$_V?n^jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#X;W#X#Y@j#Y#];W#]#^Aa#^#o;W#o;'S$_;'S;=`$v<%lO$_V@qY!SPjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VAf[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#Y;W#Y#ZB[#Z#o;W#o;'S$_;'S;=`$v<%lO$_VBcY!QPjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VCW[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#W;W#W#XC|#X#o;W#o;'S$_;'S;=`$v<%lO$_VDTYjSvROt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VDx]jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#UEq#U#b;W#b#cIX#c#o;W#o;'S$_;'S;=`$v<%lO$_VEv[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aFl#a#o;W#o;'S$_;'S;=`$v<%lO$_VFq[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#g;W#g#hGg#h#o;W#o;'S$_;'S;=`$v<%lO$_VGl[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#X;W#X#YHb#Y#o;W#o;'S$_;'S;=`$v<%lO$_VHiYnRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VI`YsRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_VJT[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#Y;W#Y#ZJy#Z#o;W#o;'S$_;'S;=`$v<%lO$_VKQY|PjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$__Kw[!lWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#i;W#i#jLm#j#o;W#o;'S$_;'S;=`$v<%lO$_VLr[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aMh#a#o;W#o;'S$_;'S;=`$v<%lO$_VMm[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#`;W#`#aNc#a#o;W#o;'S$_;'S;=`$v<%lO$_VNjYpRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_V! _[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#f;W#f#g!!T#g#o;W#o;'S$_;'S;=`$v<%lO$_V!![YhRjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$_^!#RY!nWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#o;W#o;'S$_;'S;=`$v<%lO$__!#x[!mWjSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#f;W#f#g!$n#g#o;W#o;'S$_;'S;=`$v<%lO$_V!$s[jSOt$_uw$_x!_$_!_!`:m!`#O$_#P#T$_#T#i;W#i#jGg#j#o;W#o;'S$_;'S;=`$v<%lO$_V!%pUzRjSOt$_uw$_x#O$_#P;'S$_;'S;=`$v<%lO$_~!&XO!v~",
tokenizers: [0, 1, 2, 3, tokenizer],
tokenizers: [tokenizer, 0, 1, 2, 3],
topRules: {"Program":[0,5]},
tokenPrec: 768
})

View File

@ -105,6 +105,24 @@ describe('Parentheses', () => {
`)
})
test('a word start with *', () => {
expect('find *cool*').toMatchTree(`
FunctionCall
Identifier find
PositionalArg
Word *cool*
`)
})
test('a word can look like a binop', () => {
expect('find cool*wow').toMatchTree(`
FunctionCall
Identifier find
PositionalArg
Word cool*wow
`)
})
test('nested parentheses', () => {
expect('(2 + (1 * 4))').toMatchTree(`
ParenExpr
@ -131,7 +149,7 @@ describe('Parentheses', () => {
})
})
describe('BinOp', () => {
describe.only('BinOp', () => {
test('addition tests', () => {
expect('2 + 3').toMatchTree(`
BinOp

View File

@ -47,6 +47,20 @@
--ansi-bright-white: #FFFFFF;
}
@font-face {
font-family: 'C64ProMono';
src: url('../../assets/C64_Pro_Mono-STYLE.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'Pixeloid Mono';
src: url('../../assets/PixeloidMono.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;

View File

@ -1,6 +1,7 @@
import index from './index.html'
const server = Bun.serve({
port: process.env.PORT ? Number(process.env.PORT) : 3001,
routes: {
'/*': index,
@ -19,7 +20,6 @@ const server = Bun.serve({
},
},
},
development: process.env.NODE_ENV !== 'production' && {
hmr: true,
console: true,

View File

@ -1,9 +1,17 @@
/**
* How to use a Signal:
*
* Create a signal:
* Create a signal with primitives:
* const nameSignal = new Signal<string>()
* const countSignal = new Signal<number>()
*
* Create a signal with objects:
* const chatSignal = new Signal<{ username: string, message: string }>()
*
* Create a signal with no data (void):
* const clickSignal = new Signal<void>()
* const clickSignal2 = new Signal() // Defaults to void
*
* Connect to the signal:
* const disconnect = chatSignal.connect((data) => {
* const {username, message} = data;
@ -11,7 +19,10 @@
* })
*
* Emit a signal:
* nameSignal.emit("Alice")
* countSignal.emit(42)
* chatSignal.emit({ username: "Chad", message: "Hey everyone, how's it going?" });
* clickSignal.emit() // No argument for void signals
*
* Forward a signal:
* const relaySignal = new Signal<{ username: string, message: string }>()
@ -25,7 +36,7 @@
* chatSignal.disconnect()
*/
export class Signal<T extends object | void> {
export class Signal<T = void> {
private listeners: Array<(data: T) => void> = []
connect(listenerOrSignal: Signal<T> | ((data: T) => void)) {