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