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