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