builtin.mjs raw
1 // TinyJS Runtime — Builtin Operations
2 // Slice, map, string operations that mirror Go's builtin functions.
3
4 // --- Slices ---
5
6 export class Slice {
7 constructor(array, offset, length, capacity, secure) {
8 this.$array = array; // backing array (JS Array)
9 this.$offset = offset; // start offset into backing array
10 this.$length = length; // number of accessible elements
11 this.$capacity = capacity; // max elements before realloc
12 // $secure is the taint bit for the `secure` slice allocation marker.
13 // When truthy, comparisons involving this slice use constant-time
14 // byte equality/ordering and any new slice derived from this one
15 // (sub-slice, append, copy destination, concat) inherits the bit.
16 this.$secure = !!secure;
17 }
18
19 get(i) {
20 if (i < 0 || i >= this.$length) {
21 throw new Error(`runtime error: index out of range [${i}] with length ${this.$length}`);
22 }
23 return this.$array[this.$offset + i];
24 }
25
26 set(i, v) {
27 if (i < 0 || i >= this.$length) {
28 throw new Error(`runtime error: index out of range [${i}] with length ${this.$length}`);
29 }
30 this.$array[this.$offset + i] = v;
31 }
32
33 addr(i) {
34 if (i < 0 || i >= this.$length) {
35 throw new Error(`runtime error: index out of range [${i}] with length ${this.$length}`);
36 }
37 const arr = this.$array;
38 const idx = this.$offset + i;
39 return {
40 $get: () => arr[idx],
41 $set: (v) => { arr[idx] = v; }
42 };
43 }
44
45 // Decode bytes to a JS string so that `+` concatenation works.
46 // In Moxie string and []byte are the same type — Slice objects must
47 // coerce to JS strings when used with the + operator.
48 toString() {
49 const buf = new Uint8Array(this.$length);
50 for (let i = 0; i < this.$length; i++) {
51 buf[i] = this.$array[this.$offset + i];
52 }
53 return new TextDecoder().decode(buf);
54 }
55 }
56
57 // Make a new slice. Object zero values must be deep-cloned per slot — otherwise
58 // every slot shares one reference and mutating any element mutates all of them.
59 export function makeSlice(len, cap, zero) {
60 if (cap === undefined || cap < len) cap = len;
61 const arr = new Array(cap);
62 const needsClone = zero !== undefined && zero !== null && typeof zero === 'object';
63 for (let i = 0; i < cap; i++) {
64 arr[i] = needsClone ? cloneValue(zero) : (zero !== undefined ? zero : 0);
65 }
66 return new Slice(arr, 0, len, cap);
67 }
68
69 // secureAlloc is the JS-target implementation of the Moxie `[]byte{:n, secure}`
70 // literal. On native targets this maps onto mmap+mlock guarded arenas with
71 // signal-handler wipe; the browser has no such primitives so the allocator
72 // degrades to a zeroed []byte slice carrying the $secure taint bit. The
73 // taint flips subsequent __moxie_eq / __moxie_lt onto the constant-time
74 // path and propagates through sliceSlice/append/copy/stringConcat so that
75 // any derived buffer retains the comparison guarantee.
76 export function secureAlloc(n) {
77 const arr = new Array(n);
78 for (let i = 0; i < n; i++) arr[i] = 0;
79 return new Slice(arr, 0, n, n, true);
80 }
81
82 // Slice a slice: s[low:high:max]
83 export function sliceSlice(s, low, high, max) {
84 if (low === undefined) low = 0;
85 if (high === undefined) high = s.$length;
86 if (max === undefined) max = s.$capacity;
87
88 if (low < 0 || high < low || max < high || max > s.$capacity) {
89 throw new Error(`runtime error: slice bounds out of range [${low}:${high}:${max}] with capacity ${s.$capacity}`);
90 }
91
92 // Slicing a secure slice produces a secure sub-slice — the taint bit
93 // follows the backing bytes, not the slice header.
94 return new Slice(s.$array, s.$offset + low, high - low, max - low, s.$secure);
95 }
96
97 // Append to slice.
98 export function append(s, ...elems) {
99 if (s == null) {
100 s = new Slice([], 0, 0, 0);
101 }
102
103 const needed = s.$length + elems.length;
104 const secure = !!s.$secure;
105
106 if (needed <= s.$capacity) {
107 // Fits in existing backing array.
108 for (let i = 0; i < elems.length; i++) {
109 s.$array[s.$offset + s.$length + i] = elems[i];
110 }
111 return new Slice(s.$array, s.$offset, needed, s.$capacity, secure);
112 }
113
114 // Need to grow. Go growth strategy: double until 256, then grow by 25%.
115 let newCap = s.$capacity;
116 if (newCap === 0) newCap = 1;
117 while (newCap < needed) {
118 if (newCap < 256) {
119 newCap *= 2;
120 } else {
121 newCap += Math.floor(newCap / 4);
122 }
123 }
124
125 const newArr = new Array(newCap);
126 for (let i = 0; i < s.$length; i++) {
127 newArr[i] = s.$array[s.$offset + i];
128 }
129 for (let i = 0; i < elems.length; i++) {
130 newArr[s.$length + i] = elems[i];
131 }
132
133 return new Slice(newArr, 0, needed, newCap, secure);
134 }
135
136 // Append a string's UTF-8 bytes to a byte slice: append([]byte, string...)
137 export function appendString(dst, s) {
138 const bytes = utf8Bytes(s);
139 const elems = [];
140 for (let i = 0; i < bytes.length; i++) elems.push(bytes[i]);
141 return append(dst, ...elems);
142 }
143
144 // Append slice to slice: append(a, b...)
145 export function appendSlice(dst, src) {
146 if (src === null || src === undefined || src.$length === 0) return dst;
147 const elems = [];
148 for (let i = 0; i < src.$length; i++) {
149 elems.push(src.$array[src.$offset + i]);
150 }
151 const result = append(dst, ...elems);
152 // Taint propagation: if src was secure, the appended bytes carry secret
153 // material into dst. Mark the result secure so downstream comparisons
154 // still run constant-time.
155 if (result && (src.$secure || (dst && dst.$secure))) {
156 result.$secure = true;
157 }
158 return result;
159 }
160
161 // Copy from src to dst. Returns number of elements copied.
162 // nil src / dst match Go semantics: zero-length, no-op copy.
163 export function copy(dst, src) {
164 if (dst === null || dst === undefined) return 0;
165 if (src === null || src === undefined) return 0;
166 // Handle string source — copy UTF-8 bytes.
167 if (typeof src === 'string') {
168 const bytes = utf8Bytes(src);
169 const n = Math.min(dst.$length, bytes.length);
170 for (let i = 0; i < n; i++) {
171 dst.$array[dst.$offset + i] = bytes[i];
172 }
173 return n;
174 }
175
176 const n = Math.min(dst.$length, src.$length);
177 // Handle overlapping slices.
178 if (dst.$array === src.$array && dst.$offset > src.$offset) {
179 for (let i = n - 1; i >= 0; i--) {
180 dst.$array[dst.$offset + i] = src.$array[src.$offset + i];
181 }
182 } else {
183 for (let i = 0; i < n; i++) {
184 dst.$array[dst.$offset + i] = src.$array[src.$offset + i];
185 }
186 }
187 // Taint propagation: copying secret bytes into dst makes dst secret.
188 // The $secure flag flips on dst's header, so any subsequent comparison
189 // where dst is an operand takes the constant-time path — closing the
190 // `copy(temp, secret); if temp == received { ... }` side-channel.
191 if (src.$secure) {
192 dst.$secure = true;
193 }
194 return n;
195 }
196
197 // Len.
198 export function len(v) {
199 if (v === null || v === undefined) return 0;
200 if (typeof v === 'string') return utf8Bytes(v).length;
201 if (v instanceof Slice) return v.$length;
202 if (v instanceof Map) return v.size;
203 if (v instanceof GoMap) return v.size();
204 if (Array.isArray(v)) return v.length;
205 return 0;
206 }
207
208 // Cap.
209 export function cap(v) {
210 if (v === null || v === undefined) return 0;
211 if (v instanceof Slice) return v.$capacity;
212 if (Array.isArray(v)) return v.length;
213 return 0;
214 }
215
216 // --- Maps ---
217
218 // Go maps need special key handling (struct keys use deep equality).
219 export class GoMap {
220 constructor() {
221 this.entries = []; // [{key, value, hash}]
222 }
223
224 get(key) {
225 for (const e of this.entries) {
226 if (deepEqual(e.key, key)) return { value: e.value, ok: true };
227 }
228 return { value: undefined, ok: false };
229 }
230
231 set(key, value) {
232 for (const e of this.entries) {
233 if (deepEqual(e.key, key)) {
234 e.value = value;
235 return;
236 }
237 }
238 this.entries.push({ key, value });
239 }
240
241 delete(key) {
242 const idx = this.entries.findIndex(e => deepEqual(e.key, key));
243 if (idx >= 0) this.entries.splice(idx, 1);
244 }
245
246 has(key) {
247 return this.entries.some(e => deepEqual(e.key, key));
248 }
249
250 size() {
251 return this.entries.length;
252 }
253
254 // Iterate: calls fn(key, value) for each entry.
255 // Order is randomized per Go spec.
256 forEach(fn) {
257 // Randomize iteration order.
258 const shuffled = [...this.entries];
259 for (let i = shuffled.length - 1; i > 0; i--) {
260 const j = Math.floor(Math.random() * (i + 1));
261 [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
262 }
263 for (const e of shuffled) {
264 fn(e.key, e.value);
265 }
266 }
267 }
268
269 // For simple key types (string, number, bool), use native Map.
270 export function makeMap(keyKind) {
271 if (keyKind === 'string' || keyKind === 'int' || keyKind === 'float64' || keyKind === 'bool') {
272 return new Map();
273 }
274 return new GoMap();
275 }
276
277 // Map lookup with comma-ok.
278 export function mapLookup(m, key) {
279 if (m === null || m === undefined) return { value: null, ok: false };
280 if (m instanceof Map) {
281 if (m.has(key)) return { value: m.get(key), ok: true };
282 return { value: null, ok: false };
283 }
284 if (m instanceof GoMap) return m.get(key);
285 return { value: null, ok: false };
286 }
287
288 // Map update.
289 export function mapUpdate(m, key, value) {
290 if (m === null || m === undefined) {
291 throw new Error('assignment to entry in nil map');
292 }
293 if (m instanceof Map) {
294 m.set(key, value);
295 } else if (m instanceof GoMap) {
296 m.set(key, value);
297 }
298 }
299
300 // Map delete.
301 export function mapDelete(m, key) {
302 if (m === null || m === undefined) return;
303 if (m instanceof Map) {
304 m.delete(key);
305 } else if (m instanceof GoMap) {
306 m.delete(key);
307 }
308 }
309
310 // Builtin clear(slice) — zeros every element in [0, length).
311 // This is the JS-side wipe primitive for the secure-allocation contract:
312 // `[]byte{:n, secure}` gives you a best-effort allocation on JS, and
313 // `clear(b)` is the caller's way of overwriting the backing before it
314 // falls out of scope. On []byte the fill value is 0, matching Go semantics.
315 export function clearSlice(s) {
316 if (s === null || s === undefined) return;
317 const arr = s.$array;
318 const off = s.$offset;
319 const n = s.$length;
320 for (let i = 0; i < n; i++) {
321 arr[off + i] = 0;
322 }
323 }
324
325 // Builtin clear(map) — deletes every entry.
326 export function clearMap(m) {
327 if (m === null || m === undefined) return;
328 if (m instanceof Map) {
329 m.clear();
330 } else if (m instanceof GoMap) {
331 m.entries.length = 0;
332 }
333 }
334
335 // --- Strings (UTF-8 byte semantics) ---
336
337 const _enc = new TextEncoder();
338 const _dec = new TextDecoder();
339
340 // One-string cache: amortizes TextEncoder cost when a loop indexes the same string.
341 let _cacheStr = '';
342 let _cacheBytes = new Uint8Array(0);
343
344 export function utf8Bytes(s) {
345 if (typeof s !== 'string') s = '' + s; // unbox String objects for cache hits
346 if (s !== _cacheStr) {
347 _cacheStr = s;
348 _cacheBytes = _enc.encode(s);
349 }
350 return _cacheBytes;
351 }
352
353 // UTF-8 byte length of a string.
354 export function byteLen(s) {
355 return utf8Bytes(s).length;
356 }
357
358 // UTF-8 byte at byte position i.
359 export function stringByteAt(s, i) {
360 return utf8Bytes(s)[i];
361 }
362
363 // for range over string — returns (byteIndex, rune) using UTF-8.
364 export function stringRange(s) {
365 const bytes = utf8Bytes(s);
366 return {
367 $bytes: bytes,
368 $pos: 0,
369 next() {
370 if (this.$pos >= this.$bytes.length) return [false, 0, 0];
371 const start = this.$pos;
372 const b0 = this.$bytes[this.$pos];
373 let cp;
374 if (b0 < 0x80) {
375 cp = b0; this.$pos += 1;
376 } else if (b0 < 0xE0) {
377 cp = ((b0 & 0x1F) << 6) | (this.$bytes[this.$pos + 1] & 0x3F);
378 this.$pos += 2;
379 } else if (b0 < 0xF0) {
380 cp = ((b0 & 0x0F) << 12) | ((this.$bytes[this.$pos + 1] & 0x3F) << 6) |
381 (this.$bytes[this.$pos + 2] & 0x3F);
382 this.$pos += 3;
383 } else {
384 cp = ((b0 & 0x07) << 18) | ((this.$bytes[this.$pos + 1] & 0x3F) << 12) |
385 ((this.$bytes[this.$pos + 2] & 0x3F) << 6) | (this.$bytes[this.$pos + 3] & 0x3F);
386 this.$pos += 4;
387 }
388 return [true, start, cp];
389 }
390 };
391 }
392
393 // String to byte slice.
394 export function stringToBytes(s) {
395 const bytes = _enc.encode(s);
396 const arr = Array.from(bytes);
397 return new Slice(arr, 0, arr.length, arr.length);
398 }
399
400 // Byte slice to string.
401 export function bytesToString(sl) {
402 if (typeof sl === 'string') return sl;
403 const bytes = new Uint8Array(sl.$length);
404 for (let i = 0; i < sl.$length; i++) {
405 bytes[i] = sl.$array[sl.$offset + i];
406 }
407 return _dec.decode(bytes);
408 }
409
410 // String to rune slice.
411 export function stringToRunes(s) {
412 const runes = [...s].map(c => c.codePointAt(0));
413 return new Slice(runes, 0, runes.length, runes.length);
414 }
415
416 // Rune slice to string.
417 export function runesToString(sl) {
418 let s = '';
419 for (let i = 0; i < sl.$length; i++) {
420 s += String.fromCodePoint(sl.$array[sl.$offset + i]);
421 }
422 return s;
423 }
424
425 // sliceBytes extracts the underlying bytes from a Slice, string, or other
426 // text-like value without allocating a new string. Returns an Array of
427 // byte values or null if the input has no bytes. Used by the constant-time
428 // comparison paths so the comparison scans raw bytes rather than forcing
429 // UTF-8 decode/encode round-trips.
430 function sliceBytes(v) {
431 if (v === null || v === undefined) return null;
432 if (v instanceof Slice) {
433 const out = new Array(v.$length);
434 for (let i = 0; i < v.$length; i++) out[i] = v.$array[v.$offset + i];
435 return out;
436 }
437 if (typeof v === 'string') {
438 return Array.from(_enc.encode(v));
439 }
440 return null;
441 }
442
443 // isSecureOperand reports whether the value should force the
444 // constant-time comparison path. A Slice carrying $secure taints its
445 // own comparisons. A native JS string is never marked secure because
446 // strings have no stable header to carry the bit — callers must copy
447 // sensitive material into a `[]byte{:n, secure}` buffer before
448 // comparison for the guarantee to apply.
449 function isSecureOperand(v) {
450 return v instanceof Slice && !!v.$secure;
451 }
452
453 // String concatenation (Go's + operator for strings).
454 // Propagates the $secure taint: if either operand is a secure Slice,
455 // the concatenated result is a secure Slice holding the combined bytes.
456 export function stringConcat(a, b) {
457 if (isSecureOperand(a) || isSecureOperand(b)) {
458 const ab = sliceBytes(a) || [];
459 const bb = sliceBytes(b) || [];
460 const arr = new Array(ab.length + bb.length);
461 for (let i = 0; i < ab.length; i++) arr[i] = ab[i];
462 for (let j = 0; j < bb.length; j++) arr[ab.length + j] = bb[j];
463 return new Slice(arr, 0, arr.length, arr.length, true);
464 }
465 return a + b;
466 }
467
468 // constantTimeBytesEqual walks the full length of both inputs and
469 // accumulates differences into a running OR. Return value depends only
470 // on the final accumulator, so execution time is a function of (length +
471 // padding), never of where the first mismatch occurs. Mirrors the
472 // semantics of Go's subtle.ConstantTimeCompare / OpenSSL CRYPTO_memcmp.
473 function constantTimeBytesEqual(a, b) {
474 const ab = sliceBytes(a);
475 const bb = sliceBytes(b);
476 if (ab === null || bb === null) return a === b;
477 // Walk the max length so a shorter operand still contributes to the
478 // accumulator via its virtual tail of zero-XOR-nonzero bytes.
479 const n = ab.length >= bb.length ? ab.length : bb.length;
480 let acc = (ab.length ^ bb.length) | 0;
481 for (let i = 0; i < n; i++) {
482 const x = i < ab.length ? ab[i] : 0;
483 const y = i < bb.length ? bb[i] : 0;
484 acc |= (x ^ y);
485 }
486 return acc === 0;
487 }
488
489 // String equality — handles both Slice objects and native JS strings.
490 // The JS backend emits this instead of === for string-typed operands,
491 // because Slice objects with identical content are !== each other.
492 //
493 // If either operand is a secure slice (carries $secure), the comparison
494 // runs in constant time over the maximum of the two lengths. Otherwise
495 // the fast path compares via string coercion.
496 export function stringEqual(a, b) {
497 if (a === b) return true;
498 if (isSecureOperand(a) || isSecureOperand(b)) {
499 return constantTimeBytesEqual(a, b);
500 }
501 return ('' + a) === ('' + b);
502 }
503
504 // String comparison.
505 export function stringCompare(a, b) {
506 if (isSecureOperand(a) || isSecureOperand(b)) {
507 // Constant-time lexicographic compare. The loop body is data-
508 // oblivious: all per-iteration branches fold into bitwise ops on
509 // 32-bit integers, so per-byte cost is fixed regardless of content.
510 //
511 // The loop maintains:
512 // diff — running OR of (x ^ y); once non-zero, a mismatch has
513 // been seen and subsequent iterations must not overwrite
514 // the result.
515 // result — -1 / 0 / 1, snapshotted at the first mismatch.
516 // seen — 0 while diff == 0; -1 once diff != 0. Used as a mask
517 // to suppress updates to `result` after the first diff.
518 const ab = sliceBytes(a) || [];
519 const bb = sliceBytes(b) || [];
520 const la = ab.length, lb = bb.length;
521 const n = la >= lb ? la : lb;
522 let diff = 0;
523 let result = 0;
524 for (let i = 0; i < n; i++) {
525 const x = i < la ? ab[i] : 0;
526 const y = i < lb ? bb[i] : 0;
527 const d = (x - y) | 0;
528 // seen: 0 while diff == 0, -1 once diff != 0
529 const seen = -(((diff | -diff) >>> 31) | 0);
530 // sign: -1 if d<0, 1 if d>0, 0 if d==0 — no branches
531 const sign = (d >> 31) + (((-d) >>> 31) & 1);
532 result = (result & seen) | (sign & ~seen);
533 diff |= (x ^ y);
534 }
535 // Length tiebreaker when all compared bytes matched. Lengths are
536 // non-secret in the Moxie model so this branch does not leak
537 // byte content.
538 if (diff === 0) {
539 return la < lb ? -1 : (la > lb ? 1 : 0);
540 }
541 return result;
542 }
543 if (a < b) return -1;
544 if (a > b) return 1;
545 return 0;
546 }
547
548 // String slice: s[low:high] — operates on UTF-8 byte boundaries.
549 export function stringSlice(s, low, high) {
550 const bytes = utf8Bytes(s);
551 if (low === undefined) low = 0;
552 if (high === undefined) high = bytes.length;
553 return _dec.decode(bytes.subarray(low, high));
554 }
555
556 // --- String↔Slice compatibility shims ---
557 // moxiejs compiles s[i] to s.addr(i).$get() assuming Slice objects.
558 // DOM bridge and other JS APIs return raw strings — shim them to behave
559 // like read-only byte Slices so compiled code works on either type.
560 //
561 // addr() has its own byte cache, separate from utf8Bytes, so that
562 // stringSlice→utf8Bytes calls can't thrash the cache during byte loops.
563
564 let _addrStr = '';
565 let _addrBytes = new Uint8Array(0);
566
567 Object.defineProperty(String.prototype, 'addr', {
568 value: function(i) {
569 const s = '' + this;
570 if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
571 const b = _addrBytes;
572 return { $get: () => b[i], $set: () => {} };
573 },
574 writable: false, configurable: true, enumerable: false
575 });
576
577 Object.defineProperty(String.prototype, 'get', {
578 value: function(i) {
579 const s = '' + this;
580 if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
581 return _addrBytes[i];
582 },
583 writable: false, configurable: true, enumerable: false
584 });
585
586 Object.defineProperty(String.prototype, '$length', {
587 get: function() {
588 const s = '' + this;
589 if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
590 return _addrBytes.length;
591 },
592 configurable: true, enumerable: false
593 });
594
595 Object.defineProperty(String.prototype, '$capacity', {
596 get: function() {
597 const s = '' + this;
598 if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
599 return _addrBytes.length;
600 },
601 configurable: true, enumerable: false
602 });
603
604 Object.defineProperty(String.prototype, '$offset', {
605 get: function() { return 0; },
606 configurable: true, enumerable: false
607 });
608
609 Object.defineProperty(String.prototype, '$array', {
610 get: function() {
611 const s = '' + this;
612 if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
613 return _addrBytes;
614 },
615 configurable: true, enumerable: false
616 });
617
618 // --- Deep equality for map keys ---
619
620 function deepEqual(a, b) {
621 if (a === b) return true;
622 if (a === null || b === null) return a === b;
623 if (typeof a !== typeof b) return false;
624 if (typeof a !== 'object') return false;
625
626 const keysA = Object.keys(a).filter(k => !k.startsWith('$'));
627 const keysB = Object.keys(b).filter(k => !k.startsWith('$'));
628 if (keysA.length !== keysB.length) return false;
629 return keysA.every(k => deepEqual(a[k], b[k]));
630 }
631
632 // Clone a Go value type (array or struct). Slices get a new backing array copy.
633 // Structs get shallow field copies (nested value types need recursive clone).
634 // Pointer wrappers ({$get,$set}) are copied by reference — Go pointer-value
635 // semantics: struct-copy propagates the pointer bits, both structs then point
636 // to the same underlying allocation. Recursive-cloning a pointer wrapper as
637 // if it were a struct would strip all $-prefixed keys and return {}, silently
638 // erasing the pointer (caught in MLS framedContent struct copy).
639 export function cloneValue(v) {
640 if (v === null || v === undefined || typeof v !== 'object') return v;
641 if (v instanceof Slice) {
642 const newArr = new Array(v.$capacity);
643 for (let i = 0; i < v.$length; i++) {
644 const elem = v.$array[v.$offset + i];
645 newArr[i] = (typeof elem === 'object' && elem !== null) ? cloneValue(elem) : elem;
646 }
647 return new Slice(newArr, 0, v.$length, v.$capacity);
648 }
649 if (Array.isArray(v)) {
650 return v.map(e => (typeof e === 'object' && e !== null) ? cloneValue(e) : e);
651 }
652 // Pointer wrapper: share by reference (Go pointer semantics).
653 if (typeof v.$get === 'function' && typeof v.$set === 'function') {
654 return v;
655 }
656 // Struct: shallow clone with recursive value cloning.
657 const obj = {};
658 for (const key of Object.keys(v)) {
659 if (key.startsWith('$')) continue; // skip $get/$set/$value
660 const val = v[key];
661 obj[key] = (typeof val === 'object' && val !== null) ? cloneValue(val) : val;
662 }
663 return obj;
664 }
665
666 // Legacy 64-bit bitwise stubs — kept for compatibility with pre-BigInt compiled code.
667 // New code uses native BigInt operators directly.
668 export function int64or(x, y) { return x | y; }
669 export function int64and(x, y) { return x & y; }
670 export function int64xor(x, y) { return x ^ y; }
671