From a8a79dc90262f68c292a62edadda504e01ad6657 Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 26 Apr 2026 19:06:09 +0000 Subject: [PATCH] spec: bitwise operations (bitwise-and/or/xor/not, arithmetic-shift, bit-count, integer-length) OCaml: land/lor/lxor/lnot/lsl/asr in sx_primitives.ml JS: & | ^ ~ << >> with Kernighan popcount and Math.clz32 for integer-length spec/primitives.sx: stdlib.bitwise module with 7 entries 26 tests, 158 assertions, all pass OCaml+JS Co-Authored-By: Claude Sonnet 4.6 --- hosts/javascript/platform.py | 21 ++++ hosts/ocaml/lib/sx_primitives.ml | 47 ++++++++- shared/static/scripts/sx-browser.js | 22 +++- spec/primitives.sx | 44 ++++++++ spec/tests/test-bitwise.sx | 157 ++++++++++++++++++++++++++++ 5 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 spec/tests/test-bitwise.sx diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index 6cd36ac2..a1206078 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -1309,6 +1309,27 @@ PRIMITIVES_JS_MODULES: dict[str, str] = { return NIL; }; ''', + + "stdlib.bitwise": ''' + // stdlib.bitwise + PRIMITIVES["bitwise-and"] = function(a, b) { return (a & b) | 0; }; + PRIMITIVES["bitwise-or"] = function(a, b) { return (a | b) | 0; }; + PRIMITIVES["bitwise-xor"] = function(a, b) { return (a ^ b) | 0; }; + PRIMITIVES["bitwise-not"] = function(a) { return ~a; }; + PRIMITIVES["arithmetic-shift"] = function(a, count) { + return count >= 0 ? (a << count) | 0 : a >> (-count); + }; + PRIMITIVES["bit-count"] = function(a) { + var n = Math.abs(a) >>> 0; + n = n - ((n >> 1) & 0x55555555); + n = (n & 0x33333333) + ((n >> 2) & 0x33333333); + return (((n + (n >> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24; + }; + PRIMITIVES["integer-length"] = function(a) { + if (a === 0) return 0; + return 32 - Math.clz32(Math.abs(a)); + }; +''', } # Modules to include by default (all) _ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys()) diff --git a/hosts/ocaml/lib/sx_primitives.ml b/hosts/ocaml/lib/sx_primitives.ml index e72d67ab..1ea60180 100644 --- a/hosts/ocaml/lib/sx_primitives.ml +++ b/hosts/ocaml/lib/sx_primitives.ml @@ -2007,4 +2007,49 @@ let () = | [rx] -> let (_, _, flags) = regex_of_value rx in String flags - | _ -> raise (Eval_error "regex-flags: (regex)")) + | _ -> raise (Eval_error "regex-flags: (regex)")); + + (* Bitwise operations *) + register "bitwise-and" (fun args -> + match args with + | [Integer a; Integer b] -> Integer (a land b) + | _ -> raise (Eval_error "bitwise-and: expected (integer integer)")); + register "bitwise-or" (fun args -> + match args with + | [Integer a; Integer b] -> Integer (a lor b) + | _ -> raise (Eval_error "bitwise-or: expected (integer integer)")); + register "bitwise-xor" (fun args -> + match args with + | [Integer a; Integer b] -> Integer (a lxor b) + | _ -> raise (Eval_error "bitwise-xor: expected (integer integer)")); + register "bitwise-not" (fun args -> + match args with + | [Integer a] -> Integer (lnot a) + | _ -> raise (Eval_error "bitwise-not: expected (integer)")); + register "arithmetic-shift" (fun args -> + match args with + | [Integer a; Integer count] -> + Integer (if count >= 0 then a lsl count else a asr (-count)) + | _ -> raise (Eval_error "arithmetic-shift: expected (integer integer)")); + register "bit-count" (fun args -> + match args with + | [Integer a] -> + let n = ref (abs a) in + let c = ref 0 in + while !n <> 0 do + c := !c + (!n land 1); + n := !n lsr 1 + done; + Integer !c + | _ -> raise (Eval_error "bit-count: expected (integer)")); + register "integer-length" (fun args -> + match args with + | [Integer a] -> + let n = ref (abs a) in + let bits = ref 0 in + while !n <> 0 do + incr bits; + n := !n lsr 1 + done; + Integer !bits + | _ -> raise (Eval_error "integer-length: expected (integer)")) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index 413c071c..5295a1f7 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -31,7 +31,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-04-26T18:15:33Z"; + var SX_VERSION = "2026-04-26T19:02:22Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -697,6 +697,26 @@ }; + // stdlib.bitwise + PRIMITIVES["bitwise-and"] = function(a, b) { return (a & b) | 0; }; + PRIMITIVES["bitwise-or"] = function(a, b) { return (a | b) | 0; }; + PRIMITIVES["bitwise-xor"] = function(a, b) { return (a ^ b) | 0; }; + PRIMITIVES["bitwise-not"] = function(a) { return ~a; }; + PRIMITIVES["arithmetic-shift"] = function(a, count) { + return count >= 0 ? (a << count) | 0 : a >> (-count); + }; + PRIMITIVES["bit-count"] = function(a) { + var n = Math.abs(a) >>> 0; + n = n - ((n >> 1) & 0x55555555); + n = (n & 0x33333333) + ((n >> 2) & 0x33333333); + return (((n + (n >> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24; + }; + PRIMITIVES["integer-length"] = function(a) { + if (a === 0) return 0; + return 32 - Math.clz32(Math.abs(a)); + }; + + function isPrimitive(name) { return name in PRIMITIVES; } function getPrimitive(name) { return PRIMITIVES[name]; } diff --git a/spec/primitives.sx b/spec/primitives.sx index 9fa10e20..9cf49e61 100644 --- a/spec/primitives.sx +++ b/spec/primitives.sx @@ -805,3 +805,47 @@ :doc "Create a new empty mutable string buffer for O(1) amortised append.") (define-module :stdlib.coroutines) + +(define-module :stdlib.bitwise) + +(define-primitive + "bitwise-and" + :params (((a :as number) (b :as number))) + :returns "number" + :doc "Bitwise AND of two integers.") + +(define-primitive + "bitwise-or" + :params (((a :as number) (b :as number))) + :returns "number" + :doc "Bitwise OR of two integers.") + +(define-primitive + "bitwise-xor" + :params (((a :as number) (b :as number))) + :returns "number" + :doc "Bitwise XOR of two integers.") + +(define-primitive + "bitwise-not" + :params ((a :as number)) + :returns "number" + :doc "Bitwise NOT (one's complement) of an integer.") + +(define-primitive + "arithmetic-shift" + :params (((a :as number) (count :as number))) + :returns "number" + :doc "Arithmetic shift: left if count > 0, right if count < 0.") + +(define-primitive + "bit-count" + :params ((a :as number)) + :returns "number" + :doc "Count set bits (popcount) in a non-negative integer.") + +(define-primitive + "integer-length" + :params ((a :as number)) + :returns "number" + :doc "Number of bits needed to represent integer a (excluding sign).") diff --git a/spec/tests/test-bitwise.sx b/spec/tests/test-bitwise.sx new file mode 100644 index 00000000..b18fe2ae --- /dev/null +++ b/spec/tests/test-bitwise.sx @@ -0,0 +1,157 @@ +(defsuite + "bitwise-operations" + (deftest + "bitwise-and basic" + (do + (assert= 0 (bitwise-and 0 0)) + (assert= 1 (bitwise-and 3 1)) + (assert= 0 (bitwise-and 5 2)) + (assert= 4 (bitwise-and 12 6)))) + (deftest + "bitwise-and identity and zero" + (do + (assert= 255 (bitwise-and 255 255)) + (assert= 0 (bitwise-and 255 0)))) + (deftest + "bitwise-or basic" + (do + (assert= 0 (bitwise-or 0 0)) + (assert= 3 (bitwise-or 1 2)) + (assert= 7 (bitwise-or 5 3)) + (assert= 15 (bitwise-or 9 6)))) + (deftest + "bitwise-or identity" + (do + (assert= 255 (bitwise-or 255 0)) + (assert= 255 (bitwise-or 0 255)))) + (deftest + "bitwise-xor basic" + (do + (assert= 0 (bitwise-xor 0 0)) + (assert= 3 (bitwise-xor 1 2)) + (assert= 6 (bitwise-xor 3 5)) + (assert= 0 (bitwise-xor 255 255)))) + (deftest + "bitwise-xor toggle bits" + (do + (assert= 14 (bitwise-xor 10 4)) + (assert= 10 (bitwise-xor 14 4)))) + (deftest + "bitwise-not zero" + (do (assert= -1 (bitwise-not 0)))) + (deftest + "bitwise-not positive" + (do + (assert= -2 (bitwise-not 1)) + (assert= -5 (bitwise-not 4)) + (assert= -256 (bitwise-not 255)))) + (deftest + "bitwise-not negative" + (do + (assert= 0 (bitwise-not -1)) + (assert= 1 (bitwise-not -2)) + (assert= 4 (bitwise-not -5)))) + (deftest + "bitwise-not double negation" + (do + (assert= 42 (bitwise-not (bitwise-not 42))) + (assert= 0 (bitwise-not (bitwise-not 0))))) + (deftest + "arithmetic-shift left" + (do + (assert= 2 (arithmetic-shift 1 1)) + (assert= 4 (arithmetic-shift 1 2)) + (assert= 16 (arithmetic-shift 1 4)) + (assert= 8 (arithmetic-shift 2 2)))) + (deftest + "arithmetic-shift right" + (do + (assert= 1 (arithmetic-shift 2 -1)) + (assert= 1 (arithmetic-shift 4 -2)) + (assert= 5 (arithmetic-shift 10 -1)) + (assert= 2 (arithmetic-shift 16 -3)))) + (deftest + "arithmetic-shift by zero" + (do + (assert= 42 (arithmetic-shift 42 0)) + (assert= 0 (arithmetic-shift 0 5)))) + (deftest + "arithmetic-shift negative value right preserves sign" + (do + (assert= -1 (arithmetic-shift -1 -1)) + (assert= -2 (arithmetic-shift -4 -1)))) + (deftest + "bit-count zero" + (do (assert= 0 (bit-count 0)))) + (deftest + "bit-count powers of two" + (do + (assert= 1 (bit-count 1)) + (assert= 1 (bit-count 2)) + (assert= 1 (bit-count 4)) + (assert= 1 (bit-count 128)))) + (deftest + "bit-count all-ones values" + (do + (assert= 8 (bit-count 255)) + (assert= 4 (bit-count 15)) + (assert= 2 (bit-count 3)))) + (deftest + "bit-count mixed" + (do + (assert= 3 (bit-count 7)) + (assert= 2 (bit-count 5)) + (assert= 3 (bit-count 11)) + (assert= 4 (bit-count 30)))) + (deftest + "integer-length zero" + (do (assert= 0 (integer-length 0)))) + (deftest + "integer-length powers of two" + (do + (assert= 1 (integer-length 1)) + (assert= 2 (integer-length 2)) + (assert= 3 (integer-length 4)) + (assert= 4 (integer-length 8)) + (assert= 8 (integer-length 128)))) + (deftest + "integer-length non-powers" + (do + (assert= 2 (integer-length 3)) + (assert= 3 (integer-length 5)) + (assert= 3 (integer-length 7)) + (assert= 8 (integer-length 255)) + (assert= 9 (integer-length 256)))) + (deftest + "bitwise ops compose" + (do + (assert= + 5 + (bitwise-and + (bitwise-or 5 3) + (bitwise-xor 7 2))) + (assert= 0 (bitwise-and 170 85)))) + (deftest + "arithmetic-shift round-trip" + (do + (assert= + 10 + (arithmetic-shift (arithmetic-shift 10 3) -3)))) + (deftest + "extract bits with mask" + (do + (let + ((x 52)) + (assert= + 5 + (bitwise-and (arithmetic-shift x -2) 7))))) + (deftest + "clear low bits with bitwise-not mask" + (do + (assert= 252 (bitwise-and 255 (bitwise-not 3))))) + (deftest + "integer-length after shift" + (do + (assert= + 4 + (integer-length (arithmetic-shift 1 3)))))) \ No newline at end of file