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