diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 408bdc2..a5327ab 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -106,11 +106,21 @@ export class Compiler { switch (node.type.id) { case terms.Number: - const number = Number(value) - if (Number.isNaN(number)) + // Handle sign prefix for hex and binary literals + // 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) - return [[`PUSH`, number]] + return [[`PUSH`, numberValue]] case terms.String: { const { parts, hasInterpolation } = getStringParts(node, input) diff --git a/src/compiler/tests/literals.test.ts b/src/compiler/tests/literals.test.ts index c1dc14b..45f9fba 100644 --- a/src/compiler/tests/literals.test.ts +++ b/src/compiler/tests/literals.test.ts @@ -1,6 +1,44 @@ import { describe } 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', () => { test('work with numbers', () => { expect('[1 2 3]').toEvaluateTo([1, 2, 3]) diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index cbc14b5..c021018 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -13,7 +13,11 @@ StringFragment { !['\\$]+ } NamedArgPrefix { $[a-z] $[a-z0-9-]* "=" } - Number { ("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)? } + Number { + ("-" | "+")? "0x" $[0-9a-fA-F]+ | + ("-" | "+")? "0b" $[01]+ | + ("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)? + } Boolean { "true" | "false" } newlineOrSemicolon { "\n" | ";" } eof { @eof } diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index 9920125..fdab5b4 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -19,7 +19,7 @@ export const parser = LRParser.deserialize({ propSources: [highlighting], skippedNodes: [0,25], repeatNodeCount: 12, - tokenData: "Cx~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'Vuw#{wx'[xy'ayz'zz{#{{|(e|}#{}!O(e!O!P#{!P!Q+e!Q![)S![!]4Q!]!^%T!^!}#{!}#O4k#O#P6a#P#Q6f#Q#R#{#R#S7P#S#T#{#T#Y7j#Y#Z9U#Z#b7j#b#c>r#c#f7j#f#g?u#g#h7j#h#i@x#i#o7j#o#p#{#p#qCY#q;'S#{;'S;=`$d<%l~#{~O#{~~CsS$QUzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qUzS!zYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[UzS#QQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%uZiYzSOY%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#V~~'aO#T~U'hUzS#PQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(RUzS#_QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(jWzSOt#{uw#{x!Q#{!Q![)S![#O#{#P;'S#{;'S;=`$d<%lO#{U)Z[zSsQOt#{uw#{x!O#{!O!P*P!P!Q#{!Q![)S![#O#{#P#R#{#R#S(e#S;'S#{;'S;=`$d<%lO#{U*UWzSOt#{uw#{x!Q#{!Q![*n![#O#{#P;'S#{;'S;=`$d<%lO#{U*uYzSsQOt#{uw#{x!Q#{!Q![*n![#O#{#P#R#{#R#S*P#S;'S#{;'S;=`$d<%lO#{U+jWzSOt#{uw#{x!P#{!P!Q,S!Q#O#{#P;'S#{;'S;=`$d<%lO#{U,X^zSOY-TYZ#{Zt-Ttu.Wuw-Twx.Wx!P-T!P!Q#{!Q!}-T!}#O2y#O#P0f#P;'S-T;'S;=`3z<%lO-TU-[^zS!bQOY-TYZ#{Zt-Ttu.Wuw-Twx.Wx!P-T!P!Q0{!Q!}-T!}#O2y#O#P0f#P;'S-T;'S;=`3z<%lO-TQ.]X!bQOY.WZ!P.W!P!Q.x!Q!}.W!}#O/g#O#P0f#P;'S.W;'S;=`0u<%lO.WQ.{P!P!Q/OQ/TU!bQ#Z#[/O#]#^/O#a#b/O#g#h/O#i#j/O#m#n/OQ/jVOY/gZ#O/g#O#P0P#P#Q.W#Q;'S/g;'S;=`0`<%lO/gQ0SSOY/gZ;'S/g;'S;=`0`<%lO/gQ0cP;=`<%l/gQ0iSOY.WZ;'S.W;'S;=`0u<%lO.WQ0xP;=`<%l.WU1QWzSOt#{uw#{x!P#{!P!Q1j!Q#O#{#P;'S#{;'S;=`$d<%lO#{U1qbzS!bQOt#{uw#{x#O#{#P#Z#{#Z#[1j#[#]#{#]#^1j#^#a#{#a#b1j#b#g#{#g#h1j#h#i#{#i#j1j#j#m#{#m#n1j#n;'S#{;'S;=`$d<%lO#{U3O[zSOY2yYZ#{Zt2ytu/guw2ywx/gx#O2y#O#P0P#P#Q-T#Q;'S2y;'S;=`3t<%lO2yU3wP;=`<%l2yU3}P;=`<%l-TU4XUzS!PQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U4rW#aQzSOt#{uw#{x!_#{!_!`5[!`#O#{#P;'S#{;'S;=`$d<%lO#{U5aVzSOt#{uw#{x#O#{#P#Q5v#Q;'S#{;'S;=`$d<%lO#{U5}U#`QzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~6fO#W~U6mU#bQzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7WUzS!WQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7o^zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#o7j#o;'S#{;'S;=`$d<%lO#{U8rUxQzSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U9Z_zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#U:Y#U#o7j#o;'S#{;'S;=`$d<%lO#{U:_`zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#`7j#`#a;a#a#o7j#o;'S#{;'S;=`$d<%lO#{U;f`zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#g7j#g#hy^#XWzSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#o7j#o;'S#{;'S;=`$d<%lO#{^?|^#ZWzSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#o7j#o;'S#{;'S;=`$d<%lO#{^AP`#YWzSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#f7j#f#gBR#g#o7j#o;'S#{;'S;=`$d<%lO#{UBW`zSOt#{uw#{x}#{}!O7j!O!Q#{!Q![7j![!_#{!_!`8k!`#O#{#P#T#{#T#i7j#i#j`#Z#be_zSOt$Ouw$Ox}$O}!O (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 20, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], diff --git a/src/parser/tests/literals.test.ts b/src/parser/tests/literals.test.ts index 3a63381..fe1b4ca 100644 --- a/src/parser/tests/literals.test.ts +++ b/src/parser/tests/literals.test.ts @@ -2,6 +2,65 @@ import { expect, describe, test } from 'bun:test' 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', () => { test('work with numbers', () => { expect('[1 2 3]').toMatchTree(`