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) {
   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    }
  13  
  14    get(i) {
  15      if (i < 0 || i >= this.$length) {
  16        throw new Error(`runtime error: index out of range [${i}] with length ${this.$length}`);
  17      }
  18      return this.$array[this.$offset + i];
  19    }
  20  
  21    set(i, v) {
  22      if (i < 0 || i >= this.$length) {
  23        throw new Error(`runtime error: index out of range [${i}] with length ${this.$length}`);
  24      }
  25      this.$array[this.$offset + i] = v;
  26    }
  27  
  28    addr(i) {
  29      if (i < 0 || i >= this.$length) {
  30        throw new Error(`runtime error: index out of range [${i}] with length ${this.$length}`);
  31      }
  32      const arr = this.$array;
  33      const idx = this.$offset + i;
  34      return {
  35        $get: () => arr[idx],
  36        $set: (v) => { arr[idx] = v; }
  37      };
  38    }
  39  
  40    // Decode bytes to a JS string so that `+` concatenation works.
  41    // In Moxie string and []byte are the same type — Slice objects must
  42    // coerce to JS strings when used with the + operator.
  43    toString() {
  44      const buf = new Uint8Array(this.$length);
  45      for (let i = 0; i < this.$length; i++) {
  46        buf[i] = this.$array[this.$offset + i];
  47      }
  48      return new TextDecoder().decode(buf);
  49    }
  50  }
  51  
  52  // Make a new slice. Object zero values must be deep-cloned per slot — otherwise
  53  // every slot shares one reference and mutating any element mutates all of them.
  54  export function makeSlice(len, cap, zero) {
  55    if (cap === undefined || cap < len) cap = len;
  56    const arr = new Array(cap);
  57    const needsClone = zero !== undefined && zero !== null && typeof zero === 'object';
  58    for (let i = 0; i < cap; i++) {
  59      arr[i] = needsClone ? cloneValue(zero) : (zero !== undefined ? zero : 0);
  60    }
  61    return new Slice(arr, 0, len, cap);
  62  }
  63  
  64  // Slice a slice: s[low:high:max]
  65  export function sliceSlice(s, low, high, max) {
  66    if (low === undefined) low = 0;
  67    if (high === undefined) high = s.$length;
  68    if (max === undefined) max = s.$capacity;
  69  
  70    if (low < 0 || high < low || max < high || max > s.$capacity) {
  71      throw new Error(`runtime error: slice bounds out of range [${low}:${high}:${max}] with capacity ${s.$capacity}`);
  72    }
  73  
  74    return new Slice(s.$array, s.$offset + low, high - low, max - low);
  75  }
  76  
  77  // Append to slice.
  78  export function append(s, ...elems) {
  79    if (s == null) {
  80      s = new Slice([], 0, 0, 0);
  81    }
  82  
  83    const needed = s.$length + elems.length;
  84  
  85    if (needed <= s.$capacity) {
  86      // Fits in existing backing array.
  87      for (let i = 0; i < elems.length; i++) {
  88        s.$array[s.$offset + s.$length + i] = elems[i];
  89      }
  90      return new Slice(s.$array, s.$offset, needed, s.$capacity);
  91    }
  92  
  93    // Need to grow. Go growth strategy: double until 256, then grow by 25%.
  94    let newCap = s.$capacity;
  95    if (newCap === 0) newCap = 1;
  96    while (newCap < needed) {
  97      if (newCap < 256) {
  98        newCap *= 2;
  99      } else {
 100        newCap += Math.floor(newCap / 4);
 101      }
 102    }
 103  
 104    const newArr = new Array(newCap);
 105    for (let i = 0; i < s.$length; i++) {
 106      newArr[i] = s.$array[s.$offset + i];
 107    }
 108    for (let i = 0; i < elems.length; i++) {
 109      newArr[s.$length + i] = elems[i];
 110    }
 111  
 112    return new Slice(newArr, 0, needed, newCap);
 113  }
 114  
 115  // Append a string's UTF-8 bytes to a byte slice: append([]byte, string...)
 116  export function appendString(dst, s) {
 117    const bytes = utf8Bytes(s);
 118    const elems = [];
 119    for (let i = 0; i < bytes.length; i++) elems.push(bytes[i]);
 120    return append(dst, ...elems);
 121  }
 122  
 123  // Append slice to slice: append(a, b...)
 124  export function appendSlice(dst, src) {
 125    const elems = [];
 126    for (let i = 0; i < src.$length; i++) {
 127      elems.push(src.$array[src.$offset + i]);
 128    }
 129    return append(dst, ...elems);
 130  }
 131  
 132  // Copy from src to dst. Returns number of elements copied.
 133  // nil src / dst match Go semantics: zero-length, no-op copy.
 134  export function copy(dst, src) {
 135    if (dst === null || dst === undefined) return 0;
 136    if (src === null || src === undefined) return 0;
 137    // Handle string source — copy UTF-8 bytes.
 138    if (typeof src === 'string') {
 139      const bytes = utf8Bytes(src);
 140      const n = Math.min(dst.$length, bytes.length);
 141      for (let i = 0; i < n; i++) {
 142        dst.$array[dst.$offset + i] = bytes[i];
 143      }
 144      return n;
 145    }
 146  
 147    const n = Math.min(dst.$length, src.$length);
 148    // Handle overlapping slices.
 149    if (dst.$array === src.$array && dst.$offset > src.$offset) {
 150      for (let i = n - 1; i >= 0; i--) {
 151        dst.$array[dst.$offset + i] = src.$array[src.$offset + i];
 152      }
 153    } else {
 154      for (let i = 0; i < n; i++) {
 155        dst.$array[dst.$offset + i] = src.$array[src.$offset + i];
 156      }
 157    }
 158    return n;
 159  }
 160  
 161  // Len.
 162  export function len(v) {
 163    if (v === null || v === undefined) return 0;
 164    if (typeof v === 'string') return utf8Bytes(v).length;
 165    if (v instanceof Slice) return v.$length;
 166    if (v instanceof Map) return v.size;
 167    if (v instanceof GoMap) return v.size();
 168    if (Array.isArray(v)) return v.length;
 169    return 0;
 170  }
 171  
 172  // Cap.
 173  export function cap(v) {
 174    if (v === null || v === undefined) return 0;
 175    if (v instanceof Slice) return v.$capacity;
 176    if (Array.isArray(v)) return v.length;
 177    return 0;
 178  }
 179  
 180  // --- Maps ---
 181  
 182  // Go maps need special key handling (struct keys use deep equality).
 183  export class GoMap {
 184    constructor() {
 185      this.entries = []; // [{key, value, hash}]
 186    }
 187  
 188    get(key) {
 189      for (const e of this.entries) {
 190        if (deepEqual(e.key, key)) return { value: e.value, ok: true };
 191      }
 192      return { value: undefined, ok: false };
 193    }
 194  
 195    set(key, value) {
 196      for (const e of this.entries) {
 197        if (deepEqual(e.key, key)) {
 198          e.value = value;
 199          return;
 200        }
 201      }
 202      this.entries.push({ key, value });
 203    }
 204  
 205    delete(key) {
 206      const idx = this.entries.findIndex(e => deepEqual(e.key, key));
 207      if (idx >= 0) this.entries.splice(idx, 1);
 208    }
 209  
 210    has(key) {
 211      return this.entries.some(e => deepEqual(e.key, key));
 212    }
 213  
 214    size() {
 215      return this.entries.length;
 216    }
 217  
 218    // Iterate: calls fn(key, value) for each entry.
 219    // Order is randomized per Go spec.
 220    forEach(fn) {
 221      // Randomize iteration order.
 222      const shuffled = [...this.entries];
 223      for (let i = shuffled.length - 1; i > 0; i--) {
 224        const j = Math.floor(Math.random() * (i + 1));
 225        [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
 226      }
 227      for (const e of shuffled) {
 228        fn(e.key, e.value);
 229      }
 230    }
 231  }
 232  
 233  // For simple key types (string, number, bool), use native Map.
 234  export function makeMap(keyKind) {
 235    if (keyKind === 'string' || keyKind === 'int' || keyKind === 'float64' || keyKind === 'bool') {
 236      return new Map();
 237    }
 238    return new GoMap();
 239  }
 240  
 241  // Map lookup with comma-ok.
 242  export function mapLookup(m, key) {
 243    if (m === null || m === undefined) return { value: null, ok: false };
 244    if (m instanceof Map) {
 245      if (m.has(key)) return { value: m.get(key), ok: true };
 246      return { value: null, ok: false };
 247    }
 248    if (m instanceof GoMap) return m.get(key);
 249    return { value: null, ok: false };
 250  }
 251  
 252  // Map update.
 253  export function mapUpdate(m, key, value) {
 254    if (m === null || m === undefined) {
 255      throw new Error('assignment to entry in nil map');
 256    }
 257    if (m instanceof Map) {
 258      m.set(key, value);
 259    } else if (m instanceof GoMap) {
 260      m.set(key, value);
 261    }
 262  }
 263  
 264  // Map delete.
 265  export function mapDelete(m, key) {
 266    if (m === null || m === undefined) return;
 267    if (m instanceof Map) {
 268      m.delete(key);
 269    } else if (m instanceof GoMap) {
 270      m.delete(key);
 271    }
 272  }
 273  
 274  // Builtin clear(slice) — zeros every element in [0, length).
 275  // This is the JS-side wipe primitive for the secure-allocation contract:
 276  // `[]byte{:n, secure}` gives you a best-effort allocation on JS, and
 277  // `clear(b)` is the caller's way of overwriting the backing before it
 278  // falls out of scope. On []byte the fill value is 0, matching Go semantics.
 279  export function clearSlice(s) {
 280    if (s === null || s === undefined) return;
 281    const arr = s.$array;
 282    const off = s.$offset;
 283    const n = s.$length;
 284    for (let i = 0; i < n; i++) {
 285      arr[off + i] = 0;
 286    }
 287  }
 288  
 289  // Builtin clear(map) — deletes every entry.
 290  export function clearMap(m) {
 291    if (m === null || m === undefined) return;
 292    if (m instanceof Map) {
 293      m.clear();
 294    } else if (m instanceof GoMap) {
 295      m.entries.length = 0;
 296    }
 297  }
 298  
 299  // --- Strings (UTF-8 byte semantics) ---
 300  
 301  const _enc = new TextEncoder();
 302  const _dec = new TextDecoder();
 303  
 304  // One-string cache: amortizes TextEncoder cost when a loop indexes the same string.
 305  let _cacheStr = '';
 306  let _cacheBytes = new Uint8Array(0);
 307  
 308  export function utf8Bytes(s) {
 309    if (typeof s !== 'string') s = '' + s; // unbox String objects for cache hits
 310    if (s !== _cacheStr) {
 311      _cacheStr = s;
 312      _cacheBytes = _enc.encode(s);
 313    }
 314    return _cacheBytes;
 315  }
 316  
 317  // UTF-8 byte length of a string.
 318  export function byteLen(s) {
 319    return utf8Bytes(s).length;
 320  }
 321  
 322  // UTF-8 byte at byte position i.
 323  export function stringByteAt(s, i) {
 324    return utf8Bytes(s)[i];
 325  }
 326  
 327  // for range over string — returns (byteIndex, rune) using UTF-8.
 328  export function stringRange(s) {
 329    const bytes = utf8Bytes(s);
 330    return {
 331      $bytes: bytes,
 332      $pos: 0,
 333      next() {
 334        if (this.$pos >= this.$bytes.length) return [false, 0, 0];
 335        const start = this.$pos;
 336        const b0 = this.$bytes[this.$pos];
 337        let cp;
 338        if (b0 < 0x80) {
 339          cp = b0; this.$pos += 1;
 340        } else if (b0 < 0xE0) {
 341          cp = ((b0 & 0x1F) << 6) | (this.$bytes[this.$pos + 1] & 0x3F);
 342          this.$pos += 2;
 343        } else if (b0 < 0xF0) {
 344          cp = ((b0 & 0x0F) << 12) | ((this.$bytes[this.$pos + 1] & 0x3F) << 6) |
 345               (this.$bytes[this.$pos + 2] & 0x3F);
 346          this.$pos += 3;
 347        } else {
 348          cp = ((b0 & 0x07) << 18) | ((this.$bytes[this.$pos + 1] & 0x3F) << 12) |
 349               ((this.$bytes[this.$pos + 2] & 0x3F) << 6) | (this.$bytes[this.$pos + 3] & 0x3F);
 350          this.$pos += 4;
 351        }
 352        return [true, start, cp];
 353      }
 354    };
 355  }
 356  
 357  // String to byte slice.
 358  export function stringToBytes(s) {
 359    const bytes = _enc.encode(s);
 360    const arr = Array.from(bytes);
 361    return new Slice(arr, 0, arr.length, arr.length);
 362  }
 363  
 364  // Byte slice to string.
 365  export function bytesToString(sl) {
 366    if (typeof sl === 'string') return sl;
 367    const bytes = new Uint8Array(sl.$length);
 368    for (let i = 0; i < sl.$length; i++) {
 369      bytes[i] = sl.$array[sl.$offset + i];
 370    }
 371    return _dec.decode(bytes);
 372  }
 373  
 374  // String to rune slice.
 375  export function stringToRunes(s) {
 376    const runes = [...s].map(c => c.codePointAt(0));
 377    return new Slice(runes, 0, runes.length, runes.length);
 378  }
 379  
 380  // Rune slice to string.
 381  export function runesToString(sl) {
 382    let s = '';
 383    for (let i = 0; i < sl.$length; i++) {
 384      s += String.fromCodePoint(sl.$array[sl.$offset + i]);
 385    }
 386    return s;
 387  }
 388  
 389  // String concatenation (Go's + operator for strings).
 390  export function stringConcat(a, b) {
 391    return a + b;
 392  }
 393  
 394  // String equality — handles both Slice objects and native JS strings.
 395  // The JS backend emits this instead of === for string-typed operands,
 396  // because Slice objects with identical content are !== each other.
 397  export function stringEqual(a, b) {
 398    if (a === b) return true;
 399    return ('' + a) === ('' + b);
 400  }
 401  
 402  // String comparison.
 403  export function stringCompare(a, b) {
 404    if (a < b) return -1;
 405    if (a > b) return 1;
 406    return 0;
 407  }
 408  
 409  // String slice: s[low:high] — operates on UTF-8 byte boundaries.
 410  export function stringSlice(s, low, high) {
 411    const bytes = utf8Bytes(s);
 412    if (low === undefined) low = 0;
 413    if (high === undefined) high = bytes.length;
 414    return _dec.decode(bytes.subarray(low, high));
 415  }
 416  
 417  // --- String↔Slice compatibility shims ---
 418  // moxiejs compiles s[i] to s.addr(i).$get() assuming Slice objects.
 419  // DOM bridge and other JS APIs return raw strings — shim them to behave
 420  // like read-only byte Slices so compiled code works on either type.
 421  //
 422  // addr() has its own byte cache, separate from utf8Bytes, so that
 423  // stringSlice→utf8Bytes calls can't thrash the cache during byte loops.
 424  
 425  let _addrStr = '';
 426  let _addrBytes = new Uint8Array(0);
 427  
 428  Object.defineProperty(String.prototype, 'addr', {
 429    value: function(i) {
 430      const s = '' + this;
 431      if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
 432      const b = _addrBytes;
 433      return { $get: () => b[i], $set: () => {} };
 434    },
 435    writable: false, configurable: true, enumerable: false
 436  });
 437  
 438  Object.defineProperty(String.prototype, 'get', {
 439    value: function(i) {
 440      const s = '' + this;
 441      if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
 442      return _addrBytes[i];
 443    },
 444    writable: false, configurable: true, enumerable: false
 445  });
 446  
 447  Object.defineProperty(String.prototype, '$length', {
 448    get: function() {
 449      const s = '' + this;
 450      if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
 451      return _addrBytes.length;
 452    },
 453    configurable: true, enumerable: false
 454  });
 455  
 456  Object.defineProperty(String.prototype, '$capacity', {
 457    get: function() {
 458      const s = '' + this;
 459      if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
 460      return _addrBytes.length;
 461    },
 462    configurable: true, enumerable: false
 463  });
 464  
 465  Object.defineProperty(String.prototype, '$offset', {
 466    get: function() { return 0; },
 467    configurable: true, enumerable: false
 468  });
 469  
 470  Object.defineProperty(String.prototype, '$array', {
 471    get: function() {
 472      const s = '' + this;
 473      if (s !== _addrStr) { _addrStr = s; _addrBytes = _enc.encode(s); }
 474      return _addrBytes;
 475    },
 476    configurable: true, enumerable: false
 477  });
 478  
 479  // --- Deep equality for map keys ---
 480  
 481  function deepEqual(a, b) {
 482    if (a === b) return true;
 483    if (a === null || b === null) return a === b;
 484    if (typeof a !== typeof b) return false;
 485    if (typeof a !== 'object') return false;
 486  
 487    const keysA = Object.keys(a).filter(k => !k.startsWith('$'));
 488    const keysB = Object.keys(b).filter(k => !k.startsWith('$'));
 489    if (keysA.length !== keysB.length) return false;
 490    return keysA.every(k => deepEqual(a[k], b[k]));
 491  }
 492  
 493  // Clone a Go value type (array or struct). Slices get a new backing array copy.
 494  // Structs get shallow field copies (nested value types need recursive clone).
 495  export function cloneValue(v) {
 496    if (v === null || v === undefined || typeof v !== 'object') return v;
 497    if (v instanceof Slice) {
 498      const newArr = new Array(v.$capacity);
 499      for (let i = 0; i < v.$length; i++) {
 500        const elem = v.$array[v.$offset + i];
 501        newArr[i] = (typeof elem === 'object' && elem !== null) ? cloneValue(elem) : elem;
 502      }
 503      return new Slice(newArr, 0, v.$length, v.$capacity);
 504    }
 505    if (Array.isArray(v)) {
 506      return v.map(e => (typeof e === 'object' && e !== null) ? cloneValue(e) : e);
 507    }
 508    // Struct: shallow clone with recursive value cloning.
 509    const obj = {};
 510    for (const key of Object.keys(v)) {
 511      if (key.startsWith('$')) continue; // skip $get/$set/$value
 512      const val = v[key];
 513      obj[key] = (typeof val === 'object' && val !== null) ? cloneValue(val) : val;
 514    }
 515    return obj;
 516  }
 517  
 518  // Legacy 64-bit bitwise stubs — kept for compatibility with pre-BigInt compiled code.
 519  // New code uses native BigInt operators directly.
 520  export function int64or(x, y) { return x | y; }
 521  export function int64and(x, y) { return x & y; }
 522  export function int64xor(x, y) { return x ^ y; }
 523