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