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