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) {
  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  export function mapLookup(m, key) {
 225    if (m === null || m === undefined) return { value: undefined, ok: false };
 226    if (m instanceof Map) {
 227      if (m.has(key)) return { value: m.get(key), ok: true };
 228      return { value: undefined, ok: false };
 229    }
 230    if (m instanceof GoMap) return m.get(key);
 231    return { value: undefined, ok: false };
 232  }
 233  
 234  // Map update.
 235  export function mapUpdate(m, key, value) {
 236    if (m === null || m === undefined) {
 237      throw new Error('assignment to entry in nil map');
 238    }
 239    if (m instanceof Map) {
 240      m.set(key, value);
 241    } else if (m instanceof GoMap) {
 242      m.set(key, value);
 243    }
 244  }
 245  
 246  // Map delete.
 247  export function mapDelete(m, key) {
 248    if (m === null || m === undefined) return;
 249    if (m instanceof Map) {
 250      m.delete(key);
 251    } else if (m instanceof GoMap) {
 252      m.delete(key);
 253    }
 254  }
 255  
 256  // --- Strings (UTF-8 byte semantics) ---
 257  
 258  const _enc = new TextEncoder();
 259  const _dec = new TextDecoder();
 260  
 261  // One-string cache: amortizes TextEncoder cost when a loop indexes the same string.
 262  let _cacheStr = '';
 263  let _cacheBytes = new Uint8Array(0);
 264  
 265  export function utf8Bytes(s) {
 266    if (s !== _cacheStr) {
 267      _cacheStr = s;
 268      _cacheBytes = _enc.encode(s);
 269    }
 270    return _cacheBytes;
 271  }
 272  
 273  // UTF-8 byte length of a string.
 274  export function byteLen(s) {
 275    return utf8Bytes(s).length;
 276  }
 277  
 278  // UTF-8 byte at byte position i.
 279  export function stringByteAt(s, i) {
 280    return utf8Bytes(s)[i];
 281  }
 282  
 283  // for range over string — returns (byteIndex, rune) using UTF-8.
 284  export function stringRange(s) {
 285    const bytes = utf8Bytes(s);
 286    return {
 287      $bytes: bytes,
 288      $pos: 0,
 289      next() {
 290        if (this.$pos >= this.$bytes.length) return [false, 0, 0];
 291        const start = this.$pos;
 292        const b0 = this.$bytes[this.$pos];
 293        let cp;
 294        if (b0 < 0x80) {
 295          cp = b0; this.$pos += 1;
 296        } else if (b0 < 0xE0) {
 297          cp = ((b0 & 0x1F) << 6) | (this.$bytes[this.$pos + 1] & 0x3F);
 298          this.$pos += 2;
 299        } else if (b0 < 0xF0) {
 300          cp = ((b0 & 0x0F) << 12) | ((this.$bytes[this.$pos + 1] & 0x3F) << 6) |
 301               (this.$bytes[this.$pos + 2] & 0x3F);
 302          this.$pos += 3;
 303        } else {
 304          cp = ((b0 & 0x07) << 18) | ((this.$bytes[this.$pos + 1] & 0x3F) << 12) |
 305               ((this.$bytes[this.$pos + 2] & 0x3F) << 6) | (this.$bytes[this.$pos + 3] & 0x3F);
 306          this.$pos += 4;
 307        }
 308        return [true, start, cp];
 309      }
 310    };
 311  }
 312  
 313  // String to byte slice.
 314  export function stringToBytes(s) {
 315    const bytes = _enc.encode(s);
 316    const arr = Array.from(bytes);
 317    return new Slice(arr, 0, arr.length, arr.length);
 318  }
 319  
 320  // Byte slice to string.
 321  export function bytesToString(sl) {
 322    if (typeof sl === 'string') return sl;
 323    const bytes = new Uint8Array(sl.$length);
 324    for (let i = 0; i < sl.$length; i++) {
 325      bytes[i] = sl.$array[sl.$offset + i];
 326    }
 327    return _dec.decode(bytes);
 328  }
 329  
 330  // String to rune slice.
 331  export function stringToRunes(s) {
 332    const runes = [...s].map(c => c.codePointAt(0));
 333    return new Slice(runes, 0, runes.length, runes.length);
 334  }
 335  
 336  // Rune slice to string.
 337  export function runesToString(sl) {
 338    let s = '';
 339    for (let i = 0; i < sl.$length; i++) {
 340      s += String.fromCodePoint(sl.$array[sl.$offset + i]);
 341    }
 342    return s;
 343  }
 344  
 345  // String concatenation (Go's + operator for strings).
 346  export function stringConcat(a, b) {
 347    return a + b;
 348  }
 349  
 350  // String comparison.
 351  export function stringCompare(a, b) {
 352    if (a < b) return -1;
 353    if (a > b) return 1;
 354    return 0;
 355  }
 356  
 357  // String slice: s[low:high] — operates on UTF-8 byte boundaries.
 358  export function stringSlice(s, low, high) {
 359    const bytes = utf8Bytes(s);
 360    if (low === undefined) low = 0;
 361    if (high === undefined) high = bytes.length;
 362    return _dec.decode(bytes.subarray(low, high));
 363  }
 364  
 365  // --- Deep equality for map keys ---
 366  
 367  function deepEqual(a, b) {
 368    if (a === b) return true;
 369    if (a === null || b === null) return a === b;
 370    if (typeof a !== typeof b) return false;
 371    if (typeof a !== 'object') return false;
 372  
 373    const keysA = Object.keys(a).filter(k => !k.startsWith('$'));
 374    const keysB = Object.keys(b).filter(k => !k.startsWith('$'));
 375    if (keysA.length !== keysB.length) return false;
 376    return keysA.every(k => deepEqual(a[k], b[k]));
 377  }
 378  
 379  // Clone a Go value type (array or struct). Slices get a new backing array copy.
 380  // Structs get shallow field copies (nested value types need recursive clone).
 381  export function cloneValue(v) {
 382    if (v === null || v === undefined || typeof v !== 'object') return v;
 383    if (v instanceof Slice) {
 384      const newArr = new Array(v.$capacity);
 385      for (let i = 0; i < v.$length; i++) {
 386        const elem = v.$array[v.$offset + i];
 387        newArr[i] = (typeof elem === 'object' && elem !== null) ? cloneValue(elem) : elem;
 388      }
 389      return new Slice(newArr, 0, v.$length, v.$capacity);
 390    }
 391    if (Array.isArray(v)) {
 392      return v.map(e => (typeof e === 'object' && e !== null) ? cloneValue(e) : e);
 393    }
 394    // Struct: shallow clone with recursive value cloning.
 395    const obj = {};
 396    for (const key of Object.keys(v)) {
 397      if (key.startsWith('$')) continue; // skip $get/$set/$value
 398      const val = v[key];
 399      obj[key] = (typeof val === 'object' && val !== null) ? cloneValue(val) : val;
 400    }
 401    return obj;
 402  }
 403  
 404  // 64-bit bitwise operations using Number arithmetic (safe up to 2^53).
 405  // JS bitwise operators truncate to 32-bit signed int; these preserve precision.
 406  export function int64or(x, y) {
 407    // Split into high (above bit 32) and low (below bit 32) parts.
 408    const xhi = Math.trunc(x / 0x100000000);
 409    const xlo = x - xhi * 0x100000000;
 410    const yhi = Math.trunc(y / 0x100000000);
 411    const ylo = y - yhi * 0x100000000;
 412    return ((xhi | yhi) >>> 0) * 0x100000000 + ((xlo | ylo) >>> 0);
 413  }
 414  
 415  export function int64and(x, y) {
 416    const xhi = Math.trunc(x / 0x100000000);
 417    const xlo = x - xhi * 0x100000000;
 418    const yhi = Math.trunc(y / 0x100000000);
 419    const ylo = y - yhi * 0x100000000;
 420    return ((xhi & yhi) >>> 0) * 0x100000000 + ((xlo & ylo) >>> 0);
 421  }
 422  
 423  export function int64xor(x, y) {
 424    const xhi = Math.trunc(x / 0x100000000);
 425    const xlo = x - xhi * 0x100000000;
 426    const yhi = Math.trunc(y / 0x100000000);
 427    const ylo = y - yhi * 0x100000000;
 428    return ((xhi ^ yhi) >>> 0) * 0x100000000 + ((xlo ^ ylo) >>> 0);
 429  }
 430