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