Compare commits
18 Commits
336dc12082
...
019f7d84b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 019f7d84b1 | |||
| 4c794944ef | |||
| 99a5aa5312 | |||
| 7bbf43a725 | |||
| 4c15526d1b | |||
| c741cfee51 | |||
| 012b8c8cf1 | |||
| 4c3f7a8bfc | |||
| fe6f54b402 | |||
| 49f3f3e09f | |||
| 0d1dce4868 | |||
| d18ab2507c | |||
| 7e69356f79 | |||
| 9863f46f38 | |||
| 45f31d0678 | |||
| 49a6320fef | |||
| 51f67ac908 | |||
| d4a772e88b |
2
bun.lock
2
bun.lock
|
|
@ -62,7 +62,7 @@
|
||||||
|
|
||||||
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
|
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
|
||||||
|
|
||||||
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#d7a971db24aea5ddcaae2c18ce9f10dab793db19", { "peerDependencies": { "typescript": "^5" } }, "d7a971db24aea5ddcaae2c18ce9f10dab793db19"],
|
"reefvm": ["reefvm@git+https://git.nose.space/defunkt/reefvm#3e2e68b31f504347225a4d705c7568a0957d629e", { "peerDependencies": { "typescript": "^5" } }, "3e2e68b31f504347225a4d705c7568a0957d629e"],
|
||||||
|
|
||||||
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
"repl": "bun generate-parser && bun bin/repl",
|
"repl": "bun generate-parser && bun bin/repl",
|
||||||
"update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm",
|
"update-reef": "rm -rf ~/.bun/install/cache/ && rm bun.lock && bun update reefvm",
|
||||||
"cli:install": "ln -s \"$(pwd)/bin/shrimp\" ~/.bun/bin/shrimp",
|
"cli:install": "ln -s \"$(pwd)/bin/shrimp\" ~/.bun/bin/shrimp",
|
||||||
"cli:remove": "rm ~/.bun/bin/shrimp",
|
"cli:remove": "rm ~/.bun/bin/shrimp"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/view": "^6.38.3",
|
"@codemirror/view": "^6.38.3",
|
||||||
|
|
@ -31,4 +31,4 @@
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"printWidth": 100
|
"printWidth": 100
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,6 +51,7 @@ function processEscapeSeq(escapeSeq: string): string {
|
||||||
|
|
||||||
export class Compiler {
|
export class Compiler {
|
||||||
instructions: ProgramItem[] = []
|
instructions: ProgramItem[] = []
|
||||||
|
labelCount = 0
|
||||||
fnLabelCount = 0
|
fnLabelCount = 0
|
||||||
ifLabelCount = 0
|
ifLabelCount = 0
|
||||||
tryLabelCount = 0
|
tryLabelCount = 0
|
||||||
|
|
@ -105,11 +106,21 @@ export class Compiler {
|
||||||
|
|
||||||
switch (node.type.id) {
|
switch (node.type.id) {
|
||||||
case terms.Number:
|
case terms.Number:
|
||||||
const number = Number(value)
|
// Handle sign prefix for hex and binary literals
|
||||||
if (Number.isNaN(number))
|
// Number() doesn't parse '-0xFF' or '+0xFF' correctly
|
||||||
|
let numberValue: number
|
||||||
|
if (value.startsWith('-') && (value.includes('0x') || value.includes('0b'))) {
|
||||||
|
numberValue = -Number(value.slice(1))
|
||||||
|
} else if (value.startsWith('+') && (value.includes('0x') || value.includes('0b'))) {
|
||||||
|
numberValue = Number(value.slice(1))
|
||||||
|
} else {
|
||||||
|
numberValue = Number(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isNaN(numberValue))
|
||||||
throw new CompilerError(`Invalid number literal: ${value}`, node.from, node.to)
|
throw new CompilerError(`Invalid number literal: ${value}`, node.from, node.to)
|
||||||
|
|
||||||
return [[`PUSH`, number]]
|
return [[`PUSH`, numberValue]]
|
||||||
|
|
||||||
case terms.String: {
|
case terms.String: {
|
||||||
const { parts, hasInterpolation } = getStringParts(node, input)
|
const { parts, hasInterpolation } = getStringParts(node, input)
|
||||||
|
|
@ -232,6 +243,24 @@ export class Compiler {
|
||||||
case '%':
|
case '%':
|
||||||
instructions.push(['MOD'])
|
instructions.push(['MOD'])
|
||||||
break
|
break
|
||||||
|
case 'band':
|
||||||
|
instructions.push(['BIT_AND'])
|
||||||
|
break
|
||||||
|
case 'bor':
|
||||||
|
instructions.push(['BIT_OR'])
|
||||||
|
break
|
||||||
|
case 'bxor':
|
||||||
|
instructions.push(['BIT_XOR'])
|
||||||
|
break
|
||||||
|
case '<<':
|
||||||
|
instructions.push(['BIT_SHL'])
|
||||||
|
break
|
||||||
|
case '>>':
|
||||||
|
instructions.push(['BIT_SHR'])
|
||||||
|
break
|
||||||
|
case '>>>':
|
||||||
|
instructions.push(['BIT_USHR'])
|
||||||
|
break
|
||||||
default:
|
default:
|
||||||
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
|
throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to)
|
||||||
}
|
}
|
||||||
|
|
@ -274,13 +303,31 @@ export class Compiler {
|
||||||
const { identifier, operator, right } = getCompoundAssignmentParts(node)
|
const { identifier, operator, right } = getCompoundAssignmentParts(node)
|
||||||
const identifierName = input.slice(identifier.from, identifier.to)
|
const identifierName = input.slice(identifier.from, identifier.to)
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
|
const opValue = input.slice(operator.from, operator.to)
|
||||||
|
|
||||||
// will throw if undefined
|
// Special handling for ??= since it needs conditional evaluation
|
||||||
instructions.push(['LOAD', identifierName])
|
if (opValue === '??=') {
|
||||||
|
instructions.push(['LOAD', identifierName])
|
||||||
|
|
||||||
|
const rightInstructions = this.#compileNode(right, input)
|
||||||
|
|
||||||
|
instructions.push(['DUP'])
|
||||||
|
instructions.push(['PUSH', null])
|
||||||
|
instructions.push(['NEQ'])
|
||||||
|
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
|
||||||
|
instructions.push(['POP'])
|
||||||
|
instructions.push(...rightInstructions)
|
||||||
|
|
||||||
|
instructions.push(['DUP'])
|
||||||
|
instructions.push(['STORE', identifierName])
|
||||||
|
|
||||||
|
return instructions
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard compound assignments: evaluate both sides, then operate
|
||||||
|
instructions.push(['LOAD', identifierName]) // will throw if undefined
|
||||||
instructions.push(...this.#compileNode(right, input))
|
instructions.push(...this.#compileNode(right, input))
|
||||||
|
|
||||||
const opValue = input.slice(operator.from, operator.to)
|
|
||||||
switch (opValue) {
|
switch (opValue) {
|
||||||
case '+=':
|
case '+=':
|
||||||
instructions.push(['ADD'])
|
instructions.push(['ADD'])
|
||||||
|
|
@ -367,7 +414,29 @@ export class Compiler {
|
||||||
|
|
||||||
case terms.FunctionCallOrIdentifier: {
|
case terms.FunctionCallOrIdentifier: {
|
||||||
if (node.firstChild?.type.id === terms.DotGet) {
|
if (node.firstChild?.type.id === terms.DotGet) {
|
||||||
return this.#compileNode(node.firstChild, input)
|
const instructions: ProgramItem[] = []
|
||||||
|
const callLabel = `.call_dotget_${++this.labelCount}`
|
||||||
|
const afterLabel = `.after_dotget_${++this.labelCount}`
|
||||||
|
|
||||||
|
instructions.push(...this.#compileNode(node.firstChild, input))
|
||||||
|
instructions.push(['DUP'])
|
||||||
|
instructions.push(['TYPE'])
|
||||||
|
instructions.push(['PUSH', 'function'])
|
||||||
|
instructions.push(['EQ'])
|
||||||
|
instructions.push(['JUMP_IF_TRUE', callLabel])
|
||||||
|
instructions.push(['DUP'])
|
||||||
|
instructions.push(['TYPE'])
|
||||||
|
instructions.push(['PUSH', 'native'])
|
||||||
|
instructions.push(['EQ'])
|
||||||
|
instructions.push(['JUMP_IF_TRUE', callLabel])
|
||||||
|
instructions.push(['JUMP', afterLabel])
|
||||||
|
instructions.push([`${callLabel}:`])
|
||||||
|
instructions.push(['PUSH', 0])
|
||||||
|
instructions.push(['PUSH', 0])
|
||||||
|
instructions.push(['CALL'])
|
||||||
|
instructions.push([`${afterLabel}:`])
|
||||||
|
|
||||||
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
||||||
return [['TRY_CALL', value]]
|
return [['TRY_CALL', value]]
|
||||||
|
|
@ -587,6 +656,18 @@ export class Compiler {
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case '??':
|
||||||
|
// Nullish coalescing: return left if not null, else right
|
||||||
|
instructions.push(...leftInstructions)
|
||||||
|
instructions.push(['DUP'])
|
||||||
|
instructions.push(['PUSH', null])
|
||||||
|
instructions.push(['NEQ'])
|
||||||
|
instructions.push(['JUMP_IF_TRUE', rightInstructions.length + 1])
|
||||||
|
instructions.push(['POP'])
|
||||||
|
instructions.push(...rightInstructions)
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new CompilerError(`Unsupported conditional operator: ${opValue}`, op.from, op.to)
|
throw new CompilerError(`Unsupported conditional operator: ${opValue}`, op.from, op.to)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
178
src/compiler/tests/bitwise.test.ts
Normal file
178
src/compiler/tests/bitwise.test.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
import { expect, describe, test } from 'bun:test'
|
||||||
|
|
||||||
|
describe('bitwise operators', () => {
|
||||||
|
describe('band (bitwise AND)', () => {
|
||||||
|
test('basic AND operation', () => {
|
||||||
|
expect('5 band 3').toEvaluateTo(1)
|
||||||
|
// 5 = 0101, 3 = 0011, result = 0001 = 1
|
||||||
|
})
|
||||||
|
|
||||||
|
test('AND with zero', () => {
|
||||||
|
expect('5 band 0').toEvaluateTo(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('AND with all bits set', () => {
|
||||||
|
expect('15 band 7').toEvaluateTo(7)
|
||||||
|
// 15 = 1111, 7 = 0111, result = 0111 = 7
|
||||||
|
})
|
||||||
|
|
||||||
|
test('AND in assignment', () => {
|
||||||
|
expect('x = 12 band 10').toEvaluateTo(8)
|
||||||
|
// 12 = 1100, 10 = 1010, result = 1000 = 8
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('bor (bitwise OR)', () => {
|
||||||
|
test('basic OR operation', () => {
|
||||||
|
expect('5 bor 3').toEvaluateTo(7)
|
||||||
|
// 5 = 0101, 3 = 0011, result = 0111 = 7
|
||||||
|
})
|
||||||
|
|
||||||
|
test('OR with zero', () => {
|
||||||
|
expect('5 bor 0').toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('OR with all bits set', () => {
|
||||||
|
expect('8 bor 4').toEvaluateTo(12)
|
||||||
|
// 8 = 1000, 4 = 0100, result = 1100 = 12
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('bxor (bitwise XOR)', () => {
|
||||||
|
test('basic XOR operation', () => {
|
||||||
|
expect('5 bxor 3').toEvaluateTo(6)
|
||||||
|
// 5 = 0101, 3 = 0011, result = 0110 = 6
|
||||||
|
})
|
||||||
|
|
||||||
|
test('XOR with itself returns zero', () => {
|
||||||
|
expect('5 bxor 5').toEvaluateTo(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('XOR with zero returns same value', () => {
|
||||||
|
expect('7 bxor 0').toEvaluateTo(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('XOR in assignment', () => {
|
||||||
|
expect('result = 8 bxor 12').toEvaluateTo(4)
|
||||||
|
// 8 = 1000, 12 = 1100, result = 0100 = 4
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('bnot (bitwise NOT)', () => {
|
||||||
|
test('NOT of positive number', () => {
|
||||||
|
expect('bnot 5').toEvaluateTo(-6)
|
||||||
|
// ~5 = -6 (two\'s complement)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('NOT of zero', () => {
|
||||||
|
expect('bnot 0').toEvaluateTo(-1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('NOT of negative number', () => {
|
||||||
|
expect('bnot -1').toEvaluateTo(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('double NOT returns original', () => {
|
||||||
|
expect('bnot (bnot 5)').toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('<< (left shift)', () => {
|
||||||
|
test('basic left shift', () => {
|
||||||
|
expect('5 << 2').toEvaluateTo(20)
|
||||||
|
// 5 << 2 = 20
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shift by zero', () => {
|
||||||
|
expect('5 << 0').toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shift by one', () => {
|
||||||
|
expect('3 << 1').toEvaluateTo(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('large shift', () => {
|
||||||
|
expect('1 << 10').toEvaluateTo(1024)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('>> (signed right shift)', () => {
|
||||||
|
test('basic right shift', () => {
|
||||||
|
expect('20 >> 2').toEvaluateTo(5)
|
||||||
|
// 20 >> 2 = 5
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shift by zero', () => {
|
||||||
|
expect('20 >> 0').toEvaluateTo(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('preserves sign for negative numbers', () => {
|
||||||
|
expect('-20 >> 2').toEvaluateTo(-5)
|
||||||
|
// Sign is preserved
|
||||||
|
})
|
||||||
|
|
||||||
|
test('negative number right shift', () => {
|
||||||
|
expect('-8 >> 1').toEvaluateTo(-4)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('>>> (unsigned right shift)', () => {
|
||||||
|
test('basic unsigned right shift', () => {
|
||||||
|
expect('20 >>> 2').toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unsigned shift of -1', () => {
|
||||||
|
expect('-1 >>> 1').toEvaluateTo(2147483647)
|
||||||
|
// -1 >>> 1 = 2147483647 (unsigned, no sign extension)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unsigned shift of negative number', () => {
|
||||||
|
expect('-8 >>> 1').toEvaluateTo(2147483644)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('compound expressions', () => {
|
||||||
|
test('multiple bitwise operations', () => {
|
||||||
|
expect('(5 band 3) bor (8 bxor 12)').toEvaluateTo(5)
|
||||||
|
// (5 & 3) | (8 ^ 12) = 1 | 4 = 5
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bitwise with variables', () => {
|
||||||
|
expect(`
|
||||||
|
a = 5
|
||||||
|
b = 3
|
||||||
|
a bor b
|
||||||
|
`).toEvaluateTo(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shift operations with variables', () => {
|
||||||
|
expect(`
|
||||||
|
x = 16
|
||||||
|
y = 2
|
||||||
|
x >> y
|
||||||
|
`).toEvaluateTo(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('mixing shifts and bitwise', () => {
|
||||||
|
expect('(8 << 1) band 15').toEvaluateTo(0)
|
||||||
|
// (8 << 1) & 15 = 16 & 15 = 0
|
||||||
|
})
|
||||||
|
|
||||||
|
test('mixing shifts and bitwise 2', () => {
|
||||||
|
expect('(7 << 1) band 15').toEvaluateTo(14)
|
||||||
|
// (7 << 1) & 15 = 14 & 15 = 14
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('precedence', () => {
|
||||||
|
test('bitwise has correct precedence with arithmetic', () => {
|
||||||
|
expect('1 + 2 band 3').toEvaluateTo(3)
|
||||||
|
// (1 + 2) & 3 = 3 & 3 = 3
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shift has correct precedence', () => {
|
||||||
|
expect('4 + 8 << 1').toEvaluateTo(24)
|
||||||
|
// (4 + 8) << 1 = 12 << 1 = 24
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -110,7 +110,10 @@ describe('compiler', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('function call with no args', () => {
|
test('function call with no args', () => {
|
||||||
expect(`bloop = do: 'bloop' end; bloop`).toEvaluateTo('bloop')
|
expect(`bloop = do: 'bleep' end; bloop`).toEvaluateTo('bleep')
|
||||||
|
expect(`bloop = [ go=do: 'bleep' end ]; bloop.go`).toEvaluateTo('bleep')
|
||||||
|
expect(`bloop = [ go=do: 'bleep' end ]; abc = do x: x end; abc (bloop.go)`).toEvaluateTo('bleep')
|
||||||
|
expect(`num = ((math.random) * 10 + 1) | math.floor; num >= 1 and num <= 10 `).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('function call with if statement and multiple expressions', () => {
|
test('function call with if statement and multiple expressions', () => {
|
||||||
|
|
@ -298,6 +301,23 @@ describe('default params', () => {
|
||||||
expect('multiply = do x y=5: x * y end; multiply 5 2').toEvaluateTo(10)
|
expect('multiply = do x y=5: x * y end; multiply 5 2').toEvaluateTo(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('null triggers default value', () => {
|
||||||
|
expect('test = do n=true: n end; test').toEvaluateTo(true)
|
||||||
|
expect('test = do n=true: n end; test false').toEvaluateTo(false)
|
||||||
|
expect('test = do n=true: n end; test null').toEvaluateTo(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('null triggers default for named parameters', () => {
|
||||||
|
expect("greet = do name='World': name end; greet name=null").toEvaluateTo('World')
|
||||||
|
expect("greet = do name='World': name end; greet name='Bob'").toEvaluateTo('Bob')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('null triggers default with multiple parameters', () => {
|
||||||
|
expect('calc = do x=10 y=20: x + y end; calc null 5').toEvaluateTo(15)
|
||||||
|
expect('calc = do x=10 y=20: x + y end; calc 3 null').toEvaluateTo(23)
|
||||||
|
expect('calc = do x=10 y=20: x + y end; calc null null').toEvaluateTo(30)
|
||||||
|
})
|
||||||
|
|
||||||
test.skip('array default', () => {
|
test.skip('array default', () => {
|
||||||
expect('abc = do alpha=[a b c]: alpha end; abc').toEvaluateTo(['a', 'b', 'c'])
|
expect('abc = do alpha=[a b c]: alpha end; abc').toEvaluateTo(['a', 'b', 'c'])
|
||||||
expect('abc = do alpha=[a b c]: alpha end; abc [x y z]').toEvaluateTo(['x', 'y', 'z'])
|
expect('abc = do alpha=[a b c]: alpha end; abc [x y z]').toEvaluateTo(['x', 'y', 'z'])
|
||||||
|
|
@ -313,3 +333,118 @@ describe('default params', () => {
|
||||||
).toEvaluateTo({ name: 'Jon', age: 21 })
|
).toEvaluateTo({ name: 'Jon', age: 21 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Nullish coalescing operator (??)', () => {
|
||||||
|
test('returns left side when not null', () => {
|
||||||
|
expect('5 ?? 10').toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns right side when left is null', () => {
|
||||||
|
expect('null ?? 10').toEvaluateTo(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns left side when left is false', () => {
|
||||||
|
expect('false ?? 10').toEvaluateTo(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns left side when left is 0', () => {
|
||||||
|
expect('0 ?? 10').toEvaluateTo(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns left side when left is empty string', () => {
|
||||||
|
expect(`'' ?? 'default'`).toEvaluateTo('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('chains left to right', () => {
|
||||||
|
expect('null ?? null ?? 42').toEvaluateTo(42)
|
||||||
|
expect('null ?? 10 ?? 20').toEvaluateTo(10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('short-circuits evaluation', () => {
|
||||||
|
const throwError = () => { throw new Error('Should not evaluate') }
|
||||||
|
expect('5 ?? throw-error').toEvaluateTo(5, { 'throw-error': throwError })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('works with variables', () => {
|
||||||
|
expect('x = null; x ?? 5').toEvaluateTo(5)
|
||||||
|
expect('y = 3; y ?? 5').toEvaluateTo(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('works with function calls', () => {
|
||||||
|
const getValue = () => null
|
||||||
|
const getDefault = () => 42
|
||||||
|
// Note: identifiers without parentheses refer to the function, not call it
|
||||||
|
// Use explicit call syntax to invoke the function
|
||||||
|
expect('(get-value) ?? (get-default)').toEvaluateTo(42, {
|
||||||
|
'get-value': getValue,
|
||||||
|
'get-default': getDefault
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Nullish coalescing assignment (??=)', () => {
|
||||||
|
test('assigns when variable is null', () => {
|
||||||
|
expect('x = null; x ??= 5; x').toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not assign when variable is not null', () => {
|
||||||
|
expect('x = 3; x ??= 10; x').toEvaluateTo(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not assign when variable is false', () => {
|
||||||
|
expect('x = false; x ??= true; x').toEvaluateTo(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not assign when variable is 0', () => {
|
||||||
|
expect('x = 0; x ??= 100; x').toEvaluateTo(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('does not assign when variable is empty string', () => {
|
||||||
|
expect(`x = ''; x ??= 'default'; x`).toEvaluateTo('')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('returns the final value', () => {
|
||||||
|
expect('x = null; x ??= 5').toEvaluateTo(5)
|
||||||
|
expect('y = 3; y ??= 10').toEvaluateTo(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('short-circuits evaluation when not null', () => {
|
||||||
|
const throwError = () => { throw new Error('Should not evaluate') }
|
||||||
|
expect('x = 5; x ??= throw-error; x').toEvaluateTo(5, { 'throw-error': throwError })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('works with expressions', () => {
|
||||||
|
expect('x = null; x ??= 2 + 3; x').toEvaluateTo(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('works with function calls', () => {
|
||||||
|
const getDefault = () => 42
|
||||||
|
expect('x = null; x ??= (get-default); x').toEvaluateTo(42, { 'get-default': getDefault })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('throws when variable is undefined', () => {
|
||||||
|
expect(() => expect('undefined-var ??= 5').toEvaluateTo(null)).toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Compound assignment operators', () => {
|
||||||
|
test('+=', () => {
|
||||||
|
expect('x = 5; x += 3; x').toEvaluateTo(8)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('-=', () => {
|
||||||
|
expect('x = 10; x -= 4; x').toEvaluateTo(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('*=', () => {
|
||||||
|
expect('x = 3; x *= 4; x').toEvaluateTo(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('/=', () => {
|
||||||
|
expect('x = 20; x /= 5; x').toEvaluateTo(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('%=', () => {
|
||||||
|
expect('x = 10; x %= 3; x').toEvaluateTo(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,44 @@
|
||||||
import { describe } from 'bun:test'
|
import { describe } from 'bun:test'
|
||||||
import { expect, test } from 'bun:test'
|
import { expect, test } from 'bun:test'
|
||||||
|
|
||||||
|
describe('number literals', () => {
|
||||||
|
test('binary literals', () => {
|
||||||
|
expect('0b110').toEvaluateTo(6)
|
||||||
|
expect('0b1010').toEvaluateTo(10)
|
||||||
|
expect('0b11111111').toEvaluateTo(255)
|
||||||
|
expect('0b0').toEvaluateTo(0)
|
||||||
|
expect('0b1').toEvaluateTo(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hex literals', () => {
|
||||||
|
expect('0xdeadbeef').toEvaluateTo(0xdeadbeef)
|
||||||
|
expect('0xdeadbeef').toEvaluateTo(3735928559)
|
||||||
|
expect('0xFF').toEvaluateTo(255)
|
||||||
|
expect('0xff').toEvaluateTo(255)
|
||||||
|
expect('0x10').toEvaluateTo(16)
|
||||||
|
expect('0x0').toEvaluateTo(0)
|
||||||
|
expect('0xABCDEF').toEvaluateTo(0xabcdef)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('decimal literals still work', () => {
|
||||||
|
expect('42').toEvaluateTo(42)
|
||||||
|
expect('3.14').toEvaluateTo(3.14)
|
||||||
|
expect('0').toEvaluateTo(0)
|
||||||
|
expect('999999').toEvaluateTo(999999)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('negative hex and binary', () => {
|
||||||
|
expect('-0xFF').toEvaluateTo(-255)
|
||||||
|
expect('-0b1010').toEvaluateTo(-10)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('positive prefix', () => {
|
||||||
|
expect('+0xFF').toEvaluateTo(255)
|
||||||
|
expect('+0b110').toEvaluateTo(6)
|
||||||
|
expect('+42').toEvaluateTo(42)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('array literals', () => {
|
describe('array literals', () => {
|
||||||
test('work with numbers', () => {
|
test('work with numbers', () => {
|
||||||
expect('[1 2 3]').toEvaluateTo([1, 2, 3])
|
expect('[1 2 3]').toEvaluateTo([1, 2, 3])
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,28 @@ type Operator = { str: string; tokenName: keyof typeof terms }
|
||||||
const operators: Array<Operator> = [
|
const operators: Array<Operator> = [
|
||||||
{ str: 'and', tokenName: 'And' },
|
{ str: 'and', tokenName: 'And' },
|
||||||
{ str: 'or', tokenName: 'Or' },
|
{ str: 'or', tokenName: 'Or' },
|
||||||
|
{ str: 'band', tokenName: 'Band' },
|
||||||
|
{ str: 'bor', tokenName: 'Bor' },
|
||||||
|
{ str: 'bxor', tokenName: 'Bxor' },
|
||||||
|
{ str: '>>>', tokenName: 'Ushr' }, // Must come before >>
|
||||||
|
{ str: '>>', tokenName: 'Shr' },
|
||||||
|
{ str: '<<', tokenName: 'Shl' },
|
||||||
{ str: '>=', tokenName: 'Gte' },
|
{ str: '>=', tokenName: 'Gte' },
|
||||||
{ str: '<=', tokenName: 'Lte' },
|
{ str: '<=', tokenName: 'Lte' },
|
||||||
{ str: '!=', tokenName: 'Neq' },
|
{ str: '!=', tokenName: 'Neq' },
|
||||||
{ str: '==', tokenName: 'EqEq' },
|
{ str: '==', tokenName: 'EqEq' },
|
||||||
|
|
||||||
// Compound assignment operators (must come before single-char operators)
|
// Compound assignment operators (must come before single-char operators)
|
||||||
|
{ str: '??=', tokenName: 'NullishEq' },
|
||||||
{ str: '+=', tokenName: 'PlusEq' },
|
{ str: '+=', tokenName: 'PlusEq' },
|
||||||
{ str: '-=', tokenName: 'MinusEq' },
|
{ str: '-=', tokenName: 'MinusEq' },
|
||||||
{ str: '*=', tokenName: 'StarEq' },
|
{ str: '*=', tokenName: 'StarEq' },
|
||||||
{ str: '/=', tokenName: 'SlashEq' },
|
{ str: '/=', tokenName: 'SlashEq' },
|
||||||
{ str: '%=', tokenName: 'ModuloEq' },
|
{ str: '%=', tokenName: 'ModuloEq' },
|
||||||
|
|
||||||
|
// Nullish coalescing (must come before it could be mistaken for other tokens)
|
||||||
|
{ str: '??', tokenName: 'NullishCoalesce' },
|
||||||
|
|
||||||
// Single-char operators
|
// Single-char operators
|
||||||
{ str: '*', tokenName: 'Star' },
|
{ str: '*', tokenName: 'Star' },
|
||||||
{ str: '=', tokenName: 'Eq' },
|
{ str: '=', tokenName: 'Eq' },
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,18 @@
|
||||||
|
|
||||||
@top Program { item* }
|
@top Program { item* }
|
||||||
|
|
||||||
@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo, PlusEq, MinusEq, StarEq, SlashEq, ModuloEq }
|
@external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo, PlusEq, MinusEq, StarEq, SlashEq, ModuloEq, Band, Bor, Bxor, Shl, Shr, Ushr, NullishCoalesce, NullishEq }
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
@precedence { Number Regex }
|
@precedence { Number Regex }
|
||||||
|
|
||||||
StringFragment { !['\\$]+ }
|
StringFragment { !['\\$]+ }
|
||||||
NamedArgPrefix { $[a-z-]+ "=" }
|
NamedArgPrefix { $[a-z] $[a-z0-9-]* "=" }
|
||||||
Number { ("-" | "+")? $[0-9]+ ('.' $[0-9]+)? }
|
Number {
|
||||||
|
("-" | "+")? "0x" $[0-9a-fA-F]+ |
|
||||||
|
("-" | "+")? "0b" $[01]+ |
|
||||||
|
("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)?
|
||||||
|
}
|
||||||
Boolean { "true" | "false" }
|
Boolean { "true" | "false" }
|
||||||
newlineOrSemicolon { "\n" | ";" }
|
newlineOrSemicolon { "\n" | ";" }
|
||||||
eof { @eof }
|
eof { @eof }
|
||||||
|
|
@ -44,10 +48,13 @@ null { @specialize[@name=Null]<Identifier, "null"> }
|
||||||
pipe @left,
|
pipe @left,
|
||||||
or @left,
|
or @left,
|
||||||
and @left,
|
and @left,
|
||||||
|
nullish @left,
|
||||||
comparison @left,
|
comparison @left,
|
||||||
multiplicative @left,
|
multiplicative @left,
|
||||||
additive @left,
|
additive @left,
|
||||||
call
|
bitwise @left,
|
||||||
|
call,
|
||||||
|
functionWithNewlines
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
|
|
@ -160,7 +167,8 @@ ConditionalOp {
|
||||||
expression !comparison Gt expression |
|
expression !comparison Gt expression |
|
||||||
expression !comparison Gte expression |
|
expression !comparison Gte expression |
|
||||||
(expression | ConditionalOp) !and And (expression | ConditionalOp) |
|
(expression | ConditionalOp) !and And (expression | ConditionalOp) |
|
||||||
(expression | ConditionalOp) !or Or (expression | ConditionalOp)
|
(expression | ConditionalOp) !or Or (expression | ConditionalOp) |
|
||||||
|
(expression | ConditionalOp) !nullish NullishCoalesce (expression | ConditionalOp)
|
||||||
}
|
}
|
||||||
|
|
||||||
Params {
|
Params {
|
||||||
|
|
@ -176,7 +184,7 @@ Assign {
|
||||||
}
|
}
|
||||||
|
|
||||||
CompoundAssign {
|
CompoundAssign {
|
||||||
AssignableIdentifier (PlusEq | MinusEq | StarEq | SlashEq | ModuloEq) consumeToTerminator
|
AssignableIdentifier (PlusEq | MinusEq | StarEq | SlashEq | ModuloEq | NullishEq) consumeToTerminator
|
||||||
}
|
}
|
||||||
|
|
||||||
BinOp {
|
BinOp {
|
||||||
|
|
@ -184,11 +192,31 @@ BinOp {
|
||||||
(expression | BinOp) !multiplicative Star (expression | BinOp) |
|
(expression | BinOp) !multiplicative Star (expression | BinOp) |
|
||||||
(expression | BinOp) !multiplicative Slash (expression | BinOp) |
|
(expression | BinOp) !multiplicative Slash (expression | BinOp) |
|
||||||
(expression | BinOp) !additive Plus (expression | BinOp) |
|
(expression | BinOp) !additive Plus (expression | BinOp) |
|
||||||
(expression | BinOp) !additive Minus (expression | BinOp)
|
(expression | BinOp) !additive Minus (expression | BinOp) |
|
||||||
|
(expression | BinOp) !bitwise Band (expression | BinOp) |
|
||||||
|
(expression | BinOp) !bitwise Bor (expression | BinOp) |
|
||||||
|
(expression | BinOp) !bitwise Bxor (expression | BinOp) |
|
||||||
|
(expression | BinOp) !bitwise Shl (expression | BinOp) |
|
||||||
|
(expression | BinOp) !bitwise Shr (expression | BinOp) |
|
||||||
|
(expression | BinOp) !bitwise Ushr (expression | BinOp)
|
||||||
}
|
}
|
||||||
|
|
||||||
ParenExpr {
|
ParenExpr {
|
||||||
leftParen (IfExpr | ambiguousFunctionCall | BinOp | expressionWithoutIdentifier | ConditionalOp | PipeExpr | FunctionDef) rightParen
|
leftParen newlineOrSemicolon* (
|
||||||
|
FunctionCallWithNewlines |
|
||||||
|
IfExpr |
|
||||||
|
ambiguousFunctionCall |
|
||||||
|
BinOp newlineOrSemicolon* |
|
||||||
|
expressionWithoutIdentifier |
|
||||||
|
ConditionalOp newlineOrSemicolon* |
|
||||||
|
PipeExpr |
|
||||||
|
FunctionDef
|
||||||
|
)
|
||||||
|
rightParen
|
||||||
|
}
|
||||||
|
|
||||||
|
FunctionCallWithNewlines[@name=FunctionCall] {
|
||||||
|
(DotGet | Identifier | ParenExpr) newlineOrSemicolon+ arg !functionWithNewlines (newlineOrSemicolon+ arg)* newlineOrSemicolon*
|
||||||
}
|
}
|
||||||
|
|
||||||
expression {
|
expression {
|
||||||
|
|
|
||||||
|
|
@ -19,48 +19,57 @@ export const
|
||||||
StarEq = 17,
|
StarEq = 17,
|
||||||
SlashEq = 18,
|
SlashEq = 18,
|
||||||
ModuloEq = 19,
|
ModuloEq = 19,
|
||||||
Identifier = 20,
|
Band = 20,
|
||||||
AssignableIdentifier = 21,
|
Bor = 21,
|
||||||
Word = 22,
|
Bxor = 22,
|
||||||
IdentifierBeforeDot = 23,
|
Shl = 23,
|
||||||
Do = 24,
|
Shr = 24,
|
||||||
Comment = 25,
|
Ushr = 25,
|
||||||
Program = 26,
|
NullishCoalesce = 26,
|
||||||
PipeExpr = 27,
|
NullishEq = 27,
|
||||||
WhileExpr = 29,
|
Identifier = 28,
|
||||||
keyword = 70,
|
AssignableIdentifier = 29,
|
||||||
ConditionalOp = 31,
|
Word = 30,
|
||||||
ParenExpr = 32,
|
IdentifierBeforeDot = 31,
|
||||||
IfExpr = 33,
|
Do = 32,
|
||||||
FunctionCall = 35,
|
Comment = 33,
|
||||||
DotGet = 36,
|
Program = 34,
|
||||||
Number = 37,
|
PipeExpr = 35,
|
||||||
PositionalArg = 38,
|
WhileExpr = 37,
|
||||||
FunctionDef = 39,
|
keyword = 79,
|
||||||
Params = 40,
|
ConditionalOp = 39,
|
||||||
NamedParam = 41,
|
ParenExpr = 40,
|
||||||
NamedArgPrefix = 42,
|
FunctionCallWithNewlines = 41,
|
||||||
String = 43,
|
DotGet = 42,
|
||||||
StringFragment = 44,
|
Number = 43,
|
||||||
Interpolation = 45,
|
PositionalArg = 44,
|
||||||
EscapeSeq = 46,
|
FunctionDef = 45,
|
||||||
Boolean = 47,
|
Params = 46,
|
||||||
Null = 48,
|
NamedParam = 47,
|
||||||
colon = 49,
|
NamedArgPrefix = 48,
|
||||||
CatchExpr = 50,
|
String = 49,
|
||||||
Block = 52,
|
StringFragment = 50,
|
||||||
FinallyExpr = 53,
|
Interpolation = 51,
|
||||||
Underscore = 56,
|
EscapeSeq = 52,
|
||||||
NamedArg = 57,
|
Boolean = 53,
|
||||||
ElseIfExpr = 58,
|
Null = 54,
|
||||||
ElseExpr = 60,
|
colon = 55,
|
||||||
FunctionCallOrIdentifier = 61,
|
CatchExpr = 56,
|
||||||
BinOp = 62,
|
Block = 58,
|
||||||
Regex = 63,
|
FinallyExpr = 59,
|
||||||
Dict = 64,
|
Underscore = 62,
|
||||||
Array = 65,
|
NamedArg = 63,
|
||||||
FunctionCallWithBlock = 66,
|
IfExpr = 64,
|
||||||
TryExpr = 67,
|
FunctionCall = 66,
|
||||||
Throw = 69,
|
ElseIfExpr = 67,
|
||||||
CompoundAssign = 71,
|
ElseExpr = 69,
|
||||||
Assign = 72
|
FunctionCallOrIdentifier = 70,
|
||||||
|
BinOp = 71,
|
||||||
|
Regex = 72,
|
||||||
|
Dict = 73,
|
||||||
|
Array = 74,
|
||||||
|
FunctionCallWithBlock = 75,
|
||||||
|
TryExpr = 76,
|
||||||
|
Throw = 78,
|
||||||
|
CompoundAssign = 80,
|
||||||
|
Assign = 81
|
||||||
|
|
|
||||||
|
|
@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer"
|
||||||
import {tokenizer, specializeKeyword} from "./tokenizer"
|
import {tokenizer, specializeKeyword} from "./tokenizer"
|
||||||
import {trackScope} from "./parserScopeContext"
|
import {trackScope} from "./parserScopeContext"
|
||||||
import {highlighting} from "./highlight"
|
import {highlighting} from "./highlight"
|
||||||
const spec_Identifier = {__proto__:null,while:60, if:68, null:96, catch:102, finally:108, end:110, else:118, try:136, throw:140}
|
const spec_Identifier = {__proto__:null,while:76, null:108, catch:114, finally:120, end:122, if:130, else:136, try:154, throw:158}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "9UQYQbOOO!dOpO'#DQO!iOSO'#DXO$_QcO'#DkO&rQcO'#EYOOQ`'#Eh'#EhO'uQRO'#DlO)[QcO'#EWO)lQbO'#C|OOQa'#Dn'#DnO+nQbO'#DoOOQa'#EY'#EYO+uQcO'#EYO+|QcO'#EXO,yQcO'#EWO-TQRO'#DuOOQ`'#EW'#EWO-iQbO'#EWO-pQQO'#EVOOQ`'#EV'#EVOOQ`'#Dw'#DwQYQbOOO-{QbO'#DTO.WQbO'#C}O.{QbO'#CyO/pQQO'#DqO.{QbO'#DsO/uObO,59lO0QQbO'#DZO0YQWO'#D[OOOO'#E`'#E`OOOO'#D|'#D|O0nOSO,59sOOQa,59s,59sOOQ`'#DS'#DSO0|QbO'#DgOOQ`'#E^'#E^OOQ`'#Dy'#DyO1WQbO,59kOOQa'#EX'#EXO.{QbO,5:WO.{QbO,5:WO.{QbO,5:WO.{QbO,59gO.{QbO,59gO.{QbO,59gO2QQRO,59hO2^QQO,59hO2fQQO,59hO2qQRO,59hO3[QRO,59hO3jQQO'#CwOOQ`'#EP'#EPO3oQbO,5:ZO3vQQO,5:YOOQa,5:Z,5:ZO4RQbO,5:ZO)lQbO,5:bO)lQbO,5:aO4]QbO,5:[O4dQbO,59cOOQ`,5:q,5:qO)lQbO'#DxOOQ`-E7u-E7uOOQ`'#Dz'#DzO5OQbO'#DUO5ZQbO'#DVOOQO'#D{'#D{O5RQQO'#DUO5iQQO,59oO5nQcO'#EXO5uQRO'#E[O6lQRO'#E[OOQO'#E['#E[O6sQQO,59iO6xQRO,59eO7PQRO,59eO4]QbO,5:]O7[QcO,5:_O8dQcO,5:_O8tQcO,5:_OOQa1G/W1G/WOOOO,59u,59uOOOO,59v,59vOOOO-E7z-E7zOOQa1G/_1G/_OOQ`,5:R,5:ROOQ`-E7w-E7wOOQa1G/r1G/rO9sQcO1G/rO9}QcO1G/rO:XQcO1G/rOOQa1G/R1G/RO<TQcO1G/RO<[QcO1G/RO<cQcO1G/ROOQa1G/S1G/SOOQ`-E7}-E7}O<jQQO1G/tOOQa1G/u1G/uO<uQbO1G/uOOQO'#EQ'#EQO<jQQO1G/tOOQa1G/t1G/tOOQ`'#ER'#ERO<uQbO1G/uO=PQbO1G/|O=kQbO1G/{O>VQbO'#DbO>hQbO'#DbO>{QbO1G/vOOQ`-E7v-E7vOOQ`,5:d,5:dOOQ`-E7x-E7xO?WQQO,59pOOQO,59q,59qOOQO-E7y-E7yO?`QbO1G/ZO4]QbO1G/TO4]QbO1G/PO?gQbO1G/wO?rQQO7+%`OOQa7+%`7+%`O?}QbO7+%aOOQa7+%a7+%aOOQO-E8O-E8OOOQ`-E8P-E8POOQ`'#D}'#D}O@XQQO'#D}O@aQbO'#EgOOQ`,59|,59|O@tQbO'#D`O@yQQO'#DcOOQ`7+%b7+%bOAOQbO7+%bOATQbO7+%bOA]QbO7+$uOAkQbO7+$uOA{QbO7+$oOBTQbO7+$kOOQ`7+%c7+%cOBYQbO7+%cOB_QbO7+%cOOQa<<Hz<<HzOOQa<<H{<<H{OOQ`,5:i,5:iOOQ`-E7{-E7{OBgQQO,59zO4]QbO,59}OOQ`<<H|<<H|OBlQbO<<H|OOQ`<<Ha<<HaOBqQbO<<HaOBvQbO<<HaOCOQbO<<HaOOQ`'#EO'#EOOCZQbO<<HZOCcQbO'#DjOOQ`<<HZ<<HZOCkQbO<<HZOOQ`<<HV<<HVOOQ`<<H}<<H}OCpQbO<<H}O4]QbO1G/fOOQ`1G/i1G/iOOQ`AN>hAN>hOOQ`AN={AN={OCuQbOAN={OCzQbOAN={OOQ`-E7|-E7|OOQ`AN=uAN=uODSQbOAN=uO.WQbO,5:SO4]QbO,5:UOOQ`AN>iAN>iOOQ`7+%Q7+%QOOQ`G23gG23gODXQbOG23gPD^QbO'#DhOOQ`G23aG23aODcQQO1G/nOOQ`1G/p1G/pOOQ`LD)RLD)RO4]QbO7+%YOOQ`<<Ht<<Ht",
|
states: "<tQYQbOOO!dOpO'#DWO!iOSO'#D_O%^QcO'#DtO(WQcO'#EdOOQ`'#Er'#ErO(qQRO'#DuO*vQcO'#EbO+aQbO'#DUOOQa'#Dw'#DwO-fQbO'#DxOOQa'#Ed'#EdO-mQcO'#EdO/kQcO'#EcO0pQcO'#EbO0}QRO'#EOOOQ`'#Eb'#EbO1fQbO'#EbO1mQQO'#EaOOQ`'#Ea'#EaOOQ`'#EQ'#EQQYQbOOO1xQbO'#DZO2TQbO'#DnO2xQbO'#DRO3mQQO'#DzO2xQbO'#D|O3rObO,59rO3}QbO'#DaO4VQWO'#DbOOOO'#Ej'#EjOOOO'#EV'#EVO4kOSO,59yOOQa,59y,59yOOQ`'#DY'#DYO4yQbO'#DmOOQ`'#Eh'#EhOOQ`'#EY'#EYO5TQbO,5:[OOQa'#Ec'#EcO2xQbO,5:aO2xQbO,5:aO2xQbO,5:aO2xQbO,5:aO2xQbO,59oO2xQbO,59oO2xQbO,59oO2xQbO,59oOOQ`'#ES'#ESO+aQbO,59pO5}QcO'#DtO6UQcO'#EdO6]QRO,59pO6gQQO,59pO6lQQO,59pO6tQQO,59pO7PQRO,59pO7iQRO,59pO7pQQO'#DPO7uQbO,5:dO7|QQO,5:cOOQa,5:d,5:dO8XQbO,5:dO8cQbO,5:kO8cQbO,5:jO9jQbO,5:eO9qQbO,59kOOQ`,5:{,5:{O8cQbO'#EROOQ`-E8O-E8OOOQ`'#ET'#ETO:]QbO'#D[O:hQbO'#D]OOQO'#EU'#EUO:`QQO'#D[O:vQQO,59uO:{QcO'#EcO;xQRO'#EqO<uQRO'#EqOOQO'#Eq'#EqO<|QQO,5:YO=RQRO,59mO=YQRO,59mO9jQbO,5:fO=hQcO,5:hO>vQcO,5:hO?dQcO,5:hOOQa1G/^1G/^OOOO,59{,59{OOOO,59|,59|OOOO-E8T-E8TOOQa1G/e1G/eOOQ`,5:X,5:XOOQ`-E8W-E8WOOQa1G/{1G/{OA`QcO1G/{OAjQcO1G/{OBxQcO1G/{OCSQcO1G/{OCaQcO1G/{OOQa1G/Z1G/ZODrQcO1G/ZODyQcO1G/ZOEQQcO1G/ZOFPQcO1G/ZOEXQcO1G/ZOOQ`-E8Q-E8QOFgQRO1G/[OFqQQO1G/[OFvQQO1G/[OGOQQO1G/[OGZQRO1G/[OGbQRO1G/[OGiQbO,59qOGsQQO1G/[OOQa1G/[1G/[OG{QQO1G/}OOQa1G0O1G0OOHWQbO1G0OOOQO'#E['#E[OG{QQO1G/}OOQa1G/}1G/}OOQ`'#E]'#E]OHWQbO1G0OOHbQbO1G0VOH|QbO1G0UOIhQbO'#DhOIyQbO'#DhOJ^QbO1G0POOQ`-E8P-E8POOQ`,5:m,5:mOOQ`-E8R-E8ROJiQQO,59vOOQO,59w,59wOOQO-E8S-E8SOJqQbO1G/aO9jQbO1G/tO9jQbO1G/XOJxQbO1G0QOKTQQO7+$vOOQa7+$v7+$vOK]QQO1G/]OKeQQO7+%iOOQa7+%i7+%iOKpQbO7+%jOOQa7+%j7+%jOOQO-E8Y-E8YOOQ`-E8Z-E8ZOOQ`'#EW'#EWOKzQQO'#EWOLSQbO'#EpOOQ`,5:S,5:SOLgQbO'#DfOLlQQO'#DiOOQ`7+%k7+%kOLqQbO7+%kOLvQbO7+%kOMOQbO7+${OM^QbO7+${OMnQbO7+%`OMvQbO7+$sOOQ`7+%l7+%lOM{QbO7+%lONQQbO7+%lOOQa<<Hb<<HbONYQbO7+$wONgQQO7+$wOOQa<<IT<<ITOOQa<<IU<<IUOOQ`,5:r,5:rOOQ`-E8U-E8UONoQQO,5:QO9jQbO,5:TOOQ`<<IV<<IVONtQbO<<IVOOQ`<<Hg<<HgONyQbO<<HgO! OQbO<<HgO! WQbO<<HgOOQ`'#EZ'#EZO! cQbO<<HzO! kQbO'#DsOOQ`<<Hz<<HzO! sQbO<<HzOOQ`<<H_<<H_OOQ`<<IW<<IWO! xQbO<<IWOOQO,5:s,5:sO! }QbO<<HcOOQO-E8V-E8VO9jQbO1G/lOOQ`1G/o1G/oOOQ`AN>qAN>qOOQ`AN>RAN>RO!![QbOAN>RO!!aQbOAN>ROOQ`-E8X-E8XOOQ`AN>fAN>fO!!iQbOAN>fO2TQbO,5:]O9jQbO,5:_OOQ`AN>rAN>rPGiQbO'#ESOOQ`7+%W7+%WOOQ`G23mG23mO!!nQbOG23mP! nQbO'#DqOOQ`G24QG24QO!!sQQO1G/wOOQ`1G/y1G/yOOQ`LD)XLD)XO9jQbO7+%cOOQ`<<H}<<H}",
|
||||||
stateData: "Dk~O!xOSiOS~OdROe_OfZOgPOhfOnhOrgOuZO!PZO!QZO!aZO!fiO!hjO!}WO#RQO#YcO#^XO#_YO~O#PkO~O|nO#RqO#TlO#UmO~OdwOfZOgPOhfOuZOzsO!PZO!QZO!YrO!aZO!}WO#RQO#^XO#_YOT!{XU!{XW!{XX!{XY!{XZ!{X[!{X]!{X~OP!{XQ!{XR!{XS!{X^!{Xl!_X!R!_X#Y!_X#a!_X#]!_X!T!_X!W!_X!X!_X!]!_X~P!wOP!|XQ!|XR!|XS!|XT!|XU!|XW!|XX!|XY!|XZ!|X[!|X]!|X^!|Xl!|X#Y!|X#a!|X#]!|X!T!|X!W!|X!X!|X!]!|X~OdwOfZOgPOhfOuZOzsO!PZO!QZO!YrO!aZO!}WO#RQO#^XO#_YO!R!|X~P%_OPyOQyORzOSzOT|OU}OW{OX{OY{OZ{O[{O]{O^xO~Ol!zX#Y!zX#a!zX!T!zX!W!zX!X!zX#]!zX!]!zX~OPyOQyORzOSzO~P(pOdROe_OfZOgPOhfOnhOrgOuZO!PZO!QZO!aZO!fiO!hjO!}WO#RQO#^XO#_YO~OdwOfZOgPOuZOzsO!PZO!QZO!aZO!}WO#RQO#Y!UO#^XO#_YO~O#`!XO~P*sOV!ZO~P%_OP!{XQ!{XR!{XS!{XT!{XU!{XW!{XX!{XY!{XZ!{X[!{X]!{X^!{X~P(pOT|OU}O~P(pOV!ZO_![O`![Oa![Ob![Oc![O~O!R!]O~P(pOl!`O#Y!_O#a!_O~Od!bOz!dO!RxP~Od!hOfZOgPOuZO!PZO!QZO!aZO!}WO#RQO#^XO#_YO~OdwOfZOgPOuZO!PZO!QZO!aZO!}WO#RQO#^XO#_YO~O!R!oO~Od!sOu!sO!}WO~Od!tO!}WO~O#R!uO#T!uO#U!uO#V!uO#W!uO#X!uO~O|nO#R!wO#TlO#UmO~OhfO!Y!xO~P.{OhfOzsO!YrOlsa!Rsa#Ysa#asa#]sa!Tsa!Wsa!Xsa!]sa~P.{OPyOQyORzOSzO#]#SOl!zX~O!R!]O#]#SOl!zX~O#]#SOP!{XQ!{XR!{XS!{X^!{Xl!zX~P#sOT|OU}O#]#SOl!zX~Ol!`O~O#`#VO~P*sOzsO#Y#XO#`#ZO~O#Y#[O#`#VO~P.{O#Y#aO~P)lOl!`O#Yka#aka#]ka!Tka!Wka!Xka!]ka~Od!bOz!dO!RxX~Ou#gO!P#gO!Q#gO#RQO~O!R#iO~O!R!{X~P!wOT|OU}O!R#OX~OT|OU}OW{OX{OY{OZ{O[{O]{O~O!R#OX~P6QO!R#jO~O!R#kO~P6QOT|OU}O!R#kO~Ol!ga#Y!ga#a!ga!T!ga!W!ga!X!ga#]!ga!]!ga~P'uOl!ga#Y!ga#a!ga!T!ga!W!ga!X!ga#]!ga!]!ga~OPyOQyORzOSzO~P7xOT|OU}O~P7xO^xOR!`iS!`il!`i#Y!`i#a!`i#]!`i!T!`i!W!`i!X!`i!]!`i~OP!`iQ!`i~P9OOPyOQyO~P9OOPyOQyOR!`iS!`il!`i#Y!`i#a!`i#]!`i!T!`i!W!`i!X!`i!]!`i~OW{OX{OY{OZ{O[{O]{OToiloi#Yoi#aoi#]oi!Roi!Toi!Woi!Xoi!]oi~OU}O~P;POU}O~P;cOUoi~P;POzsO#Y#XO#`#nO~O#Y#[O#`#pO~P.{Ol!`O#Y!ji#a!ji!T!ji!W!ji!X!ji#]!ji!]!ji~Ol!`O#Y!ii#a!ii!T!ii!W!ii!X!ii#]!ii!]!ii~Ol!`O!T!UX!W!UX!X!UX!]!UX~O#Y#sO!T#ZP!W#ZP!X#ZP!]#ZP~P)lO!T#wO!W#xO!X#yO~Oz!dO!Rxa~O#Y#}O~P)lO!T#wO!W#xO!X$QO~OzsO#Y#XO#`$TO~O#Y#[O#`$UO~P.{Ol!`O#Y$VO~O#Y#sO!T#ZX!W#ZX!X#ZX!]#ZX~P)lOd$XO~O!R$YO~O!X$ZO~O!W#xO!X$ZO~Ol!`O!T#wO!W#xO!X$]O~O#Y#sO!T#ZP!W#ZP!X#ZP~P)lO!X$dO!]$cO~O!X$fO~O!X$gO~O!W#xO!X$gO~O!R$iO~O!X$kO~O!X$lO~O!W#xO!X$lO~O!T#wO!W#xO!X$lO~O!X$pO!]$cO~Or$rO!R$sO~O!X$pO~O!X$tO~O!X$vO~O!W#xO!X$vO~O!X$yO~O!X$|O~Or$rO~O!R$}O~Ou!a~",
|
stateData: "!!{~O#SOSqOS~OlROm_OnZOoPOpfOvhO{ZO!VZO!WZO!cgO!jZO!oiO!qjO#XWO#YcO#]QO#hXO#iYO~O#ZkO~O!SnO#]qO#_lO#`mO~OlwOnZOoPOpfO{ZO!QsO!VZO!WZO!`rO!jZO#XWO#]QO#hXO#iYOP#VXQ#VXR#VXS#VXT#VXU#VXW#VXX#VXY#VXZ#VX[#VX]#VX^#VXd#VXe#VXf#VXg#VXh#VXi#VXj#VXt!hX!X!hX#g!hX~O#Y!hX#k!hX!Z!hX!^!hX!_!hX!f!hX~P!wOlwOnZOoPOpfO{ZO!QsO!VZO!WZO!`rO!jZO#XWO#]QO#hXO#iYOP#WXQ#WXR#WXS#WXT#WXU#WXW#WXX#WXY#WXZ#WX[#WX]#WX^#WXd#WXe#WXf#WXg#WXh#WXi#WXj#WXt#WX#g#WX~O#Y#WX#k#WX!X#WX!Z#WX!^#WX!_#WX!f#WX~P%tOPyOQyORzOSzOT}OU!OOW|OX|OY|OZ|O[|O]|O^xOd{Oe{Of{Og{Oh{Oi{Oj!PO~OPyOQyORzOSzOd{Oe{Of{Og{Oh{Oi{Ot#UX~O#Y#UX#k#UX!Z#UX!^#UX!_#UX#g#UX!f#UX~P*ROl!SOm_OnZOoPOpfOvhO{ZO!VZO!WZO!cgO!jZO!oiO!qjO#XWO#Y!QO#]QO#hXO#iYO~OlwOnZOoPO{ZO!QsO!VZO!WZO!jZO#XWO#Y!QO#]QO#hXO#iYO~O#j!_O~P,kOV!aO#Y#WX#k#WX!Z#WX!^#WX!_#WX!f#WX~P&pOP#VXQ#VXR#VXS#VXT#VXU#VXW#VXX#VXY#VXZ#VX[#VX]#VX^#VXd#VXe#VXf#VXg#VXh#VXi#VXj#VXt#UX~O#Y#UX#k#UX!Z#UX!^#UX!_#UX#g#UX!f#UX~P.WOt#UX#Y#UX#k#UX!Z#UX!^#UX!_#UX#g#UX!f#UX~OT}OU!OOj!PO~P0UOV!aO_!bO`!bOa!bOb!bOc!bOk!bO~O!X!cO~P0UOt!fO#Y!eO#k!eO~Ol!hO!Q!jO!X!OP~Ol!nOnZOoPO{ZO!VZO!WZO!jZO#XWO#]QO#hXO#iYO~OlwOnZOoPO{ZO!VZO!WZO!jZO#XWO#]QO#hXO#iYO~O!X!uO~Ol!yO{!yO#XWO~Ol!zO#XWO~O#]!{O#_!{O#`!{O#a!{O#b!{O#c!{O~O!SnO#]!}O#_lO#`mO~OpfO!`#OO~P2xOpfO!QsO!`rOt!da!X!da#Y!da#k!da#g!da!Z!da!^!da!_!da!f!da~P2xO#Y!QO~P!wO#Y!QO~P%tO#Y!QO#g#gO~P*RO#g#gO~O#g#gOt#UX~O!X!cO#g#gOt#UX~O#g#gO~P.WOT}OU!OOj!PO#Y!QOt#UX~O#g#gO~P7WOt!fO~O#j#iO~P,kO!QsO#Y#kO#j#mO~O#Y#nO#j#iO~P2xOlROm_OnZOoPOpfOvhO{ZO!VZO!WZO!cgO!jZO!oiO!qjO#XWO#]QO#hXO#iYO~O#Y#sO~P8cOt!fO#Ysa#ksa#gsa!Zsa!^sa!_sa!fsa~Ol!hO!Q!jO!X!OX~O{#yO!V#yO!W#yO#]QO~O!X#{O~OpfO!QsO!`rOT#VXU#VXW#VXX#VXY#VXZ#VX[#VX]#VXj#VX!X#VX~P2xOT}OU!OOj!PO!X#eX~OT}OU!OOW|OX|OY|OZ|O[|O]|Oj!PO~O!X#eX~P<WO!X#|O~O!X#}O~P<WOT}OU!OOj!PO!X#}O~Ot!pa#Y!pa#k!pa!Z!pa!^!pa!_!pa#g!pa!f!pa~P(qOPyOQyORzOSzOd{Oe{Of{Og{Oh{Oi{O~Ot!pa#Y!pa#k!pa!Z!pa!^!pa!_!pa#g!pa!f!pa~P>UOT}OU!OOj!POt!pa#Y!pa#k!pa!Z!pa!^!pa!_!pa#g!pa!f!pa~O^xOR!iiS!iid!iie!iif!iig!iih!iii!iit!ii#Y!ii#k!ii#g!ii!Z!ii!^!ii!_!ii!f!ii~OP!iiQ!ii~P@XOPyOQyO~P@XOPyOQyOd!iie!iif!iig!iih!iii!iit!ii#Y!ii#k!ii#g!ii!Z!ii!^!ii!_!ii!f!ii~OR!iiS!ii~PAtORzOSzO^xO~PAtORzOSzO~PAtOW|OX|OY|OZ|O[|O]|OTwijwitwi#Ywi#kwi#gwi!Xwi!Zwi!^wi!_wi!fwi~OU!OO~PCkOU!OO~PC}OUwi~PCkOT}OU!OOjwitwi#Ywi#kwi#gwi!Xwi!Zwi!^wi!_wi!fwi~OW|OX|OY|OZ|O[|O]|O~PEXO#Y!QO#g$QO~P*RO#g$QO~O#g$QOt#UX~O!X!cO#g$QOt#UX~O#g$QO~P.WO#g$QO~P7WOpfO!`rO~P,kO#Y!QO#g$QO~O!QsO#Y#kO#j$TO~O#Y#nO#j$VO~P2xOt!fO#Y!si#k!si!Z!si!^!si!_!si#g!si!f!si~Ot!fO#Y!ri#k!ri!Z!ri!^!ri!_!ri#g!ri!f!ri~Ot!fO!Z![X!^![X!_![X!f![X~O#Y$YO!Z#dP!^#dP!_#dP!f#dP~P8cO!Z$^O!^$_O!_$`O~O!Q!jO!X!Oa~O#Y$dO~P8cO!Z$^O!^$_O!_$gO~O#Y!QO#g$jO~O#Y!QO#gyi~O!QsO#Y#kO#j$mO~O#Y#nO#j$nO~P2xOt!fO#Y$oO~O#Y$YO!Z#dX!^#dX!_#dX!f#dX~P8cOl$qO~O!X$rO~O!_$sO~O!^$_O!_$sO~Ot!fO!Z$^O!^$_O!_$uO~O#Y$YO!Z#dP!^#dP!_#dP~P8cO!_$|O!f${O~O!_%OO~O!_%PO~O!^$_O!_%PO~OpfO!`rO#gyq~P,kO#Y!QO#gyq~O!X%UO~O!_%WO~O!_%XO~O!^$_O!_%XO~O!Z$^O!^$_O!_%XO~O!_%]O!f${O~O!X%`O!c%_O~O!_%]O~O!_%aO~OpfO!`rO#gyy~P,kO!_%dO~O!^$_O!_%dO~O!_%gO~O!_%jO~O!X%kO~O{!j~",
|
||||||
goto: "4l#]PPPPPPPPPPPPPPPPPPPPPPPPPPP#^P#tP$Y%Q#^P&T&mP'l'r(c(fP(lP)j)jPPP)nP)z*dPPP*z+^P+b+h+|P,m-h#t#tP#tP#t#t.e.k.w/P/V/a/g/n/t/z0UPPP0`0d1W2oP3nP3tP3zPPPPPP4O4Ur`Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}R!PWu`OWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}r^Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}Q!SWS!ig$rQ!nhQ!rjQ#O}R#Q|xSOWeg!Z![!]!`!o#a#i#j#k#u#}$Y$i$r$s$}vZRSYhjsvxyz{|}!V!Y!h#W#]#oQ!skR!tltTOWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}T!kg$rtROWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}vwRSYhjsvxyz{|}!V!Y!h#W#]#oT!hg$rXtRSv!hr`Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}WrRSv!hQ!PWR!xsR!gfX!ef!c!f#f!pZORSWYeghjsvxyz{|}!V!Y!Z![!]!`!h!o#W#]#a#i#j#k#o#u#}$Y$i$r$s$}R#g!dTnQpQ#{#bQ$S#lQ$_#|R$n$`Q#b!]Q#l!oQ$O#jQ$P#kQ$j$YQ$u$iQ${$sR%O$}Q#z#bQ$R#lQ$[#{Q$^#|Q$h$SS$m$_$`R$w$nWtRSv!hQ!WYQ#U!VX#X!W#U#Y#mT$a$O$bQ$e$OR$q$buTOWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}rVOe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}Q!OWQ!qjQ!zyR!}z!qZORSWYeghjsvxyz{|}!V!Y!Z![!]!`!h!o#W#]#a#i#j#k#o#u#}$Y$i$r$s$}zZRSYghjsvxyz{|}!V!Y!h#W#]#o$ru[OWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}QeOR!ae^!^b!T#^#_#`#t#|R#c!^UvRS!hR!yvQ!cfR#e!cQ!ffQ#f!cT#h!f#fQpQR!vpS#u#a#}R$W#uQ$b$OR$o$bQ!VYR#T!VQ#Y!WQ#m#UT#q#Y#mQ#]!YQ#o#WT#r#]#oTdOeSbOeQ!TWQ#^!ZQ#_![`#`!]!o#j#k$Y$i$s$}Q#d!`U#t#a#u#}R#|#itUOWe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}WrRSv!hQ!YYS!jg$rQ!mhQ!pjQ!xsQ!zxQ!{yQ!|zQ#O{Q#P|Q#R}Q#W!VX#[!Y#W#]#or]Oe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}zwRSYghjsvxyz{|}!V!Y!h#W#]#o$rR!RWQ!lgR$z$rXuRSv!hToQpQ#v#aR$`#}raOe!Z![!]!`!o#a#i#j#k#u#}$Y$i$s$}R!QW",
|
goto: "8f#gPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP#hP$RP$h%f&t&zP(U(b)[)_P)eP*l*lPPP*pP*|+fPPP+|#hP,f-PP-T-Z-pP.g/k$R$RP$RP$R$R0q0w1T1w1}2X2_2f2l2v2|3WPPP3b3f4Z6PPPP7ZP7kPPPPP7o7u7{r`Oe!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kQ!WWR#a!Rw`OWe!R!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kr^Oe!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kQ!ZWS!og%_Q!thQ!xjQ#W!OQ#Y}Q#]!PR#d!RvSOeg!a!b!c!f!u#s#{#|#}$[$d$r%U%_%`%k!WZRSYhjsvxyz{|}!O!P!S!T!]!`!n#e#j#o$U$k%S%bS!TW!RQ!ykR!zlQ!VWR#`!RrROe!a!b!c!f!u#s#{#|#}$[$d$r%U%`%k!WwRSYhjsvxyz{|}!O!P!S!T!]!`!n#e#j#o$U$k%S%bS!SW!RT!ng%_etRSv!S!T!n#e$k%S%br`Oe!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kdrRSv!S!T!n#e$k%S%bQ!WWQ#OsR#a!RR!mfX!kf!i!l#x#SZORSWYeghjsvxyz{|}!O!P!R!S!T!]!`!a!b!c!f!n!u#e#j#o#s#{#|#}$U$[$d$k$r%S%U%_%`%b%kR#y!jTnQpQ$b#tQ$i$OQ$w$cR%Z$xQ#t!cQ$O!uQ$e#|Q$f#}Q%V$rQ%c%UQ%i%`R%l%kQ$a#tQ$h$OQ$t$bQ$v$cQ%Q$iS%Y$w$xR%e%ZdtRSv!S!T!n#e$k%S%bQ!^YQ#h!]X#k!^#h#l$SvTOWe!R!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kT!qg%_T$y$e$zQ$}$eR%^$zwTOWe!R!a!b!c!f!u#s#{#|#}$[$d$r%U%`%krVOe!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kQ!UWQ!wjQ#QyQ#TzQ#V{R#_!R#TZORSWYeghjsvxyz{|}!O!P!R!S!T!]!`!a!b!c!f!n!u#e#j#o#s#{#|#}$U$[$d$k$r%S%U%_%`%b%k![ZRSYghjsvxyz{|}!O!P!S!T!]!`!n#e#j#o$U$k%S%_%bw[OWe!R!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kQeOR!ge^!db![#p#q#r$Z$cR#u!dQ!RWQ!]Y`#^!R!]#e#f$P$k%S%bS#e!S!TS#f!U!ZS$P#_#dQ$k$RR%S$lQ!ifR#w!iQ!lfQ#x!iT#z!l#xQpQR!|pS$[#s$dR$p$[Q$l$RR%T$lYvRS!S!T!nR#PvQ$z$eR%[$zQ#l!^Q$S#hT$W#l$SQ#o!`Q$U#jT$X#o$UTdOeSbOeS![W!RQ#p!aQ#q!b`#r!c!u#|#}$r%U%`%kQ#v!fU$Z#s$[$dR$c#{vUOWe!R!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kdrRSv!S!T!n#e$k%S%bQ!`YS!pg%_Q!shQ!vjQ#OsQ#QxQ#RyQ#SzQ#U{Q#W|Q#X}Q#Z!OQ#[!PQ#j!]X#n!`#j#o$Ur]Oe!a!b!c!f!u#s#{#|#}$[$d$r%U%`%k![wRSYghjsvxyz{|}!O!P!S!T!]!`!n#e#j#o$U$k%S%_%bQ!YWR#c!R[uRSv!S!T!nQ$R#eV%R$k%S%bToQpQ$]#sR$x$dQ!rgR%h%_raOe!a!b!c!f!u#s#{#|#}$[$d$r%U%`%kQ!XWR#b!R",
|
||||||
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo PlusEq MinusEq StarEq SlashEq ModuloEq Identifier AssignableIdentifier Word IdentifierBeforeDot Do Comment Program PipeExpr operator WhileExpr keyword ConditionalOp ParenExpr IfExpr keyword FunctionCall DotGet Number PositionalArg FunctionDef Params NamedParam NamedArgPrefix String StringFragment Interpolation EscapeSeq Boolean Null colon CatchExpr keyword Block FinallyExpr keyword keyword Underscore NamedArg ElseIfExpr keyword ElseExpr FunctionCallOrIdentifier BinOp Regex Dict Array FunctionCallWithBlock TryExpr keyword Throw keyword CompoundAssign Assign",
|
nodeNames: "⚠ Star Slash Plus Minus And Or Eq EqEq Neq Lt Lte Gt Gte Modulo PlusEq MinusEq StarEq SlashEq ModuloEq Band Bor Bxor Shl Shr Ushr NullishCoalesce NullishEq Identifier AssignableIdentifier Word IdentifierBeforeDot Do Comment Program PipeExpr operator WhileExpr keyword ConditionalOp ParenExpr FunctionCall DotGet Number PositionalArg FunctionDef Params NamedParam NamedArgPrefix String StringFragment Interpolation EscapeSeq Boolean Null colon CatchExpr keyword Block FinallyExpr keyword keyword Underscore NamedArg IfExpr keyword FunctionCall ElseIfExpr keyword ElseExpr FunctionCallOrIdentifier BinOp Regex Dict Array FunctionCallWithBlock TryExpr keyword Throw keyword CompoundAssign Assign",
|
||||||
maxTerm: 109,
|
maxTerm: 119,
|
||||||
context: trackScope,
|
context: trackScope,
|
||||||
nodeProps: [
|
nodeProps: [
|
||||||
["closedBy", 49,"end"]
|
["closedBy", 55,"end"]
|
||||||
],
|
],
|
||||||
propSources: [highlighting],
|
propSources: [highlighting],
|
||||||
skippedNodes: [0,25],
|
skippedNodes: [0,33],
|
||||||
repeatNodeCount: 11,
|
repeatNodeCount: 12,
|
||||||
tokenData: "C_~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'Vuw#{wx'[xy'ayz'zz{#{{|(e|}#{}!O+X!O!P#{!P!Q-n!Q![)S![!]6Z!]!^%T!^!}#{!}#O6t#O#P8j#P#Q8o#Q#R#{#R#S9Y#S#T#{#T#Y,Y#Y#Z9s#Z#b,Y#b#c>q#c#f,Y#f#g?n#g#h,Y#h#i@k#i#o,Y#o#p#{#p#qBo#q;'S#{;'S;=`$d<%l~#{~O#{~~CYS$QU|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qU|S!xYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[U|S#YQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZiY|SOY%nYZ#{Zt%ntu&huw%nwx&hx#O%n#O#P&h#P;'S%n;'S;=`'P<%lO%nY&mSiYOY&hZ;'S&h;'S;=`&y<%lO&hY&|P;=`<%l&h^'SP;=`<%l%n~'[O#T~~'aO#R~U'hU|S!}QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RU|S#]QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jW|SOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)ZY|SuQOt#{uw#{x!O#{!O!P)y!P!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U*OW|SOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U*oW|SuQOt#{uw#{x!Q#{!Q![*h![#O#{#P;'S#{;'S;=`$d<%lO#{U+^^|SOt#{uw#{x}#{}!O,Y!O!Q#{!Q![)S![!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{U,_[|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{U-[UzQ|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U-sW|SOt#{uw#{x!P#{!P!Q.]!Q#O#{#P;'S#{;'S;=`$d<%lO#{U.b^|SOY/^YZ#{Zt/^tu0auw/^wx0ax!P/^!P!Q#{!Q!}/^!}#O5S#O#P2o#P;'S/^;'S;=`6T<%lO/^U/e^|S!aQOY/^YZ#{Zt/^tu0auw/^wx0ax!P/^!P!Q3U!Q!}/^!}#O5S#O#P2o#P;'S/^;'S;=`6T<%lO/^Q0fX!aQOY0aZ!P0a!P!Q1R!Q!}0a!}#O1p#O#P2o#P;'S0a;'S;=`3O<%lO0aQ1UP!P!Q1XQ1^U!aQ#Z#[1X#]#^1X#a#b1X#g#h1X#i#j1X#m#n1XQ1sVOY1pZ#O1p#O#P2Y#P#Q0a#Q;'S1p;'S;=`2i<%lO1pQ2]SOY1pZ;'S1p;'S;=`2i<%lO1pQ2lP;=`<%l1pQ2rSOY0aZ;'S0a;'S;=`3O<%lO0aQ3RP;=`<%l0aU3ZW|SOt#{uw#{x!P#{!P!Q3s!Q#O#{#P;'S#{;'S;=`$d<%lO#{U3zb|S!aQOt#{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;=`$d<%lO#{U5X[|SOY5SYZ#{Zt5Stu1puw5Swx1px#O5S#O#P2Y#P#Q/^#Q;'S5S;'S;=`5}<%lO5SU6QP;=`<%l5SU6WP;=`<%l/^U6bU|S!RQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U6{W#_Q|SOt#{uw#{x!_#{!_!`7e!`#O#{#P;'S#{;'S;=`$d<%lO#{U7jV|SOt#{uw#{x#O#{#P#Q8P#Q;'S#{;'S;=`$d<%lO#{U8WU#^Q|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~8oO#U~U8vU#`Q|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9aU|S!YQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9x]|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#U:q#U#o,Y#o;'S#{;'S;=`$d<%lO#{U:v^|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#`,Y#`#a;r#a#o,Y#o;'S#{;'S;=`$d<%lO#{U;w^|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#g,Y#g#h<s#h#o,Y#o;'S#{;'S;=`$d<%lO#{U<x^|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#X,Y#X#Y=t#Y#o,Y#o;'S#{;'S;=`$d<%lO#{U={[!PQ|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^>x[#VW|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^?u[#XW|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#o,Y#o;'S#{;'S;=`$d<%lO#{^@r^#WW|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#f,Y#f#gAn#g#o,Y#o;'S#{;'S;=`$d<%lO#{UAs^|SOt#{uw#{x}#{}!O,Y!O!_#{!_!`-T!`#O#{#P#T#{#T#i,Y#i#j<s#j#o,Y#o;'S#{;'S;=`$d<%lO#{UBvUlQ|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~C_O#a~",
|
tokenData: "IS~R}OX$OXY$mYZ%WZp$Opq$mqs$Ost%qtu'Yuw$Owx'_xy'dyz'}z{$O{|(h|}$O}!O(h!O!P$O!P!Q0o!Q!R)Y!R![+w![!]9[!]!^%W!^!}$O!}#O9u#O#P;k#P#Q;p#Q#R$O#R#S<Z#S#T$O#T#Y<t#Y#Z>`#Z#b<t#b#cC|#c#f<t#f#gEP#g#h<t#h#iFS#i#o<t#o#p$O#p#qHd#q;'S$O;'S;=`$g<%l~$O~O$O~~H}S$TU!SSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OS$jP;=`<%l$O^$tU!SS#SYOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU%_U!SS#YQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O^%xZqY!SSOY%qYZ$OZt%qtu&kuw%qwx&kx#O%q#O#P&k#P;'S%q;'S;=`'S<%lO%qY&pSqYOY&kZ;'S&k;'S;=`&|<%lO&kY'PP;=`<%l&k^'VP;=`<%l%q~'_O#_~~'dO#]~U'kU!SS#XQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU(UU!SS#gQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU(mX!SSOt$Ouw$Ox!Q$O!Q!R)Y!R![+w![#O$O#P;'S$O;'S;=`$g<%lO$OU)a`!SS{QOt$Ouw$Ox!O$O!O!P*c!P!Q$O!Q![+w![#O$O#P#R$O#R#S,t#S#U$O#U#V-c#V#l$O#l#m.w#m;'S$O;'S;=`$g<%lO$OU*hW!SSOt$Ouw$Ox!Q$O!Q![+Q![#O$O#P;'S$O;'S;=`$g<%lO$OU+XY!SS{QOt$Ouw$Ox!Q$O!Q![+Q![#O$O#P#R$O#R#S*c#S;'S$O;'S;=`$g<%lO$OU,O[!SS{QOt$Ouw$Ox!O$O!O!P*c!P!Q$O!Q![+w![#O$O#P#R$O#R#S,t#S;'S$O;'S;=`$g<%lO$OU,yW!SSOt$Ouw$Ox!Q$O!Q![+w![#O$O#P;'S$O;'S;=`$g<%lO$OU-hX!SSOt$Ouw$Ox!Q$O!Q!R.T!R!S.T!S#O$O#P;'S$O;'S;=`$g<%lO$OU.[X!SS{QOt$Ouw$Ox!Q$O!Q!R.T!R!S.T!S#O$O#P;'S$O;'S;=`$g<%lO$OU.|[!SSOt$Ouw$Ox!Q$O!Q![/r![!c$O!c!i/r!i#O$O#P#T$O#T#Z/r#Z;'S$O;'S;=`$g<%lO$OU/y[!SS{QOt$Ouw$Ox!Q$O!Q![/r![!c$O!c!i/r!i#O$O#P#T$O#T#Z/r#Z;'S$O;'S;=`$g<%lO$OU0tW!SSOt$Ouw$Ox!P$O!P!Q1^!Q#O$O#P;'S$O;'S;=`$g<%lO$OU1c^!SSOY2_YZ$OZt2_tu3buw2_wx3bx!P2_!P!Q$O!Q!}2_!}#O8T#O#P5p#P;'S2_;'S;=`9U<%lO2_U2f^!SS!jQOY2_YZ$OZt2_tu3buw2_wx3bx!P2_!P!Q6V!Q!}2_!}#O8T#O#P5p#P;'S2_;'S;=`9U<%lO2_Q3gX!jQOY3bZ!P3b!P!Q4S!Q!}3b!}#O4q#O#P5p#P;'S3b;'S;=`6P<%lO3bQ4VP!P!Q4YQ4_U!jQ#Z#[4Y#]#^4Y#a#b4Y#g#h4Y#i#j4Y#m#n4YQ4tVOY4qZ#O4q#O#P5Z#P#Q3b#Q;'S4q;'S;=`5j<%lO4qQ5^SOY4qZ;'S4q;'S;=`5j<%lO4qQ5mP;=`<%l4qQ5sSOY3bZ;'S3b;'S;=`6P<%lO3bQ6SP;=`<%l3bU6[W!SSOt$Ouw$Ox!P$O!P!Q6t!Q#O$O#P;'S$O;'S;=`$g<%lO$OU6{b!SS!jQOt$Ouw$Ox#O$O#P#Z$O#Z#[6t#[#]$O#]#^6t#^#a$O#a#b6t#b#g$O#g#h6t#h#i$O#i#j6t#j#m$O#m#n6t#n;'S$O;'S;=`$g<%lO$OU8Y[!SSOY8TYZ$OZt8Ttu4quw8Twx4qx#O8T#O#P5Z#P#Q2_#Q;'S8T;'S;=`9O<%lO8TU9RP;=`<%l8TU9XP;=`<%l2_U9cU!SS!XQOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU9|W#iQ!SSOt$Ouw$Ox!_$O!_!`:f!`#O$O#P;'S$O;'S;=`$g<%lO$OU:kV!SSOt$Ouw$Ox#O$O#P#Q;Q#Q;'S$O;'S;=`$g<%lO$OU;XU#hQ!SSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~;pO#`~U;wU#jQ!SSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU<bU!SS!`QOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU<y^!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#o<t#o;'S$O;'S;=`$g<%lO$OU=|U!QQ!SSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$OU>e_!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#U?d#U#o<t#o;'S$O;'S;=`$g<%lO$OU?i`!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#`<t#`#a@k#a#o<t#o;'S$O;'S;=`$g<%lO$OU@p`!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#g<t#g#hAr#h#o<t#o;'S$O;'S;=`$g<%lO$OUAw`!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#X<t#X#YBy#Y#o<t#o;'S$O;'S;=`$g<%lO$OUCQ^!VQ!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#o<t#o;'S$O;'S;=`$g<%lO$O^DT^#aW!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#o<t#o;'S$O;'S;=`$g<%lO$O^EW^#cW!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#o<t#o;'S$O;'S;=`$g<%lO$O^FZ`#bW!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#f<t#f#gG]#g#o<t#o;'S$O;'S;=`$g<%lO$OUGb`!SSOt$Ouw$Ox}$O}!O<t!O!Q$O!Q![<t![!_$O!_!`=u!`#O$O#P#T$O#T#i<t#i#jAr#j#o<t#o;'S$O;'S;=`$g<%lO$OUHkUtQ!SSOt$Ouw$Ox#O$O#P;'S$O;'S;=`$g<%lO$O~ISO#k~",
|
||||||
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#P~~", 11)],
|
tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#Z~~", 11)],
|
||||||
topRules: {"Program":[0,26]},
|
topRules: {"Program":[0,34]},
|
||||||
specialized: [{term: 20, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
specialized: [{term: 28, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 28, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
|
||||||
tokenPrec: 1634
|
tokenPrec: 2202
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,118 @@ describe('Parentheses', () => {
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Number 3`)
|
Number 3`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('function call with named args on multiple lines in parens', () => {
|
||||||
|
expect(`(tail
|
||||||
|
arg1=true
|
||||||
|
arg2=30
|
||||||
|
)`).toMatchTree(`
|
||||||
|
ParenExpr
|
||||||
|
FunctionCall
|
||||||
|
Identifier tail
|
||||||
|
NamedArg
|
||||||
|
NamedArgPrefix arg1=
|
||||||
|
Boolean true
|
||||||
|
NamedArg
|
||||||
|
NamedArgPrefix arg2=
|
||||||
|
Number 30
|
||||||
|
`)
|
||||||
|
|
||||||
|
expect(`(
|
||||||
|
tail
|
||||||
|
arg1=true
|
||||||
|
arg2=30
|
||||||
|
)`).toMatchTree(`
|
||||||
|
ParenExpr
|
||||||
|
FunctionCall
|
||||||
|
Identifier tail
|
||||||
|
NamedArg
|
||||||
|
NamedArgPrefix arg1=
|
||||||
|
Boolean true
|
||||||
|
NamedArg
|
||||||
|
NamedArgPrefix arg2=
|
||||||
|
Number 30
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('binop with newlines in parens', () => {
|
||||||
|
expect(`(
|
||||||
|
1 + 2
|
||||||
|
)`).toMatchTree(`
|
||||||
|
ParenExpr
|
||||||
|
BinOp
|
||||||
|
Number 1
|
||||||
|
Plus +
|
||||||
|
Number 2`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('comparison with newlines in parens', () => {
|
||||||
|
expect(`(
|
||||||
|
1 < 2
|
||||||
|
)`).toMatchTree(`
|
||||||
|
ParenExpr
|
||||||
|
ConditionalOp
|
||||||
|
Number 1
|
||||||
|
Lt <
|
||||||
|
Number 2`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('function call with multiple identifiers on separate lines in parens', () => {
|
||||||
|
expect(`(echo
|
||||||
|
arg1
|
||||||
|
arg2
|
||||||
|
arg3
|
||||||
|
)`).toMatchTree(`
|
||||||
|
ParenExpr
|
||||||
|
FunctionCall
|
||||||
|
Identifier echo
|
||||||
|
PositionalArg
|
||||||
|
Identifier arg1
|
||||||
|
PositionalArg
|
||||||
|
Identifier arg2
|
||||||
|
PositionalArg
|
||||||
|
Identifier arg3`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Number literals', () => {
|
||||||
|
test('allows underscores in integer literals', () => {
|
||||||
|
expect('10_000').toMatchTree(`Number 10_000`)
|
||||||
|
expect('1_000_000').toMatchTree(`Number 1_000_000`)
|
||||||
|
expect('100_000').toMatchTree(`Number 100_000`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows underscores in decimal literals', () => {
|
||||||
|
expect('3.14_159').toMatchTree(`Number 3.14_159`)
|
||||||
|
expect('1_000.50').toMatchTree(`Number 1_000.50`)
|
||||||
|
expect('0.000_001').toMatchTree(`Number 0.000_001`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows underscores in negative numbers', () => {
|
||||||
|
expect('-10_000').toMatchTree(`Number -10_000`)
|
||||||
|
expect('-3.14_159').toMatchTree(`Number -3.14_159`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('allows underscores in positive numbers with explicit sign', () => {
|
||||||
|
expect('+10_000').toMatchTree(`Number +10_000`)
|
||||||
|
expect('+3.14_159').toMatchTree(`Number +3.14_159`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('works in expressions', () => {
|
||||||
|
expect('1_000 + 2_000').toMatchTree(`
|
||||||
|
BinOp
|
||||||
|
Number 1_000
|
||||||
|
Plus +
|
||||||
|
Number 2_000`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('works in function calls', () => {
|
||||||
|
expect('echo 10_000').toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier echo
|
||||||
|
PositionalArg
|
||||||
|
Number 10_000`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('BinOp', () => {
|
describe('BinOp', () => {
|
||||||
|
|
@ -595,6 +707,87 @@ describe('CompoundAssign', () => {
|
||||||
PositionalArg
|
PositionalArg
|
||||||
Number 3`)
|
Number 3`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('parses ??= operator', () => {
|
||||||
|
expect('x ??= 5').toMatchTree(`
|
||||||
|
CompoundAssign
|
||||||
|
AssignableIdentifier x
|
||||||
|
NullishEq ??=
|
||||||
|
Number 5`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses ??= with expression', () => {
|
||||||
|
expect('config ??= get-default').toMatchTree(`
|
||||||
|
CompoundAssign
|
||||||
|
AssignableIdentifier config
|
||||||
|
NullishEq ??=
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier get-default`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Nullish coalescing operator', () => {
|
||||||
|
test('? can still end an identifier', () => {
|
||||||
|
expect('what?').toMatchTree(`
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier what?`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('?? can still end an identifier', () => {
|
||||||
|
expect('what??').toMatchTree(`
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier what??`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('?? can still be in a word', () => {
|
||||||
|
expect('what??the').toMatchTree(`
|
||||||
|
FunctionCallOrIdentifier
|
||||||
|
Identifier what??the`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('?? can still start a word', () => {
|
||||||
|
expect('??what??the').toMatchTree(`
|
||||||
|
Word ??what??the`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses ?? operator', () => {
|
||||||
|
expect('x ?? 5').toMatchTree(`
|
||||||
|
ConditionalOp
|
||||||
|
Identifier x
|
||||||
|
NullishCoalesce ??
|
||||||
|
Number 5`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses chained ?? operators', () => {
|
||||||
|
expect('a ?? b ?? c').toMatchTree(`
|
||||||
|
ConditionalOp
|
||||||
|
ConditionalOp
|
||||||
|
Identifier a
|
||||||
|
NullishCoalesce ??
|
||||||
|
Identifier b
|
||||||
|
NullishCoalesce ??
|
||||||
|
Identifier c`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses ?? with expressions', () => {
|
||||||
|
expect('get-value ?? default-value').toMatchTree(`
|
||||||
|
ConditionalOp
|
||||||
|
Identifier get-value
|
||||||
|
NullishCoalesce ??
|
||||||
|
Identifier default-value`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses ?? with parenthesized function call', () => {
|
||||||
|
expect('get-value ?? (default 10)').toMatchTree(`
|
||||||
|
ConditionalOp
|
||||||
|
Identifier get-value
|
||||||
|
NullishCoalesce ??
|
||||||
|
ParenExpr
|
||||||
|
FunctionCall
|
||||||
|
Identifier default
|
||||||
|
PositionalArg
|
||||||
|
Number 10`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('DotGet whitespace sensitivity', () => {
|
describe('DotGet whitespace sensitivity', () => {
|
||||||
|
|
|
||||||
72
src/parser/tests/bitwise.test.ts
Normal file
72
src/parser/tests/bitwise.test.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { expect, describe, test } from 'bun:test'
|
||||||
|
|
||||||
|
import '../shrimp.grammar' // Importing this so changes cause it to retest!
|
||||||
|
|
||||||
|
describe('bitwise operators - grammar', () => {
|
||||||
|
test('parses band (bitwise AND)', () => {
|
||||||
|
expect('5 band 3').toMatchTree(`
|
||||||
|
BinOp
|
||||||
|
Number 5
|
||||||
|
Band band
|
||||||
|
Number 3`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses bor (bitwise OR)', () => {
|
||||||
|
expect('5 bor 3').toMatchTree(`
|
||||||
|
BinOp
|
||||||
|
Number 5
|
||||||
|
Bor bor
|
||||||
|
Number 3`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses bxor (bitwise XOR)', () => {
|
||||||
|
expect('5 bxor 3').toMatchTree(`
|
||||||
|
BinOp
|
||||||
|
Number 5
|
||||||
|
Bxor bxor
|
||||||
|
Number 3`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses << (left shift)', () => {
|
||||||
|
expect('5 << 2').toMatchTree(`
|
||||||
|
BinOp
|
||||||
|
Number 5
|
||||||
|
Shl <<
|
||||||
|
Number 2`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses >> (signed right shift)', () => {
|
||||||
|
expect('20 >> 2').toMatchTree(`
|
||||||
|
BinOp
|
||||||
|
Number 20
|
||||||
|
Shr >>
|
||||||
|
Number 2`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses >>> (unsigned right shift)', () => {
|
||||||
|
expect('-1 >>> 1').toMatchTree(`
|
||||||
|
BinOp
|
||||||
|
Number -1
|
||||||
|
Ushr >>>
|
||||||
|
Number 1`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('parses bnot (bitwise NOT) as function call', () => {
|
||||||
|
expect('bnot 5').toMatchTree(`
|
||||||
|
FunctionCall
|
||||||
|
Identifier bnot
|
||||||
|
PositionalArg
|
||||||
|
Number 5`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('bitwise operators work in expressions', () => {
|
||||||
|
expect('x = 5 band 3').toMatchTree(`
|
||||||
|
Assign
|
||||||
|
AssignableIdentifier x
|
||||||
|
Eq =
|
||||||
|
BinOp
|
||||||
|
Number 5
|
||||||
|
Band band
|
||||||
|
Number 3`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -2,6 +2,65 @@ import { expect, describe, test } from 'bun:test'
|
||||||
|
|
||||||
import '../shrimp.grammar' // Importing this so changes cause it to retest!
|
import '../shrimp.grammar' // Importing this so changes cause it to retest!
|
||||||
|
|
||||||
|
describe('number literals', () => {
|
||||||
|
test('binary numbers', () => {
|
||||||
|
expect('0b110').toMatchTree(`
|
||||||
|
Number 0b110
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hex numbers', () => {
|
||||||
|
expect('0xdeadbeef').toMatchTree(`
|
||||||
|
Number 0xdeadbeef
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hex numbers uppercase', () => {
|
||||||
|
expect('0xFF').toMatchTree(`
|
||||||
|
Number 0xFF
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('decimal numbers still work', () => {
|
||||||
|
expect('42').toMatchTree(`
|
||||||
|
Number 42
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('negative binary', () => {
|
||||||
|
expect('-0b110').toMatchTree(`
|
||||||
|
Number -0b110
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('negative hex', () => {
|
||||||
|
expect('-0xFF').toMatchTree(`
|
||||||
|
Number -0xFF
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('positive prefix binary', () => {
|
||||||
|
expect('+0b110').toMatchTree(`
|
||||||
|
Number +0b110
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('positive prefix hex', () => {
|
||||||
|
expect('+0xFF').toMatchTree(`
|
||||||
|
Number +0xFF
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('hex and binary in arrays', () => {
|
||||||
|
expect('[0xFF 0b110 42]').toMatchTree(`
|
||||||
|
Array
|
||||||
|
Number 0xFF
|
||||||
|
Number 0b110
|
||||||
|
Number 42
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('array literals', () => {
|
describe('array literals', () => {
|
||||||
test('work with numbers', () => {
|
test('work with numbers', () => {
|
||||||
expect('[1 2 3]').toMatchTree(`
|
expect('[1 2 3]').toMatchTree(`
|
||||||
|
|
|
||||||
|
|
@ -193,6 +193,15 @@ const chooseIdentifierToken = (input: InputStream, stack: Stack): number => {
|
||||||
|
|
||||||
const nextCh = getFullCodePoint(input, peekPos)
|
const nextCh = getFullCodePoint(input, peekPos)
|
||||||
const nextCh2 = getFullCodePoint(input, peekPos + 1)
|
const nextCh2 = getFullCodePoint(input, peekPos + 1)
|
||||||
|
const nextCh3 = getFullCodePoint(input, peekPos + 2)
|
||||||
|
|
||||||
|
// Check for ??= (three-character compound operator)
|
||||||
|
if (nextCh === 63 /* ? */ && nextCh2 === 63 /* ? */ && nextCh3 === 61 /* = */) {
|
||||||
|
const charAfterOp = getFullCodePoint(input, peekPos + 3)
|
||||||
|
if (isWhiteSpace(charAfterOp) || charAfterOp === -1 /* EOF */) {
|
||||||
|
return AssignableIdentifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for compound assignment operators: +=, -=, *=, /=, %=
|
// Check for compound assignment operators: +=, -=, *=, /=, %=
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
} from 'reefvm'
|
} from 'reefvm'
|
||||||
|
|
||||||
import { dict } from './dict'
|
import { dict } from './dict'
|
||||||
|
import { json } from './json'
|
||||||
import { load } from './load'
|
import { load } from './load'
|
||||||
import { list } from './list'
|
import { list } from './list'
|
||||||
import { math } from './math'
|
import { math } from './math'
|
||||||
|
|
@ -13,6 +14,7 @@ import { str } from './str'
|
||||||
|
|
||||||
export const globals = {
|
export const globals = {
|
||||||
dict,
|
dict,
|
||||||
|
json,
|
||||||
load,
|
load,
|
||||||
list,
|
list,
|
||||||
math,
|
math,
|
||||||
|
|
@ -40,6 +42,7 @@ export const globals = {
|
||||||
'var?': function (this: VM, v: string) {
|
'var?': function (this: VM, v: string) {
|
||||||
return typeof v !== 'string' || this.scope.has(v)
|
return typeof v !== 'string' || this.scope.has(v)
|
||||||
},
|
},
|
||||||
|
ref: (fn: Function) => fn,
|
||||||
|
|
||||||
// env
|
// env
|
||||||
args: Bun.argv.slice(1),
|
args: Bun.argv.slice(1),
|
||||||
|
|
@ -60,6 +63,7 @@ export const globals = {
|
||||||
|
|
||||||
// boolean/logic
|
// boolean/logic
|
||||||
not: (v: any) => !v,
|
not: (v: any) => !v,
|
||||||
|
bnot: (n: number) => ~(n | 0),
|
||||||
|
|
||||||
// utilities
|
// utilities
|
||||||
inc: (n: number) => n + 1,
|
inc: (n: number) => n + 1,
|
||||||
|
|
|
||||||
7
src/prelude/json.ts
Normal file
7
src/prelude/json.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
export const json = {
|
||||||
|
encode: (s: any) => JSON.stringify(s),
|
||||||
|
decode: (s: string) => JSON.parse(s),
|
||||||
|
}
|
||||||
|
|
||||||
|
; (json as any).parse = json.decode
|
||||||
|
; (json as any).stringify = json.encode
|
||||||
|
|
@ -73,6 +73,13 @@ export const list = {
|
||||||
const realItems = items.map(item => item.value)
|
const realItems = items.map(item => item.value)
|
||||||
return toValue(realList.splice(realStart, realDeleteCount, ...realItems))
|
return toValue(realList.splice(realStart, realDeleteCount, ...realItems))
|
||||||
},
|
},
|
||||||
|
insert: (list: Value, index: Value, item: Value) => {
|
||||||
|
if (list.type !== 'array') return toNull()
|
||||||
|
const realList = list.value as any[]
|
||||||
|
const realIndex = index.value as number
|
||||||
|
realList.splice(realIndex, 0, item)
|
||||||
|
return toValue(realList.length)
|
||||||
|
},
|
||||||
|
|
||||||
// sequence operations
|
// sequence operations
|
||||||
reverse: (list: any[]) => list.slice().reverse(),
|
reverse: (list: any[]) => list.slice().reverse(),
|
||||||
|
|
@ -142,4 +149,5 @@ export const list = {
|
||||||
; (list.push as any).raw = true
|
; (list.push as any).raw = true
|
||||||
; (list.pop as any).raw = true
|
; (list.pop as any).raw = true
|
||||||
; (list.shift as any).raw = true
|
; (list.shift as any).raw = true
|
||||||
; (list.unshift as any).raw = true
|
; (list.unshift as any).raw = true
|
||||||
|
; (list.insert as any).raw = true
|
||||||
|
|
@ -91,3 +91,15 @@ describe('environment', () => {
|
||||||
await expect(`list.first args | str.ends-with? 'shrimp.test.ts'`).toEvaluateTo(true)
|
await expect(`list.first args | str.ends-with? 'shrimp.test.ts'`).toEvaluateTo(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('ref', () => {
|
||||||
|
expect(`rnd = do x: true end; rnd | type`).toEvaluateTo('boolean')
|
||||||
|
expect(`rnd = do x: true end; ref rnd | type`).toEvaluateTo('function')
|
||||||
|
|
||||||
|
expect(`math.random | type`).toEvaluateTo('number')
|
||||||
|
expect(`ref math.random | type`).toEvaluateTo('native')
|
||||||
|
|
||||||
|
expect(`rnd = math.random; rnd | type`).toEvaluateTo('number')
|
||||||
|
expect(`rnd = ref math.random; rnd | type`).toEvaluateTo('number')
|
||||||
|
expect(`rnd = ref math.random; ref rnd | type`).toEvaluateTo('native')
|
||||||
|
})
|
||||||
|
|
|
||||||
84
src/prelude/tests/json.test.ts
Normal file
84
src/prelude/tests/json.test.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { expect, describe, test } from 'bun:test'
|
||||||
|
|
||||||
|
describe('json', () => {
|
||||||
|
test('json.decode', () => {
|
||||||
|
expect(`json.decode '[1,2,3]'`).toEvaluateTo([1, 2, 3])
|
||||||
|
expect(`json.decode '"heya"'`).toEvaluateTo('heya')
|
||||||
|
expect(`json.decode '[true, false, null]'`).toEvaluateTo([true, false, null])
|
||||||
|
expect(`json.decode '{"a": true, "b": false, "c": "yeah"}'`).toEvaluateTo({ a: true, b: false, c: "yeah" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('json.encode', () => {
|
||||||
|
expect(`json.encode [1 2 3]`).toEvaluateTo('[1,2,3]')
|
||||||
|
expect(`json.encode 'heya'`).toEvaluateTo('"heya"')
|
||||||
|
expect(`json.encode [true false null]`).toEvaluateTo('[true,false,null]')
|
||||||
|
expect(`json.encode [a=true b=false c='yeah'] | json.decode`).toEvaluateTo({ a: true, b: false, c: "yeah" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('edge cases - empty structures', () => {
|
||||||
|
expect(`json.decode '[]'`).toEvaluateTo([])
|
||||||
|
expect(`json.decode '{}'`).toEvaluateTo({})
|
||||||
|
expect(`json.encode []`).toEvaluateTo('[]')
|
||||||
|
expect(`json.encode [=]`).toEvaluateTo('{}')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('edge cases - special characters in strings', () => {
|
||||||
|
expect(`json.decode '"hello\\\\nworld"'`).toEvaluateTo('hello\nworld')
|
||||||
|
expect(`json.decode '"tab\\\\there"'`).toEvaluateTo('tab\there')
|
||||||
|
expect(`json.decode '"forward/slash"'`).toEvaluateTo('forward/slash')
|
||||||
|
expect(`json.decode '"with\\\\\\\\backslash"'`).toEvaluateTo('with\\backslash')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('numbers - integers and floats', () => {
|
||||||
|
expect(`json.decode '42'`).toEvaluateTo(42)
|
||||||
|
expect(`json.decode '0'`).toEvaluateTo(0)
|
||||||
|
expect(`json.decode '-17'`).toEvaluateTo(-17)
|
||||||
|
expect(`json.decode '3.14159'`).toEvaluateTo(3.14159)
|
||||||
|
expect(`json.decode '-0.5'`).toEvaluateTo(-0.5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('numbers - scientific notation', () => {
|
||||||
|
expect(`json.decode '1e10'`).toEvaluateTo(1e10)
|
||||||
|
expect(`json.decode '2.5e-3'`).toEvaluateTo(2.5e-3)
|
||||||
|
expect(`json.decode '1.23E+5'`).toEvaluateTo(1.23e5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('unicode - emoji and special characters', () => {
|
||||||
|
expect(`json.decode '"hello 👋"'`).toEvaluateTo('hello 👋')
|
||||||
|
expect(`json.decode '"🎉🚀✨"'`).toEvaluateTo('🎉🚀✨')
|
||||||
|
expect(`json.encode '你好'`).toEvaluateTo('"你好"')
|
||||||
|
expect(`json.encode 'café'`).toEvaluateTo('"café"')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested structures - arrays', () => {
|
||||||
|
expect(`json.decode '[[1,2],[3,4],[5,6]]'`).toEvaluateTo([[1, 2], [3, 4], [5, 6]])
|
||||||
|
expect(`json.decode '[1,[2,[3,[4]]]]'`).toEvaluateTo([1, [2, [3, [4]]]])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested structures - objects', () => {
|
||||||
|
expect(`json.decode '{"user":{"name":"Alice","age":30}}'`).toEvaluateTo({
|
||||||
|
user: { name: 'Alice', age: 30 }
|
||||||
|
})
|
||||||
|
expect(`json.decode '{"a":{"b":{"c":"deep"}}}'`).toEvaluateTo({
|
||||||
|
a: { b: { c: 'deep' } }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('nested structures - mixed arrays and objects', () => {
|
||||||
|
expect(`json.decode '[{"id":1,"tags":["a","b"]},{"id":2,"tags":["c"]}]'`).toEvaluateTo([
|
||||||
|
{ id: 1, tags: ['a', 'b'] },
|
||||||
|
{ id: 2, tags: ['c'] }
|
||||||
|
])
|
||||||
|
expect(`json.decode '{"items":[1,2,3],"meta":{"count":3}}'`).toEvaluateTo({
|
||||||
|
items: [1, 2, 3],
|
||||||
|
meta: { count: 3 }
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('error handling - invalid json', () => {
|
||||||
|
expect(`json.decode '{invalid}'`).toFailEvaluation()
|
||||||
|
expect(`json.decode '[1,2,3'`).toFailEvaluation()
|
||||||
|
expect(`json.decode 'undefined'`).toFailEvaluation()
|
||||||
|
expect(`json.decode ''`).toFailEvaluation()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -349,6 +349,22 @@ describe('collections', () => {
|
||||||
await expect(`arr = [1 2 3 4 5]; list.splice arr 3 2; arr`).toEvaluateTo([1, 2, 3])
|
await expect(`arr = [1 2 3 4 5]; list.splice arr 3 2; arr`).toEvaluateTo([1, 2, 3])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('list.insert adds element at index and mutates array', async () => {
|
||||||
|
await expect(`arr = [1 2 4 5]; list.insert arr 2 3; arr`).toEvaluateTo([1, 2, 3, 4, 5])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('list.insert returns array length', async () => {
|
||||||
|
await expect(`list.insert [1 2 4] 2 3`).toEvaluateTo(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('list.insert at start', async () => {
|
||||||
|
await expect(`arr = [2 3]; list.insert arr 0 1; arr`).toEvaluateTo([1, 2, 3])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('list.insert at end', async () => {
|
||||||
|
await expect(`arr = [1 2]; list.insert arr 2 99; arr`).toEvaluateTo([1, 2, 99])
|
||||||
|
})
|
||||||
|
|
||||||
test('list.sort with no callback sorts ascending', async () => {
|
test('list.sort with no callback sorts ascending', async () => {
|
||||||
await expect(`list.sort [3 1 4 1 5] null`).toEvaluateTo([1, 1, 3, 4, 5])
|
await expect(`list.sort [3 1 4 1 5] null`).toEvaluateTo([1, 1, 3, 4, 5])
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user