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