From 3c6d0913c0b16f346017685065950ab3cd75d3dd Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 7 Nov 2025 22:42:26 -0800 Subject: [PATCH] bitwise operators --- src/compiler/compiler.ts | 18 +++ src/compiler/tests/bitwise.test.ts | 178 +++++++++++++++++++++++++++++ src/parser/operatorTokenizer.ts | 6 + src/parser/shrimp.grammar | 11 +- src/parser/shrimp.terms.ts | 96 ++++++++-------- src/parser/shrimp.ts | 26 ++--- src/parser/tests/bitwise.test.ts | 72 ++++++++++++ src/prelude/index.ts | 1 + 8 files changed, 348 insertions(+), 60 deletions(-) create mode 100644 src/compiler/tests/bitwise.test.ts create mode 100644 src/parser/tests/bitwise.test.ts diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 9ce7bce..2d693e1 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -232,6 +232,24 @@ export class Compiler { case '%': instructions.push(['MOD']) 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: throw new CompilerError(`Unsupported binary operator: ${opValue}`, op.from, op.to) } diff --git a/src/compiler/tests/bitwise.test.ts b/src/compiler/tests/bitwise.test.ts new file mode 100644 index 0000000..0d39a52 --- /dev/null +++ b/src/compiler/tests/bitwise.test.ts @@ -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 + }) + }) +}) diff --git a/src/parser/operatorTokenizer.ts b/src/parser/operatorTokenizer.ts index 3c85400..c909543 100644 --- a/src/parser/operatorTokenizer.ts +++ b/src/parser/operatorTokenizer.ts @@ -5,6 +5,12 @@ type Operator = { str: string; tokenName: keyof typeof terms } const operators: Array = [ { str: 'and', tokenName: 'And' }, { 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: 'Lte' }, { str: '!=', tokenName: 'Neq' }, diff --git a/src/parser/shrimp.grammar b/src/parser/shrimp.grammar index 97908d9..a4e22b0 100644 --- a/src/parser/shrimp.grammar +++ b/src/parser/shrimp.grammar @@ -6,7 +6,7 @@ @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 } @tokens { @precedence { Number Regex } @@ -45,6 +45,7 @@ null { @specialize[@name=Null] } or @left, and @left, comparison @left, + bitwise @left, multiplicative @left, additive @left, call @@ -184,7 +185,13 @@ BinOp { (expression | BinOp) !multiplicative Star (expression | BinOp) | (expression | BinOp) !multiplicative Slash (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 { diff --git a/src/parser/shrimp.terms.ts b/src/parser/shrimp.terms.ts index 04cb710..9c80715 100644 --- a/src/parser/shrimp.terms.ts +++ b/src/parser/shrimp.terms.ts @@ -19,48 +19,54 @@ export const StarEq = 17, SlashEq = 18, ModuloEq = 19, - Identifier = 20, - AssignableIdentifier = 21, - Word = 22, - IdentifierBeforeDot = 23, - Do = 24, - Comment = 25, - Program = 26, - PipeExpr = 27, - WhileExpr = 29, - keyword = 70, - ConditionalOp = 31, - ParenExpr = 32, - IfExpr = 33, - FunctionCall = 35, - DotGet = 36, - Number = 37, - PositionalArg = 38, - FunctionDef = 39, - Params = 40, - NamedParam = 41, - NamedArgPrefix = 42, - String = 43, - StringFragment = 44, - Interpolation = 45, - EscapeSeq = 46, - Boolean = 47, - Null = 48, - colon = 49, - CatchExpr = 50, - Block = 52, - FinallyExpr = 53, - Underscore = 56, - NamedArg = 57, - ElseIfExpr = 58, - ElseExpr = 60, - FunctionCallOrIdentifier = 61, - BinOp = 62, - Regex = 63, - Dict = 64, - Array = 65, - FunctionCallWithBlock = 66, - TryExpr = 67, - Throw = 69, - CompoundAssign = 71, - Assign = 72 + Band = 20, + Bor = 21, + Bxor = 22, + Shl = 23, + Shr = 24, + Ushr = 25, + Identifier = 26, + AssignableIdentifier = 27, + Word = 28, + IdentifierBeforeDot = 29, + Do = 30, + Comment = 31, + Program = 32, + PipeExpr = 33, + WhileExpr = 35, + keyword = 76, + ConditionalOp = 37, + ParenExpr = 38, + IfExpr = 39, + FunctionCall = 41, + DotGet = 42, + Number = 43, + PositionalArg = 44, + FunctionDef = 45, + Params = 46, + NamedParam = 47, + NamedArgPrefix = 48, + String = 49, + StringFragment = 50, + Interpolation = 51, + EscapeSeq = 52, + Boolean = 53, + Null = 54, + colon = 55, + CatchExpr = 56, + Block = 58, + FinallyExpr = 59, + Underscore = 62, + NamedArg = 63, + ElseIfExpr = 64, + ElseExpr = 66, + FunctionCallOrIdentifier = 67, + BinOp = 68, + Regex = 69, + Dict = 70, + Array = 71, + FunctionCallWithBlock = 72, + TryExpr = 73, + Throw = 75, + CompoundAssign = 77, + Assign = 78 diff --git a/src/parser/shrimp.ts b/src/parser/shrimp.ts index fa41719..8a7b29c 100644 --- a/src/parser/shrimp.ts +++ b/src/parser/shrimp.ts @@ -4,24 +4,24 @@ import {operatorTokenizer} from "./operatorTokenizer" import {tokenizer, specializeKeyword} from "./tokenizer" import {trackScope} from "./parserScopeContext" 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:72, if:80, null:108, catch:114, finally:120, end:122, else:130, try:148, throw:152} export const parser = LRParser.deserialize({ 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/ROVQbO'#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<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`<jQcO1G/xO=`QcO1G/xO>qQcO1G/xOOQa1G/X1G/XOASQcO1G/XOAZQcO1G/XOAbQcO1G/XOOQa1G/Y1G/YOOQ`-E8T-E8TOAiQQO1G/zOOQa1G/{1G/{OAtQbO1G/{OOQO'#EW'#EWOAiQQO1G/zOOQa1G/z1G/zOOQ`'#EX'#EXOAtQbO1G/{OBOQbO1G0SOBjQbO1G0ROCUQbO'#DhOCgQbO'#DhOCzQbO1G/|OOQ`-E7|-E7|OOQ`,5:j,5:jOOQ`-E8O-E8OODVQQO,59vOOQO,59w,59wOOQO-E8P-E8POD_QbO1G/aO6xQbO1G/ZO6xQbO1G/VODfQbO1G/}ODqQQO7+%fOOQa7+%f7+%fOD|QbO7+%gOOQa7+%g7+%gOOQO-E8U-E8UOOQ`-E8V-E8VOOQ`'#ET'#ETOEWQQO'#ETOE`QbO'#EmOOQ`,5:S,5:SOEsQbO'#DfOExQQO'#DiOOQ`7+%h7+%hOE}QbO7+%hOFSQbO7+%hOF[QbO7+${OFjQbO7+${OFzQbO7+$uOGSQbO7+$qOOQ`7+%i7+%iOGXQbO7+%iOG^QbO7+%iOOQa<nAN>nOOQ`AN>RAN>ROHtQbOAN>ROHyQbOAN>ROOQ`-E8S-E8SOOQ`AN={AN={OIRQbOAN={O1[QbO,5:YO6xQbO,5:[OOQ`AN>oAN>oOOQ`7+%W7+%WOOQ`G23mG23mOIWQbOG23mPI]QbO'#DnOOQ`G23gG23gOIbQQO1G/tOOQ`1G/v1G/vOOQ`LD)XLD)XO6xQbO7+%`OOQ`<c#Y#o,w#o;'S#{;'S;=`$d<%lO#{U>j[!PQ|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^?g[#VW|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^@d[#XW|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^Aa^#WW|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#f,w#f#gB]#g#o,w#o;'S#{;'S;=`$d<%lO#{UBb^|SOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#i,w#i#j=b#j#o,w#o;'S#{;'S;=`$d<%lO#{UCeUlQ|SOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~C|O#a~", - tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#P~~", 11)], - topRules: {"Program":[0,26]}, - 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}], - tokenPrec: 1634 + tokenData: "C|~R|OX#{XY$jYZ%TZp#{pq$jqs#{st%ntu'tuw#{wx'yxy(Oyz(iz{#{{|)S|}#{}!O+v!O!P#{!P!Q.]!Q![)q![!]6x!]!^%T!^!}#{!}#O7c#O#P9X#P#Q9^#Q#R#{#R#S9w#S#T#{#T#Y,w#Y#Z:b#Z#b,w#b#c?`#c#f,w#f#g@]#g#h,w#h#iAY#i#o,w#o#p#{#p#qC^#q;'S#{;'S;=`$d<%l~#{~O#{~~CwS$QU!SSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{S$gP;=`<%l#{^$qU!SS#OYOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U%[U!SS#`QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^%sW!SSOp#{pq&]qt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{^&dZoY!SSOY&]YZ#{Zt&]tu'Vuw&]wx'Vx#O&]#O#P'V#P;'S&];'S;=`'n<%lO&]Y'[SoYOY'VZ;'S'V;'S;=`'h<%lO'VY'kP;=`<%l'V^'qP;=`<%l&]~'yO#Z~~(OO#X~U(VU!SS#TQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U(pU!SS#cQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U)XW!SSOt#{uw#{x!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U)xY!SS{QOt#{uw#{x!O#{!O!P*h!P!Q#{!Q![)q![#O#{#P;'S#{;'S;=`$d<%lO#{U*mW!SSOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+^W!SS{QOt#{uw#{x!Q#{!Q![+V![#O#{#P;'S#{;'S;=`$d<%lO#{U+{^!SSOt#{uw#{x}#{}!O,w!O!Q#{!Q![)q![!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U,|[!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{U-yU!QQ!SSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U.bW!SSOt#{uw#{x!P#{!P!Q.z!Q#O#{#P;'S#{;'S;=`$d<%lO#{U/P^!SSOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q#{!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{U0S^!SS!gQOY/{YZ#{Zt/{tu1Ouw/{wx1Ox!P/{!P!Q3s!Q!}/{!}#O5q#O#P3^#P;'S/{;'S;=`6r<%lO/{Q1TX!gQOY1OZ!P1O!P!Q1p!Q!}1O!}#O2_#O#P3^#P;'S1O;'S;=`3m<%lO1OQ1sP!P!Q1vQ1{U!gQ#Z#[1v#]#^1v#a#b1v#g#h1v#i#j1v#m#n1vQ2bVOY2_Z#O2_#O#P2w#P#Q1O#Q;'S2_;'S;=`3W<%lO2_Q2zSOY2_Z;'S2_;'S;=`3W<%lO2_Q3ZP;=`<%l2_Q3aSOY1OZ;'S1O;'S;=`3m<%lO1OQ3pP;=`<%l1OU3xW!SSOt#{uw#{x!P#{!P!Q4b!Q#O#{#P;'S#{;'S;=`$d<%lO#{U4ib!SS!gQOt#{uw#{x#O#{#P#Z#{#Z#[4b#[#]#{#]#^4b#^#a#{#a#b4b#b#g#{#g#h4b#h#i#{#i#j4b#j#m#{#m#n4b#n;'S#{;'S;=`$d<%lO#{U5v[!SSOY5qYZ#{Zt5qtu2_uw5qwx2_x#O5q#O#P2w#P#Q/{#Q;'S5q;'S;=`6l<%lO5qU6oP;=`<%l5qU6uP;=`<%l/{U7PU!SS!XQOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U7jW#eQ!SSOt#{uw#{x!_#{!_!`8S!`#O#{#P;'S#{;'S;=`$d<%lO#{U8XV!SSOt#{uw#{x#O#{#P#Q8n#Q;'S#{;'S;=`$d<%lO#{U8uU#dQ!SSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~9^O#[~U9eU#fQ!SSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:OU!SS!`QOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{U:g]!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#U;`#U#o,w#o;'S#{;'S;=`$d<%lO#{U;e^!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#`,w#`#ac#Y#o,w#o;'S#{;'S;=`$d<%lO#{U>j[!VQ!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^?g[#]W!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^@d[#_W!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#o,w#o;'S#{;'S;=`$d<%lO#{^Aa^#^W!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#f,w#f#gB]#g#o,w#o;'S#{;'S;=`$d<%lO#{UBb^!SSOt#{uw#{x}#{}!O,w!O!_#{!_!`-r!`#O#{#P#T#{#T#i,w#i#j=b#j#o,w#o;'S#{;'S;=`$d<%lO#{UCeUrQ!SSOt#{uw#{x#O#{#P;'S#{;'S;=`$d<%lO#{~C|O#g~", + tokenizers: [operatorTokenizer, 1, 2, 3, tokenizer, new LocalTokenGroup("[~RP!O!PU~ZO#V~~", 11)], + topRules: {"Program":[0,32]}, + specialized: [{term: 26, get: (value: any, stack: any) => (specializeKeyword(value, stack) << 1), external: specializeKeyword},{term: 26, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}], + tokenPrec: 1863 }) diff --git a/src/parser/tests/bitwise.test.ts b/src/parser/tests/bitwise.test.ts new file mode 100644 index 0000000..5ccc8ed --- /dev/null +++ b/src/parser/tests/bitwise.test.ts @@ -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`) + }) +}) diff --git a/src/prelude/index.ts b/src/prelude/index.ts index 488414b..4771cfd 100644 --- a/src/prelude/index.ts +++ b/src/prelude/index.ts @@ -56,6 +56,7 @@ export const globals = { // boolean/logic not: (v: any) => !v, + bnot: (n: number) => ~(n | 0), // utilities inc: (n: number) => n + 1,