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