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