#define BLOB_IMPLEMENTATION #include "pit_internal.h" #include // #define DUMP_BUDDY #ifdef DUMP_BUDDY #include "buddy_debug.c" #endif #ifdef HEAP_CHECK void heap_check_fail(void *ptr, JSContext *ctx) { uint8_t *p = (uint8_t *)ptr; fprintf(stderr, "\n=== HEAP_CHECK: invalid heap pointer ===\n"); fprintf(stderr, " pointer: %p\n", ptr); fprintf(stderr, " heap: [%p, %p)\n", (void *)ctx->heap_base, (void *)ctx->heap_free); fprintf(stderr, " ct_pool: [%p, %p)\n", (void *)ctx->ct_base, (void *)ctx->ct_end); if (!JS_IsNull(ctx->reg_current_frame)) { fprintf(stderr, " stack trace:\n"); JSFrame *frame = (JSFrame *)JS_VALUE_GET_PTR(ctx->reg_current_frame); uint32_t pc = ctx->current_register_pc; int first = 1; while (frame) { objhdr_t hdr = *(objhdr_t *)JS_VALUE_GET_PTR(frame->function); if (objhdr_type(hdr) != OBJ_FUNCTION) break; JSFunction *fn = (JSFunction *)JS_VALUE_GET_PTR(frame->function); const char *name = NULL, *file = NULL; uint16_t line = 0; if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code) { JSCodeRegister *code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; file = code->filename_cstr; name = code->name_cstr; if (!first) pc = (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16); if (code->line_table && pc < code->instr_count) line = code->line_table[pc].line; } fprintf(stderr, " %s (%s:%u)\n", name ? name : "", file ? file : "", line); if (JS_IsNull(frame->caller)) break; frame = (JSFrame *)JS_VALUE_GET_PTR(frame->caller); first = 0; pc = 0; } } fprintf(stderr, "=======================================\n"); fflush(stderr); abort(); } #endif static inline JS_BOOL JS_IsInteger (JSValue v) { if (JS_VALUE_GET_TAG(v) == JS_TAG_INT) return true; if (JS_VALUE_GET_TAG(v) != JS_TAG_SHORT_FLOAT) return false; double d = JS_VALUE_GET_FLOAT64(v); return d == (double)(int64_t)d; } JSClassID js_class_id_alloc = JS_CLASS_INIT_COUNT; /* === Public API wrappers (non-inline, for quickjs.h declarations) === These delegate to the static inline versions in pit_internal.h. */ JS_BOOL JS_IsStone(JSValue v) { return mist_is_stone(v); } JS_BOOL JS_IsArray(JSValue v) { return mist_is_array(v); } JS_BOOL JS_IsRecord(JSValue v) { return mist_is_record(v); } JS_BOOL JS_IsFunction(JSValue v) { return mist_is_function(v); } JS_BOOL JS_IsBlob(JSValue v) { return mist_is_blob(v); } JS_BOOL JS_IsText(JSValue v) { return mist_is_text(v); } JS_BOOL JS_IsCode(JSValue v) { return mist_is_gc_object(v) && objhdr_type(*chase(v)) == OBJ_CODE; } JS_BOOL JS_IsForwarded(JSValue v) { return mist_is_gc_object(v) && objhdr_type(*chase(v)) == OBJ_FORWARD; } JS_BOOL JS_IsFrame(JSValue v) { return mist_is_gc_object(v) && objhdr_type(*chase(v)) == OBJ_FRAME; } uint64_t get_text_hash (JSText *text) { uint64_t len = text->length; size_t word_count = (len + 1) / 2; if (objhdr_s (text->hdr)) { /* Stoned text: check for cached hash */ if (text->hash != 0) return text->hash; /* Compute and cache hash */ text->hash = fash64_hash_words (text->packed, word_count, len); if (!text->hash) text->hash = 1; return text->hash; } else { /* Pre-text: compute hash on the fly */ return fash64_hash_words (text->packed, word_count, len); } } /* Pack UTF-32 characters into 64-bit words (2 chars per word) */ void pack_utf32_to_words (const uint32_t *utf32, uint32_t len, uint64_t *packed) { for (uint32_t i = 0; i < len; i += 2) { uint64_t hi = utf32[i]; uint64_t lo = (i + 1 < len) ? utf32[i + 1] : 0; packed[i / 2] = (hi << 32) | lo; } } /* Compare two packed UTF-32 texts for equality */ int text_equal (JSText *a, const uint64_t *packed_b, uint32_t len_b) { uint32_t len_a = (uint32_t)a->length; if (len_a != len_b) return 0; size_t word_count = (len_a + 1) / 2; return memcmp (a->packed, packed_b, word_count * sizeof (uint64_t)) == 0; } int JS_IsPretext (JSValue v) { if (!JS_IsText (v)) return 0; JSText *text = (JSText *)JS_VALUE_GET_PTR (v); return !objhdr_s (text->hdr); } JSValue *JS_PushGCRef (JSContext *ctx, JSGCRef *ref) { if (ref == ctx->top_gc_ref) { fprintf(stderr, "[warn] JS_PushGCRef duplicate top ref (non-fatal)\n"); return &ref->val; } ref->prev = ctx->top_gc_ref; ctx->top_gc_ref = ref; ref->val = JS_NULL; return &ref->val; } #ifdef DUMP_GC_REFS #ifdef HAVE_ASAN void __sanitizer_print_stack_trace(void); #else #include #endif static void dump_gc_ref_backtrace(const char *label) { fprintf(stderr, " [%s C backtrace]:\n", label); #ifdef HAVE_ASAN __sanitizer_print_stack_trace(); #else void *bt[16]; int n = backtrace(bt, 16); backtrace_symbols_fd(bt, n, 2); #endif fprintf(stderr, "\n"); } #endif JSValue JS_PopGCRef (JSContext *ctx, JSGCRef *ref) { if (ctx->top_gc_ref != ref) { fprintf(stderr, "WARN: JS_PopGCRef mismatch (expected %p, got %p)\n", (void *)ctx->top_gc_ref, (void *)ref); #ifdef DUMP_GC_REFS /* Walk the stack to show what's between top and the ref being popped */ int depth = 0; JSGCRef *walk = ctx->top_gc_ref; while (walk && walk != ref && depth < 16) { fprintf(stderr, " orphan #%d: ref=%p val=0x%llx\n", depth, (void *)walk, (unsigned long long)walk->val); walk = walk->prev; depth++; } if (walk == ref) fprintf(stderr, " (%d orphaned ref(s) between top and pop target)\n", depth); else fprintf(stderr, " (pop target not found in stack — chain is corrupt)\n"); dump_gc_ref_backtrace("pop site"); #endif } ctx->top_gc_ref = ref->prev; return ref->val; } JSValue *JS_AddGCRef (JSContext *ctx, JSGCRef *ref) { if (ref == ctx->last_gc_ref) { fprintf(stderr, "[warn] JS_AddGCRef duplicate tail ref (non-fatal)\n"); return &ref->val; } ref->prev = ctx->last_gc_ref; ctx->last_gc_ref = ref; ref->val = JS_NULL; return &ref->val; } void JS_DeleteGCRef (JSContext *ctx, JSGCRef *ref) { JSGCRef **pref, *ref1; pref = &ctx->last_gc_ref; for (;;) { ref1 = *pref; if (ref1 == NULL) abort (); if (ref1 == ref) { *pref = ref1->prev; break; } pref = &ref1->prev; } } /* JS_FRAME/JS_ROOT/JS_LOCAL helper functions */ JSGCRef *JS_GetGCFrame (JSContext *ctx) { return ctx->top_gc_ref; } JSLocalRef *JS_GetLocalFrame (JSContext *ctx) { return ctx->top_local_ref; } void JS_PushLocalRef (JSContext *ctx, JSLocalRef *ref) { assert(ref != ctx->top_local_ref && "JS_LOCAL used in a loop — same address pushed twice"); ref->prev = ctx->top_local_ref; ctx->top_local_ref = ref; } void JS_RestoreFrame (JSContext *ctx, JSGCRef *gc_frame, JSLocalRef *local_frame) { ctx->top_gc_ref = gc_frame; ctx->top_local_ref = local_frame; } void *ct_alloc (JSContext *ctx, size_t bytes, size_t align) { /* Align the request */ bytes = (bytes + align - 1) & ~(align - 1); /* Check if we have space in the constant text pool */ if (ctx->ct_base && ctx->ct_free + bytes <= ctx->ct_end) { void *ptr = ctx->ct_free; ctx->ct_free += bytes; return ptr; } /* No pool space - allocate a page */ size_t page_size = sizeof (CTPage) + bytes; CTPage *page = malloc (page_size); if (!page) return NULL; page->next = ctx->ct_pages; page->size = bytes; ctx->ct_pages = page; return page->data; } /* Free all constant text pool pages */ void ct_free_all (JSContext *ctx) { CTPage *page = ctx->ct_pages; while (page) { CTPage *next = page->next; free (page); page = next; } ctx->ct_pages = NULL; } int ct_resize (JSContext *ctx) { uint32_t new_size, new_threshold; uint32_t *new_hash; JSText **new_array; if (ctx->ct_size == 0) { /* Initial allocation */ new_size = CT_INITIAL_SIZE; } else { /* Double the size */ new_size = ctx->ct_size * 2; } new_threshold = new_size * 3 / 4; /* 75% load factor */ /* Allocate new hash table (use runtime malloc, not bump allocator) */ new_hash = js_malloc_rt (new_size * sizeof (uint32_t)); if (!new_hash) return -1; memset (new_hash, 0, new_size * sizeof (uint32_t)); /* Allocate new text array (one extra for 1-based indexing) */ new_array = js_malloc_rt ((new_size + 1) * sizeof (JSText *)); if (!new_array) { js_free_rt(new_hash); return -1; } memset (new_array, 0, (new_size + 1) * sizeof (JSText *)); /* Rehash existing entries */ if (ctx->ct_count > 0) { uint32_t mask = new_size - 1; for (uint32_t id = 1; id <= ctx->ct_count; id++) { JSText *text = ctx->ct_array[id]; new_array[id] = text; /* Compute hash and find slot */ uint64_t hash = get_text_hash (text); uint32_t slot = hash & mask; while (new_hash[slot] != 0) slot = (slot + 1) & mask; new_hash[slot] = id; } } /* Free old tables */ if (ctx->ct_hash) js_free_rt (ctx->ct_hash); if (ctx->ct_array) js_free_rt (ctx->ct_array); ctx->ct_hash = new_hash; ctx->ct_array = new_array; ctx->ct_size = new_size; ctx->ct_resize_threshold = new_threshold; return 0; } JSValue intern_text_to_value (JSContext *ctx, const uint32_t *utf32, uint32_t len) { /* Pack UTF-32 for hashing and comparison */ size_t word_count = (len + 1) / 2; uint64_t *packed = alloca (word_count * sizeof (uint64_t)); pack_utf32_to_words (utf32, len, packed); uint64_t hash = fash64_hash_words (packed, word_count, len); /* Look up in hash table */ uint32_t mask = ctx->ct_size - 1; uint32_t slot = hash & mask; while (ctx->ct_hash[slot] != 0) { uint32_t id = ctx->ct_hash[slot]; JSText *existing = ctx->ct_array[id]; if (text_equal (existing, packed, len)) { /* Found existing entry */ return JS_MKPTR (existing); } slot = (slot + 1) & mask; } /* Not found - create new entry */ if (ctx->ct_count >= ctx->ct_resize_threshold) { if (ct_resize (ctx) < 0) return JS_NULL; /* OOM */ /* Recompute slot after resize */ mask = ctx->ct_size - 1; slot = hash & mask; while (ctx->ct_hash[slot] != 0) slot = (slot + 1) & mask; } /* Allocate JSText in constant text pool */ size_t text_size = sizeof (JSText) + word_count * sizeof (uint64_t); JSText *text = ct_alloc (ctx, text_size, 8); if (!text) return JS_NULL; /* OOM */ /* Initialize the text */ text->hdr = objhdr_make (len, OBJ_TEXT, false, false, false, true); /* s=1 for stoned */ text->length = len; text->hash = hash; memcpy (text->packed, packed, word_count * sizeof (uint64_t)); /* Add to intern table */ uint32_t new_id = ++ctx->ct_count; ctx->ct_hash[slot] = new_id; ctx->ct_array[new_id] = text; return JS_MKPTR (text); } JSValue js_key_new (JSContext *ctx, const char *str) { size_t len = strlen (str); /* Try immediate ASCII first (≤7 ASCII chars) */ if (len <= MIST_ASCII_MAX_LEN) { JSValue imm = MIST_TryNewImmediateASCII (str, len); if (!JS_IsNull (imm)) return imm; } /* Check length limit */ if (len > JS_KEY_MAX_LEN) return JS_NULL; /* Convert UTF-8 to UTF-32 for interning - use stack buffer */ const uint8_t *p = (const uint8_t *)str; const uint8_t *p_end = p + len; uint32_t utf32_buf[JS_KEY_MAX_LEN]; uint32_t utf32_len = 0; while (p < p_end && utf32_len < JS_KEY_MAX_LEN) { uint32_t c; const uint8_t *p_next; if (*p < 0x80) { c = *p++; } else { c = unicode_from_utf8 (p, p_end - p, &p_next); if (c == (uint32_t)-1) { c = 0xFFFD; /* replacement char for invalid UTF-8 */ p++; } else { p = p_next; } } utf32_buf[utf32_len++] = c; } return intern_text_to_value (ctx, utf32_buf, utf32_len); } /* JS_NewAtomString - creates JSValue string from C string (for compatibility) */ JSValue JS_NewAtomString (JSContext *ctx, const char *str) { return js_key_new (ctx, str); } JSValue js_key_new_len (JSContext *ctx, const char *str, size_t len) { /* Try immediate ASCII first (≤7 ASCII chars) */ if (len <= MIST_ASCII_MAX_LEN) { JSValue imm = MIST_TryNewImmediateASCII (str, len); if (!JS_IsNull (imm)) return imm; } /* Check length limit */ if (len > JS_KEY_MAX_LEN) return JS_NULL; /* Convert UTF-8 to UTF-32 for interning */ const uint8_t *p = (const uint8_t *)str; const uint8_t *p_end = p + len; uint32_t utf32_buf[JS_KEY_MAX_LEN]; uint32_t utf32_len = 0; while (p < p_end && utf32_len < JS_KEY_MAX_LEN) { uint32_t c; const uint8_t *p_next; if (*p < 0x80) { c = *p++; } else { c = unicode_from_utf8 (p, p_end - p, &p_next); if (c == (uint32_t)-1) { c = 0xFFFD; p++; } else { p = p_next; } } utf32_buf[utf32_len++] = c; } return intern_text_to_value (ctx, utf32_buf, utf32_len); } uint64_t js_key_hash (JSValue key) { if (MIST_IsImmediateASCII (key)) { /* Hash immediate ASCII the same way as heap strings for consistency */ int len = MIST_GetImmediateASCIILen (key); if (len == 0) return 1; size_t word_count = (len + 1) / 2; uint64_t packed[4]; /* Max 7 chars = 4 words */ for (size_t i = 0; i < word_count; i++) { uint32_t c0 = (i * 2 < (size_t)len) ? MIST_GetImmediateASCIIChar (key, i * 2) : 0; uint32_t c1 = (i * 2 + 1 < (size_t)len) ? MIST_GetImmediateASCIIChar (key, i * 2 + 1) : 0; packed[i] = ((uint64_t)c0 << 32) | c1; } uint64_t h = fash64_hash_words (packed, word_count, len); return h ? h : 1; } if (!JS_IsPtr (key)) return 0; /* Use chase to follow forwarding pointers */ void *ptr = chase (key); objhdr_t hdr = *(objhdr_t *)ptr; uint8_t type = objhdr_type (hdr); if (type == OBJ_TEXT) { /* For JSText (stoned strings), use get_text_hash */ JSText *text = (JSText *)ptr; return get_text_hash (text); } if (type == OBJ_RECORD) { JSRecord *rec = (JSRecord *)ptr; uint32_t rec_id = REC_GET_REC_ID(rec); if (rec_id == 0) return 0; return fash64_hash_one (rec_id); } return 0; } JS_BOOL js_key_equal (JSValue a, JSValue b) { if (a == b) return TRUE; /* Use chase to follow forwarding pointers */ if (MIST_IsImmediateASCII (a)) { if (MIST_IsImmediateASCII (b)) return FALSE; if (!JS_IsPtr (b)) return FALSE; JSText *tb = (JSText *)chase (b); if (objhdr_type (tb->hdr) != OBJ_TEXT) return FALSE; return JSText_equal_ascii (tb, a); } if (MIST_IsImmediateASCII (b)) { if (!JS_IsPtr (a)) return FALSE; JSText *ta = (JSText *)chase (a); if (objhdr_type (ta->hdr) != OBJ_TEXT) return FALSE; return JSText_equal_ascii (ta, b); } if (!JS_IsPtr (a) || !JS_IsPtr (b)) return FALSE; void *pa = chase (a); void *pb = chase (b); objhdr_t ha = *(objhdr_t *)pa; objhdr_t hb = *(objhdr_t *)pb; uint8_t type_a = objhdr_type (ha); uint8_t type_b = objhdr_type (hb); if (type_a != type_b) return FALSE; if (type_a == OBJ_RECORD) return FALSE; /* pointer equality handled above */ if (type_a == OBJ_TEXT) return JSText_equal ((JSText *)pa, (JSText *)pb); return FALSE; } JS_BOOL js_key_equal_str (JSValue a, const char *str) { size_t len = strlen (str); if (MIST_IsImmediateASCII (a)) { int imm_len = MIST_GetImmediateASCIILen (a); if ((size_t)imm_len != len) return FALSE; for (int i = 0; i < imm_len; i++) { if (MIST_GetImmediateASCIIChar (a, i) != str[i]) return FALSE; } return TRUE; } if (!JS_IsPtr (a)) return FALSE; JSText *ta = (JSText *)chase (a); if (objhdr_type (ta->hdr) != OBJ_TEXT) return FALSE; uint64_t txt_len = objhdr_cap56 (ta->hdr); if (txt_len != len) return FALSE; /* Compare character by character (UTF-32 vs ASCII) */ for (size_t i = 0; i < len; i++) { if (string_get (ta, i) != (uint32_t)(unsigned char)str[i]) return FALSE; } return TRUE; } int rec_find_slot (JSRecord *rec, JSValue k) { uint64_t mask = objhdr_cap56 (rec->mist_hdr); uint64_t h64 = js_key_hash (k); uint64_t slot = (h64 & mask); if (slot == 0) slot = 1; /* slot 0 is reserved */ uint64_t first_tomb = 0; for (uint64_t i = 0; i <= mask; i++) { JSValue slot_key = rec->slots[slot].key; if (rec_key_is_empty (slot_key)) { /* Empty slot - key not found */ return first_tomb ? -(int)first_tomb : -(int)slot; } if (rec_key_is_tomb (slot_key)) { /* Tombstone - remember for insertion but keep searching */ if (first_tomb == 0) first_tomb = slot; } else if (js_key_equal (slot_key, k)) { /* Found it */ return (int)slot; } slot = (slot + 1) & mask; if (slot == 0) slot = 1; } /* Table full (shouldn't happen with proper load factor) */ return first_tomb ? -(int)first_tomb : -1; } JSValue rec_get (JSContext *ctx, JSRecord *rec, JSValue k) { if (rec_key_is_empty (k) || rec_key_is_tomb (k)) return JS_NULL; /* Walk prototype chain */ JSRecord *p = rec; while (p) { int slot = rec_find_slot (p, k); if (slot > 0) { return p->slots[slot].val; } p = JS_IsNull (p->proto) ? NULL : JS_VALUE_GET_RECORD (p->proto); } return JS_NULL; } size_t gc_object_size (void *ptr); /* forward declaration for growth-forward size storage */ int rec_resize (JSContext *ctx, JSValue *pobj, uint64_t new_mask) { /* Protect the source object with a GC ref in case js_malloc triggers GC */ JSGCRef obj_ref; JS_AddGCRef (ctx, &obj_ref); obj_ref.val = *pobj; JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (*pobj); uint64_t old_mask = objhdr_cap56 (rec->mist_hdr); /* Allocate new record with larger capacity - may trigger GC! */ size_t slots_size = sizeof (slot) * (new_mask + 1); size_t total_size = sizeof (JSRecord) + slots_size; if (total_size >= 100000) { fprintf(stderr, "LARGE_REC_RESIZE: new_mask=%llu total=%zu old_mask=%llu\n", (unsigned long long)new_mask, total_size, (unsigned long long)old_mask); } JSRecord *new_rec = js_malloc (ctx, total_size); if (!new_rec) { JS_DeleteGCRef (ctx, &obj_ref); return -1; } /* Re-get record from GC ref - it may have moved during GC */ rec = (JSRecord *)JS_VALUE_GET_OBJ (obj_ref.val); old_mask = objhdr_cap56 (rec->mist_hdr); /* Initialize new record */ new_rec->mist_hdr = objhdr_make (new_mask, OBJ_RECORD, false, false, false, false); new_rec->proto = rec->proto; /* JSValue — copies directly */ new_rec->len = 0; /* Initialize all slots to empty */ for (uint64_t i = 0; i <= new_mask; i++) { new_rec->slots[i].key = JS_NULL; new_rec->slots[i].val = JS_NULL; } /* Copy slot[0] (class_id, rec_id, opaque) */ new_rec->slots[0].key = rec->slots[0].key; new_rec->slots[0].val = rec->slots[0].val; /* Rehash all valid entries from old to new */ for (uint64_t i = 1; i <= old_mask; i++) { JSValue k = rec->slots[i].key; if (!rec_key_is_empty (k) && !rec_key_is_tomb (k)) { /* Insert into new record using linear probing */ uint64_t h64 = js_key_hash (k); uint64_t slot = (h64 & new_mask); if (slot == 0) slot = 1; while (!rec_key_is_empty (new_rec->slots[slot].key)) { slot = (slot + 1) & new_mask; if (slot == 0) slot = 1; } new_rec->slots[slot].key = k; new_rec->slots[slot].val = rec->slots[i].val; new_rec->len++; } } /* Install forward header at old location so stale references can find the new record */ size_t old_size = gc_object_size (rec); rec->mist_hdr = objhdr_make_fwd (new_rec); *((size_t *)((uint8_t *)rec + sizeof (objhdr_t))) = old_size; /* Update caller's JSValue to point to new record */ *pobj = JS_MKPTR (new_rec); JS_DeleteGCRef (ctx, &obj_ref); return 0; } int rec_set_own (JSContext *ctx, JSValue *pobj, JSValue k, JSValue val) { if (rec_key_is_empty (k) || rec_key_is_tomb (k)) { return -1; } JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (*pobj); int slot = rec_find_slot (rec, k); if (slot > 0) { /* Existing key - replace value */ rec->slots[slot].val = val; return 0; } /* New key - check if resize needed (75% load factor) */ uint32_t mask = (uint32_t)objhdr_cap56 (rec->mist_hdr); if ((rec->len + 1) * 4 > mask * 3) { /* Over 75% load factor - resize. Protect key and value in case GC runs. */ JSGCRef k_ref, val_ref; JS_AddGCRef (ctx, &k_ref); JS_AddGCRef (ctx, &val_ref); k_ref.val = k; val_ref.val = val; uint32_t new_mask = (mask + 1) * 2 - 1; if (rec_resize (ctx, pobj, new_mask) < 0) { JS_DeleteGCRef (ctx, &val_ref); JS_DeleteGCRef (ctx, &k_ref); return -1; } /* Re-get values after resize (they may have moved during GC) */ k = k_ref.val; val = val_ref.val; JS_DeleteGCRef (ctx, &val_ref); JS_DeleteGCRef (ctx, &k_ref); /* Re-get rec after resize (pobj now points to new record) */ rec = (JSRecord *)JS_VALUE_GET_OBJ (*pobj); /* Re-find slot after resize */ slot = rec_find_slot (rec, k); } /* Insert at -slot */ int insert_slot = -slot; rec->slots[insert_slot].key = k; rec->slots[insert_slot].val = val; rec->len++; #ifdef VALIDATE_GC /* Verify the just-inserted key is findable */ int verify = rec_find_slot (rec, k); if (verify <= 0) { fprintf (stderr, "rec_set_own: INSERTED KEY NOT FINDABLE! slot=%d insert_slot=%d len=%u\n", verify, insert_slot, (uint32_t)rec->len); abort (); } #endif return 0; } JSRecord *js_new_record_class (JSContext *ctx, uint32_t initial_mask, JSClassID class_id) { if (initial_mask == 0) initial_mask = JS_RECORD_INITIAL_MASK; /* Allocate record + inline slots in one bump allocation */ size_t slots_size = sizeof (slot) * (initial_mask + 1); size_t total_size = sizeof (JSRecord) + slots_size; JSRecord *rec = js_malloc (ctx, total_size); if (!rec) return NULL; rec->mist_hdr = objhdr_make (initial_mask, OBJ_RECORD, false, false, false, false); rec->proto = JS_NULL; rec->len = 0; /* Initialize all slots to empty (JS_NULL) */ for (uint32_t i = 0; i <= initial_mask; i++) { rec->slots[i].key = JS_NULL; rec->slots[i].val = JS_NULL; } /* slots[0] is reserved: store class_id (low 32) and rec_id (high 32) in key */ uint32_t rec_id = ++ctx->rec_key_next; rec->slots[0].key = (JSValue)class_id | ((JSValue)rec_id << 32); rec->slots[0].val = 0; /* opaque pointer, initially NULL */ return rec; } int JS_SetPropertyInternal (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val) { if (!JS_IsRecord (this_obj)) { return -1; } /* Use a local copy that rec_set_own can update if resize happens */ JSValue obj = this_obj; return rec_set_own (ctx, &obj, prop, val); } void *js_malloc_rt (size_t size) { return malloc(size); } void js_free_rt (void *ptr) { free (ptr); } void *js_realloc_rt (void *ptr, size_t size) { return realloc (ptr, size); } void *js_mallocz_rt (size_t size) { void *ptr; ptr = js_malloc_rt (size); if (!ptr) return NULL; return memset (ptr, 0, size); } char *js_strdup_rt (const char *str) { if (!str) return NULL; size_t len = strlen(str) + 1; char *dup = js_malloc_rt(len); if (dup) memcpy(dup, str, len); return dup; } /* Throw out of memory in case of error */ void *js_malloc (JSContext *ctx, size_t size) { /* Align size to 8 bytes */ size = (size + 7) & ~7; #ifdef FORCE_GC_AT_MALLOC /* Force GC on every allocation for testing */ { int need_space = (uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end; if (ctx_gc(ctx, need_space, size) < 0) { JS_RaiseOOM(ctx); return NULL; } /* If still no room after GC, grow and retry (same logic as normal path) */ if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { size_t live = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base); size_t need = live + size; size_t ns = ctx->current_block_size; while (ns < need && ns < buddy_max_block(&ctx->rt->buddy)) ns *= 2; ctx->next_block_size = ns; if (ctx_gc (ctx, 1, size) < 0) { JS_RaiseOOM(ctx); return NULL; } ctx->next_block_size = ctx->current_block_size; if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { JS_RaiseOOM(ctx); return NULL; } } } #else /* Check if we have space in current block */ if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { /* Trigger GC to reclaim memory */ if (ctx_gc (ctx, 1, size) < 0) { JS_RaiseOOM(ctx); return NULL; } /* Re-check after GC — if still no room, grow and retry. The second GC is cheap: data was just compacted so there is almost no garbage to skip. */ if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { size_t live = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base); size_t need = live + size; size_t ns = ctx->current_block_size; while (ns < need && ns < buddy_max_block(&ctx->rt->buddy)) ns *= 2; #ifdef DUMP_GC printf (" growing %zu -> %zu for %zu byte alloc (live %zu)\n", ctx->current_block_size, ns, size, live); fflush (stdout); #endif ctx->next_block_size = ns; if (ctx_gc (ctx, 1, size) < 0) { JS_RaiseOOM(ctx); return NULL; } /* The retry pass relocates compacted data — 0% recovery is expected. Reset next_block_size so the poor-recovery heuristic inside ctx_gc doesn't cascade into further unnecessary doubling. */ ctx->next_block_size = ctx->current_block_size; if ((uint8_t *)ctx->heap_free + size > (uint8_t *)ctx->heap_end) { JS_RaiseOOM(ctx); return NULL; } } } #endif void *ptr = ctx->heap_free; ctx->heap_free = (uint8_t *)ctx->heap_free + size; return ptr; } /* Throw out of memory in case of error */ void *js_mallocz (JSContext *ctx, size_t size) { void *ptr = js_malloc (ctx, size); if (!ptr) return NULL; return memset (ptr, 0, size); } /* Parser memory functions - use system allocator to avoid GC issues */ /* Forward declarations for ppretext_end */ JSText *js_alloc_string (JSContext *ctx, int max_len); static inline void string_put (JSText *p, int idx, uint32_t c); PPretext *ppretext_init (int capacity) { PPretext *p = pjs_malloc (sizeof (PPretext)); if (!p) return NULL; if (capacity <= 0) capacity = 32; p->data = pjs_malloc (capacity * sizeof (uint32_t)); if (!p->data) { pjs_free (p); return NULL; } p->len = 0; p->cap = capacity; return p; } void ppretext_free (PPretext *p) { if (p) { pjs_free (p->data); pjs_free (p); } } no_inline PPretext *ppretext_realloc (PPretext *p, int new_cap) { uint32_t *new_data = pjs_realloc (p->data, new_cap * sizeof (uint32_t)); if (!new_data) return NULL; p->data = new_data; p->cap = new_cap; return p; } PPretext *ppretext_putc (PPretext *p, uint32_t c) { if (p->len >= p->cap) { int new_cap = max_int (p->len + 1, p->cap * 3 / 2); if (!ppretext_realloc (p, new_cap)) return NULL; } p->data[p->len++] = c; return p; } JSValue ppretext_end (JSContext *ctx, PPretext *p) { if (!p) return JS_EXCEPTION; int len = p->len; if (len == 0) { ppretext_free (p); return JS_KEY_empty; } /* Allocate heap string (single allocation) */ JSText *str = js_alloc_string (ctx, len); if (!str) { ppretext_free (p); return JS_EXCEPTION; } for (int i = 0; i < len; i++) { string_put (str, i, p->data[i]); } str->length = len; str->hash = 0; str->hdr = objhdr_set_s (str->hdr, true); ppretext_free (p); return JS_MKPTR (str); } /* Append a JSValue string to a PPretext (parser pretext) */ PPretext *ppretext_append_jsvalue (PPretext *p, JSValue str) { int len = js_string_value_len (str); for (int i = 0; i < len; i++) { uint32_t c = js_string_value_get (str, i); p = ppretext_putc (p, c); if (!p) return NULL; } return p; } /* Append an integer to a PPretext */ PPretext *ppretext_append_int (PPretext *p, int n) { char buf[16]; int len = snprintf (buf, sizeof (buf), "%d", n); for (int i = 0; i < len; i++) { p = ppretext_putc (p, buf[i]); if (!p) return NULL; } return p; } /* Convert a JSValue string to a property key. Returns the value as-is for immediates and heap texts. No allocation — cannot trigger GC. */ JSValue js_key_from_string (JSContext *ctx, JSValue val) { if (MIST_IsImmediateASCII (val)) return val; if (JS_IsText (val)) return val; return JS_NULL; } static JSClass const js_std_class_def[] = { { NULL, NULL, NULL }, /* placeholder (was JS_CLASS_ERROR, now unused) */ { "regexp", js_regexp_finalizer, NULL }, /* JS_CLASS_REGEXP */ { "blob", NULL, NULL }, /* JS_CLASS_BLOB - registered separately */ }; static int init_class_range (JSContext *ctx, JSClass const *tab, int start, int count) { JSClassDef cm_s, *cm = &cm_s; int i, class_id; for (i = 0; i < count; i++) { class_id = i + start; memset (cm, 0, sizeof (*cm)); cm->finalizer = tab[i].finalizer; if (JS_NewClass1 (ctx, class_id, cm, tab[i].class_name) < 0) return -1; } return 0; } /* Create a new buddy pool of the given size */ static BuddyPool *buddy_pool_new(size_t pool_size) { BuddyPool *pool = js_mallocz_rt(sizeof(BuddyPool)); if (!pool) return NULL; pool->base = mmap(NULL, pool_size, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0); if (pool->base == MAP_FAILED) { js_free_rt(pool); return NULL; } pool->total_size = pool_size; /* Compute max_order = log2(pool_size) */ uint8_t order = BUDDY_MIN_ORDER; while ((1ULL << order) < pool_size) order++; pool->max_order = order; pool->alloc_count = 0; for (int i = 0; i < BUDDY_MAX_LEVELS; i++) pool->free_lists[i] = NULL; /* One free block spanning the entire pool */ BuddyBlock *blk = (BuddyBlock *)pool->base; blk->order = pool->max_order; blk->is_free = 1; blk->next = NULL; blk->prev = NULL; int level = pool->max_order - BUDDY_MIN_ORDER; pool->free_lists[level] = blk; return pool; } /* Try to allocate from a specific pool */ static void *buddy_pool_alloc(BuddyPool *pool, uint8_t order) { int level = order - BUDDY_MIN_ORDER; int levels = pool->max_order - BUDDY_MIN_ORDER + 1; /* Search for a free block at this level or higher */ int found = -1; for (int i = level; i < levels; i++) { if (pool->free_lists[i]) { found = i; break; } } if (found < 0) return NULL; /* Remove the block from its free list */ BuddyBlock *blk = pool->free_lists[found]; pool->free_lists[found] = blk->next; if (blk->next) blk->next->prev = NULL; /* Split down to the target level */ uint8_t cur_order = BUDDY_MIN_ORDER + found; while (cur_order > order) { cur_order--; int split_level = cur_order - BUDDY_MIN_ORDER; /* Right half becomes a free buddy */ uint8_t *right = (uint8_t *)blk + (1ULL << cur_order); BuddyBlock *rblk = (BuddyBlock *)right; rblk->order = cur_order; rblk->is_free = 1; rblk->prev = NULL; rblk->next = pool->free_lists[split_level]; if (rblk->next) rblk->next->prev = rblk; pool->free_lists[split_level] = rblk; } pool->alloc_count++; return (void *)blk; } /* Allocate a block of the given size */ static void *buddy_alloc(BuddyAllocator *b, size_t size) { /* Lazy-init: create first pool on demand */ if (!b->pools) { BuddyPool *pool = buddy_pool_new(b->initial_size); if (!pool) { fprintf(stderr, "buddy_alloc: initial pool mmap failed\n"); abort(); } pool->next = NULL; b->pools = pool; b->total_mapped = pool->total_size; b->next_pool_size = b->initial_size * 2; } /* Compute order from size */ uint8_t order = BUDDY_MIN_ORDER; while ((1ULL << order) < size) order++; /* Walk pools, try to allocate from each */ for (BuddyPool *pool = b->pools; pool; pool = pool->next) { if (order <= pool->max_order) { void *ptr = buddy_pool_alloc(pool, order); if (ptr) { #ifdef DUMP_BUDDY buddy_dump(pool, "alloc", (uint8_t *)ptr, order); #endif return ptr; } } } /* No pool could satisfy — create a new one */ size_t new_pool_size = b->next_pool_size; size_t needed = 1ULL << order; if (new_pool_size < needed) new_pool_size = needed; /* Check cap */ if (b->cap && b->total_mapped + new_pool_size > b->cap) return NULL; BuddyPool *pool = buddy_pool_new(new_pool_size); if (!pool) return NULL; /* Prepend to list */ pool->next = b->pools; b->pools = pool; b->total_mapped += pool->total_size; b->next_pool_size = new_pool_size * 2; void *ptr = buddy_pool_alloc(pool, order); #ifdef DUMP_BUDDY if (ptr) buddy_dump(pool, "alloc", (uint8_t *)ptr, order); #endif return ptr; } /* Free a block and coalesce with its buddy if possible */ static void buddy_free(BuddyAllocator *b, void *ptr, size_t size) { uint8_t order = BUDDY_MIN_ORDER; while ((1ULL << order) < size) order++; /* Find the pool containing ptr */ BuddyPool *pool = NULL; BuddyPool **prev_link = &b->pools; for (BuddyPool *p = b->pools; p; prev_link = &p->next, p = p->next) { if ((uint8_t *)ptr >= p->base && (uint8_t *)ptr < p->base + p->total_size) { pool = p; break; } } if (!pool) { fprintf(stderr, "buddy_free: ptr %p not in any pool\n", ptr); abort(); } uint8_t *block = (uint8_t *)ptr; #ifdef DUMP_BUDDY buddy_dump(pool, "free", block, order); #endif /* Coalesce with buddy */ while (order < pool->max_order) { size_t offset = block - pool->base; size_t buddy_offset = offset ^ (1ULL << order); uint8_t *buddy_addr = pool->base + buddy_offset; /* Check if buddy is on the free list at this level */ int level = order - BUDDY_MIN_ORDER; BuddyBlock *buddy = NULL; for (BuddyBlock *p = pool->free_lists[level]; p; p = p->next) { if ((uint8_t *)p == buddy_addr) { buddy = p; break; } } if (!buddy) break; /* Remove buddy from free list */ if (buddy->prev) buddy->prev->next = buddy->next; else pool->free_lists[level] = buddy->next; if (buddy->next) buddy->next->prev = buddy->prev; /* Merge: take the lower address */ if (buddy_addr < block) block = buddy_addr; order++; } /* Add merged block to free list */ int level = order - BUDDY_MIN_ORDER; BuddyBlock *blk = (BuddyBlock *)block; blk->order = order; blk->is_free = 1; blk->prev = NULL; blk->next = pool->free_lists[level]; if (blk->next) blk->next->prev = blk; pool->free_lists[level] = blk; pool->alloc_count--; /* Release empty pools (but keep at least one) */ if (pool->alloc_count == 0 && b->pools != pool) { *prev_link = pool->next; b->total_mapped -= pool->total_size; munmap(pool->base, pool->total_size); js_free_rt(pool); } else if (pool->alloc_count == 0 && pool->next) { /* pool is the head but not the only one — unlink it */ b->pools = pool->next; b->total_mapped -= pool->total_size; munmap(pool->base, pool->total_size); js_free_rt(pool); } } /* Destroy buddy allocator and free all pools */ void buddy_destroy (BuddyAllocator *b) { BuddyPool *pool = b->pools; while (pool) { BuddyPool *next = pool->next; munmap(pool->base, pool->total_size); js_free_rt(pool); pool = next; } b->pools = NULL; b->total_mapped = 0; } /* Maximum block size the buddy allocator can hand out */ static size_t buddy_max_block(BuddyAllocator *b) { return b->cap ? b->cap : SIZE_MAX; } /* ============================================================ Heap block allocation wrappers ============================================================ */ static void *heap_block_alloc(JSRuntime *rt, size_t size) { return buddy_alloc(&rt->buddy, size); } static void heap_block_free(JSRuntime *rt, void *ptr, size_t size) { buddy_free(&rt->buddy, ptr, size); } /* ============================================================ Bump Allocator and Cheney GC ============================================================ */ /* Forward declarations for GC helpers */ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size); JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end); void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end); size_t gc_object_size (void *ptr); /* Alignment for GC object sizes - must match max alignment requirement */ #define GC_ALIGN 8 static inline size_t gc_align_up (size_t n) { return (n + GC_ALIGN - 1) & ~(GC_ALIGN - 1); } /* Get size of a heap object based on its type */ size_t gc_object_size (void *ptr) { objhdr_t hdr = *(objhdr_t *)ptr; uint8_t type = objhdr_type (hdr); uint64_t cap = objhdr_cap56 (hdr); switch (type) { case OBJ_ARRAY: { /* JSArray + inline values array. Cap is element capacity. */ size_t values_size = sizeof (JSValue) * cap; return gc_align_up (sizeof (JSArray) + values_size); } case OBJ_TEXT: { /* JSText: header + pad + hdr + length + packed chars */ size_t word_count = (cap + 1) / 2; return gc_align_up (sizeof (JSText) + word_count * sizeof (uint64_t)); } case OBJ_RECORD: { /* JSRecord + inline tab. Cap is mask, so tab size is mask+1 entries. */ size_t tab_size = sizeof (JSRecordEntry) * (cap + 1); return gc_align_up (sizeof (JSRecord) + tab_size); } case OBJ_BLOB: { /* JSBlob + inline bit data. cap56 = capacity in bits. */ size_t word_count = (cap + 63) / 64; return gc_align_up (sizeof (JSBlob) + word_count * sizeof (word_t)); } case OBJ_FUNCTION: return gc_align_up (sizeof (JSFunction)); case OBJ_FRAME: { /* JSFrame + slots array. cap56 stores slot count */ uint64_t slot_count = cap; return gc_align_up (sizeof (JSFrame) + slot_count * sizeof (JSValue)); } default: /* Unknown type - fatal error, heap is corrupt or scan desync'd */ fflush(stdout); fprintf (stderr, "gc_object_size: unknown type %d at %p, hdr=0x%llx cap=%llu\n", type, ptr, (unsigned long long)hdr, (unsigned long long)cap); /* Dump surrounding memory for debugging */ uint64_t *words = (uint64_t *)ptr; fprintf (stderr, " words: [0]=0x%llx [1]=0x%llx [2]=0x%llx [3]=0x%llx\n", (unsigned long long)words[0], (unsigned long long)words[1], (unsigned long long)words[2], (unsigned long long)words[3]); fflush(stderr); abort (); } } static inline int ptr_in_range (void *p, uint8_t *b, uint8_t *e) { uint8_t *q = (uint8_t *)p; return q >= b && q < e; } JSValue gc_copy_value (JSContext *ctx, JSValue v, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) { if (!JS_IsPtr (v)) return v; for (;;) { void *ptr = JS_VALUE_GET_PTR (v); if (!ptr_in_range (ptr, from_base, from_end)) return v; objhdr_t *hdr_ptr = (objhdr_t *)ptr; objhdr_t hdr = *hdr_ptr; uint8_t type = objhdr_type (hdr); if (type == OBJ_FORWARD) { void *t = objhdr_fwd_ptr (hdr); /* If it already points into to-space, it's a GC forward. */ if (ptr_in_range (t, to_base, *to_free)) return JS_MKPTR (t); /* Otherwise it's a growth-forward (still in from-space). Chase. */ v = JS_MKPTR (t); continue; } if (type != OBJ_ARRAY && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME && type != OBJ_BLOB) { fprintf (stderr, "gc_copy_value: invalid type %d at %p (hdr=0x%llx)\n", type, ptr, (unsigned long long)hdr); fflush (stderr); abort (); } size_t size = gc_object_size (hdr_ptr); size_t copy_size = size; uint16_t new_cap = 0; /* Frame shortening: returned frames (caller == JS_NULL) only need [this][args][closure_locals] — shrink during copy. */ if (0 && type == OBJ_FRAME) { JSFrame *f = (JSFrame *)hdr_ptr; if (JS_IsNull (f->caller) && JS_IsPtr (f->function)) { /* fn may be forwarded, but kind (offset 18) and u.cell.code (offset 24) are past the 16 bytes overwritten by fwd+size. */ JSFunction *fn = (JSFunction *)JS_VALUE_GET_PTR (f->function); if (fn->kind == JS_FUNC_KIND_REGISTER) { JSCode *jc = (JSCode *)JS_VALUE_GET_PTR (FN_READ_CODE(fn)); if (jc && jc->kind == JS_CODE_KIND_REGISTER && jc->u.reg.code && jc->u.reg.code->nr_close_slots > 0) { uint16_t cs = 1 + jc->u.reg.code->arity + jc->u.reg.code->nr_close_slots; uint64_t orig = objhdr_cap56 (f->header); if (cs < orig) { new_cap = cs; copy_size = gc_align_up (sizeof (JSFrame) + cs * sizeof (JSValue)); } } } } } if (*to_free + copy_size > to_end) { fprintf (stderr, "gc_copy_value: out of to-space, need %zu bytes\n", copy_size); abort (); } void *new_ptr = *to_free; memcpy (new_ptr, hdr_ptr, copy_size); *to_free += copy_size; if (new_cap > 0) ((JSFrame *)new_ptr)->header = objhdr_set_cap56 (((JSFrame *)new_ptr)->header, new_cap); /* Stash ORIGINAL size for from-space linear walks */ *hdr_ptr = objhdr_make_fwd (new_ptr); *((size_t *)((uint8_t *)hdr_ptr + sizeof (objhdr_t))) = size; return JS_MKPTR (new_ptr); } } /* Recursively scan a code tree's cpools to arbitrary nesting depth */ static void gc_scan_code_tree (JSContext *ctx, JSCodeRegister *code, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) { for (uint32_t i = 0; i < code->cpool_count; i++) code->cpool[i] = gc_copy_value (ctx, code->cpool[i], from_base, from_end, to_base, to_free, to_end); code->name = gc_copy_value (ctx, code->name, from_base, from_end, to_base, to_free, to_end); for (uint32_t i = 0; i < code->func_count; i++) if (code->functions[i]) gc_scan_code_tree (ctx, code->functions[i], from_base, from_end, to_base, to_free, to_end); } /* Scan a copied object and update its internal references */ void gc_scan_object (JSContext *ctx, void *ptr, uint8_t *from_base, uint8_t *from_end, uint8_t *to_base, uint8_t **to_free, uint8_t *to_end) { objhdr_t hdr = *(objhdr_t *)ptr; uint8_t type = objhdr_type (hdr); switch (type) { case OBJ_ARRAY: { JSArray *arr = (JSArray *)ptr; for (uint32_t i = 0; i < arr->len; i++) { arr->values[i] = gc_copy_value (ctx, arr->values[i], from_base, from_end, to_base, to_free, to_end); } break; } case OBJ_RECORD: { JSRecord *rec = (JSRecord *)ptr; uint32_t mask = (uint32_t)objhdr_cap56 (rec->mist_hdr); #ifdef DUMP_GC_DETAIL printf(" record: slots=%u used=%u proto=0x%llx\n", mask + 1, (uint32_t)rec->len, (unsigned long long)rec->proto); fflush(stdout); #endif /* Copy prototype */ rec->proto = gc_copy_value (ctx, rec->proto, from_base, from_end, to_base, to_free, to_end); /* Copy table entries — skip slot[0] which stores packed metadata (class_id | rec_id << 32), not JSValues */ for (uint32_t i = 1; i <= mask; i++) { JSValue k = rec->slots[i].key; if (!rec_key_is_empty (k) && !rec_key_is_tomb (k)) { rec->slots[i].key = gc_copy_value (ctx, k, from_base, from_end, to_base, to_free, to_end); rec->slots[i].val = gc_copy_value (ctx, rec->slots[i].val, from_base, from_end, to_base, to_free, to_end); } } break; } case OBJ_FUNCTION: { JSFunction *fn = (JSFunction *)ptr; /* Scan the function name */ fn->name = gc_copy_value (ctx, fn->name, from_base, from_end, to_base, to_free, to_end); if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code) { /* Scan code tree to arbitrary nesting depth */ gc_scan_code_tree (ctx, JS_VALUE_GET_CODE(fn->u.cell.code)->u.reg.code, from_base, from_end, to_base, to_free, to_end); /* Scan outer_frame and env_record */ fn->u.cell.outer_frame = gc_copy_value (ctx, fn->u.cell.outer_frame, from_base, from_end, to_base, to_free, to_end); fn->u.cell.env_record = gc_copy_value (ctx, fn->u.cell.env_record, from_base, from_end, to_base, to_free, to_end); } else if (fn->kind == JS_FUNC_KIND_NATIVE) { fn->u.cell.outer_frame = gc_copy_value (ctx, fn->u.cell.outer_frame, from_base, from_end, to_base, to_free, to_end); fn->u.cell.env_record = gc_copy_value (ctx, fn->u.cell.env_record, from_base, from_end, to_base, to_free, to_end); } break; } case OBJ_TEXT: case OBJ_BLOB: /* No internal references to scan */ break; case OBJ_FRAME: { /* JSFrame - scan function, caller, address, and slots */ JSFrame *frame = (JSFrame *)ptr; frame->function = gc_copy_value (ctx, frame->function, from_base, from_end, to_base, to_free, to_end); frame->caller = gc_copy_value (ctx, frame->caller, from_base, from_end, to_base, to_free, to_end); frame->address = gc_copy_value (ctx, frame->address, from_base, from_end, to_base, to_free, to_end); /* Scan all slots */ uint64_t slot_count = objhdr_cap56 (frame->header); for (uint64_t i = 0; i < slot_count; i++) { JSValue sv = frame->slots[i]; #ifdef VALIDATE_GC if (JS_IsPtr (sv)) { void *sp = JS_VALUE_GET_PTR (sv); if (!is_ct_ptr (ctx, sp) && ptr_in_range (sp, from_base, from_end)) { objhdr_t sh = *(objhdr_t *)sp; uint8_t st = objhdr_type (sh); if (st != OBJ_FORWARD && st != OBJ_ARRAY && st != OBJ_TEXT && st != OBJ_RECORD && st != OBJ_FUNCTION && st != OBJ_FRAME && st != OBJ_BLOB) { const char *fname = "?"; const char *ffile = "?"; uint16_t fnslots = 0; if (JS_IsPtr (frame->function)) { objhdr_t fh = *(objhdr_t *)JS_VALUE_GET_PTR (frame->function); if (objhdr_type (fh) == OBJ_FUNCTION) { JSFunction *fn = (JSFunction *)JS_VALUE_GET_PTR (frame->function); if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code) { JSCodeRegister *_vc = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; if (_vc->name_cstr) fname = _vc->name_cstr; if (_vc->filename_cstr) ffile = _vc->filename_cstr; fnslots = _vc->nr_slots; } } } fprintf (stderr, "VALIDATE_GC: frame %p slot[%llu]=%p bad type %d (hdr=0x%llx) fn=%s (%s) nr_slots=%d\n", ptr, (unsigned long long)i, sp, st, (unsigned long long)sh, fname, ffile, fnslots); fflush (stderr); } } } #endif frame->slots[i] = gc_copy_value (ctx, sv, from_base, from_end, to_base, to_free, to_end); } break; } default: /* Unknown type during scan - fatal error */ fprintf (stderr, "gc_scan_object: unknown object type %d at %p\n", type, ptr); abort (); } } /* Cheney copying GC - collect garbage and compact live objects allow_grow: if true, grow heap when recovery is poor alloc_size: the allocation that triggered GC — used to size the new block */ int ctx_gc (JSContext *ctx, int allow_grow, size_t alloc_size) { #ifdef DUMP_GC_TIMING struct timespec gc_t0, gc_t1; clock_gettime(CLOCK_MONOTONIC, &gc_t0); #endif JSRuntime *rt = ctx->rt; size_t old_used = ctx->heap_free - ctx->heap_base; size_t old_heap_size = ctx->current_block_size; /* Save OLD heap bounds before allocating new block Use heap_free (not heap_end) as from_end - only the portion up to heap_free contains valid objects. Beyond heap_free is uninitialized garbage. */ uint8_t *from_base = ctx->heap_base; uint8_t *from_end = ctx->heap_free; #ifdef DUMP_GC_DETAIL printf("ctx_gc: from_base=%p from_end=%p size=%zu\n", (void*)from_base, (void*)from_end, old_heap_size); #endif /* Size the new block. Start at current_block_size (guaranteed >= used portion, so all live data fits). Only grow when: - next_block_size was bumped by the poor-recovery heuristic, or - alloc_size alone exceeds the block (rare large allocation). Crucially, do NOT add live_est to the sizing — it counts garbage and causes exponential heap growth even with excellent recovery. */ size_t new_size = ctx->current_block_size; if (allow_grow) { if (ctx->next_block_size > new_size) new_size = ctx->next_block_size; while (new_size < alloc_size && new_size < buddy_max_block(&ctx->rt->buddy)) new_size *= 2; } uint8_t *new_block = heap_block_alloc (rt, new_size); if (!new_block) { /* Try with same size */ new_size = ctx->current_block_size; new_block = heap_block_alloc (rt, new_size); if (!new_block) return -1; } uint8_t *to_base = new_block; uint8_t *to_free = new_block; uint8_t *to_end = new_block + new_size; #ifdef VALIDATE_GC /* Pre-GC: walk live frame chain and check for bad slot values */ if (JS_IsPtr (ctx->reg_current_frame)) { JSFrame *cf = (JSFrame *)JS_VALUE_GET_PTR (ctx->reg_current_frame); while (cf) { objhdr_t cfh = cf->header; while (objhdr_type (cfh) == OBJ_FORWARD) { cf = (JSFrame *)objhdr_fwd_ptr (cfh); cfh = cf->header; } if (objhdr_type (cfh) != OBJ_FRAME) break; uint64_t sc = objhdr_cap56 (cfh); for (uint64_t si = 0; si < sc; si++) { JSValue sv = cf->slots[si]; if (JS_IsPtr (sv)) { void *sp = JS_VALUE_GET_PTR (sv); if (!is_ct_ptr (ctx, sp) && ptr_in_range (sp, from_base, from_end)) { objhdr_t th = *(objhdr_t *)sp; void *orig_sp = sp; while (objhdr_type (th) == OBJ_FORWARD) { sp = objhdr_fwd_ptr (th); if (!ptr_in_range (sp, from_base, from_end)) break; th = *(objhdr_t *)sp; } uint8_t tt = objhdr_type (th); if (tt != OBJ_FORWARD && tt != OBJ_ARRAY && tt != OBJ_TEXT && tt != OBJ_RECORD && tt != OBJ_FUNCTION && tt != OBJ_FRAME && tt != OBJ_BLOB) { const char *fn_name = "?"; JSValue fn_v = cf->function; if (JS_IsPtr (fn_v)) { objhdr_t fnh = *(objhdr_t *)JS_VALUE_GET_PTR (fn_v); while (objhdr_type (fnh) == OBJ_FORWARD) { fn_v = JS_MKPTR (objhdr_fwd_ptr (fnh)); fnh = *(objhdr_t *)JS_VALUE_GET_PTR (fn_v); } if (objhdr_type (fnh) == OBJ_FUNCTION) { JSFunction *fnp = (JSFunction *)JS_VALUE_GET_PTR (fn_v); if (fnp->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(FN_READ_CODE(fnp))->u.reg.code && JS_VALUE_GET_CODE(FN_READ_CODE(fnp))->u.reg.code->name_cstr) fn_name = JS_VALUE_GET_CODE(FN_READ_CODE(fnp))->u.reg.code->name_cstr; } } fprintf (stderr, "VALIDATE_GC: pre-gc frame %p slot[%llu] -> %p (chased %p) bad type %d (hdr=0x%llx) fn=%s\n", (void *)cf, (unsigned long long)si, orig_sp, sp, tt, (unsigned long long)th, fn_name); fflush (stderr); } } } } if (JS_IsNull (cf->caller)) break; cf = (JSFrame *)JS_VALUE_GET_PTR (cf->caller); } } #endif /* Copy roots: global object, class prototypes, exception, etc. */ #ifdef DUMP_GC_DETAIL printf(" roots: global_obj = 0x%llx\n", (unsigned long long)ctx->global_obj); fflush(stdout); if (JS_IsPtr(ctx->global_obj)) { void *gptr = JS_VALUE_GET_PTR(ctx->global_obj); printf(" ptr=%p in_from=%d is_stone=%d\n", gptr, ((uint8_t*)gptr >= from_base && (uint8_t*)gptr < from_end), is_ct_ptr(ctx, gptr)); fflush(stdout); } #endif ctx->global_obj = gc_copy_value (ctx, ctx->global_obj, from_base, from_end, to_base, &to_free, to_end); #ifdef DUMP_GC_DETAIL printf(" after copy: global_obj = 0x%llx\n", (unsigned long long)ctx->global_obj); fflush(stdout); #endif #ifdef DUMP_GC_DETAIL printf(" roots: regexp_ctor\n"); fflush(stdout); #endif ctx->regexp_ctor = gc_copy_value (ctx, ctx->regexp_ctor, from_base, from_end, to_base, &to_free, to_end); #ifdef DUMP_GC_DETAIL printf(" roots: throw_type_error\n"); fflush(stdout); #endif ctx->throw_type_error = gc_copy_value (ctx, ctx->throw_type_error, from_base, from_end, to_base, &to_free, to_end); /* Copy current exception if pending */ #ifdef DUMP_GC_DETAIL printf(" roots: current_exception\n"); fflush(stdout); #endif ctx->current_exception = gc_copy_value (ctx, ctx->current_exception, from_base, from_end, to_base, &to_free, to_end); /* Copy actor identity key */ ctx->actor_sym = gc_copy_value (ctx, ctx->actor_sym, from_base, from_end, to_base, &to_free, to_end); /* Copy log callback function */ ctx->log_callback_js = gc_copy_value (ctx, ctx->log_callback_js, from_base, from_end, to_base, &to_free, to_end); /* Copy class prototypes */ #ifdef DUMP_GC_DETAIL printf(" roots: class_proto (count=%d)\n", ctx->class_count); fflush(stdout); #endif for (int i = 0; i < ctx->class_count; i++) { ctx->class_proto[i] = gc_copy_value (ctx, ctx->class_proto[i], from_base, from_end, to_base, &to_free, to_end); } /* Copy register VM current frame */ #ifdef DUMP_GC_DETAIL printf(" roots: reg_current_frame\n"); fflush(stdout); #endif ctx->reg_current_frame = gc_copy_value (ctx, ctx->reg_current_frame, from_base, from_end, to_base, &to_free, to_end); /* Copy JS_PUSH_VALUE/JS_POP_VALUE roots */ #ifdef DUMP_GC_DETAIL printf(" roots: top_gc_ref\n"); fflush(stdout); #endif for (JSGCRef *ref = ctx->top_gc_ref; ref != NULL; ref = ref->prev) { ref->val = gc_copy_value (ctx, ref->val, from_base, from_end, to_base, &to_free, to_end); } /* Copy JS_LOCAL roots (update C locals through pointers) */ #ifdef DUMP_GC_DETAIL printf(" roots: top_local_ref\n"); fflush(stdout); #endif for (JSLocalRef *ref = ctx->top_local_ref; ref != NULL; ref = ref->prev) { *ref->ptr = gc_copy_value (ctx, *ref->ptr, from_base, from_end, to_base, &to_free, to_end); } /* Copy JS_AddGCRef/JS_DeleteGCRef roots */ #ifdef DUMP_GC_DETAIL printf(" roots: last_gc_ref\n"); fflush(stdout); #endif for (JSGCRef *ref = ctx->last_gc_ref; ref != NULL; ref = ref->prev) { ref->val = gc_copy_value (ctx, ref->val, from_base, from_end, to_base, &to_free, to_end); } /* Copy auto-rooted C call argv arrays */ for (CCallRoot *r = ctx->c_call_root; r != NULL; r = r->prev) { for (int i = 0; i < r->argc; i++) { r->argv[i] = gc_copy_value (ctx, r->argv[i], from_base, from_end, to_base, &to_free, to_end); } } /* Scan external C-side roots (actor letters, timers) */ if (ctx->gc_scan_external) ctx->gc_scan_external(ctx, from_base, from_end, to_base, &to_free, to_end); /* Cheney scan: scan copied objects to find more references */ uint8_t *scan = to_base; #ifdef DUMP_GC_DETAIL printf(" scan: to_base=%p to_free=%p to_end=%p\n", (void*)to_base, (void*)to_free, (void*)to_end); fflush(stdout); #endif while (scan < to_free) { #ifdef DUMP_GC_DETAIL objhdr_t scan_hdr = *(objhdr_t *)scan; printf(" scan %p: type=%d hdr=0x%llx", (void*)scan, objhdr_type(scan_hdr), (unsigned long long)scan_hdr); fflush(stdout); #endif size_t obj_size = gc_object_size (scan); #ifdef DUMP_GC_DETAIL printf(" size=%zu\n", obj_size); fflush(stdout); #endif gc_scan_object (ctx, scan, from_base, from_end, to_base, &to_free, to_end); scan += obj_size; } #ifdef VALIDATE_GC { JSRecord *grec = JS_VALUE_GET_RECORD(ctx->global_obj); uint32_t mask = (uint32_t)objhdr_cap56(grec->mist_hdr); for (uint32_t i = 1; i <= mask; i++) { JSValue k = grec->slots[i].key; if (!rec_key_is_empty(k) && !rec_key_is_tomb(k)) { if (!JS_IsPtr(k)) { fprintf(stderr, "VALIDATE_GC: global slot[%u] key is not a pointer (tag=0x%llx)\n", i, (unsigned long long)k); } else { void *kp = JS_VALUE_GET_PTR(k); if (!ptr_in_range(kp, to_base, to_free) && !is_ct_ptr(ctx, kp)) { fprintf(stderr, "VALIDATE_GC: global slot[%u] key=%p outside valid ranges\n", i, kp); } } JSValue v = grec->slots[i].val; if (JS_IsPtr(v)) { void *vp = JS_VALUE_GET_PTR(v); if (!ptr_in_range(vp, to_base, to_free) && !is_ct_ptr(ctx, vp)) { fprintf(stderr, "VALIDATE_GC: global slot[%u] val=%p outside valid ranges\n", i, vp); } } } } } #endif /* Finalize garbage records that have class finalizers */ { uint8_t *p = from_base; uint8_t *prev_p = NULL; size_t prev_size = 0; uint8_t prev_type = 0; while (p < from_end) { objhdr_t hdr = *(objhdr_t *)p; uint8_t type = objhdr_type (hdr); size_t size; if (type == OBJ_FORWARD) { size = *((size_t *)(p + sizeof (objhdr_t))); if (size == 0 || size > (size_t)(from_end - from_base) || (size & 7) != 0) { uint64_t *w = (uint64_t *)p; fprintf (stderr, "gc_finalize_walk: bad fwd size=%zu at p=%p prev_p=%p prev_type=%d prev_size=%zu\n" " words: [0]=0x%llx [1]=0x%llx [2]=0x%llx [3]=0x%llx\n" " prev words: [0]=0x%llx [1]=0x%llx [2]=0x%llx [3]=0x%llx\n", size, (void *)p, (void *)prev_p, prev_type, prev_size, (unsigned long long)w[0], (unsigned long long)w[1], (unsigned long long)w[2], (unsigned long long)w[3], prev_p ? ((unsigned long long *)prev_p)[0] : 0, prev_p ? ((unsigned long long *)prev_p)[1] : 0, prev_p ? ((unsigned long long *)prev_p)[2] : 0, prev_p ? ((unsigned long long *)prev_p)[3] : 0); break; } } else if (type != OBJ_ARRAY && type != OBJ_BLOB && type != OBJ_TEXT && type != OBJ_RECORD && type != OBJ_FUNCTION && type != OBJ_FRAME) { uint64_t *w = (uint64_t *)p; fprintf (stderr, "gc_finalize_walk: bad type=%d at p=%p hdr=0x%llx prev_p=%p prev_type=%d prev_size=%zu\n" " words: [0]=0x%llx [1]=0x%llx [2]=0x%llx [3]=0x%llx\n" " prev words: [0]=0x%llx [1]=0x%llx [2]=0x%llx [3]=0x%llx\n", type, (void *)p, (unsigned long long)hdr, (void *)prev_p, prev_type, prev_size, (unsigned long long)w[0], (unsigned long long)w[1], (unsigned long long)w[2], (unsigned long long)w[3], prev_p ? ((unsigned long long *)prev_p)[0] : 0, prev_p ? ((unsigned long long *)prev_p)[1] : 0, prev_p ? ((unsigned long long *)prev_p)[2] : 0, prev_p ? ((unsigned long long *)prev_p)[3] : 0); break; } else { size = gc_object_size (p); if (type == OBJ_RECORD) { JSRecord *rec = (JSRecord *)p; uint32_t class_id = REC_GET_CLASS_ID (rec); if (class_id != 0 && (int)class_id < ctx->class_count) { JSClassFinalizer *fn = ctx->class_array[class_id].finalizer; if (fn) { #ifdef DUMP_GC_FINALIZER fprintf (stderr, "gc_finalize: class_id=%u name=%s rec=%p\n", class_id, ctx->class_array[class_id].class_name, (void *)rec); #endif fn (rt, JS_MKPTR (rec)); } } } } prev_p = p; prev_type = type; prev_size = size; p += size; } } heap_block_free (rt, from_base, old_heap_size); /* Update context with new block */ size_t new_used = to_free - to_base; /* Update GC stats */ ctx->gc_count++; ctx->gc_bytes_copied += new_used; size_t recovered = old_used > new_used ? old_used - new_used : 0; #ifdef DUMP_GC_TIMING clock_gettime(CLOCK_MONOTONIC, &gc_t1); double gc_ms = (gc_t1.tv_sec - gc_t0.tv_sec) * 1000.0 + (gc_t1.tv_nsec - gc_t0.tv_nsec) / 1e6; fprintf(stderr, "GC #%u: %.2f ms | copied %zu KB | old %zu KB -> new %zu KB | recovered %zu KB (%.0f%%)\n", ctx->gc_count, gc_ms, new_used / 1024, old_used / 1024, new_size / 1024, recovered / 1024, old_used > 0 ? (100.0 * recovered / old_used) : 0.0); #endif ctx->heap_base = to_base; ctx->heap_free = to_free; ctx->heap_end = to_end; ctx->current_block_size = new_size; #ifdef DUMP_GC_DETAIL /* Verify global_obj is in valid heap range after GC */ if (JS_IsPtr(ctx->global_obj)) { void *gptr = JS_VALUE_GET_PTR(ctx->global_obj); if ((uint8_t*)gptr < to_base || (uint8_t*)gptr >= to_free) { printf(" WARNING: global_obj=%p outside [%p, %p) after GC!\n", gptr, (void*)to_base, (void*)to_free); fflush(stdout); } } #endif /* If <40% recovered, grow next block size for future allocations. First poor recovery: double. Consecutive poor: quadruple. Skip under FORCE_GC_AT_MALLOC — forced GC on every allocation creates artificially poor recovery (no time to accumulate garbage), which would cause runaway exponential heap growth. */ #ifdef DUMP_GC int will_grow = 0; #endif #ifdef FORCE_GC_AT_MALLOC if (0) { #else if (allow_grow && recovered > 0 && old_used > 0 && recovered < old_used * 2 / 5) { #endif size_t factor = ctx->gc_poor_streak >= 1 ? 4 : 2; size_t grown = new_size * factor; if (grown <= buddy_max_block(&ctx->rt->buddy)) { ctx->next_block_size = grown; #ifdef DUMP_GC will_grow = 1; #endif } ctx->gc_poor_streak++; } else { ctx->gc_poor_streak = 0; } #ifdef DUMP_GC { /* Walk to-space and tally memory by object type */ static const char *type_names[] = { [OBJ_ARRAY] = "array", [OBJ_TEXT] = "text", [OBJ_RECORD] = "record", [OBJ_FUNCTION] = "function", [OBJ_FRAME] = "frame", }; size_t type_bytes[8] = {0}; size_t type_count[8] = {0}; uint8_t *p = to_base; while (p < to_free) { uint8_t t = objhdr_type (*(objhdr_t *)p); size_t sz = gc_object_size (p); type_bytes[t] += sz; type_count[t]++; p += sz; } printf ("\nGC: %zu -> %zu bytes, recovered %zu (%.1f%%)%s\n", old_heap_size, new_size, recovered, old_used > 0 ? (recovered * 100.0 / old_used) : 0.0, will_grow ? ", will grow (poor recovery)" : ""); printf (" live %zu / %zu bytes (%.0f%% full):", new_used, new_size, new_size > 0 ? (new_used * 100.0 / new_size) : 0.0); for (int i = 0; i < 7; i++) { if (type_count[i] == 0) continue; printf (" %s %zu(%zu)", type_names[i], type_bytes[i], type_count[i]); } printf ("\n"); fflush (stdout); } #endif /* Fire GC hook if registered */ if (ctx->trace_hook && (ctx->trace_type & JS_HOOK_GC)) ctx->trace_hook(ctx, JS_HOOK_GC, NULL, ctx->trace_data); /* Check memory limit — kill actor if heap exceeds cap. Use JS_Log "memory" channel which always fprintf's (safe during GC). */ if (ctx->heap_memory_limit > 0 && ctx->current_block_size > ctx->heap_memory_limit) { JS_Log(ctx, "memory", "%s: heap %zuKB exceeds limit %zuMB, killing", ctx->name ? ctx->name : ctx->id, ctx->current_block_size / 1024, ctx->heap_memory_limit / (1024 * 1024)); return -1; } return 0; } JSRuntime *JS_NewRuntime (void) { JSRuntime *rt; rt = malloc (sizeof (JSRuntime)); if (!rt) return NULL; memset (rt, 0, sizeof (*rt)); rt->buddy.initial_size = BUDDY_DEFAULT_POOL; rt->buddy.next_pool_size = BUDDY_DEFAULT_POOL; return rt; } void JS_SetMemoryLimit (JSRuntime *rt, size_t limit) { rt->malloc_limit = limit; } void JS_SetPoolSize (JSRuntime *rt, size_t initial, size_t cap) { rt->buddy.initial_size = initial; rt->buddy.next_pool_size = initial; rt->buddy.cap = cap; } /* Helpers to call system memory functions (for memory allocated by external libs) */ #define malloc(s) malloc_is_forbidden (s) #define free(p) free_is_forbidden (p) #define realloc(p, s) realloc_is_forbidden (p, s) void JS_SetPauseFlag (JSContext *ctx, int value) { atomic_store_explicit (&ctx->pause_flag, value, memory_order_relaxed); } int JS_GetVMCallDepth(JSContext *ctx) { return ctx->vm_call_depth; } void JS_SetHeapMemoryLimit(JSContext *ctx, size_t limit) { ctx->heap_memory_limit = limit; } /* Allocate a string using bump allocation from context heap. Note: the string contents are uninitialized */ JSText *js_alloc_string (JSContext *ctx, int max_len) { JSText *str; /* Allocate packed UTF-32: 2 chars per 64-bit word. */ size_t data_words = (max_len + 1) / 2; size_t size = sizeof (JSText) + data_words * sizeof (uint64_t); str = js_malloc (ctx, size); if (unlikely (!str)) { JS_RaiseOOM(ctx); return NULL; } /* Initialize objhdr_t with OBJ_TEXT type and capacity in cap56 */ str->hdr = objhdr_make (max_len, OBJ_TEXT, false, false, false, false); str->length = 0; str->hash = 0; /* Zero packed data so odd-length strings have deterministic padding. js_malloc is a bump allocator and does not zero memory; without this, the last word's unused low 32 bits contain garbage, causing fash64_hash_words and JSText_equal (memcmp) to produce inconsistent results for different allocations of the same text content. */ memset (str->packed, 0, data_words * sizeof (uint64_t)); return str; } void JS_FreeRuntime (JSRuntime *rt) { /* Destroy buddy allocator */ buddy_destroy (&rt->buddy); sys_free (rt); } /* Forward declarations for intrinsics */ static void JS_AddIntrinsicBaseObjects (JSContext *ctx); static void JS_AddIntrinsicRegExp (JSContext *ctx); JSContext *JS_NewContextRawWithHeapSize (JSRuntime *rt, size_t heap_size) { JSContext *ctx; /* Round up to buddy allocator minimum */ size_t min_size = 1ULL << BUDDY_MIN_ORDER; if (heap_size < min_size) heap_size = min_size; /* Round up to power of 2 for buddy allocator */ size_t actual = min_size; while (actual < heap_size && actual < buddy_max_block(&rt->buddy)) { actual <<= 1; } heap_size = actual; ctx = js_mallocz_rt (sizeof (JSContext)); if (!ctx) return NULL; ctx->trace_hook = NULL; ctx->rt = rt; /* Bootstrap class_array and class_proto together via JS_NewClass1 */ if (init_class_range (ctx, js_std_class_def, JS_CLASS_OBJECT, countof (js_std_class_def)) < 0) { js_free_rt (ctx->class_array); js_free_rt (ctx->class_proto); js_free_rt (ctx); return NULL; } ctx->regexp_ctor = JS_NULL; /* Initialize register VM frame root */ ctx->reg_current_frame = JS_NULL; ctx->c_call_root = NULL; /* Initialize VM suspend/resume state */ ctx->suspended = 0; ctx->suspended_pc = 0; ctx->vm_call_depth = 0; ctx->heap_memory_limit = 0; ctx->actor_label = NULL; JS_AddGCRef(ctx, &ctx->suspended_frame_ref); ctx->suspended_frame_ref.val = JS_NULL; /* Initialize per-context execution state (moved from JSRuntime) */ ctx->current_exception = JS_NULL; ctx->actor_sym = JS_NULL; ctx->log_callback_js = JS_NULL; ctx->log_callback = NULL; /* Register actor GCRef fields so the Cheney GC can relocate them. */ JS_AddGCRef(ctx, &ctx->idx_buffer_ref); JS_AddGCRef(ctx, &ctx->on_exception_ref); JS_AddGCRef(ctx, &ctx->message_handle_ref); JS_AddGCRef(ctx, &ctx->unneeded_ref); JS_AddGCRef(ctx, &ctx->actor_sym_ref); ctx->idx_buffer_ref.val = JS_NULL; ctx->on_exception_ref.val = JS_NULL; ctx->message_handle_ref.val = JS_NULL; ctx->unneeded_ref.val = JS_NULL; ctx->actor_sym_ref.val = JS_NULL; /* Initialize constant text pool (avoids overflow pages for common case) */ { size_t ct_pool_size = 64 * 1024; /* 64KB initial CT pool */ ctx->ct_base = js_malloc_rt (ct_pool_size); if (!ctx->ct_base) { js_free_rt (ctx->class_array); js_free_rt (ctx->class_proto); js_free_rt (ctx); return NULL; } ctx->ct_free = ctx->ct_base; ctx->ct_end = ctx->ct_base + ct_pool_size; } /* Initialize constant text intern table */ ctx->ct_pages = NULL; ctx->ct_array = NULL; ctx->ct_hash = NULL; ctx->ct_count = 0; ctx->ct_size = 0; ctx->ct_resize_threshold = 0; if (ct_resize (ctx) < 0) { js_free_rt (ctx->class_array); js_free_rt (ctx->class_proto); js_free_rt (ctx); return NULL; } /* Allocate initial heap block for bump allocation */ ctx->current_block_size = heap_size; ctx->next_block_size = ctx->current_block_size; ctx->heap_base = heap_block_alloc (rt, ctx->current_block_size); if (!ctx->heap_base) { js_free_rt (ctx->ct_hash); js_free_rt (ctx->ct_array); js_free_rt (ctx->class_array); js_free_rt (ctx->class_proto); js_free_rt (ctx); return NULL; } ctx->heap_free = ctx->heap_base; ctx->heap_end = ctx->heap_base + ctx->current_block_size; #ifdef DUMP_GC_DETAIL printf("Context init: heap_base=%p heap_end=%p size=%zu\n", (void*)ctx->heap_base, (void*)ctx->heap_end, ctx->current_block_size); #endif #ifdef DUMP_GC_DETAIL printf("After BasicObjects: heap_base=%p heap_end=%p heap_free=%p\n", (void*)ctx->heap_base, (void*)ctx->heap_end, (void*)ctx->heap_free); #endif return ctx; } JSContext *JS_NewContextRaw (JSRuntime *rt) { return JS_NewContextRawWithHeapSize (rt, 1ULL << BUDDY_MIN_ORDER); } JSContext *JS_NewContext (JSRuntime *rt) { JSContext *ctx = JS_NewContextRaw (rt); if (!ctx) return NULL; JS_AddIntrinsicBaseObjects (ctx); JS_AddIntrinsicRegExp (ctx); obj_set_stone (JS_VALUE_GET_RECORD (ctx->global_obj)); return ctx; } JSContext *JS_NewContextWithHeapSize (JSRuntime *rt, size_t heap_size) { JSContext *ctx = JS_NewContextRawWithHeapSize (rt, heap_size); if (!ctx) return NULL; JS_AddIntrinsicBaseObjects (ctx); JS_AddIntrinsicRegExp (ctx); obj_set_stone (JS_VALUE_GET_RECORD (ctx->global_obj)); return ctx; } void JS_SetGCScanExternal(JSContext *ctx, JS_GCScanFn fn) { ctx->gc_scan_external = fn; } void JS_SetActorSym (JSContext *ctx, JSValue sym) { ctx->actor_sym = sym; } JSValue JS_GetActorSym (JSContext *ctx) { return ctx->actor_sym; } void JS_SetClassProto (JSContext *ctx, JSClassID class_id, JSValue obj) { assert (class_id < ctx->class_count); set_value (ctx, &ctx->class_proto[class_id], obj); } JSValue JS_GetClassProto (JSContext *ctx, JSClassID class_id) { assert (class_id < ctx->class_count); return ctx->class_proto[class_id]; } void JS_FreeContext (JSContext *ctx) { JSRuntime *rt = ctx->rt; int i; cell_rt_free_native_state(ctx); JS_DeleteGCRef(ctx, &ctx->suspended_frame_ref); JS_DeleteGCRef(ctx, &ctx->idx_buffer_ref); JS_DeleteGCRef(ctx, &ctx->on_exception_ref); JS_DeleteGCRef(ctx, &ctx->message_handle_ref); JS_DeleteGCRef(ctx, &ctx->unneeded_ref); JS_DeleteGCRef(ctx, &ctx->actor_sym_ref); for (i = 0; i < ctx->class_count; i++) { } /* Finalize all remaining records with class finalizers before teardown */ if (ctx->heap_base) { uint8_t *p = ctx->heap_base; while (p < ctx->heap_free) { objhdr_t hdr = *(objhdr_t *)p; uint8_t type = objhdr_type (hdr); size_t size; if (type == OBJ_FORWARD) { size = *((size_t *)(p + sizeof (objhdr_t))); } else { size = gc_object_size (p); if (type == OBJ_RECORD) { JSRecord *rec = (JSRecord *)p; uint32_t class_id = REC_GET_CLASS_ID (rec); if (class_id != 0 && (int)class_id < ctx->class_count) { JSClassFinalizer *fn = ctx->class_array[class_id].finalizer; if (fn) { #ifdef DUMP_GC_FINALIZER fprintf (stderr, "teardown_finalize: class_id=%u name=%s rec=%p\n", class_id, ctx->class_array[class_id].class_name, (void *)rec); #endif fn (rt, JS_MKPTR (rec)); } } } } p += size; } } js_free_rt (ctx->class_array); js_free_rt (ctx->class_proto); /* Free constant text pool and intern table */ ct_free_all (ctx); if (ctx->ct_base) js_free_rt (ctx->ct_base); js_free_rt (ctx->ct_hash); js_free_rt (ctx->ct_array); /* Free heap block */ if (ctx->heap_base) { heap_block_free (rt, ctx->heap_base, ctx->current_block_size); ctx->heap_base = NULL; ctx->heap_free = NULL; ctx->heap_end = NULL; } js_free_rt (ctx); } JSRuntime *JS_GetRuntime (JSContext *ctx) { return ctx->rt; } void JS_SetMaxStackSize (JSContext *ctx, size_t stack_size) { ctx->stack_limit = stack_size; } JSText *js_alloc_string (JSContext *ctx, int max_len); static __maybe_unused void JS_DumpChar (FILE *fo, int c, int sep) { if (c == sep || c == '\\') { fputc ('\\', fo); fputc (c, fo); } else if (c >= ' ' && c <= 126) { fputc (c, fo); } else if (c == '\n') { fputc ('\\', fo); fputc ('n', fo); } else { fprintf (fo, "\\u%04x", c); } } __maybe_unused void JS_DumpString (JSRuntime *rt, const JSText *p) { int i; if (p == NULL) { printf (""); return; } putchar ('"'); for (i = 0; i < (int)JSText_len (p); i++) { JS_DumpChar (stdout, string_get (p, i), '"'); } putchar ('"'); } static inline BOOL JS_IsEmptyString (JSValue v) { return v == JS_EMPTY_TEXT; } /* JSClass support */ /* a new class ID is allocated if *pclass_id != 0 */ JSClassID JS_NewClassID (JSClassID *pclass_id) { JSClassID class_id; class_id = *pclass_id; if (class_id == 0) { class_id = js_class_id_alloc++; *pclass_id = class_id; } return class_id; } JSClassID JS_GetClassID (JSValue v) { JSRecord *rec; if (!JS_IsRecord (v)) return JS_INVALID_CLASS_ID; rec = JS_VALUE_GET_RECORD (v); return REC_GET_CLASS_ID(rec); } BOOL JS_IsRegisteredClass (JSContext *ctx, JSClassID class_id) { return (class_id < ctx->class_count && ctx->class_array[class_id].class_id != 0); } /* create a new object internal class. Return -1 if error, 0 if OK. The finalizer can be NULL if none is needed. */ int JS_NewClass1 (JSContext *ctx, JSClassID class_id, const JSClassDef *class_def, const char *name) { int new_size, i; JSClass *cl, *new_class_array; JSValue *new_class_proto; if (class_id >= (1 << 16)) return -1; if (class_id < ctx->class_count && ctx->class_array[class_id].class_id != 0) return -1; if (class_id >= ctx->class_count) { new_size = max_int (JS_CLASS_INIT_COUNT, max_int (class_id + 1, ctx->class_count * 3 / 2)); /* reallocate the class array */ new_class_array = js_realloc_rt (ctx->class_array, sizeof (JSClass) * new_size); if (!new_class_array) return -1; memset (new_class_array + ctx->class_count, 0, (new_size - ctx->class_count) * sizeof (JSClass)); ctx->class_array = new_class_array; /* reallocate the class proto array */ new_class_proto = js_realloc_rt (ctx->class_proto, sizeof (JSValue) * new_size); if (!new_class_proto) return -1; for (i = ctx->class_count; i < new_size; i++) new_class_proto[i] = JS_NULL; ctx->class_proto = new_class_proto; ctx->class_count = new_size; } cl = &ctx->class_array[class_id]; cl->class_id = class_id; cl->class_name = name; /* name is already a const char* */ cl->finalizer = class_def->finalizer; return 0; } int JS_NewClass (JSContext *ctx, JSClassID class_id, const JSClassDef *class_def) { /* class_name is stored directly as const char* */ return JS_NewClass1 (ctx, class_id, class_def, class_def->class_name); } JSValue js_new_string8_len (JSContext *ctx, const char *buf, int len) { JSText *str; int i; /* Empty string - return immediate empty */ if (len <= 0) { return MIST_TryNewImmediateASCII ("", 0); } /* Try immediate ASCII for short strings (≤7 chars) */ if (len <= MIST_ASCII_MAX_LEN) { JSValue imm = MIST_TryNewImmediateASCII (buf, len); if (!JS_IsNull (imm)) return imm; } /* Fall back to heap string */ str = js_alloc_string (ctx, len); if (!str) return JS_ThrowMemoryError (ctx); for (i = 0; i < len; i++) string_put (str, i, buf[i]); str->length = len; return pretext_end (ctx, str); } JSValue js_new_string8 (JSContext *ctx, const char *buf) { return js_new_string8_len (ctx, buf, strlen (buf)); } /* GC-safe substring: takes JSValue (which must be rooted by caller) */ static JSValue js_sub_string (JSContext *ctx, JSText *p, int start, int end) { int i; int len = end - start; if (start == 0 && end == (int)JSText_len (p)) { return JS_MKPTR (p); } /* Root the source string as a JSValue so it survives js_alloc_string GC */ JSGCRef src_ref; JS_PushGCRef (ctx, &src_ref); src_ref.val = JS_MKPTR (p); JSText *str = js_alloc_string (ctx, len); if (!str) { JS_PopGCRef (ctx, &src_ref); return JS_EXCEPTION; } /* Re-chase p after allocation */ p = JS_VALUE_GET_STRING (src_ref.val); for (i = 0; i < len; i++) string_put (str, i, string_get (p, start + i)); str->length = len; JS_PopGCRef (ctx, &src_ref); return pretext_end (ctx, str); } /* Substring from a JSValue (handles both immediate ASCII and heap strings) */ static JSValue js_sub_string_val (JSContext *ctx, JSValue src, int start, int end) { int len = end - start; if (len <= 0) return JS_NewString (ctx, ""); if (MIST_IsImmediateASCII (src)) { char buf[MIST_ASCII_MAX_LEN + 1]; for (int i = 0; i < len; i++) buf[i] = (char)MIST_GetImmediateASCIIChar (src, start + i); return js_new_string8_len (ctx, buf, len); } /* Heap string — fast path for short ASCII substrings (avoids heap alloc) */ JSText *p = JS_VALUE_GET_STRING (src); if (len <= MIST_ASCII_MAX_LEN) { char buf[MIST_ASCII_MAX_LEN]; int all_ascii = 1; for (int i = 0; i < len; i++) { uint32_t c = string_get (p, start + i); if (c >= 0x80) { all_ascii = 0; break; } buf[i] = (char)c; } if (all_ascii) { JSValue imm = MIST_TryNewImmediateASCII (buf, len); if (!JS_IsNull (imm)) return imm; } } /* Heap string — delegate to existing js_sub_string */ return js_sub_string (ctx, p, start, end); } /* Allocate a new pretext (mutable JSText) with initial capacity */ JSText *pretext_init (JSContext *ctx, int capacity) { if (capacity <= 0) capacity = 16; JSText *s = js_alloc_string (ctx, capacity); if (!s) return NULL; s->length = 0; return s; } /* Reallocate a pretext to hold new_len characters */ static no_inline JSText *pretext_realloc (JSContext *ctx, JSText *s, int new_len) { if (new_len > JS_STRING_LEN_MAX) { JS_RaiseDisrupt (ctx, "string too long"); return NULL; } int old_cap = (int)objhdr_cap56 (s->hdr); int old_len = (int)s->length; /* Grow by 50%, ensuring we have at least new_len capacity */ int new_cap = max_int (new_len, old_cap * 3 / 2); /* Protect source object with a GC ref before allocating (GC may move it) */ JSGCRef src_ref; JS_PushGCRef (ctx, &src_ref); src_ref.val = JS_MKPTR (s); /* Allocate new string - this may trigger GC */ JSText *new_str = js_alloc_string (ctx, new_cap); if (!new_str) { JS_PopGCRef (ctx, &src_ref); return NULL; } /* Get possibly-moved source pointer after GC */ s = (JSText *)chase (src_ref.val); JS_PopGCRef (ctx, &src_ref); /* Copy data from old string to new */ new_str->length = old_len; for (int i = 0; i < old_len; i++) { string_put (new_str, i, string_get (s, i)); } return new_str; } no_inline JSText *pretext_putc_slow (JSContext *ctx, JSText *s, uint32_t c) { int len = (int)s->length; int cap = (int)objhdr_cap56 (s->hdr); if (unlikely (len >= cap)) { s = pretext_realloc (ctx, s, len + 1); if (!s) return NULL; } string_put (s, len, c); s->length++; return s; } /* 0 <= c <= 0x10ffff */ JSText *pretext_putc (JSContext *ctx, JSText *s, uint32_t c) { int len = (int)s->length; int cap = (int)objhdr_cap56 (s->hdr); if (likely (len < cap)) { string_put (s, len, c); s->length++; return s; } return pretext_putc_slow (ctx, s, c); } static JSText *pretext_write8 (JSContext *ctx, JSText *s, const uint8_t *p, int len) { int cur_len = (int)s->length; int cap = (int)objhdr_cap56 (s->hdr); if (cur_len + len > cap) { s = pretext_realloc (ctx, s, cur_len + len); if (!s) return NULL; } for (int i = 0; i < len; i++) { string_put (s, cur_len + i, p[i]); } s->length += len; return s; } /* appending an ASCII string */ static JSText *pretext_puts8 (JSContext *ctx, JSText *s, const char *str) { return pretext_write8 (ctx, s, (const uint8_t *)str, strlen (str)); } static JSText *pretext_concat (JSContext *ctx, JSText *s, const JSText *p, uint32_t from, uint32_t to) { if (to <= from) return s; int len = (int)(to - from); int cur_len = (int)s->length; int cap = (int)objhdr_cap56 (s->hdr); if (cur_len + len > cap) { /* Root p across pretext_realloc which can trigger GC */ JSGCRef p_ref; JS_PushGCRef (ctx, &p_ref); p_ref.val = JS_MKPTR ((void *)p); s = pretext_realloc (ctx, s, cur_len + len); p = (const JSText *)chase (p_ref.val); JS_PopGCRef (ctx, &p_ref); if (!s) return NULL; } for (int i = 0; i < len; i++) { string_put (s, cur_len + i, string_get (p, (int)from + i)); } s->length += len; return s; } JSText *pretext_concat_value (JSContext *ctx, JSText *s, JSValue v) { if (MIST_IsImmediateASCII (v)) { int len = MIST_GetImmediateASCIILen (v); char buf[8]; for (int i = 0; i < len; i++) buf[i] = MIST_GetImmediateASCIIChar (v, i); return pretext_write8 (ctx, s, (const uint8_t *)buf, len); } if (JS_IsText (v)) { JSText *p = JS_VALUE_GET_STRING (v); return pretext_concat (ctx, s, p, 0, (uint32_t)JSText_len (p)); } /* Slow path: v needs conversion — root s across JS_ToString which can allocate and trigger GC */ JSGCRef s_ref; JS_PushGCRef (ctx, &s_ref); s_ref.val = JS_MKPTR (s); JSValue v1 = JS_ToString (ctx, v); s = (JSText *)chase (s_ref.val); /* re-fetch after possible GC */ JS_PopGCRef (ctx, &s_ref); if (JS_IsException (v1)) return NULL; if (MIST_IsImmediateASCII (v1)) { int len = MIST_GetImmediateASCIILen (v1); char buf[8]; for (int i = 0; i < len; i++) buf[i] = MIST_GetImmediateASCIIChar (v1, i); s = pretext_write8 (ctx, s, (const uint8_t *)buf, len); return s; } JSText *p = JS_VALUE_GET_STRING (v1); s = pretext_concat (ctx, s, p, 0, (uint32_t)JSText_len (p)); return s; } /* Finalize a pretext into an immutable JSValue string */ JSValue pretext_end (JSContext *ctx, JSText *s) { if (!s) return JS_EXCEPTION; int len = (int)s->length; if (len == 0) return JS_KEY_empty; /* Promote short ASCII strings to immediate values */ if (len <= MIST_ASCII_MAX_LEN) { char buf[MIST_ASCII_MAX_LEN]; int all_ascii = 1; for (int i = 0; i < len; i++) { uint32_t c = string_get (s, i); if (c >= 0x80) { all_ascii = 0; break; } buf[i] = (char)c; } if (all_ascii) { JSValue imm = MIST_TryNewImmediateASCII (buf, len); if (!JS_IsNull (imm)) return imm; } } /* length is already set by caller; cap56 stays as allocated capacity */ s->hash = 0; s->hdr = objhdr_set_s (s->hdr, true); /* mark as stone */ return JS_MKPTR (s); } /* create a string from a UTF-8 buffer */ JSValue JS_NewStringLen (JSContext *ctx, const char *buf, size_t buf_len) { if (buf_len > JS_STRING_LEN_MAX) return JS_RaiseDisrupt (ctx, "string too long"); /* Try immediate ASCII first (<=7 ASCII chars) */ if (buf_len <= MIST_ASCII_MAX_LEN) { JSValue ret = MIST_TryNewImmediateASCII (buf, buf_len); if (!JS_IsNull (ret)) return ret; } /* Count actual codepoints for allocation */ const uint8_t *p = (const uint8_t *)buf; const uint8_t *end = p + buf_len; int codepoint_count = 0; while (p < end) { if (*p < 128) { p++; codepoint_count++; } else { const uint8_t *next; int c = unicode_from_utf8 (p, (int)(end - p), &next); if (c < 0) { /* Invalid UTF-8 byte, treat as single byte */ p++; } else { p = next; } codepoint_count++; } } JSText *str = js_alloc_string (ctx, codepoint_count); if (!str) return JS_ThrowMemoryError (ctx); /* Decode UTF-8 to UTF-32 */ p = (const uint8_t *)buf; int i = 0; while (p < end) { uint32_t c; if (*p < 128) { c = *p++; } else { const uint8_t *next; int decoded = unicode_from_utf8 (p, (int)(end - p), &next); if (decoded < 0) { /* Invalid UTF-8 byte, use replacement char or the byte itself */ c = *p++; } else { c = (uint32_t)decoded; p = next; } } string_put (str, i++, c); } str->length = codepoint_count; return pretext_end (ctx, str); } JSValue JS_ConcatString3 (JSContext *ctx, const char *str1, JSValue str2, const char *str3) { JSText *b; int len1, len3, str2_len; JSGCRef str2_ref; if (!JS_IsText (str2)) { str2 = JS_ToString (ctx, str2); if (JS_IsException (str2)) goto fail; } /* Root str2 — pretext_init/pretext_write8/pretext_concat_value allocate and can trigger GC, which would move the heap string str2 points to */ JS_PushGCRef (ctx, &str2_ref); str2_ref.val = str2; str2_len = js_string_value_len (str2_ref.val); len1 = strlen (str1); len3 = strlen (str3); b = pretext_init (ctx, len1 + str2_len + len3); if (!b) goto fail_pop; b = pretext_write8 (ctx, b, (const uint8_t *)str1, len1); if (!b) goto fail_pop; b = pretext_concat_value (ctx, b, str2_ref.val); if (!b) goto fail_pop; b = pretext_write8 (ctx, b, (const uint8_t *)str3, len3); if (!b) goto fail_pop; JS_PopGCRef (ctx, &str2_ref); return pretext_end (ctx, b); fail_pop: JS_PopGCRef (ctx, &str2_ref); fail: return JS_EXCEPTION; } /* return (NULL, 0) if exception. */ /* return pointer into a JSText with a live ref_count */ /* cesu8 determines if non-BMP1 codepoints are encoded as 1 or 2 utf-8 * sequences */ const char *JS_ToCStringLen2 (JSContext *ctx, size_t *plen, JSValue val1, BOOL cesu8) { JSGCRef val_ref; char *q, *ret; size_t size; int i, len; JS_PushGCRef (ctx, &val_ref); if (!JS_IsText (val1)) { val_ref.val = JS_ToString (ctx, val1); if (JS_IsException (val_ref.val)) goto fail; } else { val_ref.val = val1; } /* Handle immediate ASCII strings */ if (MIST_IsImmediateASCII (val_ref.val)) { len = MIST_GetImmediateASCIILen (val_ref.val); ret = js_malloc_rt (len + 1); /* Use non-GC heap for returned C string */ if (!ret) goto fail; /* Re-read from val_ref after potential GC */ for (i = 0; i < len; i++) { ret[i] = MIST_GetImmediateASCIIChar (val_ref.val, i); } ret[len] = '\0'; if (plen) *plen = len; JS_PopGCRef (ctx, &val_ref); return ret; } /* Handle heap strings (JSText) */ JSText *str = JS_VALUE_GET_STRING (val_ref.val); len = (int)JSText_len (str); /* Calculate UTF-8 size */ size = 0; for (i = 0; i < len; i++) { uint32_t c = string_get (str, i); if (c < 0x80) size += 1; else if (c < 0x800) size += 2; else if (c < 0x10000) size += 3; else size += 4; } ret = js_malloc_rt (size + 1); /* Use non-GC heap for returned C string */ if (!ret) goto fail; /* str pointer is still valid - no GC triggered by js_malloc_rt */ /* Re-extract for safety in case code above changes */ str = JS_VALUE_GET_STRING (val_ref.val); q = ret; for (i = 0; i < len; i++) { uint32_t c = string_get (str, i); q += unicode_to_utf8 ((uint8_t *)q, c); } *q = '\0'; if (plen) *plen = size; JS_PopGCRef (ctx, &val_ref); return ret; fail: JS_PopGCRef (ctx, &val_ref); if (plen) *plen = 0; return NULL; } void JS_FreeCString (JSContext *ctx, const char *ptr) { /* Free C string allocated from non-GC heap */ js_free_rt ((void *)ptr); (void)ctx; (void)ptr; } /* Helper for string value comparison (handles immediate and heap strings) */ int js_string_compare_value (JSContext *ctx, JSValue op1, JSValue op2, BOOL eq_only) { (void)ctx; if (eq_only && op1 == op2) return 0; int len1 = js_string_value_len (op1); int len2 = js_string_value_len (op2); if (eq_only && len1 != len2) return 1; int len = min_int (len1, len2); for (int i = 0; i < len; i++) { uint32_t c1 = js_string_value_get (op1, i); uint32_t c2 = js_string_value_get (op2, i); if (c1 != c2) { return (c1 < c2) ? -1 : 1; } } if (len1 == len2) return 0; return (len1 < len2) ? -1 : 1; } int js_string_compare_value_nocase (JSContext *ctx, JSValue op1, JSValue op2) { (void)ctx; int len1 = js_string_value_len (op1); int len2 = js_string_value_len (op2); if (len1 != len2) return 1; for (int i = 0; i < len1; i++) { uint32_t c1 = js_string_value_get (op1, i); uint32_t c2 = js_string_value_get (op2, i); if (c1 >= 'A' && c1 <= 'Z') c1 += 32; if (c2 >= 'A' && c2 <= 'Z') c2 += 32; if (c1 != c2) return 1; } return 0; } JSValue JS_ConcatString (JSContext *ctx, JSValue op1, JSValue op2) { if (unlikely (!JS_IsText (op1))) { /* Root op2 across JS_ToString which can trigger GC */ JSGCRef op2_guard; JS_PushGCRef (ctx, &op2_guard); op2_guard.val = op2; op1 = JS_ToString (ctx, op1); op2 = op2_guard.val; JS_PopGCRef (ctx, &op2_guard); if (JS_IsException (op1)) { return JS_EXCEPTION; } } if (unlikely (!JS_IsText (op2))) { /* Root op1 across JS_ToString which can trigger GC */ JSGCRef op1_guard; JS_PushGCRef (ctx, &op1_guard); op1_guard.val = op1; op2 = JS_ToString (ctx, op2); op1 = op1_guard.val; JS_PopGCRef (ctx, &op1_guard); if (JS_IsException (op2)) { return JS_EXCEPTION; } } int len1 = js_string_value_len (op1); int len2 = js_string_value_len (op2); int new_len = len1 + len2; JSValue ret_val = JS_NULL; /* Try to create immediate ASCII if short enough and all ASCII */ if (new_len <= MIST_ASCII_MAX_LEN) { char buf[8]; BOOL all_ascii = TRUE; for (int i = 0; i < len1 && all_ascii; i++) { uint32_t c = js_string_value_get (op1, i); if (c >= 0x80) all_ascii = FALSE; else buf[i] = (char)c; } for (int i = 0; i < len2 && all_ascii; i++) { uint32_t c = js_string_value_get (op2, i); if (c >= 0x80) all_ascii = FALSE; else buf[len1 + i] = (char)c; } if (all_ascii) { ret_val = MIST_TryNewImmediateASCII (buf, new_len); } } if (JS_IsNull (ret_val)) { /* Protect op1 and op2 from GC during allocation */ JSGCRef op1_ref, op2_ref; JS_PushGCRef (ctx, &op1_ref); op1_ref.val = op1; JS_PushGCRef (ctx, &op2_ref); op2_ref.val = op2; JSText *p = js_alloc_string (ctx, new_len); if (!p) { JS_PopGCRef (ctx, &op2_ref); JS_PopGCRef (ctx, &op1_ref); return JS_EXCEPTION; } /* Get possibly-moved values after GC */ op1 = op1_ref.val; op2 = op2_ref.val; JS_PopGCRef (ctx, &op2_ref); JS_PopGCRef (ctx, &op1_ref); /* Copy characters using string_put/get */ for (int i = 0; i < len1; i++) { string_put (p, i, js_string_value_get (op1, i)); } for (int i = 0; i < len2; i++) { string_put (p, len1 + i, js_string_value_get (op2, i)); } p->length = new_len; ret_val = pretext_end (ctx, p); } return ret_val; } /* Concat with over-allocated capacity and NO stoning. Used by MACH_CONCAT self-assign (s = s + x) slow path so that subsequent appends can reuse the excess capacity in-place. */ JSValue JS_ConcatStringGrow (JSContext *ctx, JSValue op1, JSValue op2) { if (unlikely (!JS_IsText (op1))) { JSGCRef op2_guard; JS_PushGCRef (ctx, &op2_guard); op2_guard.val = op2; op1 = JS_ToString (ctx, op1); op2 = op2_guard.val; JS_PopGCRef (ctx, &op2_guard); if (JS_IsException (op1)) return JS_EXCEPTION; } if (unlikely (!JS_IsText (op2))) { JSGCRef op1_guard; JS_PushGCRef (ctx, &op1_guard); op1_guard.val = op1; op2 = JS_ToString (ctx, op2); op1 = op1_guard.val; JS_PopGCRef (ctx, &op1_guard); if (JS_IsException (op2)) return JS_EXCEPTION; } int len1 = js_string_value_len (op1); int len2 = js_string_value_len (op2); int new_len = len1 + len2; /* Try immediate ASCII for short results */ if (new_len <= MIST_ASCII_MAX_LEN) { char buf[8]; BOOL all_ascii = TRUE; for (int i = 0; i < len1 && all_ascii; i++) { uint32_t c = js_string_value_get (op1, i); if (c >= 0x80) all_ascii = FALSE; else buf[i] = (char)c; } for (int i = 0; i < len2 && all_ascii; i++) { uint32_t c = js_string_value_get (op2, i); if (c >= 0x80) all_ascii = FALSE; else buf[len1 + i] = (char)c; } if (all_ascii) { JSValue imm = MIST_TryNewImmediateASCII (buf, new_len); if (!JS_IsNull (imm)) return imm; } } /* Allocate with 2x growth factor, minimum 16 */ int capacity = new_len * 2; if (capacity < 16) capacity = 16; JSGCRef op1_ref, op2_ref; JS_PushGCRef (ctx, &op1_ref); op1_ref.val = op1; JS_PushGCRef (ctx, &op2_ref); op2_ref.val = op2; JSText *p = js_alloc_string (ctx, capacity); if (!p) { JS_PopGCRef (ctx, &op2_ref); JS_PopGCRef (ctx, &op1_ref); return JS_EXCEPTION; } op1 = op1_ref.val; op2 = op2_ref.val; JS_PopGCRef (ctx, &op2_ref); JS_PopGCRef (ctx, &op1_ref); for (int i = 0; i < len1; i++) string_put (p, i, js_string_value_get (op1, i)); for (int i = 0; i < len2; i++) string_put (p, len1 + i, js_string_value_get (op2, i)); p->length = new_len; /* Do NOT stone — leave mutable so in-place append can reuse capacity */ return JS_MKPTR (p); } /* WARNING: proto must be an object or JS_NULL */ JSValue JS_NewObjectProtoClass (JSContext *ctx, JSValue proto_val, JSClassID class_id) { JSGCRef proto_ref; JS_PushGCRef (ctx, &proto_ref); proto_ref.val = proto_val; JSRecord *rec = js_new_record_class (ctx, 0, class_id); proto_val = proto_ref.val; /* Get potentially-updated value after GC */ JS_PopGCRef (ctx, &proto_ref); if (!rec) return JS_EXCEPTION; /* Set prototype if provided */ if (JS_IsRecord (proto_val)) { rec->proto = proto_val; } return JS_MKPTR (rec); } JSValue JS_NewObjectClass (JSContext *ctx, int class_id) { return JS_NewObjectProtoClass (ctx, ctx->class_proto[class_id], class_id); } JSValue JS_NewObjectProto (JSContext *ctx, JSValue proto) { return JS_NewObjectProtoClass (ctx, proto, JS_CLASS_OBJECT); } /* Create an intrinsic array with specified capacity Uses bump allocation - values are inline after the JSArray struct */ JSValue JS_NewArrayLen (JSContext *ctx, uint32_t len) { JSArray *arr; uint32_t cap; cap = len > 0 ? len : JS_ARRAY_INITIAL_SIZE; size_t values_size = sizeof (JSValue) * cap; size_t total_size = sizeof (JSArray) + values_size; arr = js_malloc (ctx, total_size); if (!arr) return JS_EXCEPTION; arr->mist_hdr = objhdr_make (cap, OBJ_ARRAY, false, false, false, false); arr->len = len; /* Initialize all values to null (values[] is inline flexible array member) */ for (uint32_t i = 0; i < cap; i++) { arr->values[i] = JS_NULL; } return JS_MKPTR (arr); } JSValue JS_NewArray (JSContext *ctx) { return JS_NewArrayLen (ctx, 0); } /* Create array with pre-allocated capacity but len=0 (for push-fill patterns) */ JSValue JS_NewArrayCap (JSContext *ctx, uint32_t cap) { if (cap == 0) cap = JS_ARRAY_INITIAL_SIZE; size_t values_size = sizeof (JSValue) * cap; size_t total_size = sizeof (JSArray) + values_size; JSArray *arr = js_malloc (ctx, total_size); if (!arr) return JS_EXCEPTION; arr->mist_hdr = objhdr_make (cap, OBJ_ARRAY, false, false, false, false); arr->len = 0; for (uint32_t i = 0; i < cap; i++) arr->values[i] = JS_NULL; return JS_MKPTR (arr); } JSValue JS_NewObject (JSContext *ctx) { /* inline JS_NewObjectClass(ctx, JS_CLASS_OBJECT); */ return JS_NewObjectProtoClass (ctx, ctx->class_proto[JS_CLASS_OBJECT], JS_CLASS_OBJECT); } /* Create object with pre-allocated hash table for n properties */ JSValue JS_NewObjectCap (JSContext *ctx, uint32_t n) { /* slot 0 is reserved, so need n+1 slots minimum. Hash table needs ~2x entries for good load factor. mask must be power-of-2 minus 1. */ uint32_t need = (n + 1) * 2; uint32_t mask = JS_RECORD_INITIAL_MASK; while (mask + 1 < need) mask = (mask << 1) | 1; JSGCRef proto_ref; JS_PushGCRef (ctx, &proto_ref); proto_ref.val = ctx->class_proto[JS_CLASS_OBJECT]; JSRecord *rec = js_new_record_class (ctx, mask, JS_CLASS_OBJECT); JSValue proto_val = proto_ref.val; JS_PopGCRef (ctx, &proto_ref); if (!rec) return JS_EXCEPTION; if (JS_IsRecord (proto_val)) rec->proto = proto_val; return JS_MKPTR (rec); } /* Note: at least 'length' arguments will be readable in 'argv' */ static JSValue JS_NewCFunction3 (JSContext *ctx, JSCFunction *func, const char *name, int length, JSCFunctionEnum cproto, int magic) { JSValue func_obj; JSFunction *f; func_obj = js_new_function (ctx, JS_FUNC_KIND_C); if (JS_IsException (func_obj)) return func_obj; f = JS_VALUE_GET_FUNCTION (func_obj); f->u.cfunc.c_function.generic = func; f->u.cfunc.cproto = cproto; f->u.cfunc.magic = magic; f->length = length; if (name) { JSGCRef fobj_ref; JS_PushGCRef (ctx, &fobj_ref); fobj_ref.val = func_obj; JSValue key = js_key_new (ctx, name); func_obj = fobj_ref.val; JS_PopGCRef (ctx, &fobj_ref); f = JS_VALUE_GET_FUNCTION (func_obj); /* re-chase after allocation */ f->name = key; } else { f->name = JS_KEY_empty; } return func_obj; } /* Note: at least 'length' arguments will be readable in 'argv' */ JSValue JS_NewCFunction2 (JSContext *ctx, JSCFunction *func, const char *name, int length, JSCFunctionEnum cproto, int magic) { return JS_NewCFunction3 (ctx, func, name, length, cproto, magic); } /* free_property is defined earlier as a stub since shapes are removed */ /* GC-safe array growth function. Takes JSValue* pointer to a GC-tracked location (like &argv[n]). Allocates new array, copies data, installs forward header at old location. */ static int js_array_grow (JSContext *ctx, JSValue *arr_ptr, word_t min_cap) { JSArray *arr = JS_VALUE_GET_ARRAY (*arr_ptr); word_t old_cap = js_array_cap (arr); if (min_cap <= old_cap) return 0; if (objhdr_s (arr->mist_hdr)) { JS_RaiseDisrupt (ctx, "cannot grow a stoned array"); return -1; } word_t new_cap = old_cap ? old_cap : JS_ARRAY_INITIAL_SIZE; while (new_cap < min_cap && new_cap <= JS_ARRAY_MAX_CAP / 2) new_cap *= 2; if (new_cap > JS_ARRAY_MAX_CAP) new_cap = JS_ARRAY_MAX_CAP; if (new_cap < min_cap) { JS_RaiseDisrupt (ctx, "array capacity overflow"); return -1; } size_t total_size = sizeof (JSArray) + sizeof (JSValue) * new_cap; JSArray *new_arr = js_malloc (ctx, total_size); if (!new_arr) return -1; /* Re-chase arr via arr_ptr (GC may have moved it during js_malloc) */ arr = JS_VALUE_GET_ARRAY (*arr_ptr); new_arr->mist_hdr = objhdr_make (new_cap, OBJ_ARRAY, false, false, false, false); new_arr->len = arr->len; JSValue old_ptr = *arr_ptr; for (word_t i = 0; i < arr->len; i++) new_arr->values[i] = arr->values[i]; for (word_t i = arr->len; i < new_cap; i++) new_arr->values[i] = JS_NULL; /* Install forward header at old location */ size_t old_arr_size = gc_object_size (arr); arr->mist_hdr = objhdr_make_fwd (new_arr); *((size_t *)((uint8_t *)arr + sizeof (objhdr_t))) = old_arr_size; /* Update the tracked JSValue to point to new array */ *arr_ptr = JS_MKPTR (new_arr); /* Fix self-references: update elements that pointed to the old array */ for (word_t i = 0; i < new_arr->len; i++) { if (new_arr->values[i] == old_ptr) new_arr->values[i] = *arr_ptr; } return 0; } /* GC-safe array push. Takes JSValue* to GC-tracked location. */ static int js_intrinsic_array_push (JSContext *ctx, JSValue *arr_ptr, JSValue val) { JSArray *arr = JS_VALUE_GET_ARRAY (*arr_ptr); if (objhdr_s (arr->mist_hdr)) { JS_RaiseDisrupt (ctx, "cannot push to a stoned array"); return -1; } if (arr->len >= js_array_cap (arr)) { /* Root val across js_array_grow which can trigger GC */ JSGCRef val_ref; JS_PushGCRef (ctx, &val_ref); val_ref.val = val; if (js_array_grow (ctx, arr_ptr, arr->len + 1) < 0) { JS_PopGCRef (ctx, &val_ref); return -1; } val = val_ref.val; JS_PopGCRef (ctx, &val_ref); arr = JS_VALUE_GET_ARRAY (*arr_ptr); /* re-chase after grow */ } arr->values[arr->len++] = val; return 0; } /* GC-safe array set. Takes JSValue* to GC-tracked location. */ static int js_intrinsic_array_set (JSContext *ctx, JSValue *arr_ptr, word_t idx, JSValue val) { JSArray *arr = JS_VALUE_GET_ARRAY (*arr_ptr); if (objhdr_s (arr->mist_hdr)) { JS_RaiseDisrupt (ctx, "cannot set on a stoned array"); return -1; } if (idx >= js_array_cap (arr)) { /* Root val across js_array_grow which can trigger GC */ JSGCRef val_ref; JS_PushGCRef (ctx, &val_ref); val_ref.val = val; if (js_array_grow (ctx, arr_ptr, idx + 1) < 0) { JS_PopGCRef (ctx, &val_ref); return -1; } val = val_ref.val; JS_PopGCRef (ctx, &val_ref); arr = JS_VALUE_GET_ARRAY (*arr_ptr); /* re-chase after grow */ } if (idx >= arr->len) { for (word_t i = arr->len; i < idx; i++) arr->values[i] = JS_NULL; arr->len = idx + 1; } arr->values[idx] = val; return 0; } /* Allocate a new function object */ JSValue js_new_function (JSContext *ctx, JSFunctionKind kind) { JSFunction *func = js_mallocz (ctx, sizeof (JSFunction)); if (!func) return JS_EXCEPTION; func->header = objhdr_make (0, OBJ_FUNCTION, false, false, false, false); func->kind = kind; func->name = JS_NULL; func->length = 0; return JS_MKPTR (func); } /* Set the disruption flag. No logging. */ JSValue JS_Disrupt (JSContext *ctx) { ctx->current_exception = JS_TRUE; return JS_EXCEPTION; } /* Log a message to a named channel. Routes through the ƿit log callback if one is set; falls back to fprintf(stderr) during bootstrap or for OOM. */ void JS_Log (JSContext *ctx, const char *channel, const char *fmt, ...) { char buf[512]; va_list ap; va_start (ap, fmt); vsnprintf (buf, sizeof (buf), fmt, ap); va_end (ap); if (ctx->log_callback && strcmp (channel, "memory") != 0) { ctx->log_callback (ctx, channel, buf); } else { fprintf (stderr, "%s\n", buf); } } /* Log to "error" channel + raise disruption. The common case. */ JSValue __attribute__ ((format (printf, 2, 3))) JS_RaiseDisrupt (JSContext *ctx, const char *fmt, ...) { char buf[512]; va_list ap; va_start (ap, fmt); vsnprintf (buf, sizeof (buf), fmt, ap); va_end (ap); if (ctx->log_callback) JS_Log (ctx, "error", "%s", buf); ctx->current_exception = JS_TRUE; return JS_EXCEPTION; } /* Log to "memory" channel + disrupt. Skips JS callback (can't allocate). Uses fprintf directly — safe even during OOM. */ JSValue JS_RaiseOOM (JSContext *ctx) { size_t used = (size_t)((uint8_t *)ctx->heap_free - (uint8_t *)ctx->heap_base); size_t block = ctx->current_block_size; size_t limit = ctx->heap_memory_limit; const char *name = ctx->name ? ctx->name : ctx->id; const char *label = ctx->actor_label; fprintf(stderr, "[memory] %s: out of memory — heap %zuKB / %zuKB block", name ? name : "?", used / 1024, block / 1024); if (limit > 0) fprintf(stderr, ", limit %zuMB", limit / (1024 * 1024)); if (label) fprintf(stderr, " [%s]", label); fprintf(stderr, "\n"); ctx->current_exception = JS_TRUE; return JS_EXCEPTION; } /* return the pending exception (cannot be called twice). */ JSValue JS_GetException (JSContext *ctx) { JSValue val = ctx->current_exception; ctx->current_exception = JS_NULL; return val; } JS_BOOL JS_HasException (JSContext *ctx) { return !JS_IsNull (ctx->current_exception); } /* in order to avoid executing arbitrary code during the stack trace generation, we only look at simple 'name' properties containing a string. */ static const char *get_func_name (JSContext *ctx, JSValue func) { if (!JS_IsRecord (func)) return NULL; JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (func); /* Create "name" key as immediate ASCII string */ JSValue name_key = MIST_TryNewImmediateASCII ("name", 4); int slot = rec_find_slot (rec, name_key); if (slot <= 0) return NULL; JSValue val = rec->slots[slot].val; if (!JS_IsText (val)) return NULL; return JS_ToCString (ctx, val); } static JSValue JS_RaiseDisruptNotAnObject (JSContext *ctx) { return JS_RaiseDisrupt (ctx, "not an object"); } static JSValue JS_RaiseDisruptInvalidClass (JSContext *ctx, int class_id) { const char *name = ctx->class_array[class_id].class_name; return JS_RaiseDisrupt (ctx, "%s object expected", name ? name : "unknown"); } /* Return an Object, JS_NULL or JS_EXCEPTION in case of exotic object. */ JSValue JS_GetPrototype (JSContext *ctx, JSValue obj) { JSValue val; if (JS_IsRecord (obj)) { JSRecord *p; p = JS_VALUE_GET_OBJ (obj); p = JS_OBJ_GET_PROTO (p); if (!p) val = JS_NULL; else val = JS_MKPTR (p); } else { /* Primitives have no prototype */ val = JS_NULL; } return val; } /* Get property from object using JSRecord-based lookup */ JSValue JS_GetProperty (JSContext *ctx, JSValue obj, JSValue prop) { if (JS_IsNull (obj)) return JS_NULL; if (JS_IsException (obj)) return JS_EXCEPTION; if (unlikely (!JS_IsRecord (obj))) { if (mist_is_blob (obj)) { JSValue proto = ctx->class_proto[JS_CLASS_BLOB]; if (!JS_IsNull (proto) && JS_IsRecord (proto)) return rec_get (ctx, JS_VALUE_GET_RECORD (proto), prop); return JS_NULL; } return JS_NULL; } /* All objects are JSRecords now */ JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (obj); return rec_get (ctx, rec, prop); } /* GC-SAFE: Collects keys to stack buffer before any allocation. Returns a JSValue array of text keys. */ JSValue JS_GetOwnPropertyNames (JSContext *ctx, JSValue obj) { uint32_t mask, count, i; if (!JS_IsRecord (obj)) { JS_RaiseDisruptNotAnObject (ctx); return JS_EXCEPTION; } JSRecord *rec = JS_VALUE_GET_OBJ (obj); mask = (uint32_t)objhdr_cap56 (rec->mist_hdr); /* Count text keys first */ count = 0; for (i = 1; i <= mask; i++) { if (JS_IsText (rec->slots[i].key)) count++; } if (count == 0) return JS_NewArrayLen (ctx, 0); /* Root obj — JS_NewArrayLen allocates and can trigger GC, which moves the record. Re-read keys from the moved record after. */ JSGCRef obj_ref; JS_PushGCRef (ctx, &obj_ref); obj_ref.val = obj; JSValue arr = JS_NewArrayLen (ctx, count); if (JS_IsException (arr)) { JS_PopGCRef (ctx, &obj_ref); return JS_EXCEPTION; } /* Re-read record pointer after possible GC */ rec = JS_VALUE_GET_OBJ (obj_ref.val); mask = (uint32_t)objhdr_cap56 (rec->mist_hdr); uint32_t idx = 0; for (i = 1; i <= mask; i++) { JSValue k = rec->slots[i].key; if (JS_IsText (k)) { JS_SetPropertyNumber (ctx, arr, idx++, k); } } JS_PopGCRef (ctx, &obj_ref); return arr; } /* Return -1 if exception, FALSE if the property does not exist, TRUE if it exists. If TRUE is returned, the property descriptor 'desc' is filled present. Now uses JSRecord-based lookup. */ int JS_GetOwnPropertyInternal (JSContext *ctx, JSValue *desc, JSRecord *p, JSValue prop) { JSRecord *rec = (JSRecord *)p; int slot = rec_find_slot (rec, prop); if (slot > 0) { if (desc) *desc = rec->slots[slot].val; return TRUE; } return FALSE; } int JS_GetOwnProperty (JSContext *ctx, JSValue *desc, JSValue obj, JSValue prop) { if (!JS_IsRecord (obj)) { JS_RaiseDisruptNotAnObject (ctx); return -1; } return JS_GetOwnPropertyInternal (ctx, desc, JS_VALUE_GET_OBJ (obj), prop); } /* GC-SAFE: Only calls rec_find_slot and reads prototype pointers. return -1 if exception otherwise TRUE or FALSE */ int JS_HasProperty (JSContext *ctx, JSValue obj, JSValue prop) { JSRecord *p; int ret; if (unlikely (!JS_IsRecord (obj))) return FALSE; p = JS_VALUE_GET_OBJ (obj); for (;;) { ret = JS_GetOwnPropertyInternal (ctx, NULL, p, prop); if (ret != 0) return ret; if (JS_IsNull (p->proto)) break; p = JS_VALUE_GET_RECORD (p->proto); } return FALSE; } static uint32_t js_string_get_length (JSValue val) { if (JS_IsPtr (val)) { void *ptr = JS_VALUE_GET_PTR (val); /* Check objhdr_t at offset 8 for type */ objhdr_t hdr = *(objhdr_t *)ptr; if (objhdr_type (hdr) == OBJ_TEXT) { /* String (JSText or JSText) */ return (uint32_t)objhdr_cap56 (hdr); } return 0; } else if (MIST_IsImmediateASCII (val)) { return MIST_GetImmediateASCIILen (val); } else { return 0; } } JSValue JS_GetPropertyValue (JSContext *ctx, JSValue this_obj, JSValue prop) { JSValue ret; uint32_t prop_tag = JS_VALUE_GET_TAG (prop); if (JS_IsNull (this_obj)) { return JS_NULL; } if (prop_tag == JS_TAG_INT) { int idx = JS_VALUE_GET_INT (prop); return JS_GetPropertyNumber (ctx, this_obj, idx); } if (prop_tag == JS_TAG_SHORT_FLOAT) { double d = JS_VALUE_GET_FLOAT64 (prop); uint32_t idx = (uint32_t)d; if (d != (double)idx) return JS_NULL; return JS_GetPropertyNumber (ctx, this_obj, idx); } /* Check for string property (immediate or heap) */ if (JS_IsText (prop)) { /* Intrinsic arrays don't support string keys */ if (JS_IsArray (this_obj)) { return JS_NULL; } /* Use text directly as key */ JSValue key = js_key_from_string (ctx, prop); ret = JS_GetProperty (ctx, this_obj, key); /* key is the original text or immediate */ return ret; } /* Handle object keys directly via objkey map */ if (JS_IsRecord (prop)) { /* Intrinsic arrays don't support object keys */ if (!JS_IsRecord (this_obj)) { return JS_NULL; } JSRecord *rec = JS_VALUE_GET_RECORD (this_obj); JSValue val = rec_get (ctx, rec, prop); return val; } /* Unknown type -> null */ return JS_NULL; } JSValue JS_SetPropertyNumber (JSContext *js, JSValue obj, int idx, JSValue val) { if (JS_IsText (obj)) { return JS_RaiseDisrupt (js, "strings are immutable"); } if (!JS_IsArray (obj)) { return JS_RaiseDisrupt (js, "cannot set with a number on a non array"); } if (idx < 0) { return JS_RaiseDisrupt (js, "array index out of bounds"); } /* Root obj since js_intrinsic_array_set may trigger GC during array grow */ JSGCRef obj_ref; JS_PushGCRef (js, &obj_ref); obj_ref.val = obj; if (js_intrinsic_array_set (js, &obj_ref.val, (word_t)idx, val) < 0) { JS_PopGCRef (js, &obj_ref); return JS_EXCEPTION; } JS_PopGCRef (js, &obj_ref); return val; } JSValue JS_GetPropertyNumber (JSContext *js, JSValue obj, int idx) { if (JS_IsArray (obj)) { JSArray *a = JS_VALUE_GET_ARRAY (obj); int len = a->len; if (idx < 0 || idx >= len) { return JS_NULL; } return a->values[idx]; } if (JS_IsText (obj)) { uint32_t len = js_string_get_length (obj); if (idx < 0 || (uint32_t)idx >= len) { return JS_NULL; } return js_sub_string_val (js, obj, idx, idx + 1); } return JS_NULL; } JSValue JS_GetPropertyStr (JSContext *ctx, JSValue this_obj, const char *prop) { if (JS_VALUE_GET_TAG (this_obj) != JS_TAG_PTR) return JS_NULL; size_t len = strlen (prop); JSValue key; JSValue ret; JSGCRef obj_ref; JS_PushGCRef (ctx, &obj_ref); obj_ref.val = this_obj; key = JS_NewStringLen (ctx, prop, len); if (JS_IsException (key)) { JS_PopGCRef (ctx, &obj_ref); return JS_EXCEPTION; } ret = JS_GetProperty (ctx, obj_ref.val, key); JS_PopGCRef (ctx, &obj_ref); return ret; } /* JS_Invoke - invoke a method on an object by name */ JSValue JS_Invoke (JSContext *ctx, JSValue this_val, JSValue method, int argc, JSValue *argv) { JSGCRef this_ref; JS_PushGCRef (ctx, &this_ref); this_ref.val = this_val; JSValue func = JS_GetProperty (ctx, this_ref.val, method); if (JS_IsException (func)) { JS_PopGCRef (ctx, &this_ref); return JS_EXCEPTION; } if (!JS_IsFunction (func)) { JS_PopGCRef (ctx, &this_ref); return JS_NULL; /* Method not found or not callable */ } JSValue ret = JS_Call (ctx, func, this_ref.val, argc, argv); JS_PopGCRef (ctx, &this_ref); return ret; } /* GC-SAFE: May trigger GC if record needs to resize */ int JS_SetProperty (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val) { if (!JS_IsRecord (this_obj)) { if (JS_IsNull (this_obj)) { JS_RaiseDisrupt (ctx, "cannot set property of null"); } else { JS_RaiseDisrupt (ctx, "cannot set property on a primitive"); } return -1; } /* All objects are now records - use record set */ JSRecord *rec = (JSRecord *)JS_VALUE_GET_OBJ (this_obj); if (unlikely (obj_is_stone (rec))) { JS_RaiseDisrupt (ctx, "object is stone"); return -1; } /* Use a local copy that rec_set_own can update if resize happens */ JSValue obj = this_obj; return rec_set_own (ctx, &obj, prop, val); } /* GC-SAFE: Protects this_obj and val in case key creation triggers GC */ int JS_SetPropertyStr (JSContext *ctx, JSValue this_obj, const char *prop, JSValue val) { /* Protect this_obj and val in case key creation triggers GC */ JSGCRef obj_ref, val_ref; JS_AddGCRef (ctx, &obj_ref); JS_AddGCRef (ctx, &val_ref); obj_ref.val = this_obj; val_ref.val = val; /* Create JSValue key from string - use js_key_new for interned stone keys */ JSValue key = js_key_new (ctx, prop); if (JS_IsException (key)) { JS_DeleteGCRef (ctx, &val_ref); JS_DeleteGCRef (ctx, &obj_ref); return -1; } int ret = JS_SetProperty (ctx, obj_ref.val, key, val_ref.val); JS_DeleteGCRef (ctx, &val_ref); JS_DeleteGCRef (ctx, &obj_ref); return ret; } /* Set property with JSValue prop/key - handles int, string, object keys */ int JS_SetPropertyValue (JSContext *ctx, JSValue this_obj, JSValue prop, JSValue val) { uint32_t prop_tag = JS_VALUE_GET_TAG (prop); if (prop_tag == JS_TAG_INT) { int idx = JS_VALUE_GET_INT (prop); JSValue ret = JS_SetPropertyNumber (ctx, this_obj, idx, val); if (JS_IsException (ret)) return -1; return 0; } if (JS_IsText (prop)) { JSValue key = js_key_from_string (ctx, prop); return JS_SetProperty (ctx, this_obj, key, val); } if (JS_IsRecord (prop)) { return JS_SetProperty (ctx, this_obj, prop, val); } return -1; } /* Property access with JSValue key - supports object keys directly */ JSValue JS_GetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key) { if (JS_IsRecord (key)) { if (!JS_IsRecord (this_obj)) return JS_NULL; JSRecord *rec = JS_VALUE_GET_RECORD (this_obj); return rec_get (ctx, rec, key); } /* For string keys, use text directly as key */ if (JS_IsText (key)) { JSValue prop_key = js_key_from_string (ctx, key); return JS_GetProperty (ctx, this_obj, prop_key); } /* For other types, try to use the value directly as a key */ return JS_GetProperty (ctx, this_obj, key); } /* rec_set_own calls rec_resize which can move the record. JS_SetProperty uses a local copy so the caller's JSValue is NOT updated; the VM must call mach_resolve_forward after store operations. */ int JS_SetPropertyKey (JSContext *ctx, JSValue this_obj, JSValue key, JSValue val) { if (JS_IsRecord (key)) { if (!JS_IsRecord (this_obj)) { JS_RaiseDisrupt (ctx, "cannot set property on this value"); return -1; } JSRecord *rec = JS_VALUE_GET_RECORD (this_obj); if (obj_is_stone (rec)) { JS_RaiseDisrupt (ctx, "cannot modify frozen object"); return -1; } return rec_set_own (ctx, &this_obj, key, val); } /* For string keys, use text directly as key */ if (JS_IsText (key)) { JSValue prop_key = js_key_from_string (ctx, key); return JS_SetPropertyInternal (ctx, this_obj, prop_key, val); } /* For other types, use the key directly */ return JS_SetPropertyInternal (ctx, this_obj, key, val); } /* GC-SAFE: no allocations. String keys pass through js_key_from_string (no interning). */ int JS_HasPropertyKey (JSContext *ctx, JSValue obj, JSValue key) { if (JS_IsRecord (key)) { if (!JS_IsRecord (obj)) return FALSE; JSRecord *rec = JS_VALUE_GET_RECORD (obj); /* Check own and prototype chain */ while (rec) { if (rec_find_slot (rec, key) > 0) return TRUE; if (JS_IsNull (rec->proto)) break; rec = JS_VALUE_GET_RECORD (rec->proto); } return FALSE; } /* For string keys, use text directly as key */ if (JS_IsText (key)) { JSValue prop_key = js_key_from_string (ctx, key); return JS_HasProperty (ctx, obj, prop_key); } /* For other types, use directly */ return JS_HasProperty (ctx, obj, key); } /* GC-SAFE: Only calls rec_find_slot and modifies slots directly */ int JS_DeletePropertyKey (JSContext *ctx, JSValue obj, JSValue key) { if (JS_IsRecord (key)) { if (!JS_IsRecord (obj)) return FALSE; JSRecord *rec = JS_VALUE_GET_RECORD (obj); if (obj_is_stone (rec)) { JS_RaiseDisrupt (ctx, "cannot modify frozen object"); return -1; } int slot = rec_find_slot (rec, key); if (slot <= 0) return FALSE; /* Delete by marking as tombstone */ rec->slots[slot].key = JS_EXCEPTION; /* tombstone */ rec->slots[slot].val = JS_NULL; rec->len--; return TRUE; } /* For string keys, use text directly as key */ if (JS_IsText (key)) { JSValue prop_key = js_key_from_string (ctx, key); return JS_DeleteProperty (ctx, obj, prop_key); } /* For other types, use directly */ return JS_DeleteProperty (ctx, obj, key); } /* compute the property flags. For each flag: (JS_PROP_HAS_x forces it, otherwise def_flags is used) Note: makes assumption about the bit pattern of the flags */ int JS_DeleteProperty (JSContext *ctx, JSValue obj, JSValue prop) { JSRecord *rec; int slot; /* Arrays do not support property deletion */ if (JS_IsArray (obj)) { JS_RaiseDisrupt (ctx, "cannot delete array element"); return -1; } if (JS_VALUE_GET_TAG (obj) != JS_TAG_PTR) { JS_RaiseDisruptNotAnObject (ctx); return -1; } rec = (JSRecord *)JS_VALUE_GET_OBJ (obj); if (obj_is_stone (rec)) { JS_RaiseDisrupt (ctx, "cannot delete property of stone object"); return -1; } slot = rec_find_slot (rec, prop); if (slot > 0) { /* Delete by marking as tombstone */ rec->slots[slot].key = JS_EXCEPTION; /* tombstone */ rec->slots[slot].val = JS_NULL; rec->len--; /* tombs tracking removed - not needed with copying GC */ return TRUE; } return TRUE; /* property not found = deletion succeeded */ } BOOL JS_IsCFunction (JSValue val, JSCFunction *func, int magic) { JSFunction *f; if (!JS_IsFunction (val)) return FALSE; f = JS_VALUE_GET_FUNCTION (val); if (f->kind == JS_FUNC_KIND_C) return (f->u.cfunc.c_function.generic == func && f->u.cfunc.magic == magic); else return FALSE; } void JS_SetOpaque (JSValue obj, void *opaque) { JSRecord *p; if (JS_VALUE_GET_TAG (obj) == JS_TAG_PTR) { p = JS_VALUE_GET_OBJ (obj); REC_SET_OPAQUE(p, opaque); } } /* return NULL if not an object of class class_id */ void *JS_GetOpaque (JSValue obj, JSClassID class_id) { JSRecord *p; if (JS_VALUE_GET_TAG (obj) != JS_TAG_PTR) return NULL; p = JS_VALUE_GET_OBJ (obj); if (REC_GET_CLASS_ID(p) != class_id) return NULL; return REC_GET_OPAQUE(p); } void *JS_GetOpaque2 (JSContext *ctx, JSValue obj, JSClassID class_id) { void *p = JS_GetOpaque (obj, class_id); if (unlikely (!p)) { JS_RaiseDisruptInvalidClass (ctx, class_id); } return p; } void *JS_GetAnyOpaque (JSValue obj, JSClassID *class_id) { JSRecord *p; if (!JS_IsRecord (obj)) { *class_id = 0; return NULL; } p = JS_VALUE_GET_OBJ (obj); *class_id = REC_GET_CLASS_ID(p); return REC_GET_OPAQUE(p); } int JS_ToBool (JSContext *ctx, JSValue val) { uint32_t tag = JS_VALUE_GET_TAG (val); /* Check for pointer types first (new tagging system) */ if (JS_IsPtr (val)) { void *ptr = JS_VALUE_GET_PTR (val); objhdr_t hdr = *(objhdr_t *)ptr; if (objhdr_type (hdr) == OBJ_TEXT) { /* String (JSText or JSText) - truthy if non-empty */ BOOL ret = objhdr_cap56 (hdr) != 0; return ret; } /* Objects (record, array, function) are truthy */ return 1; } switch (tag) { case JS_TAG_INT: return JS_VALUE_GET_INT (val) != 0; case JS_TAG_BOOL: return JS_VALUE_GET_BOOL (val); case JS_TAG_NULL: return 0; case JS_TAG_EXCEPTION: return -1; case JS_TAG_STRING_IMM: { BOOL ret = MIST_GetImmediateASCIILen (val) != 0; return ret; } default: if (JS_TAG_IS_FLOAT64 (tag)) { double d = JS_VALUE_GET_FLOAT64 (val); return d != 0; /* NaN impossible in short floats */ } else { return TRUE; } } } /* return an exception in case of memory error. Return JS_NAN if invalid syntax */ /* XXX: directly use js_atod() */ JSValue js_atof (JSContext *ctx, const char *str, const char **pp, int radix, int flags) { const char *p, *p_start; int sep, is_neg; BOOL is_float; int atod_type = flags & ATOD_TYPE_MASK; char buf1[64], *buf; int i, j, len; BOOL buf_allocated = FALSE; JSValue val; JSATODTempMem atod_mem; /* optional separator between digits */ sep = (flags & ATOD_ACCEPT_UNDERSCORES) ? '_' : 256; p = str; p_start = p; is_neg = 0; if (p[0] == '+') { p++; p_start++; if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN)) goto no_radix_prefix; } else if (p[0] == '-') { p++; p_start++; is_neg = 1; if (!(flags & ATOD_ACCEPT_PREFIX_AFTER_SIGN)) goto no_radix_prefix; } if (p[0] == '0') { if ((p[1] == 'x' || p[1] == 'X') && (radix == 0 || radix == 16)) { p += 2; radix = 16; } else if ((p[1] == 'o' || p[1] == 'O') && radix == 0 && (flags & ATOD_ACCEPT_BIN_OCT)) { p += 2; radix = 8; } else if ((p[1] == 'b' || p[1] == 'B') && radix == 0 && (flags & ATOD_ACCEPT_BIN_OCT)) { p += 2; radix = 2; } else if ((p[1] >= '0' && p[1] <= '9') && radix == 0 && (flags & ATOD_ACCEPT_LEGACY_OCTAL)) { int i; sep = 256; for (i = 1; (p[i] >= '0' && p[i] <= '7'); i++) continue; if (p[i] == '8' || p[i] == '9') goto no_prefix; p += 1; radix = 8; } else { goto no_prefix; } /* there must be a digit after the prefix */ if (to_digit ((uint8_t)*p) >= radix) goto fail; no_prefix:; } else { no_radix_prefix: if (!(flags & ATOD_INT_ONLY) && (atod_type == ATOD_TYPE_FLOAT64) && strstart (p, "Infinity", &p)) { double d = 1.0 / 0.0; if (is_neg) d = -d; val = JS_NewFloat64 (ctx, d); goto done; } } if (radix == 0) radix = 10; is_float = FALSE; p_start = p; while (to_digit ((uint8_t)*p) < radix || (*p == sep && (radix != 10 || p != p_start + 1 || p[-1] != '0') && to_digit ((uint8_t)p[1]) < radix)) { p++; } if (!(flags & ATOD_INT_ONLY)) { if (*p == '.' && (p > p_start || to_digit ((uint8_t)p[1]) < radix)) { is_float = TRUE; p++; if (*p == sep) goto fail; while (to_digit ((uint8_t)*p) < radix || (*p == sep && to_digit ((uint8_t)p[1]) < radix)) p++; } if (p > p_start && (((*p == 'e' || *p == 'E') && radix == 10) || ((*p == 'p' || *p == 'P') && (radix == 2 || radix == 8 || radix == 16)))) { const char *p1 = p + 1; is_float = TRUE; if (*p1 == '+') { p1++; } else if (*p1 == '-') { p1++; } if (is_digit ((uint8_t)*p1)) { p = p1 + 1; while (is_digit ((uint8_t)*p) || (*p == sep && is_digit ((uint8_t)p[1]))) p++; } } } if (p == p_start) goto fail; buf = buf1; buf_allocated = FALSE; len = p - p_start; if (unlikely ((len + 2) > sizeof (buf1))) { buf = js_malloc_rt (len + 2); /* no exception raised */ if (!buf) goto mem_error; buf_allocated = TRUE; } /* remove the separators and the radix prefixes */ j = 0; if (is_neg) buf[j++] = '-'; for (i = 0; i < len; i++) { if (p_start[i] != '_') buf[j++] = p_start[i]; } buf[j] = '\0'; if (flags & ATOD_ACCEPT_SUFFIX) { if (*p == 'n') { p++; atod_type = ATOD_TYPE_BIG_INT; } else { if (is_float && radix != 10) goto fail; } } else { if (atod_type == ATOD_TYPE_FLOAT64) { if (is_float && radix != 10) goto fail; } } switch (atod_type) { case ATOD_TYPE_FLOAT64: { double d; d = js_atod (buf, NULL, radix, is_float ? 0 : JS_ATOD_INT_ONLY, &atod_mem); /* return int or float64 */ val = JS_NewFloat64 (ctx, d); } break; default: abort (); } done: if (buf_allocated) js_free_rt (buf); if (pp) *pp = p; return val; fail: val = JS_NAN; goto done; mem_error: val = JS_RaiseOOM(ctx); goto done; } JSValue JS_ToNumber (JSContext *ctx, JSValue val) { uint32_t tag; JSValue ret; /* Handle pointer types (new tagging system) */ if (JS_IsPtr (val)) { void *ptr = JS_VALUE_GET_PTR (val); objhdr_t hdr = *(objhdr_t *)ptr; if (objhdr_type (hdr) == OBJ_TEXT) { /* String */ return JS_RaiseDisrupt (ctx, "cannot convert text to a number"); } /* Objects */ return JS_RaiseDisrupt (ctx, "cannot convert object to number"); } tag = JS_VALUE_GET_NORM_TAG (val); switch (tag) { case JS_TAG_SHORT_FLOAT: case JS_TAG_INT: case JS_TAG_EXCEPTION: ret = val; break; case JS_TAG_NULL: ret = JS_NewInt32 (ctx, 0); break; case JS_TAG_BOOL: ret = JS_NewInt32 (ctx, JS_VALUE_GET_BOOL (val)); break; case JS_TAG_STRING_IMM: return JS_RaiseDisrupt (ctx, "cannot convert text to a number"); default: ret = JS_NAN; break; } return ret; } static __exception int __JS_ToFloat64 (JSContext *ctx, double *pres, JSValue val) { double d; uint32_t tag; val = JS_ToNumber (ctx, val); if (JS_IsException (val)) goto fail; tag = JS_VALUE_GET_NORM_TAG (val); switch (tag) { case JS_TAG_INT: d = JS_VALUE_GET_INT (val); break; case JS_TAG_FLOAT64: d = JS_VALUE_GET_FLOAT64 (val); break; default: abort (); } *pres = d; return 0; fail: *pres = NAN; return -1; } int JS_ToFloat64 (JSContext *ctx, double *pres, JSValue val) { uint32_t tag; tag = JS_VALUE_GET_TAG (val); if (tag == JS_TAG_INT) { *pres = JS_VALUE_GET_INT (val); return 0; } else if (JS_TAG_IS_FLOAT64 (tag)) { *pres = JS_VALUE_GET_FLOAT64 (val); return 0; } else { return __JS_ToFloat64 (ctx, pres, val); } } /* Note: the integer value is satured to 32 bits */ int JS_ToInt32Sat (JSContext *ctx, int *pres, JSValue val) { uint32_t tag; int ret; redo: tag = JS_VALUE_GET_NORM_TAG (val); switch (tag) { case JS_TAG_INT: ret = JS_VALUE_GET_INT (val); break; case JS_TAG_NULL: ret = 0; break; case JS_TAG_BOOL: ret = JS_VALUE_GET_BOOL (val); break; case JS_TAG_EXCEPTION: *pres = 0; return -1; case JS_TAG_FLOAT64: { double d = JS_VALUE_GET_FLOAT64 (val); /* NaN impossible in short floats */ if (d < INT32_MIN) ret = INT32_MIN; else if (d > INT32_MAX) ret = INT32_MAX; else ret = (int)d; } break; default: val = JS_ToNumber (ctx, val); if (JS_IsException (val)) { *pres = 0; return -1; } goto redo; } *pres = ret; return 0; } int JS_ToInt32Clamp (JSContext *ctx, int *pres, JSValue val, int min, int max, int min_offset) { int res = JS_ToInt32Sat (ctx, pres, val); if (res == 0) { if (*pres < min) { *pres += min_offset; if (*pres < min) *pres = min; } else { if (*pres > max) *pres = max; } } return res; } int JS_ToInt64Sat (JSContext *ctx, int64_t *pres, JSValue val) { uint32_t tag; redo: tag = JS_VALUE_GET_NORM_TAG (val); switch (tag) { case JS_TAG_INT: *pres = JS_VALUE_GET_INT (val); return 0; case JS_TAG_BOOL: *pres = JS_VALUE_GET_BOOL (val); return 0; case JS_TAG_NULL: *pres = 0; return 0; case JS_TAG_EXCEPTION: *pres = 0; return -1; case JS_TAG_FLOAT64: { double d = JS_VALUE_GET_FLOAT64 (val); /* NaN impossible in short floats */ if (d < INT64_MIN) *pres = INT64_MIN; else if (d >= 0x1p63) /* must use INT64_MAX + 1 because INT64_MAX cannot be exactly represented as a double */ *pres = INT64_MAX; else *pres = (int64_t)d; } return 0; default: val = JS_ToNumber (ctx, val); if (JS_IsException (val)) { *pres = 0; return -1; } goto redo; } } int JS_ToInt64Clamp (JSContext *ctx, int64_t *pres, JSValue val, int64_t min, int64_t max, int64_t neg_offset) { int res = JS_ToInt64Sat (ctx, pres, val); if (res == 0) { if (*pres < 0) *pres += neg_offset; if (*pres < min) *pres = min; else if (*pres > max) *pres = max; } return res; } /* Same as JS_ToInt32() but with a 64 bit result. Return (<0, 0) in case of exception */ int JS_ToInt64 (JSContext *ctx, int64_t *pres, JSValue val) { uint32_t tag; int64_t ret; redo: tag = JS_VALUE_GET_NORM_TAG (val); switch (tag) { case JS_TAG_INT: ret = JS_VALUE_GET_INT (val); break; case JS_TAG_NULL: ret = 0; break; case JS_TAG_BOOL: ret = JS_VALUE_GET_BOOL (val); break; case JS_TAG_FLOAT64: { JSFloat64Union u; double d; int e; d = JS_VALUE_GET_FLOAT64 (val); u.d = d; /* we avoid doing fmod(x, 2^64) */ e = (u.u64 >> 52) & 0x7ff; if (likely (e <= (1023 + 62))) { /* fast case */ ret = (int64_t)d; } else if (e <= (1023 + 62 + 53)) { uint64_t v; /* remainder modulo 2^64 */ v = (u.u64 & (((uint64_t)1 << 52) - 1)) | ((uint64_t)1 << 52); ret = v << ((e - 1023) - 52); /* take the sign into account */ if (u.u64 >> 63) ret = -ret; } else { ret = 0; /* also handles NaN and +inf */ } } break; default: val = JS_ToNumber (ctx, val); if (JS_IsException (val)) { *pres = 0; return -1; } goto redo; } *pres = ret; return 0; } /* return (<0, 0) in case of exception */ int JS_ToInt32 (JSContext *ctx, int32_t *pres, JSValue val) { uint32_t tag; int32_t ret; redo: tag = JS_VALUE_GET_NORM_TAG (val); switch (tag) { case JS_TAG_INT: ret = JS_VALUE_GET_INT (val); break; case JS_TAG_BOOL: ret = JS_VALUE_GET_BOOL (val); break; case JS_TAG_NULL: ret = 0; break; case JS_TAG_FLOAT64: { JSFloat64Union u; double d; int e; d = JS_VALUE_GET_FLOAT64 (val); u.d = d; /* we avoid doing fmod(x, 2^32) */ e = (u.u64 >> 52) & 0x7ff; if (likely (e <= (1023 + 30))) { /* fast case */ ret = (int32_t)d; } else if (e <= (1023 + 30 + 53)) { uint64_t v; /* remainder modulo 2^32 */ v = (u.u64 & (((uint64_t)1 << 52) - 1)) | ((uint64_t)1 << 52); v = v << ((e - 1023) - 52 + 32); ret = v >> 32; /* take the sign into account */ if (u.u64 >> 63) ret = -ret; } else { ret = 0; /* also handles NaN and +inf */ } } break; default: *pres = 0; return -1; val = JS_ToNumber (ctx, val); if (JS_IsException (val)) { *pres = 0; return -1; } goto redo; } *pres = ret; return 0; } #define MAX_SAFE_INTEGER (((int64_t)1 << 53) - 1) /* convert a value to a length between 0 and MAX_SAFE_INTEGER. return -1 for exception */ static __exception int JS_ToLength (JSContext *ctx, int64_t *plen, JSValue val) { int res = JS_ToInt64Clamp (ctx, plen, val, 0, MAX_SAFE_INTEGER, 0); return res; } static JSValue js_dtoa2 (JSContext *ctx, double d, int radix, int n_digits, int flags) { char buf[1088]; JSDTOATempMem dtoa_mem; int len = js_dtoa (buf, d, radix, n_digits, flags, &dtoa_mem); return js_new_string8_len (ctx, buf, len); } JSValue JS_ToString (JSContext *ctx, JSValue val) { uint32_t tag; char buf[32]; /* Handle pointer types (new tagging system) */ if (JS_IsPtr (val)) { void *ptr = JS_VALUE_GET_PTR (val); objhdr_t hdr = *(objhdr_t *)ptr; uint8_t mist_type = objhdr_type (hdr); if (mist_type == OBJ_TEXT) { /* String - return as-is */ return val; } /* Objects (record, array, function) */ return JS_KEY_true; } tag = JS_VALUE_GET_NORM_TAG (val); switch (tag) { case JS_TAG_STRING_IMM: return val; case JS_TAG_INT: { size_t len; len = i32toa (buf, JS_VALUE_GET_INT (val)); return js_new_string8_len (ctx, buf, len); } break; case JS_TAG_BOOL: return JS_VALUE_GET_BOOL (val) ? JS_KEY_true : JS_KEY_false; case JS_TAG_NULL: return JS_KEY_null; case JS_TAG_EXCEPTION: return JS_EXCEPTION; case JS_TAG_SHORT_FLOAT: return js_dtoa2 (ctx, JS_VALUE_GET_FLOAT64 (val), 10, 0, JS_DTOA_FORMAT_FREE); default: return js_new_string8 (ctx, "[unsupported type]"); } } static JSValue JS_ToStringCheckObject (JSContext *ctx, JSValue val) { uint32_t tag = JS_VALUE_GET_TAG (val); if (tag == JS_TAG_NULL) return JS_RaiseDisrupt (ctx, "null is forbidden"); return JS_ToString (ctx, val); } static JSValue JS_ToQuotedString (JSContext *ctx, JSValue val1) { int i, len; uint32_t c; JSText *b; char buf[16]; JSGCRef val_ref; JSValue val = JS_ToStringCheckObject (ctx, val1); if (JS_IsException (val)) return val; /* Root val — pretext_init/pretext_putc allocate and can trigger GC, which would move the heap string val points to */ JS_PushGCRef (ctx, &val_ref); val_ref.val = val; /* Use js_string_value_len to handle both immediate and heap strings */ len = js_string_value_len (val_ref.val); b = pretext_init (ctx, len + 2); if (!b) goto fail; b = pretext_putc (ctx, b, '\"'); if (!b) goto fail; for (i = 0; i < len; i++) { c = js_string_value_get (val_ref.val, i); switch (c) { case '\t': c = 't'; goto quote; case '\r': c = 'r'; goto quote; case '\n': c = 'n'; goto quote; case '\b': c = 'b'; goto quote; case '\f': c = 'f'; goto quote; case '\"': case '\\': quote: b = pretext_putc (ctx, b, '\\'); if (!b) goto fail; b = pretext_putc (ctx, b, c); if (!b) goto fail; break; default: if (c < 32 || is_surrogate (c)) { snprintf (buf, sizeof (buf), "\\u%04x", c); b = pretext_puts8 (ctx, b, buf); if (!b) goto fail; } else { b = pretext_putc (ctx, b, c); if (!b) goto fail; } break; } } b = pretext_putc (ctx, b, '\"'); if (!b) goto fail; JS_PopGCRef (ctx, &val_ref); return pretext_end (ctx, b); fail: JS_PopGCRef (ctx, &val_ref); return JS_EXCEPTION; } #define JS_PRINT_MAX_DEPTH 8 typedef struct { JSRuntime *rt; JSContext *ctx; /* may be NULL */ JSPrintValueOptions options; JSPrintValueWrite *write_func; void *write_opaque; int level; JSRecord *print_stack[JS_PRINT_MAX_DEPTH]; /* level values */ } JSPrintValueState; static void js_print_value (JSPrintValueState *s, JSValue val); static void js_putc (JSPrintValueState *s, char c) { s->write_func (s->write_opaque, &c, 1); } static void js_puts (JSPrintValueState *s, const char *str) { s->write_func (s->write_opaque, str, strlen (str)); } static void __attribute__ ((format (printf, 2, 3))) js_printf (JSPrintValueState *s, const char *fmt, ...) { va_list ap; char buf[256]; va_start (ap, fmt); vsnprintf (buf, sizeof (buf), fmt, ap); va_end (ap); s->write_func (s->write_opaque, buf, strlen (buf)); } static void js_print_float64 (JSPrintValueState *s, double d) { JSDTOATempMem dtoa_mem; char buf[32]; int len; len = js_dtoa (buf, d, 10, 0, JS_DTOA_FORMAT_FREE | JS_DTOA_MINUS_ZERO, &dtoa_mem); s->write_func (s->write_opaque, buf, len); } static void js_dump_char (JSPrintValueState *s, int c, int sep) { if (c == sep || c == '\\') { js_putc (s, '\\'); js_putc (s, c); } else if (c >= ' ' && c <= 126) { js_putc (s, c); } else if (c == '\n') { js_putc (s, '\\'); js_putc (s, 'n'); } else { js_printf (s, "\\u%04x", c); } } static void js_print_string_rec (JSPrintValueState *s, JSValue val, int sep, uint32_t pos) { if (MIST_IsImmediateASCII (val)) { /* Immediate ASCII string */ int len = MIST_GetImmediateASCIILen (val); if (pos < s->options.max_string_length) { uint32_t i, l; l = min_uint32 (len, s->options.max_string_length - pos); for (i = 0; i < l; i++) { js_dump_char (s, MIST_GetImmediateASCIIChar (val, i), sep); } } } else if (JS_IsPtr (val) && objhdr_type (*(objhdr_t *)JS_VALUE_GET_PTR (val)) == OBJ_TEXT) { /* Heap text (JSText) */ JSText *p = (JSText *)JS_VALUE_GET_PTR (val); uint32_t i, len; if (pos < s->options.max_string_length) { len = min_uint32 ((uint32_t)JSText_len (p), s->options.max_string_length - pos); for (i = 0; i < len; i++) { js_dump_char (s, string_get (p, i), sep); } } } else { js_printf (s, "", (int)JS_VALUE_GET_TAG (val)); } } static void js_print_string (JSPrintValueState *s, JSValue val) { int sep = '\"'; js_putc (s, sep); js_print_string_rec (s, val, sep, 0); js_putc (s, sep); if (js_string_get_length (val) > s->options.max_string_length) { uint32_t n = js_string_get_length (val) - s->options.max_string_length; js_printf (s, "... %u more character%s", n, n > 1 ? "s" : ""); } } static void js_print_value (JSPrintValueState *s, JSValue val) { uint32_t tag = JS_VALUE_GET_NORM_TAG (val); const char *str; /* Handle pointer types first (new tagging system) */ if (JS_IsPtr (val)) { void *ptr = JS_VALUE_GET_PTR (val); /* Check objhdr_t at offset 8 for type */ objhdr_t hdr = *(objhdr_t *)ptr; uint8_t mist_type = objhdr_type (hdr); if (mist_type == OBJ_TEXT) { /* String (JSText or JSText) */ js_print_string (s, val); return; } return; } switch (tag) { case JS_TAG_INT: js_printf (s, "%d", JS_VALUE_GET_INT (val)); break; case JS_TAG_BOOL: if (JS_VALUE_GET_BOOL (val)) str = "true"; else str = "false"; goto print_str; case JS_TAG_NULL: str = "null"; goto print_str; case JS_TAG_EXCEPTION: str = "exception"; goto print_str; print_str: js_puts (s, str); break; case JS_TAG_SHORT_FLOAT: js_print_float64 (s, JS_VALUE_GET_FLOAT64 (val)); break; case JS_TAG_STRING_IMM: js_print_string (s, val); break; default: js_printf (s, "[unknown tag %d]", tag); break; } } void JS_PrintValueSetDefaultOptions (JSPrintValueOptions *options) { memset (options, 0, sizeof (*options)); options->max_depth = 2; options->max_string_length = 1000; options->max_item_count = 100; } static void JS_PrintValueInternal (JSRuntime *rt, JSContext *ctx, JSPrintValueWrite *write_func, void *write_opaque, JSValue val, const JSPrintValueOptions *options) { JSPrintValueState ss, *s = &ss; if (options) s->options = *options; else JS_PrintValueSetDefaultOptions (&s->options); if (s->options.max_depth <= 0) s->options.max_depth = JS_PRINT_MAX_DEPTH; else s->options.max_depth = min_int (s->options.max_depth, JS_PRINT_MAX_DEPTH); if (s->options.max_string_length == 0) s->options.max_string_length = UINT32_MAX; if (s->options.max_item_count == 0) s->options.max_item_count = UINT32_MAX; s->rt = rt; s->ctx = ctx; s->write_func = write_func; s->write_opaque = write_opaque; s->level = 0; js_print_value (s, val); } void JS_PrintValueRT (JSRuntime *rt, JSPrintValueWrite *write_func, void *write_opaque, JSValue val, const JSPrintValueOptions *options) { JS_PrintValueInternal (rt, NULL, write_func, write_opaque, val, options); } void JS_PrintValue (JSContext *ctx, JSPrintValueWrite *write_func, void *write_opaque, JSValue val, const JSPrintValueOptions *options) { JS_PrintValueInternal (ctx->rt, ctx, write_func, write_opaque, val, options); } void js_dump_value_write (void *opaque, const char *buf, size_t len) { FILE *fo = opaque; fwrite (buf, 1, len, fo); } /* print_atom removed - atoms no longer used */ __maybe_unused void JS_DumpValue (JSContext *ctx, const char *str, JSValue val) { printf ("%s=", str); JS_PrintValue (ctx, js_dump_value_write, stdout, val, NULL); printf ("\n"); } __maybe_unused void JS_DumpObjectHeader (JSRuntime *rt) { printf ("%14s %4s %4s %14s %s\n", "ADDRESS", "REFS", "SHRF", "PROTO", "CONTENT"); } /* for debug only: dump an object without side effect */ __maybe_unused void JS_DumpObject (JSRuntime *rt, JSRecord *rec) { JSPrintValueOptions options; printf ("%14p ", (void *)rec); /* Print prototype from JSRecord */ if (!JS_IsNull (rec->proto)) { printf ("%14p ", JS_VALUE_GET_PTR (rec->proto)); } else { printf ("%14s ", "-"); } JS_PrintValueSetDefaultOptions (&options); options.max_depth = 1; options.show_hidden = TRUE; options.raw_dump = TRUE; JS_PrintValueRT (rt, js_dump_value_write, stdout, JS_MKPTR (rec), &options); printf ("\n"); } __maybe_unused void JS_DumpGCObject (JSRuntime *rt, objhdr_t *p) { if (objhdr_type (*p) == OBJ_RECORD) { JS_DumpObject (rt, (JSRecord *)p); } else { switch (objhdr_type (*p)) { case OBJ_ARRAY: printf ("[array]"); break; case OBJ_RECORD: printf ("[record]"); break; default: printf ("[unknown %d]", objhdr_type (*p)); break; } printf ("\n"); } } /* Simplified equality: no NaN (becomes null), no coercion, no SameValue distinction */ BOOL js_strict_eq (JSContext *ctx, JSValue op1, JSValue op2) { /* Fast path: identical values */ if (op1 == op2) return TRUE; int tag1 = JS_VALUE_GET_NORM_TAG (op1); int tag2 = JS_VALUE_GET_NORM_TAG (op2); /* Different types are never equal (no coercion) */ /* Special case: INT and FLOAT can be equal */ if (tag1 != tag2) { if (!((tag1 == JS_TAG_INT || tag1 == JS_TAG_FLOAT64) && (tag2 == JS_TAG_INT || tag2 == JS_TAG_FLOAT64))) return FALSE; } switch (tag1) { case JS_TAG_INT: case JS_TAG_FLOAT64: { /* Numbers: unpack and compare */ double d1 = (tag1 == JS_TAG_INT) ? (double)JS_VALUE_GET_INT (op1) : JS_VALUE_GET_FLOAT64 (op1); double d2 = (tag2 == JS_TAG_INT) ? (double)JS_VALUE_GET_INT (op2) : JS_VALUE_GET_FLOAT64 (op2); return d1 == d2; } case JS_TAG_STRING_IMM: /* Immediate text vs immediate text (handled by op1 == op2 fast path) */ /* or vs heap text */ if (JS_IsText (op2)) return js_string_compare_value (ctx, op1, op2, TRUE) == 0; return FALSE; case JS_TAG_PTR: /* Heap text vs heap text or vs immediate text */ if (JS_IsText (op1) && JS_IsText (op2)) return js_string_compare_value (ctx, op1, op2, TRUE) == 0; /* Records/objects: pointer equality (op1 == op2 handles same object) */ return FALSE; /* Different objects */ case JS_TAG_BOOL: case JS_TAG_NULL: /* Already handled by op1 == op2 fast path */ return FALSE; default: return FALSE; } } BOOL JS_StrictEq (JSContext *ctx, JSValue op1, JSValue op2) { return js_strict_eq (ctx, op1, op2); } static JSValue js_throw_type_error (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { return JS_RaiseDisrupt (ctx, "invalid property access"); } JSValue js_call_c_function (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) { JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj); JSCFunctionEnum cproto = f->u.cfunc.cproto; JSCFunctionType func = f->u.cfunc.c_function; JSValue ret_val; /* Auto-root argv: copy to C stack and register as GC root. GC will update arg_copy[] in-place, so C functions can read argv[n] across allocation points without manual guarding. */ JSValue arg_copy[argc > 0 ? argc : 1]; if (argc > 0) memcpy (arg_copy, argv, argc * sizeof (JSValue)); CCallRoot root = { arg_copy, argc, ctx->c_call_root }; ctx->c_call_root = &root; if (unlikely (ctx->trace_hook) && (ctx->trace_type & JS_HOOK_CALL)) { js_debug dbg; js_debug_info (ctx, func_obj, &dbg); ctx->trace_hook (ctx, JS_HOOK_CALL, &dbg, ctx->trace_data); } #ifdef VALIDATE_GC uint8_t *pre_heap_base = ctx->heap_base; #endif switch (cproto) { case JS_CFUNC_generic: ret_val = func.generic (ctx, this_obj, argc, arg_copy); break; case JS_CFUNC_generic_magic: ret_val = func.generic_magic (ctx, this_obj, argc, arg_copy, f->u.cfunc.magic); break; case JS_CFUNC_f_f: { double d1; if (unlikely (JS_ToFloat64 (ctx, &d1, arg_copy[0]))) { ret_val = JS_EXCEPTION; break; } ret_val = JS_NewFloat64 (ctx, func.f_f (d1)); } break; case JS_CFUNC_f_f_f: { double d1, d2; if (unlikely (JS_ToFloat64 (ctx, &d1, arg_copy[0]))) { ret_val = JS_EXCEPTION; break; } if (unlikely (JS_ToFloat64 (ctx, &d2, arg_copy[1]))) { ret_val = JS_EXCEPTION; break; } ret_val = JS_NewFloat64 (ctx, func.f_f_f (d1, d2)); } break; /* Fixed-arity fast paths — args passed by value at dispatch time */ case JS_CFUNC_0: ret_val = func.f0 (ctx, this_obj); break; case JS_CFUNC_1: ret_val = func.f1 (ctx, this_obj, arg_copy[0]); break; case JS_CFUNC_2: ret_val = func.f2 (ctx, this_obj, arg_copy[0], arg_copy[1]); break; case JS_CFUNC_3: ret_val = func.f3 (ctx, this_obj, arg_copy[0], arg_copy[1], arg_copy[2]); break; case JS_CFUNC_4: ret_val = func.f4 (ctx, this_obj, arg_copy[0], arg_copy[1], arg_copy[2], arg_copy[3]); break; /* Pure functions (no this_val) */ case JS_CFUNC_PURE: ret_val = func.pure (ctx, argc, arg_copy); break; case JS_CFUNC_PURE_0: ret_val = func.pure0 (ctx); break; case JS_CFUNC_PURE_1: ret_val = func.pure1 (ctx, arg_copy[0]); break; case JS_CFUNC_PURE_2: ret_val = func.pure2 (ctx, arg_copy[0], arg_copy[1]); break; case JS_CFUNC_PURE_3: ret_val = func.pure3 (ctx, arg_copy[0], arg_copy[1], arg_copy[2]); break; case JS_CFUNC_PURE_4: ret_val = func.pure4 (ctx, arg_copy[0], arg_copy[1], arg_copy[2], arg_copy[3]); break; default: abort (); } #ifdef VALIDATE_GC if (ctx->heap_base != pre_heap_base && JS_IsPtr (ret_val)) { void *rp = JS_VALUE_GET_PTR (ret_val); if (!is_ct_ptr (ctx, rp) && ((uint8_t *)rp < ctx->heap_base || (uint8_t *)rp >= ctx->heap_free)) { /* Note: f is stale after GC (func_obj was passed by value), so we cannot read f->name here. Just report the pointer. */ fprintf (stderr, "VALIDATE_GC: C function returned stale ptr=%p " "heap=[%p,%p) after GC\n", rp, (void *)ctx->heap_base, (void *)ctx->heap_free); fflush (stderr); } } #endif ctx->c_call_root = root.prev; if (unlikely (ctx->trace_hook) && (ctx->trace_type & JS_HOOK_RET)) ctx->trace_hook (ctx, JS_HOOK_RET, NULL, ctx->trace_data); return ret_val; } /* Central function dispatcher — replaces the old bytecode VM entry point. Now dispatches to MACH (register VM), MCODE, or C functions. */ JSValue JS_CallInternal (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv, int flags) { (void)flags; if (js_poll_interrupts (ctx)) return JS_EXCEPTION; if (!JS_IsFunction (func_obj)) return JS_RaiseDisrupt (ctx, "not a function"); JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj); if (unlikely (f->length >= 0 && argc > f->length)) { char buf[KEY_GET_STR_BUF_SIZE]; return JS_RaiseDisrupt (ctx, "too many arguments for %s: expected %d, got %d", JS_KeyGetStr (ctx, buf, KEY_GET_STR_BUF_SIZE, f->name), f->length, argc); } switch (f->kind) { case JS_FUNC_KIND_C: case JS_FUNC_KIND_C_DATA: return js_call_c_function (ctx, func_obj, this_obj, argc, argv); case JS_FUNC_KIND_REGISTER: return JS_CallRegisterVM (ctx, JS_VALUE_GET_CODE(FN_READ_CODE(f))->u.reg.code, this_obj, argc, argv, f->u.cell.env_record, f->u.cell.outer_frame); case JS_FUNC_KIND_NATIVE: return cell_native_dispatch (ctx, func_obj, this_obj, argc, argv); default: return JS_RaiseDisrupt (ctx, "not a function"); } } /* Call helper used by runtime callback intrinsics. Caps argc to function arity so helper callbacks never rely on over-arity calls. */ static inline JSValue js_call_internal_capped (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) { JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj); if (f->length >= 0 && argc > f->length) argc = f->length; return JS_CallInternal (ctx, func_obj, this_obj, argc, argc > 0 ? argv : NULL, 0); } JSValue JS_Call (JSContext *ctx, JSValue func_obj, JSValue this_obj, int argc, JSValue *argv) { if (js_poll_interrupts (ctx)) return JS_EXCEPTION; if (unlikely (!JS_IsFunction (func_obj))) return JS_RaiseDisrupt (ctx, "not a function"); JSFunction *f = JS_VALUE_GET_FUNCTION (func_obj); if (unlikely (f->length >= 0 && argc > f->length)) { char buf[KEY_GET_STR_BUF_SIZE]; return JS_RaiseDisrupt (ctx, "too many arguments for %s: expected %d, got %d", JS_KeyGetStr (ctx, buf, KEY_GET_STR_BUF_SIZE, f->name), f->length, argc); } switch (f->kind) { case JS_FUNC_KIND_C: return js_call_c_function (ctx, func_obj, this_obj, argc, argv); case JS_FUNC_KIND_REGISTER: return JS_CallRegisterVM (ctx, JS_VALUE_GET_CODE(FN_READ_CODE(f))->u.reg.code, this_obj, argc, argv, f->u.cell.env_record, f->u.cell.outer_frame); case JS_FUNC_KIND_NATIVE: return cell_native_dispatch (ctx, func_obj, this_obj, argc, argv); default: return JS_RaiseDisrupt (ctx, "not a function"); } } /*******************************************************************/ /* runtime functions & objects */ int check_exception_free (JSContext *ctx, JSValue obj) { return JS_IsException (obj); } JSValue find_key (JSContext *ctx, const char *name) { /* Create an interned JSValue key from C string */ return js_key_new (ctx, name); } static int JS_InstantiateFunctionListItem (JSContext *ctx, JSValue *obj_ptr, JSValue key, const JSCFunctionListEntry *e) { JSValue val; switch (e->def_type) { case JS_DEF_ALIAS: /* using autoinit for aliases is not safe */ { JSValue key1 = find_key (ctx, e->u.alias.name); switch (e->u.alias.base) { case -1: val = JS_GetProperty (ctx, *obj_ptr, key1); break; case 0: val = JS_GetProperty (ctx, ctx->global_obj, key1); break; default: abort (); } /* key1 is interned, no need to free */ } break; case JS_DEF_CFUNC: val = JS_NewCFunction2 (ctx, e->u.func.cfunc.generic, e->name, e->u.func.length, e->u.func.cproto, e->magic); break; case JS_DEF_PROP_INT32: val = JS_NewInt32 (ctx, e->u.i32); break; case JS_DEF_PROP_INT64: val = JS_NewInt64 (ctx, e->u.i64); break; case JS_DEF_PROP_DOUBLE: val = __JS_NewFloat64 (ctx, e->u.f64); break; case JS_DEF_PROP_UNDEFINED: val = JS_NULL; break; case JS_DEF_PROP_STRING: val = JS_NewAtomString (ctx, e->u.str); break; case JS_DEF_OBJECT: val = JS_NewObject (ctx); if (JS_IsException (val)) return -1; JS_SetPropertyFunctionList (ctx, val, e->u.prop_list.tab, e->u.prop_list.len); break; default: abort (); } JS_SetPropertyInternal (ctx, *obj_ptr, key, val); return 0; } int JS_SetPropertyFunctionList (JSContext *ctx, JSValue obj, const JSCFunctionListEntry *tab, int len) { int i, ret; /* Root obj since allocations in the loop can trigger GC */ JSGCRef obj_ref; obj_ref.val = obj; obj_ref.prev = ctx->last_gc_ref; ctx->last_gc_ref = &obj_ref; for (i = 0; i < len; i++) { const JSCFunctionListEntry *e = &tab[i]; JSValue key = find_key (ctx, e->name); if (JS_IsNull (key)) { ctx->last_gc_ref = obj_ref.prev; return -1; } ret = JS_InstantiateFunctionListItem (ctx, &obj_ref.val, key, e); /* key is interned, no need to free */ if (ret) { ctx->last_gc_ref = obj_ref.prev; return -1; } } ctx->last_gc_ref = obj_ref.prev; return 0; } __exception int js_get_length32 (JSContext *ctx, uint32_t *pres, JSValue obj) { int tag = JS_VALUE_GET_TAG (obj); /* Fast path for intrinsic arrays */ if (JS_IsArray (obj)) { JSArray *arr = JS_VALUE_GET_ARRAY (obj); *pres = arr->len; return 0; } if (JS_IsFunction (obj)) { JSFunction *fn = JS_VALUE_GET_FUNCTION (obj); *pres = fn->length; return 0; } if (mist_is_blob (obj)) { JSBlob *bd = (JSBlob *)chase (obj); *pres = (uint32_t)bd->length; return 0; } if (tag == JS_TAG_STRING || tag == JS_TAG_STRING_IMM) { JSText *p = JS_VALUE_GET_STRING (obj); *pres = JSText_len (p); return 0; } JSValue len_val; len_val = JS_GetProperty (ctx, obj, JS_KEY_length); if (JS_IsException (len_val)) { *pres = 0; return -1; } return JS_ToUint32 (ctx, pres, len_val); } __exception int js_get_length64 (JSContext *ctx, int64_t *pres, JSValue obj) { /* Fast path for intrinsic arrays */ if (JS_IsArray (obj)) { JSArray *arr = JS_VALUE_GET_ARRAY (obj); *pres = arr->len; return 0; } JSValue len_val; len_val = JS_GetProperty (ctx, obj, JS_KEY_length); if (JS_IsException (len_val)) { *pres = 0; return -1; } return JS_ToLength (ctx, pres, len_val); } int JS_GetLength (JSContext *ctx, JSValue obj, int64_t *pres) { return js_get_length64 (ctx, pres, obj); } void free_arg_list (JSContext *ctx, JSValue *tab, uint32_t len) { (void)ctx; (void)len; js_free_rt(tab); } /* XXX: should use ValueArray */ JSValue *build_arg_list (JSContext *ctx, uint32_t *plen, JSValue *parray_arg) { uint32_t len, i; JSValue *tab; /* Fast path for intrinsic arrays */ if (JS_IsArray (*parray_arg)) { JSArray *arr = JS_VALUE_GET_ARRAY (*parray_arg); len = arr->len; if (len > JS_MAX_LOCAL_VARS) { JS_RaiseDisrupt ( ctx, "too many arguments in function call (only %d allowed)", JS_MAX_LOCAL_VARS); return NULL; } tab = js_mallocz_rt (sizeof (tab[0]) * max_uint32 (1, len)); if (!tab) return NULL; arr = JS_VALUE_GET_ARRAY (*parray_arg); for (i = 0; i < len; i++) { tab[i] = arr->values[i]; } *plen = len; return tab; } JS_RaiseDisrupt (ctx, "not an array"); return NULL; } static JSValue js_array_includes (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSValue found = js_cell_array_find (ctx, this_val, argc, argv); if (JS_IsException (found)) return JS_EXCEPTION; if (JS_IsNull (found)) return JS_NewBool (ctx, FALSE); return JS_NewBool (ctx, TRUE); } /* return < 0 if exception or TRUE/FALSE */ static int js_is_regexp (JSContext *ctx, JSValue obj); /* RegExp */ void js_regexp_finalizer (JSRuntime *rt, JSValue val) { JSRegExp *re = JS_GetOpaque (val, JS_CLASS_REGEXP); if (re) { js_free_rt (re->pattern); js_free_rt (re->bytecode); js_free_rt (re); } (void)rt; } /* create a string containing the RegExp bytecode */ JSValue js_compile_regexp (JSContext *ctx, JSValue pattern, JSValue flags) { const char *str; int re_flags, mask; uint8_t *re_bytecode_buf; size_t i, len; int re_bytecode_len; JSValue ret; char error_msg[64]; re_flags = 0; if (!JS_IsNull (flags)) { str = JS_ToCStringLen (ctx, &len, flags); if (!str) return JS_EXCEPTION; /* XXX: re_flags = LRE_FLAG_OCTAL unless strict mode? */ for (i = 0; i < len; i++) { switch (str[i]) { case 'd': mask = LRE_FLAG_INDICES; break; case 'g': mask = LRE_FLAG_GLOBAL; break; case 'i': mask = LRE_FLAG_IGNORECASE; break; case 'm': mask = LRE_FLAG_MULTILINE; break; case 's': mask = LRE_FLAG_DOTALL; break; case 'u': mask = LRE_FLAG_UNICODE; break; case 'v': mask = LRE_FLAG_UNICODE_SETS; break; case 'y': mask = LRE_FLAG_STICKY; break; default: goto bad_flags; } if ((re_flags & mask) != 0) { bad_flags: JS_FreeCString (ctx, str); goto bad_flags1; } re_flags |= mask; } JS_FreeCString (ctx, str); } /* 'u' and 'v' cannot be both set */ if ((re_flags & LRE_FLAG_UNICODE_SETS) && (re_flags & LRE_FLAG_UNICODE)) { bad_flags1: return JS_RaiseDisrupt (ctx, "invalid regular expression flags"); } str = JS_ToCStringLen2 ( ctx, &len, pattern, !(re_flags & (LRE_FLAG_UNICODE | LRE_FLAG_UNICODE_SETS))); if (!str) return JS_EXCEPTION; re_bytecode_buf = lre_compile (&re_bytecode_len, error_msg, sizeof (error_msg), str, len, re_flags, ctx); JS_FreeCString (ctx, str); if (!re_bytecode_buf) { JS_RaiseDisrupt (ctx, "%s", error_msg); return JS_EXCEPTION; } ret = js_new_string8_len (ctx, (const char *)re_bytecode_buf, re_bytecode_len); js_free_rt (re_bytecode_buf); return ret; } /* create a RegExp object from a string containing the RegExp bytecode and the source pattern */ JSValue js_regexp_constructor_internal (JSContext *ctx, JSValue pattern, JSValue bc) { JSValue obj; JSRecord *p; JSRegExp *re; const char *pat_cstr; size_t pat_len; int bc_len, i; /* sanity check - need strings for pattern and bytecode */ if (!JS_IsText (bc) || !JS_IsText (pattern)) { JS_RaiseDisrupt (ctx, "string expected"); fail: return JS_EXCEPTION; } /* Root pattern and bc across allocating calls */ JSGCRef pat_ref, bc_ref; JS_PushGCRef (ctx, &pat_ref); pat_ref.val = pattern; JS_PushGCRef (ctx, &bc_ref); bc_ref.val = bc; obj = JS_NewObjectProtoClass (ctx, ctx->class_proto[JS_CLASS_REGEXP], JS_CLASS_REGEXP); if (JS_IsException (obj)) { JS_PopGCRef (ctx, &bc_ref); JS_PopGCRef (ctx, &pat_ref); goto fail; } /* Root obj across allocating calls */ JSGCRef obj_ref; JS_PushGCRef (ctx, &obj_ref); obj_ref.val = obj; #define REGEXP_CLEANUP() do { JS_PopGCRef (ctx, &obj_ref); JS_PopGCRef (ctx, &bc_ref); JS_PopGCRef (ctx, &pat_ref); } while(0) /* Allocate JSRegExp off-heap (not on GC heap) so opaque pointer stays valid after GC */ re = js_malloc_rt (sizeof(JSRegExp)); if (!re) { REGEXP_CLEANUP (); JS_RaiseOOM(ctx); goto fail; } p = JS_VALUE_GET_OBJ (obj_ref.val); REC_SET_OPAQUE(p, re); re->pattern = NULL; re->bytecode = NULL; /* Extract pattern as UTF-8 C string */ pat_cstr = JS_ToCStringLen (ctx, &pat_len, pat_ref.val); if (!pat_cstr) { REGEXP_CLEANUP (); goto fail; } re->pattern = js_malloc_rt (pat_len + 1); if (!re->pattern) { JS_FreeCString (ctx, pat_cstr); REGEXP_CLEANUP (); goto fail; } memcpy (re->pattern, pat_cstr, pat_len + 1); re->pattern_len = (uint32_t)pat_len; JS_FreeCString (ctx, pat_cstr); /* Extract bytecode as raw bytes via string_get (not JS_ToCStringLen which UTF-8 encodes and would mangle bytes >= 128) */ bc = bc_ref.val; if (MIST_IsImmediateASCII (bc)) { bc_len = MIST_GetImmediateASCIILen (bc); re->bytecode = js_malloc_rt (bc_len); if (!re->bytecode) { REGEXP_CLEANUP (); goto fail; } for (i = 0; i < bc_len; i++) re->bytecode[i] = (uint8_t)MIST_GetImmediateASCIIChar (bc, i); } else { JSText *bc_str = (JSText *)chase (bc_ref.val); bc_len = (int)JSText_len (bc_str); re->bytecode = js_malloc_rt (bc_len); if (!re->bytecode) { REGEXP_CLEANUP (); goto fail; } for (i = 0; i < bc_len; i++) re->bytecode[i] = (uint8_t)string_get (bc_str, i); } re->bytecode_len = (uint32_t)bc_len; { JSValue key = JS_KEY_STR (ctx, "lastIndex"); obj = obj_ref.val; /* re-read after JS_KEY_STR allocation */ JS_SetPropertyInternal (ctx, obj, key, JS_NewInt32 (ctx, 0)); } obj = obj_ref.val; REGEXP_CLEANUP (); return obj; } #undef REGEXP_CLEANUP static JSRegExp *js_get_regexp (JSContext *ctx, JSValue obj, BOOL throw_error) { if (JS_VALUE_GET_TAG (obj) == JS_TAG_PTR) { JSRecord *p = JS_VALUE_GET_OBJ (obj); if (REC_GET_CLASS_ID(p) == JS_CLASS_REGEXP) return (JSRegExp *)REC_GET_OPAQUE(p); } if (throw_error) { JS_RaiseDisruptInvalidClass (ctx, JS_CLASS_REGEXP); } return NULL; } /* return < 0 if exception or TRUE/FALSE */ static int js_is_regexp (JSContext *ctx, JSValue obj) { JSValue m; if (!mist_is_gc_object (obj)) return FALSE; m = JS_GetPropertyStr (ctx, obj, "Symbol.match"); if (JS_IsException (m)) return -1; if (!JS_IsNull (m)) return JS_ToBool (ctx, m); return js_get_regexp (ctx, obj, FALSE) != NULL; } JSValue js_regexp_constructor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSValue pattern, flags, bc, val; JSValue pat, flags1; JSRegExp *re; int pat_is_regexp; pat = argv[0]; flags1 = argv[1]; pat_is_regexp = js_is_regexp (ctx, pat); if (pat_is_regexp < 0) return JS_EXCEPTION; /* If called with a regexp and no flags, just return a copy */ if (pat_is_regexp && JS_IsNull (flags1)) { re = js_get_regexp (ctx, pat, FALSE); if (re) return pat; } re = js_get_regexp (ctx, pat, FALSE); if (re) { pattern = JS_NewString (ctx, re->pattern); if (JS_IsException (pattern)) goto fail; if (JS_IsNull (flags1)) { bc = js_new_string8_len (ctx, (const char *)re->bytecode, re->bytecode_len); if (JS_IsException (bc)) goto fail; goto no_compilation; } else { flags = JS_ToString (ctx, flags1); if (JS_IsException (flags)) goto fail; } } else { flags = JS_NULL; if (pat_is_regexp) { pattern = JS_GetProperty (ctx, pat, JS_KEY_source); if (JS_IsException (pattern)) goto fail; if (JS_IsNull (flags1)) { flags = JS_GetProperty (ctx, pat, JS_KEY_flags); if (JS_IsException (flags)) goto fail; } else { flags = flags1; } } else { pattern = pat; flags = flags1; } if (JS_IsNull (pattern)) { pattern = JS_KEY_empty; } else { val = pattern; pattern = JS_ToString (ctx, val); if (JS_IsException (pattern)) goto fail; } } bc = js_compile_regexp (ctx, pattern, flags); if (JS_IsException (bc)) goto fail; no_compilation: return js_regexp_constructor_internal (ctx, pattern, bc); fail: return JS_EXCEPTION; } static JSValue js_regexp_compile (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSRegExp *re1, *re; JSValue pattern1, flags1; JSValue bc, pattern; const char *pat_cstr; size_t pat_len; int bc_len, i; re = js_get_regexp (ctx, this_val, TRUE); if (!re) return JS_EXCEPTION; pattern1 = argv[0]; flags1 = argv[1]; re1 = js_get_regexp (ctx, pattern1, FALSE); if (re1) { if (!JS_IsNull (flags1)) return JS_RaiseDisrupt (ctx, "flags must be undefined"); pattern = JS_NewString (ctx, re1->pattern); if (JS_IsException (pattern)) goto fail; bc = js_new_string8_len (ctx, (const char *)re1->bytecode, re1->bytecode_len); if (JS_IsException (bc)) goto fail; } else { bc = JS_NULL; if (JS_IsNull (pattern1)) pattern = JS_KEY_empty; else pattern = JS_ToString (ctx, pattern1); if (JS_IsException (pattern)) goto fail; bc = js_compile_regexp (ctx, pattern, flags1); if (JS_IsException (bc)) goto fail; } /* Free old C buffers */ js_free_rt (re->pattern); re->pattern = NULL; js_free_rt (re->bytecode); re->bytecode = NULL; /* Extract pattern as UTF-8 C string */ pat_cstr = JS_ToCStringLen (ctx, &pat_len, pattern); if (!pat_cstr) goto fail; re->pattern = js_malloc_rt (pat_len + 1); if (!re->pattern) { JS_FreeCString (ctx, pat_cstr); goto fail; } memcpy (re->pattern, pat_cstr, pat_len + 1); re->pattern_len = (uint32_t)pat_len; JS_FreeCString (ctx, pat_cstr); /* Extract bytecode as raw bytes */ if (MIST_IsImmediateASCII (bc)) { bc_len = MIST_GetImmediateASCIILen (bc); re->bytecode = js_malloc_rt (bc_len); if (!re->bytecode) goto fail; for (i = 0; i < bc_len; i++) re->bytecode[i] = (uint8_t)MIST_GetImmediateASCIIChar (bc, i); } else { JSText *bc_str = (JSText *)JS_VALUE_GET_PTR (bc); bc_len = (int)JSText_len (bc_str); re->bytecode = js_malloc_rt (bc_len); if (!re->bytecode) goto fail; for (i = 0; i < bc_len; i++) re->bytecode[i] = (uint8_t)string_get (bc_str, i); } re->bytecode_len = (uint32_t)bc_len; { JSValue key = JS_KEY_STR (ctx, "lastIndex"); int ret = JS_SetProperty (ctx, this_val, key, JS_NewInt32 (ctx, 0)); if (ret < 0) return JS_EXCEPTION; } return this_val; fail: return JS_EXCEPTION; } JSValue js_regexp_toString (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSValue pattern, flags; if (!mist_is_gc_object (this_val)) return JS_RaiseDisruptNotAnObject (ctx); JSText *b = pretext_init (ctx, 0); if (!b) return JS_EXCEPTION; /* Root b across allocating calls (JS_GetProperty can trigger GC) */ JSGCRef b_ref; JS_PushGCRef (ctx, &b_ref); b_ref.val = JS_MKPTR (b); b = pretext_putc (ctx, b, '/'); if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; } b_ref.val = JS_MKPTR (b); pattern = JS_GetProperty (ctx, this_val, JS_KEY_source); b = (JSText *)chase (b_ref.val); b = pretext_concat_value (ctx, b, pattern); if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; } b_ref.val = JS_MKPTR (b); b = pretext_putc (ctx, b, '/'); if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; } b_ref.val = JS_MKPTR (b); flags = JS_GetProperty (ctx, this_val, JS_KEY_flags); b = (JSText *)chase (b_ref.val); b = pretext_concat_value (ctx, b, flags); if (!b) { JS_PopGCRef (ctx, &b_ref); return JS_EXCEPTION; } JS_PopGCRef (ctx, &b_ref); return pretext_end (ctx, b); } int lre_check_timeout (void *opaque) { JSContext *ctx = opaque; if (cell_rt_native_active (ctx)) { atomic_store_explicit (&ctx->pause_flag, 0, memory_order_relaxed); return 0; } return atomic_load_explicit (&ctx->pause_flag, memory_order_relaxed) >= 2; } void *lre_realloc (void *opaque, void *ptr, size_t size) { (void)opaque; /* No JS exception is raised here */ return js_realloc_rt (ptr, size); } /* Convert UTF-32 JSText to UTF-16 buffer for regex engine. Returns allocated uint16_t buffer (via js_malloc_rt) that must be freed with js_free_rt. Sets *out_len to number of uint16 code units. */ static uint16_t *js_string_to_utf16 (JSContext *ctx, JSText *str, int *out_len) { int len = (int)JSText_len (str); /* Worst case: each UTF-32 char becomes 2 UTF-16 surrogates */ uint16_t *buf = js_malloc_rt (len * 2 * sizeof (uint16_t)); if (!buf) { JS_RaiseOOM(ctx); return NULL; } int j = 0; for (int i = 0; i < len; i++) { uint32_t c = string_get (str, i); if (c < 0x10000) { buf[j++] = (uint16_t)c; } else { /* Encode as surrogate pair */ c -= 0x10000; buf[j++] = (uint16_t)(0xD800 | (c >> 10)); buf[j++] = (uint16_t)(0xDC00 | (c & 0x3FF)); } } *out_len = j; return buf; } static JSValue js_regexp_exec (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSRegExp *re = js_get_regexp (ctx, this_val, TRUE); JSText *str; JSGCRef str_ref, this_ref; JSValue ret, res, val, groups, captures_arr, match0; uint8_t *re_bytecode; uint8_t **capture, *str_buf; uint16_t *utf16_buf = NULL; int rc, capture_count, shift, i, re_flags; int utf16_len = 0; int64_t last_index; const char *group_name_ptr; if (!re) return JS_EXCEPTION; /* Root this_val across allocating calls */ JS_PushGCRef (ctx, &this_ref); this_ref.val = this_val; JS_PushGCRef (ctx, &str_ref); str_ref.val = JS_ToString (ctx, argv[0]); if (JS_IsException (str_ref.val)) { JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &this_ref); return JS_EXCEPTION; } /* Ensure str_val is a heap string for JS_VALUE_GET_STRING */ if (MIST_IsImmediateASCII (str_ref.val)) { int imm_len = MIST_GetImmediateASCIILen (str_ref.val); JSText *hs = js_alloc_string (ctx, imm_len > 0 ? imm_len : 1); if (!hs) { JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &this_ref); return JS_EXCEPTION; } for (int ci = 0; ci < imm_len; ci++) string_put (hs, ci, MIST_GetImmediateASCIIChar (str_ref.val, ci)); hs->length = imm_len; hs->hash = 0; hs->hdr = objhdr_set_s (hs->hdr, true); str_ref.val = JS_MKPTR (hs); } ret = JS_EXCEPTION; res = JS_NULL; groups = JS_NULL; captures_arr = JS_NULL; match0 = JS_NULL; capture = NULL; val = JS_GetPropertyStr (ctx, this_ref.val, "lastIndex"); if (JS_IsException (val) || JS_ToLength (ctx, &last_index, val)) goto fail; /* Re-chase re after allocating calls (JS_ToString, js_alloc_string, JS_GetPropertyStr) */ re = js_get_regexp (ctx, this_ref.val, TRUE); re_bytecode = re->bytecode; re_flags = lre_get_flags (re_bytecode); if ((re_flags & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) == 0) last_index = 0; capture_count = lre_get_capture_count (re_bytecode); if (capture_count > 0) { capture = js_malloc_rt (sizeof (capture[0]) * capture_count * 2); if (!capture) { JS_RaiseOOM(ctx); goto fail; } } /* Refresh str after potential GC from js_malloc */ str = JS_VALUE_GET_STRING (str_ref.val); /* Convert UTF-32 string to UTF-16 for regex engine (uses js_malloc_rt, no GC) */ utf16_buf = js_string_to_utf16 (ctx, str, &utf16_len); if (!utf16_buf) goto fail; shift = 1; /* UTF-16 mode */ str_buf = (uint8_t *)utf16_buf; /* Refresh str after potential GC from js_string_to_utf16 */ str = JS_VALUE_GET_STRING (str_ref.val); if (last_index > (int)JSText_len (str)) { rc = 2; } else { rc = lre_exec (capture, re_bytecode, str_buf, last_index, (int)JSText_len (str), shift, ctx); } if (rc != 1) { if (rc >= 0) { if (rc == 2 || (re_flags & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY))) { if (JS_SetPropertyStr (ctx, this_ref.val, "lastIndex", JS_NewInt32 (ctx, 0)) < 0) goto fail; } ret = JS_NULL; goto done; } if (rc == LRE_RET_TIMEOUT) JS_RaiseDisrupt (ctx, "interrupted"); else JS_RaiseDisrupt (ctx, "out of memory in regexp execution"); goto fail; } if (re_flags & (LRE_FLAG_GLOBAL | LRE_FLAG_STICKY)) { if (JS_SetPropertyStr (ctx, this_ref.val, "lastIndex", JS_NewInt32 (ctx, (capture[1] - str_buf) >> shift)) < 0) goto fail; } res = JS_NewObjectProto (ctx, JS_NULL); if (JS_IsException (res)) goto fail; /* Root res, captures_arr, groups, match0 across allocating calls */ JSGCRef res_ref, cap_ref, grp_ref, m0_ref; JS_PushGCRef (ctx, &res_ref); res_ref.val = res; JS_PushGCRef (ctx, &cap_ref); cap_ref.val = JS_NULL; JS_PushGCRef (ctx, &grp_ref); grp_ref.val = JS_NULL; JS_PushGCRef (ctx, &m0_ref); m0_ref.val = JS_NULL; #define REGEXP_RESULT_CLEANUP() do { \ JS_PopGCRef (ctx, &m0_ref); \ JS_PopGCRef (ctx, &grp_ref); \ JS_PopGCRef (ctx, &cap_ref); \ JS_PopGCRef (ctx, &res_ref); \ } while(0) { int cap_groups = (capture_count > 1) ? (capture_count - 1) : 0; captures_arr = JS_NewArrayLen (ctx, cap_groups); if (JS_IsException (captures_arr)) { REGEXP_RESULT_CLEANUP (); goto fail; } cap_ref.val = captures_arr; } group_name_ptr = lre_get_groupnames (re_bytecode); if (group_name_ptr) { groups = JS_NewObjectProto (ctx, JS_NULL); if (JS_IsException (groups)) { REGEXP_RESULT_CLEANUP (); goto fail; } grp_ref.val = groups; } { int match_start = -1; int match_end = -1; for (i = 0; i < capture_count; i++) { const char *name = NULL; uint8_t **m = &capture[2 * i]; int start = -1; int end = -1; JSValue s; if (group_name_ptr && i > 0) { if (*group_name_ptr) name = group_name_ptr; group_name_ptr += strlen (group_name_ptr) + 1; } if (m[0] && m[1]) { start = (m[0] - str_buf) >> shift; end = (m[1] - str_buf) >> shift; } s = JS_NULL; if (start != -1) { str = JS_VALUE_GET_STRING (str_ref.val); s = js_sub_string (ctx, str, start, end); if (JS_IsException (s)) { REGEXP_RESULT_CLEANUP (); goto fail; } } if (i == 0) { match_start = start; match_end = end; match0 = s; m0_ref.val = match0; continue; } if (name) { groups = grp_ref.val; if (JS_SetPropertyStr (ctx, groups, name, s) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; } } captures_arr = cap_ref.val; if (JS_IsException (JS_SetPropertyNumber (ctx, captures_arr, (uint32_t)(i - 1), s))) { REGEXP_RESULT_CLEANUP (); goto fail; } } if (match_start < 0) match_start = 0; if (match_end < match_start) match_end = match_start; res = res_ref.val; if (JS_SetPropertyStr (ctx, res, "index", JS_NewInt32 (ctx, match_start)) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; } res = res_ref.val; if (JS_SetPropertyStr (ctx, res, "end", JS_NewInt32 (ctx, match_end)) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; } res = res_ref.val; match0 = m0_ref.val; if (JS_SetPropertyStr (ctx, res, "match", match0) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; } res = res_ref.val; captures_arr = cap_ref.val; if (JS_SetPropertyStr (ctx, res, "captures", captures_arr) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; } groups = grp_ref.val; if (!JS_IsNull (groups)) { res = res_ref.val; if (JS_SetPropertyStr (ctx, res, "groups", groups) < 0) { REGEXP_RESULT_CLEANUP (); goto fail; } } else { res = res_ref.val; JS_SetPropertyStr (ctx, res, "groups", JS_NULL); } } ret = res_ref.val; REGEXP_RESULT_CLEANUP (); done: JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &this_ref); js_free_rt (capture); js_free_rt (utf16_buf); return ret; fail: JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &this_ref); js_free_rt (capture); js_free_rt (utf16_buf); return JS_EXCEPTION; } static const JSCFunctionListEntry js_regexp_proto_funcs[] = { JS_CFUNC_DEF ("exec", 1, js_regexp_exec), JS_CFUNC_DEF ("compile", 2, js_regexp_compile), JS_CFUNC_DEF ("toString", 0, js_regexp_toString), }; void JS_AddIntrinsicRegExpCompiler (JSContext *ctx) { ctx->compile_regexp = js_compile_regexp; } static void JS_AddIntrinsicRegExp (JSContext *ctx) { JSValue obj; JS_AddIntrinsicRegExpCompiler (ctx); ctx->class_proto[JS_CLASS_REGEXP] = JS_NewObject (ctx); JS_SetPropertyFunctionList (ctx, ctx->class_proto[JS_CLASS_REGEXP], js_regexp_proto_funcs, countof (js_regexp_proto_funcs)); obj = JS_NewCFunction2 (ctx, js_regexp_constructor, "RegExp", 2, JS_CFUNC_generic, 0); JS_SetPropertyStr (ctx, ctx->global_obj, "RegExp", obj); ctx->regexp_ctor = obj; } /* Convert a cJSON node to a JSValue */ static JSValue cjson_to_jsvalue (JSContext *ctx, const cJSON *item) { if (!item || cJSON_IsNull (item)) return JS_NULL; if (cJSON_IsFalse (item)) return JS_FALSE; if (cJSON_IsTrue (item)) return JS_TRUE; if (cJSON_IsNumber (item)) return JS_NewFloat64 (ctx, item->valuedouble); if (cJSON_IsString (item)) return JS_NewString (ctx, item->valuestring); if (cJSON_IsArray (item)) { int n = cJSON_GetArraySize (item); JSGCRef arr_ref; JS_AddGCRef (ctx, &arr_ref); arr_ref.val = JS_NewArrayLen (ctx, n); for (int i = 0; i < n; i++) { cJSON *child = cJSON_GetArrayItem (item, i); /* Evaluate recursive call before reading arr_ref.val — the call allocates and can trigger GC which moves the array */ JSValue elem = cjson_to_jsvalue (ctx, child); JS_SetPropertyNumber (ctx, arr_ref.val, i, elem); } JSValue result = arr_ref.val; JS_DeleteGCRef (ctx, &arr_ref); return result; } if (cJSON_IsObject (item)) { JSGCRef obj_ref; JS_AddGCRef (ctx, &obj_ref); obj_ref.val = JS_NewObject (ctx); for (cJSON *child = item->child; child; child = child->next) { JSValue val = cjson_to_jsvalue (ctx, child); JS_SetPropertyStr (ctx, obj_ref.val, child->string, val); } JSValue result = obj_ref.val; JS_DeleteGCRef (ctx, &obj_ref); return result; } return JS_NULL; } JSValue JS_ParseJSON (JSContext *ctx, const char *buf, size_t buf_len, const char *filename) { (void)filename; (void)buf_len; cJSON *root = cJSON_Parse (buf); if (!root) return JS_RaiseDisrupt (ctx, "JSON parse error"); JSValue result = cjson_to_jsvalue (ctx, root); cJSON_Delete (root); return result; } JSValue JS_ParseJSON2 (JSContext *ctx, const char *buf, size_t buf_len, const char *filename, int flags) { (void)flags; return JS_ParseJSON (ctx, buf, buf_len, filename); } typedef struct JSONStringifyContext { JSContext *ctx; JSValue replacer_func; JSValue stack; JSValue property_list; JSValue gap; JSValue empty; JSGCRef b_root; /* GC root for buffer - use JSC_B_GET/SET macros */ BOOL compact_arrays; BOOL in_compact_array; } JSONStringifyContext; /* Macros to access the buffer from the rooted JSValue */ #define JSC_B_GET(jsc) JS_VALUE_GET_STRING((jsc)->b_root.val) #define JSC_B_SET(jsc, ptr) ((jsc)->b_root.val = JS_MKPTR(ptr)) #define JSC_B_PUTC(jsc, c) do { \ JSText *_b = pretext_putc(ctx, JSC_B_GET(jsc), c); \ if (!_b) goto exception; \ JSC_B_SET(jsc, _b); \ } while(0) #define JSC_B_CONCAT(jsc, v) do { \ JSText *_b = pretext_concat_value(ctx, JSC_B_GET(jsc), v); \ if (!_b) goto exception; \ JSC_B_SET(jsc, _b); \ } while(0) static JSValue js_json_check (JSContext *ctx, JSONStringifyContext *jsc, JSValue holder, JSValue val, JSValue key) { JSValue v; JSValue args[2]; /* check for object.toJSON method */ /* ECMA specifies this is done only for Object and BigInt */ if (mist_is_gc_object (val)) { JSValue f = JS_GetProperty (ctx, val, JS_KEY_toJSON); if (JS_IsException (f)) goto exception; if (JS_IsFunction (f)) { v = JS_Call (ctx, f, val, 1, &key); val = v; if (JS_IsException (val)) goto exception; } else { } } if (!JS_IsNull (jsc->replacer_func)) { args[0] = key; args[1] = val; v = JS_Call (ctx, jsc->replacer_func, holder, 2, args); val = v; if (JS_IsException (val)) goto exception; } switch (JS_VALUE_GET_NORM_TAG (val)) { case JS_TAG_PTR: /* includes arrays (OBJ_ARRAY) via mist_hdr */ if (JS_IsFunction (val)) break; /* fall through */ case JS_TAG_STRING_IMM: case JS_TAG_INT: case JS_TAG_FLOAT64: case JS_TAG_BOOL: case JS_TAG_NULL: case JS_TAG_EXCEPTION: return val; default: break; } return JS_NULL; exception: return JS_EXCEPTION; } /* Check if val is already on the visited stack (circular reference detection). Uses identity comparison (===) since we're checking for the same object. */ static BOOL json_stack_has (JSContext *ctx, JSValue stack, JSValue val) { if (!JS_IsArray (stack)) return FALSE; JSArray *arr = JS_VALUE_GET_ARRAY (stack); for (word_t i = 0; i < arr->len; i++) { if (JS_StrictEq (ctx, arr->values[i], val)) return TRUE; } return FALSE; } static int js_json_to_str (JSContext *ctx, JSONStringifyContext *jsc, JSValue holder, JSValue val, JSValue indent) { JSValue v; int64_t i, len; int ret; BOOL has_content; JSGCRef val_ref, indent_ref, indent1_ref, sep_ref, sep1_ref, tab_ref, prop_ref, v_ref; /* Root all values that can be heap pointers and survive across GC points */ JS_PushGCRef (ctx, &val_ref); JS_PushGCRef (ctx, &indent_ref); JS_PushGCRef (ctx, &indent1_ref); JS_PushGCRef (ctx, &sep_ref); JS_PushGCRef (ctx, &sep1_ref); JS_PushGCRef (ctx, &tab_ref); JS_PushGCRef (ctx, &prop_ref); JS_PushGCRef (ctx, &v_ref); val_ref.val = val; indent_ref.val = indent; indent1_ref.val = JS_NULL; sep_ref.val = JS_NULL; sep1_ref.val = JS_NULL; tab_ref.val = JS_NULL; prop_ref.val = JS_NULL; v_ref.val = JS_NULL; /* Heap strings are JS_TAG_PTR but must be quoted, not iterated as objects */ if (JS_IsText (val_ref.val) && !MIST_IsImmediateASCII (val_ref.val)) { val_ref.val = JS_ToQuotedString (ctx, val_ref.val); if (JS_IsException (val_ref.val)) goto exception; goto concat_value; } if (mist_is_gc_object ( val_ref.val)) { /* includes arrays (OBJ_ARRAY) since they have JS_TAG_PTR */ if (json_stack_has (ctx, jsc->stack, val_ref.val)) { JS_RaiseDisrupt (ctx, "circular reference"); goto exception; } indent1_ref.val = JS_ConcatString (ctx, indent_ref.val, jsc->gap); if (JS_IsException (indent1_ref.val)) goto exception; if (!JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) { sep_ref.val = JS_ConcatString3 (ctx, "\n", indent1_ref.val, ""); if (JS_IsException (sep_ref.val)) goto exception; sep1_ref.val = js_new_string8 (ctx, " "); if (JS_IsException (sep1_ref.val)) goto exception; } else { sep_ref.val = jsc->empty; sep1_ref.val = jsc->empty; } if (JS_ArrayPush (ctx, &jsc->stack, val_ref.val) < 0) goto exception; ret = JS_IsArray (val_ref.val); if (ret < 0) goto exception; if (ret) { if (js_get_length64 (ctx, &len, val_ref.val)) goto exception; /* Check if this is a leaf array for compact mode. Leaf = every element is a primitive or string (no arrays or objects). */ BOOL was_compact = jsc->in_compact_array; if (jsc->compact_arrays && !jsc->in_compact_array && !JS_IsEmptyString (jsc->gap)) { BOOL is_leaf = TRUE; for (i = 0; i < len && is_leaf; i++) { v = JS_GetPropertyNumber (ctx, val_ref.val, i); if (JS_IsException (v)) goto exception; if (JS_IsArray (v) > 0) { is_leaf = FALSE; } else if (mist_is_gc_object (v) && !JS_IsText (v)) { is_leaf = FALSE; } } if (is_leaf) jsc->in_compact_array = TRUE; } JSC_B_PUTC (jsc, '['); for (i = 0; i < len; i++) { if (i > 0) { JSC_B_PUTC (jsc, ','); } if (jsc->in_compact_array && !was_compact) { if (i > 0) JSC_B_PUTC (jsc, ' '); } else { JSC_B_CONCAT (jsc, sep_ref.val); } v = JS_GetPropertyNumber (ctx, val_ref.val, i); if (JS_IsException (v)) goto exception; v_ref.val = v; /* root v — JS_ToString below can trigger GC */ /* XXX: could do this string conversion only when needed */ prop_ref.val = JS_ToString (ctx, JS_NewInt64 (ctx, i)); if (JS_IsException (prop_ref.val)) goto exception; v = js_json_check (ctx, jsc, val_ref.val, v_ref.val, prop_ref.val); prop_ref.val = JS_NULL; if (JS_IsException (v)) goto exception; if (JS_IsNull (v)) v = JS_NULL; if (js_json_to_str (ctx, jsc, val_ref.val, v, indent1_ref.val)) goto exception; } if (len > 0 && !JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) { JSC_B_PUTC (jsc, '\n'); JSC_B_CONCAT (jsc, indent_ref.val); } JSC_B_PUTC (jsc, ']'); jsc->in_compact_array = was_compact; } else { if (!JS_IsNull (jsc->property_list)) tab_ref.val = jsc->property_list; else tab_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val); if (JS_IsException (tab_ref.val)) goto exception; if (js_get_length64 (ctx, &len, tab_ref.val)) goto exception; JSC_B_PUTC (jsc, '{'); has_content = FALSE; for (i = 0; i < len; i++) { prop_ref.val = JS_GetPropertyNumber (ctx, tab_ref.val, i); if (JS_IsException (prop_ref.val)) goto exception; v = JS_GetPropertyValue (ctx, val_ref.val, prop_ref.val); if (JS_IsException (v)) goto exception; v = js_json_check (ctx, jsc, val_ref.val, v, prop_ref.val); if (JS_IsException (v)) goto exception; if (!JS_IsNull (v)) { v_ref.val = v; /* root v — allocations below can trigger GC */ if (has_content) { JSC_B_PUTC (jsc, ','); } prop_ref.val = JS_ToQuotedString (ctx, prop_ref.val); if (JS_IsException (prop_ref.val)) { goto exception; } JSC_B_CONCAT (jsc, sep_ref.val); JSC_B_CONCAT (jsc, prop_ref.val); JSC_B_PUTC (jsc, ':'); JSC_B_CONCAT (jsc, sep1_ref.val); if (js_json_to_str (ctx, jsc, val_ref.val, v_ref.val, indent1_ref.val)) goto exception; has_content = TRUE; } } if (has_content && !JS_IsEmptyString (jsc->gap) && !jsc->in_compact_array) { JSC_B_PUTC (jsc, '\n'); JSC_B_CONCAT (jsc, indent_ref.val); } JSC_B_PUTC (jsc, '}'); } v = JS_ArrayPop (ctx, jsc->stack); if (JS_IsException (v)) goto exception; goto done; } switch (JS_VALUE_GET_NORM_TAG (val_ref.val)) { case JS_TAG_STRING_IMM: val_ref.val = JS_ToQuotedString (ctx, val_ref.val); if (JS_IsException (val_ref.val)) goto exception; goto concat_value; case JS_TAG_FLOAT64: if (!isfinite (JS_VALUE_GET_FLOAT64 (val_ref.val))) { val_ref.val = JS_NULL; } goto concat_value; case JS_TAG_INT: case JS_TAG_BOOL: case JS_TAG_NULL: concat_value: { JSText *_b = pretext_concat_value (ctx, JSC_B_GET (jsc), val_ref.val); if (!_b) goto exception_ret; JSC_B_SET (jsc, _b); goto done; } default: goto done; } done: JS_PopGCRef (ctx, &v_ref); JS_PopGCRef (ctx, &prop_ref); JS_PopGCRef (ctx, &tab_ref); JS_PopGCRef (ctx, &sep1_ref); JS_PopGCRef (ctx, &sep_ref); JS_PopGCRef (ctx, &indent1_ref); JS_PopGCRef (ctx, &indent_ref); JS_PopGCRef (ctx, &val_ref); return 0; exception_ret: JS_PopGCRef (ctx, &v_ref); JS_PopGCRef (ctx, &prop_ref); JS_PopGCRef (ctx, &tab_ref); JS_PopGCRef (ctx, &sep1_ref); JS_PopGCRef (ctx, &sep_ref); JS_PopGCRef (ctx, &indent1_ref); JS_PopGCRef (ctx, &indent_ref); JS_PopGCRef (ctx, &val_ref); return -1; exception: JS_PopGCRef (ctx, &v_ref); JS_PopGCRef (ctx, &prop_ref); JS_PopGCRef (ctx, &tab_ref); JS_PopGCRef (ctx, &sep1_ref); JS_PopGCRef (ctx, &sep_ref); JS_PopGCRef (ctx, &indent1_ref); JS_PopGCRef (ctx, &indent_ref); JS_PopGCRef (ctx, &val_ref); return -1; } JSValue JS_JSONStringify (JSContext *ctx, JSValue obj, JSValue replacer, JSValue space0, BOOL compact_arrays) { JSONStringifyContext jsc_s, *jsc = &jsc_s; JSValue val, v, space, ret, wrapper; int res; int64_t i, j, n; JSGCRef obj_ref; JSLocalRef *saved_local_frame = JS_GetLocalFrame (ctx); /* Root obj since GC can happen during stringify setup */ JS_PushGCRef (ctx, &obj_ref); obj_ref.val = obj; jsc->ctx = ctx; jsc->replacer_func = JS_NULL; jsc->stack = JS_NULL; jsc->property_list = JS_NULL; jsc->gap = JS_NULL; jsc->empty = JS_KEY_empty; jsc->compact_arrays = compact_arrays; jsc->in_compact_array = FALSE; ret = JS_NULL; wrapper = JS_NULL; /* Root all jsc fields that hold heap objects — GC can fire during stringify and would move these objects without updating the struct */ JSLocalRef lr_stack, lr_plist, lr_gap, lr_replacer; lr_stack.ptr = &jsc->stack; JS_PushLocalRef (ctx, &lr_stack); lr_plist.ptr = &jsc->property_list; JS_PushLocalRef (ctx, &lr_plist); lr_gap.ptr = &jsc->gap; JS_PushLocalRef (ctx, &lr_gap); lr_replacer.ptr = &jsc->replacer_func; JS_PushLocalRef (ctx, &lr_replacer); /* Root the buffer for GC safety */ JS_PushGCRef (ctx, &jsc->b_root); { JSText *b_init = pretext_init (ctx, 0); if (!b_init) goto exception; JSC_B_SET (jsc, b_init); } jsc->stack = JS_NewArray (ctx); if (JS_IsException (jsc->stack)) goto exception; if (JS_IsFunction (replacer)) { jsc->replacer_func = replacer; } else { res = JS_IsArray (replacer); if (res < 0) goto exception; if (res) { /* XXX: enumeration is not fully correct */ jsc->property_list = JS_NewArray (ctx); if (JS_IsException (jsc->property_list)) goto exception; if (js_get_length64 (ctx, &n, replacer)) goto exception; for (i = j = 0; i < n; i++) { JSValue present; v = JS_GetPropertyNumber (ctx, replacer, i); if (JS_IsException (v)) goto exception; if (mist_is_gc_object (v)) { /* Objects are not valid property list items */ continue; } else if (JS_IsNumber (v)) { v = JS_ToString (ctx, v); if (JS_IsException (v)) goto exception; } else if (!JS_IsText (v)) { continue; } present = js_array_includes (ctx, jsc->property_list, 1, (JSValue *)&v); if (JS_IsException (present)) { goto exception; } if (!JS_ToBool (ctx, present)) { JS_SetPropertyNumber (ctx, jsc->property_list, j++, v); } else { } } } } space = space0; if (JS_IsNumber (space)) { int n; if (JS_ToInt32Clamp (ctx, &n, space, 0, 10, 0)) goto exception; jsc->gap = js_new_string8_len (ctx, " ", n); } else if (JS_IsText (space)) { JSText *p = JS_VALUE_GET_STRING (space); jsc->gap = js_sub_string (ctx, p, 0, min_int ((int)JSText_len (p), 10)); } else { jsc->gap = jsc->empty; } if (JS_IsException (jsc->gap)) goto exception; wrapper = JS_NewObject (ctx); if (JS_IsException (wrapper)) goto exception; if (JS_SetPropertyInternal (ctx, wrapper, JS_KEY_empty, obj_ref.val) < 0) goto exception; val = obj_ref.val; val = js_json_check (ctx, jsc, wrapper, val, jsc->empty); if (JS_IsException (val)) goto exception; if (JS_IsNull (val)) { ret = JS_NULL; goto done1; } if (js_json_to_str (ctx, jsc, wrapper, val, jsc->empty)) goto exception; ret = pretext_end (ctx, JSC_B_GET (jsc)); goto done; exception: ret = JS_EXCEPTION; done1: done: JS_PopGCRef (ctx, &jsc->b_root); /* Restore local refs pushed for jsc fields */ ctx->top_local_ref = saved_local_frame; JS_PopGCRef (ctx, &obj_ref); return ret; } /* ============================================================================ * Cell Script Native Global Functions * ============================================================================ * These functions implement the core Cell script primitives: * - text: string conversion and manipulation * - number: number conversion and math utilities * - array: array creation and manipulation * - object: object creation and manipulation * - fn: function utilities * ============================================================================ */ /* ---------------------------------------------------------------------------- * number function and sub-functions * ---------------------------------------------------------------------------- */ /* number(val, format) - convert to number */ static JSValue js_cell_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue val = argv[0]; int tag = JS_VALUE_GET_TAG (val); /* Handle boolean */ if (tag == JS_TAG_BOOL) { return JS_NewInt32 (ctx, JS_VALUE_GET_BOOL (val) ? 1 : 0); } /* Handle number - return as-is */ if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64) { return val; } /* Handle string (immediate ASCII or heap JSText) */ if (JS_IsText (val)) { const char *str = JS_ToCString (ctx, val); if (!str) return JS_EXCEPTION; JSValue result; /* Check for format argument */ if (argc > 1 && JS_VALUE_GET_TAG (argv[1]) == JS_TAG_INT) { /* Radix conversion */ int radix = JS_VALUE_GET_INT (argv[1]); if (radix < 2 || radix > 36) { JS_FreeCString (ctx, str); return JS_NULL; } char *endptr; long long n = strtoll (str, &endptr, radix); if (endptr == str || *endptr != '\0') { JS_FreeCString (ctx, str); return JS_NULL; } result = JS_NewInt64 (ctx, n); } else if (argc > 1 && (JS_VALUE_GET_TAG (argv[1]) == JS_TAG_STRING || JS_VALUE_GET_TAG (argv[1]) == JS_TAG_STRING_IMM)) { /* Format string */ const char *format = JS_ToCString (ctx, argv[1]); if (!format) { JS_FreeCString (ctx, str); return JS_EXCEPTION; } char *clean = alloca (strlen (str) + 1); const char *p = str; char *q = clean; if (strcmp (format, "u") == 0) { /* underbar separator */ while (*p) { if (*p != '_') *q++ = *p; p++; } } else if (strcmp (format, "d") == 0 || strcmp (format, "l") == 0) { /* comma separator */ while (*p) { if (*p != ',') *q++ = *p; p++; } } else if (strcmp (format, "s") == 0) { /* space separator */ while (*p) { if (*p != ' ') *q++ = *p; p++; } } else if (strcmp (format, "v") == 0) { /* European style: period separator, comma decimal */ while (*p) { if (*p == '.') { p++; continue; } if (*p == ',') { *q++ = '.'; p++; continue; } *q++ = *p++; } } else if (strcmp (format, "b") == 0) { *q = '\0'; char *endptr; long long n = strtoll (str, &endptr, 2); JS_FreeCString (ctx, format); JS_FreeCString (ctx, str); if (endptr == str) return JS_NULL; return JS_NewInt64 (ctx, n); } else if (strcmp (format, "o") == 0) { *q = '\0'; char *endptr; long long n = strtoll (str, &endptr, 8); JS_FreeCString (ctx, format); JS_FreeCString (ctx, str); if (endptr == str) return JS_NULL; return JS_NewInt64 (ctx, n); } else if (strcmp (format, "h") == 0) { *q = '\0'; char *endptr; long long n = strtoll (str, &endptr, 16); JS_FreeCString (ctx, format); JS_FreeCString (ctx, str); if (endptr == str) return JS_NULL; return JS_NewInt64 (ctx, n); } else if (strcmp (format, "t") == 0) { *q = '\0'; char *endptr; long long n = strtoll (str, &endptr, 32); JS_FreeCString (ctx, format); JS_FreeCString (ctx, str); if (endptr == str) return JS_NULL; return JS_NewInt64 (ctx, n); } else if (strcmp (format, "j") == 0) { /* JavaScript style prefix */ JS_FreeCString (ctx, format); int radix = 10; const char *start = str; if (str[0] == '0' && (str[1] == 'x' || str[1] == 'X')) { radix = 16; start = str + 2; } else if (str[0] == '0' && (str[1] == 'o' || str[1] == 'O')) { radix = 8; start = str + 2; } else if (str[0] == '0' && (str[1] == 'b' || str[1] == 'B')) { radix = 2; start = str + 2; } if (radix != 10) { char *endptr; long long n = strtoll (start, &endptr, radix); JS_FreeCString (ctx, str); if (endptr == start) return JS_NULL; return JS_NewInt64 (ctx, n); } double d = strtod (str, NULL); JS_FreeCString (ctx, str); return JS_NewFloat64 (ctx, d); } else { /* Unknown format, just copy */ strcpy (clean, str); q = clean + strlen (clean); } *q = '\0'; double d = strtod (clean, NULL); JS_FreeCString (ctx, format); JS_FreeCString (ctx, str); if (isnan (d)) return JS_NULL; return JS_NewFloat64 (ctx, d); } else { /* Default: parse as decimal */ char *endptr; double d = strtod (str, &endptr); JS_FreeCString (ctx, str); if (endptr == str || isnan (d)) return JS_NULL; result = JS_NewFloat64 (ctx, d); } return result; } return JS_NULL; } /* number.whole(n) - truncate to integer */ static JSValue js_cell_number_whole (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; return JS_NewFloat64 (ctx, trunc (d)); } /* number.fraction(n) - get fractional part */ static JSValue js_cell_number_fraction (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; return JS_NewFloat64 (ctx, d - trunc (d)); } /* number.floor(n, place) - floor with optional decimal place */ static JSValue js_cell_number_floor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; if (argc < 2 || JS_IsNull (argv[1])) { return JS_NewFloat64 (ctx, floor (d)); } int place; if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL; if (place == 0) return JS_NewFloat64 (ctx, floor (d)); double mult = pow (10, -place); return JS_NewFloat64 (ctx, floor (d * mult) / mult); } /* number.ceiling(n, place) - ceiling with optional decimal place */ static JSValue js_cell_number_ceiling (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; if (argc < 2 || JS_IsNull (argv[1])) { return JS_NewFloat64 (ctx, ceil (d)); } int place; if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL; if (place == 0) return JS_NewFloat64 (ctx, ceil (d)); double mult = pow (10, -place); return JS_NewFloat64 (ctx, ceil (d * mult) / mult); } /* number.abs(n) - absolute value */ static JSValue js_cell_number_abs (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; int tag = JS_VALUE_GET_TAG (argv[0]); if (tag != JS_TAG_INT && tag != JS_TAG_FLOAT64) return JS_NULL; double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; return JS_NewFloat64 (ctx, fabs (d)); } /* number.round(n, place) - round with optional decimal place */ static JSValue js_cell_number_round (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; if (argc < 2 || JS_IsNull (argv[1])) { return JS_NewFloat64 (ctx, round (d)); } int place; if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL; if (place == 0) return JS_NewFloat64 (ctx, round (d)); double mult = pow (10, -place); return JS_NewFloat64 (ctx, round (d * mult) / mult); } /* number.sign(n) - return sign (-1, 0, 1) */ static JSValue js_cell_number_sign (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; if (d < 0) return JS_NewInt32 (ctx, -1); if (d > 0) return JS_NewInt32 (ctx, 1); return JS_NewInt32 (ctx, 0); } /* number.trunc(n, place) - truncate with optional decimal place */ static JSValue js_cell_number_trunc (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { double d; if (JS_ToFloat64 (ctx, &d, argv[0])) return JS_NULL; if (argc < 2 || JS_IsNull (argv[1])) { return JS_NewFloat64 (ctx, trunc (d)); } int place; if (JS_ToInt32 (ctx, &place, argv[1])) return JS_NULL; if (place == 0) return JS_NewFloat64 (ctx, trunc (d)); double mult = pow (10, -place); return JS_NewFloat64 (ctx, trunc (d * mult) / mult); } /* number.min(...vals) - minimum value */ static JSValue js_cell_number_min (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc == 0) return JS_NULL; double result; if (JS_ToFloat64 (ctx, &result, argv[0])) return JS_NULL; for (int i = 1; i < argc; i++) { double d; if (JS_ToFloat64 (ctx, &d, argv[i])) return JS_NULL; if (d < result) result = d; } return JS_NewFloat64 (ctx, result); } /* number.max(...vals) - maximum value */ static JSValue js_cell_number_max (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc == 0) return JS_NULL; double result; if (JS_ToFloat64 (ctx, &result, argv[0])) return JS_NULL; for (int i = 1; i < argc; i++) { double d; if (JS_ToFloat64 (ctx, &d, argv[i])) return JS_NULL; if (d > result) result = d; } return JS_NewFloat64 (ctx, result); } /* number.remainder(dividend, divisor) - remainder after division */ static JSValue js_cell_number_remainder (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; double dividend, divisor; if (JS_ToFloat64 (ctx, ÷nd, argv[0])) return JS_NULL; if (JS_ToFloat64 (ctx, &divisor, argv[1])) return JS_NULL; if (divisor == 0) return JS_NULL; return JS_NewFloat64 (ctx, dividend - (trunc (dividend / divisor) * divisor)); } /* ---------------------------------------------------------------------------- * text function and sub-functions * ---------------------------------------------------------------------------- */ /* Helper: convert number to string with radix */ static JSValue js_cell_number_to_radix_string (JSContext *ctx, double num, int radix) { if (radix < 2 || radix > 36) return JS_NULL; /* For base 10, handle floating point properly */ if (radix == 10) { char buf[64]; /* Check if it's an integer */ if (trunc (num) == num && num >= -9007199254740991.0 && num <= 9007199254740991.0) { snprintf (buf, sizeof (buf), "%.0f", num); } else { /* Use %g to get a reasonable representation without trailing zeros */ snprintf (buf, sizeof (buf), "%.15g", num); } return JS_NewString (ctx, buf); } /* For other radixes, use integer conversion */ static const char digits[] = "0123456789abcdefghijklmnopqrstuvwxyz"; char buf[70]; int len = 0; int negative = 0; int64_t n = (int64_t)trunc (num); if (n < 0) { negative = 1; n = -n; } if (n == 0) { buf[len++] = '0'; } else { while (n > 0) { buf[len++] = digits[n % radix]; n /= radix; } } if (negative) { buf[len++] = '-'; } /* Reverse the string */ char result[72]; int j = 0; for (int i = len - 1; i >= 0; i--) { result[j++] = buf[i]; } result[j] = '\0'; return JS_NewString (ctx, result); } /* Helper: add separator every n digits from right, returns JSValue string */ static JSValue add_separator (JSContext *ctx, const char *str, char sep, int n, int prepend_neg) { if (n <= 0) return JS_NewString (ctx, str); int negative = (str[0] == '-'); const char *start = negative ? str + 1 : str; /* Find decimal point */ const char *decimal = strchr (start, '.'); int int_len = decimal ? (int)(decimal - start) : (int)strlen (start); int dec_len = decimal ? (int)strlen (decimal) : 0; int num_seps = (int_len - 1) / n; int total = (negative ? 1 : 0) + prepend_neg + int_len + num_seps + dec_len; JSText *pt = pretext_init (ctx, total); if (!pt) return JS_EXCEPTION; int pos = 0; if (prepend_neg) string_put (pt, pos++, '-'); if (negative) string_put (pt, pos++, '-'); int count = int_len % n; if (count == 0) count = n; for (int i = 0; i < int_len; i++) { if (i > 0 && count == 0) { string_put (pt, pos++, (uint32_t)sep); count = n; } string_put (pt, pos++, (uint32_t)(unsigned char)start[i]); count--; } for (int i = 0; i < dec_len; i++) string_put (pt, pos++, (uint32_t)(unsigned char)decimal[i]); pt->length = pos; return pretext_end (ctx, pt); } /* Helper: format number with format string */ static JSValue js_cell_format_number (JSContext *ctx, double num, const char *format) { int separation = 0; char style = '\0'; int places = 0; int i = 0; /* Parse separation digit */ if (format[i] >= '0' && format[i] <= '9') { separation = format[i] - '0'; i++; } /* Parse style letter */ if (format[i]) { style = format[i]; i++; } else { return JS_NULL; } /* Parse places digits */ if (format[i] >= '0' && format[i] <= '9') { places = format[i] - '0'; i++; if (format[i] >= '0' && format[i] <= '9') { places = places * 10 + (format[i] - '0'); i++; } } /* Invalid if more characters */ if (format[i] != '\0') return JS_NULL; char buf[128]; switch (style) { case 'e': { /* Exponential */ if (places > 0) snprintf (buf, sizeof (buf), "%.*e", places, num); else snprintf (buf, sizeof (buf), "%e", num); return JS_NewString (ctx, buf); } case 'n': { /* Number - scientific for extreme values */ if (fabs (num) >= 1e21 || (fabs (num) < 1e-6 && num != 0)) { snprintf (buf, sizeof (buf), "%e", num); } else if (places > 0) { snprintf (buf, sizeof (buf), "%.*f", places, num); } else { snprintf (buf, sizeof (buf), "%g", num); } return JS_NewString (ctx, buf); } case 's': { /* Space separated */ if (separation == 0) separation = 3; snprintf (buf, sizeof (buf), "%.*f", places, num); return add_separator (ctx, buf, ' ', separation, 0); } case 'u': { /* Underbar separated */ snprintf (buf, sizeof (buf), "%.*f", places, num); if (separation > 0) return add_separator (ctx, buf, '_', separation, 0); return JS_NewString (ctx, buf); } case 'd': case 'l': { /* Decimal/locale with comma separator */ if (separation == 0) separation = 3; if (places == 0 && style == 'd') places = 2; snprintf (buf, sizeof (buf), "%.*f", places, num); return add_separator (ctx, buf, ',', separation, 0); } case 'v': { /* European style: comma decimal, period separator */ snprintf (buf, sizeof (buf), "%.*f", places, num); /* Replace . with , */ for (char *p = buf; *p; p++) { if (*p == '.') *p = ','; } if (separation > 0) return add_separator (ctx, buf, '.', separation, 0); return JS_NewString (ctx, buf); } case 'i': { /* Integer base 10 */ if (places == 0) places = 1; int64_t n = (int64_t)trunc (num); int neg = n < 0; if (neg) n = -n; snprintf (buf, sizeof (buf), "%lld", (long long)n); int len = strlen (buf); /* Pad with zeros */ if (len < places) { memmove (buf + (places - len), buf, len + 1); memset (buf, '0', places - len); } if (separation > 0) return add_separator (ctx, buf, '_', separation, neg); if (neg) { memmove (buf + 1, buf, strlen (buf) + 1); buf[0] = '-'; } return JS_NewString (ctx, buf); } case 'b': { /* Binary */ if (places == 0) places = 1; return js_cell_number_to_radix_string (ctx, num, 2); } case 'o': { /* Octal */ if (places == 0) places = 1; int64_t n = (int64_t)trunc (num); snprintf (buf, sizeof (buf), "%llo", (long long)(n < 0 ? -n : n)); /* Uppercase and pad */ for (char *p = buf; *p; p++) *p = toupper (*p); int len = strlen (buf); if (len < places) { memmove (buf + (places - len), buf, len + 1); memset (buf, '0', places - len); } if (n < 0) { memmove (buf + 1, buf, strlen (buf) + 1); buf[0] = '-'; } return JS_NewString (ctx, buf); } case 'h': { /* Hexadecimal */ if (places == 0) places = 1; int64_t n = (int64_t)trunc (num); snprintf (buf, sizeof (buf), "%llX", (long long)(n < 0 ? -n : n)); int len = strlen (buf); if (len < places) { memmove (buf + (places - len), buf, len + 1); memset (buf, '0', places - len); } if (n < 0) { memmove (buf + 1, buf, strlen (buf) + 1); buf[0] = '-'; } return JS_NewString (ctx, buf); } case 't': { /* Base32 */ if (places == 0) places = 1; return js_cell_number_to_radix_string (ctx, num, 32); } } return JS_NULL; } /* Forward declaration for blob helper */ static JSBlob *checked_get_blob (JSContext *ctx, JSValue val); /* modulo(dividend, divisor) - result has sign of divisor */ static JSValue js_cell_modulo (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; double dividend, divisor; if (JS_ToFloat64 (ctx, ÷nd, argv[0])) return JS_NULL; if (JS_ToFloat64 (ctx, &divisor, argv[1])) return JS_NULL; /* If either operand is NaN, return null */ if (isnan (dividend) || isnan (divisor)) return JS_NULL; /* If divisor is 0, return null */ if (divisor == 0) return JS_NULL; /* If dividend is 0, return 0 */ if (dividend == 0) return JS_NewFloat64 (ctx, 0.0); /* modulo = dividend - (divisor * floor(dividend / divisor)) */ double result = dividend - (divisor * floor (dividend / divisor)); return JS_NewFloat64 (ctx, result); } /* not(bool) - negate a boolean value */ static JSValue js_cell_not (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; if (!JS_IsBool (argv[0])) return JS_NULL; return JS_NewBool (ctx, !JS_ToBool (ctx, argv[0])); } /* neg(number) - negate a number */ static JSValue js_cell_neg (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; double num; if (JS_ToFloat64 (ctx, &num, argv[0])) return JS_NULL; if (isnan (num)) return JS_NULL; return JS_NewFloat64 (ctx, -num); } /* character(value) - get character from text or codepoint */ JSValue js_cell_character (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NewString (ctx, ""); JSValue arg = argv[0]; int tag = JS_VALUE_GET_TAG (arg); /* Handle string - return first character */ if (JS_IsText (arg)) { if (js_string_value_len (arg) == 0) return JS_NewString (ctx, ""); return js_sub_string_val (ctx, arg, 0, 1); } /* Handle integer - return character from codepoint */ if (tag == JS_TAG_INT) { int32_t val = JS_VALUE_GET_INT (arg); if (val < 0 || val > 0x10FFFF) return JS_NewString (ctx, ""); uint32_t codepoint = (uint32_t)val; if (codepoint < 0x80) { char buf[2] = { (char)codepoint, '\0' }; return JS_NewString (ctx, buf); } /* Create single-codepoint UTF-32 string */ JSText *str = js_alloc_string (ctx, 1); if (!str) return JS_EXCEPTION; string_put (str, 0, codepoint); str->length = 1; return pretext_end (ctx, str); } /* Handle float - convert to integer if non-negative and within range */ if (tag == JS_TAG_FLOAT64) { double d = JS_VALUE_GET_FLOAT64 (arg); if (isnan (d) || d < 0 || d > 0x10FFFF || d != trunc (d)) return JS_NewString (ctx, ""); uint32_t codepoint = (uint32_t)d; if (codepoint < 0x80) { char buf[2] = { (char)codepoint, '\0' }; return JS_NewString (ctx, buf); } /* Create single-codepoint UTF-32 string */ JSText *str = js_alloc_string (ctx, 1); if (!str) return JS_EXCEPTION; string_put (str, 0, codepoint); str->length = 1; return pretext_end (ctx, str); } return JS_NewString (ctx, ""); } /* text(arg, format) - main text function */ static JSValue js_cell_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue arg = argv[0]; int tag = JS_VALUE_GET_TAG (arg); /* Handle string / rope */ if (JS_IsText (arg)) { JSValue str = JS_ToString (ctx, arg); /* owned + flattens rope */ if (JS_IsException (str)) return JS_EXCEPTION; if (argc == 1) return str; if (argc >= 2) { int tag1 = JS_VALUE_GET_TAG (argv[1]); if (tag1 == JS_TAG_INT || tag1 == JS_TAG_FLOAT64) { int len = js_string_value_len (str); int from, to; if (JS_ToInt32 (ctx, &from, argv[1])) return JS_EXCEPTION; if (from < 0) from += len; if (from < 0) from = 0; if (from > len) from = len; to = len; if (argc >= 3) { if (JS_ToInt32 (ctx, &to, argv[2])) return JS_EXCEPTION; if (to < 0) to += len; if (to < 0) to = 0; if (to > len) to = len; } if (from > to) return JS_NULL; return js_sub_string_val (ctx, str, from, to); } } return str; } /* Handle blob - convert to text representation */ if (mist_is_blob (arg)) { JSBlob *bd = checked_get_blob (ctx, arg); if (!objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "text: blob must be stone"); char format = '\0'; if (argc > 1) { const char *fmt = JS_ToCString (ctx, argv[1]); if (!fmt) return JS_EXCEPTION; format = fmt[0]; JS_FreeCString (ctx, fmt); } size_t byte_len = (bd->length + 7) / 8; const uint8_t *data = (const uint8_t *)bd->bits; if (format == 'h') { static const char hex[] = "0123456789abcdef"; int exact = (int)(byte_len * 2); JSText *pt = pretext_init (ctx, exact); if (!pt) return JS_EXCEPTION; bd = (JSBlob *)chase (argv[0]); data = (const uint8_t *)bd->bits; for (size_t i = 0; i < byte_len; i++) { string_put (pt, (int)(i * 2), (uint32_t)hex[(data[i] >> 4) & 0xF]); string_put (pt, (int)(i * 2 + 1), (uint32_t)hex[data[i] & 0xF]); } pt->length = exact; return pretext_end (ctx, pt); } else if (format == 'b') { int exact = (int)bd->length; JSText *pt = pretext_init (ctx, exact); if (!pt) return JS_EXCEPTION; bd = (JSBlob *)chase (argv[0]); data = (const uint8_t *)bd->bits; for (size_t i = 0; i < (size_t)bd->length; i++) { size_t byte_idx = i / 8; size_t bit_idx = i % 8; string_put (pt, (int)i, (data[byte_idx] & (1u << bit_idx)) ? '1' : '0'); } pt->length = exact; return pretext_end (ctx, pt); } else if (format == 'o') { int exact = (int)(((size_t)bd->length + 2) / 3); JSText *pt = pretext_init (ctx, exact); if (!pt) return JS_EXCEPTION; bd = (JSBlob *)chase (argv[0]); data = (const uint8_t *)bd->bits; for (int i = 0; i < exact; i++) { int val = 0; for (int j = 0; j < 3; j++) { size_t bit_pos = (size_t)i * 3 + (size_t)j; if (bit_pos < (size_t)bd->length) { size_t byte_idx = bit_pos / 8; size_t bit_idx = bit_pos % 8; if (data[byte_idx] & (1u << bit_idx)) val |= (1 << j); } } string_put (pt, i, (uint32_t)('0' + val)); } pt->length = exact; return pretext_end (ctx, pt); } else if (format == 't') { static const char b32[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; int exact = (int)(((size_t)bd->length + 4) / 5); JSText *pt = pretext_init (ctx, exact); if (!pt) return JS_EXCEPTION; bd = (JSBlob *)chase (argv[0]); data = (const uint8_t *)bd->bits; for (int i = 0; i < exact; i++) { int val = 0; for (int j = 0; j < 5; j++) { size_t bit_pos = (size_t)i * 5 + (size_t)j; if (bit_pos < (size_t)bd->length) { size_t byte_idx = bit_pos / 8; size_t bit_idx = bit_pos % 8; if (data[byte_idx] & (1u << bit_idx)) val |= (1 << j); } } string_put (pt, i, (uint32_t)b32[val & 31]); } pt->length = exact; return pretext_end (ctx, pt); } else { if (bd->length % 8 != 0) return JS_RaiseDisrupt (ctx, "text: blob not byte-aligned for UTF-8"); /* Copy blob data to a temp buffer before JS_NewStringLen, because JS_NewStringLen allocates internally (js_alloc_string) which can trigger GC, moving the blob and invalidating data. */ char *tmp = pjs_malloc (byte_len); if (!tmp) return JS_ThrowMemoryError (ctx); memcpy (tmp, data, byte_len); JSValue result = JS_NewStringLen (ctx, tmp, byte_len); pjs_free (tmp); return result; } } /* Handle number */ if (tag == JS_TAG_INT || tag == JS_TAG_FLOAT64) { double num; if (JS_ToFloat64 (ctx, &num, arg)) return JS_EXCEPTION; if (argc > 1) { int tag1 = JS_VALUE_GET_TAG (argv[1]); if (tag1 == JS_TAG_INT) { int radix = JS_VALUE_GET_INT (argv[1]); return js_cell_number_to_radix_string (ctx, num, radix); } if (tag1 == JS_TAG_STRING || tag1 == JS_TAG_STRING_IMM) { const char *format = JS_ToCString (ctx, argv[1]); if (!format) return JS_EXCEPTION; JSValue result = js_cell_format_number (ctx, num, format); JS_FreeCString (ctx, format); return result; } } return js_cell_number_to_radix_string (ctx, num, 10); } /* Handle array */ if (JS_IsArray (arg)) { int64_t len; JSGCRef arg_ref; JS_AddGCRef(ctx, &arg_ref); arg_ref.val = arg; if (js_get_length64 (ctx, &len, arg_ref.val)) { JS_DeleteGCRef(ctx, &arg_ref); return JS_EXCEPTION; } const char *separator = ""; BOOL sep_alloc = FALSE; if (argc > 1 && JS_VALUE_IS_TEXT (argv[1])) { separator = JS_ToCString (ctx, argv[1]); if (!separator) return JS_EXCEPTION; sep_alloc = TRUE; } JSText *b = pretext_init (ctx, 0); if (!b) { if (sep_alloc) JS_FreeCString (ctx, separator); return JS_EXCEPTION; } /* Root b across allocating calls (JS_GetPropertyNumber, JS_ToString) */ JSGCRef b_ref; JS_AddGCRef (ctx, &b_ref); b_ref.val = JS_MKPTR (b); for (int64_t i = 0; i < len; i++) { if (i > 0 && separator[0]) { b = (JSText *)chase (b_ref.val); b = pretext_puts8 (ctx, b, separator); if (!b) goto array_fail; b_ref.val = JS_MKPTR (b); } b = (JSText *)chase (b_ref.val); /* re-chase before use */ JSValue item = JS_GetPropertyNumber (ctx, arg_ref.val, i); if (JS_IsException (item)) goto array_fail; if (!JS_VALUE_IS_TEXT (item)) { if (sep_alloc) JS_FreeCString (ctx, separator); JS_DeleteGCRef (ctx, &b_ref); JS_DeleteGCRef (ctx, &arg_ref); return JS_RaiseDisrupt (ctx, "text: array element is not a string"); } JSValue item_str = JS_ToString (ctx, item); if (JS_IsException (item_str)) goto array_fail; b = (JSText *)chase (b_ref.val); /* re-chase after JS_ToString */ b = pretext_concat_value (ctx, b, item_str); if (!b) goto array_fail; b_ref.val = JS_MKPTR (b); } b = (JSText *)chase (b_ref.val); if (sep_alloc) JS_FreeCString (ctx, separator); JS_DeleteGCRef (ctx, &b_ref); JS_DeleteGCRef (ctx, &arg_ref); return pretext_end (ctx, b); array_fail: if (sep_alloc) JS_FreeCString (ctx, separator); JS_DeleteGCRef (ctx, &b_ref); JS_DeleteGCRef (ctx, &arg_ref); return JS_EXCEPTION; } /* Handle function - return source or native stub */ if (JS_IsFunction (arg)) { JSFunction *fn = JS_VALUE_GET_FUNCTION (arg); const char *pref = "function "; const char *suff = "() {\n [native code]\n}"; const char *name_cstr = NULL; int nlen = 0; if (!JS_IsNull (fn->name)) { name_cstr = JS_ToCString (ctx, fn->name); if (name_cstr) nlen = (int)strlen (name_cstr); } int plen = (int)strlen (pref); int slen = (int)strlen (suff); JSText *pt = pretext_init (ctx, plen + nlen + slen); if (!pt) { if (name_cstr) JS_FreeCString (ctx, name_cstr); return JS_EXCEPTION; } pt = pretext_puts8 (ctx, pt, pref); if (pt && name_cstr) pt = pretext_write8 (ctx, pt, (const uint8_t *)name_cstr, nlen); if (name_cstr) JS_FreeCString (ctx, name_cstr); if (pt) pt = pretext_puts8 (ctx, pt, suff); return pretext_end (ctx, pt); } return JS_ToString (ctx, arg); return JS_RaiseDisrupt (ctx, "Could not convert to text. Tag is %d", tag); } /* text.lower(str) - convert to lowercase */ JSValue js_cell_text_lower (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; if (!JS_VALUE_IS_TEXT (argv[0])) return JS_NULL; /* Handle immediate ASCII - no GC concern */ if (MIST_IsImmediateASCII (argv[0])) { int len = MIST_GetImmediateASCIILen (argv[0]); JSText *b = pretext_init (ctx, len); if (!b) return JS_EXCEPTION; for (int i = 0; i < len; i++) { uint32_t c = MIST_GetImmediateASCIIChar (argv[0], i); if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; b = pretext_putc (ctx, b, c); if (!b) return JS_EXCEPTION; } return pretext_end (ctx, b); } /* Heap text: must re-chase after GC points */ int len = (int)JSText_len (JS_VALUE_GET_STRING (argv[0])); JSText *b = pretext_init (ctx, len); if (!b) return JS_EXCEPTION; for (int i = 0; i < len; i++) { JSText *p = JS_VALUE_GET_STRING (argv[0]); /* Re-chase each iteration */ uint32_t c = string_get (p, i); if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; b = pretext_putc (ctx, b, c); if (!b) return JS_EXCEPTION; } return pretext_end (ctx, b); } /* text.upper(str) - convert to uppercase */ JSValue js_cell_text_upper (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; if (!JS_VALUE_IS_TEXT (argv[0])) return JS_NULL; /* Handle immediate ASCII - no GC concern */ if (MIST_IsImmediateASCII (argv[0])) { int len = MIST_GetImmediateASCIILen (argv[0]); JSText *b = pretext_init (ctx, len); if (!b) return JS_EXCEPTION; for (int i = 0; i < len; i++) { uint32_t c = MIST_GetImmediateASCIIChar (argv[0], i); if (c >= 'a' && c <= 'z') c = c - 'a' + 'A'; b = pretext_putc (ctx, b, c); if (!b) return JS_EXCEPTION; } return pretext_end (ctx, b); } /* Heap text: must re-chase after GC points */ int len = (int)JSText_len (JS_VALUE_GET_STRING (argv[0])); JSText *b = pretext_init (ctx, len); if (!b) return JS_EXCEPTION; for (int i = 0; i < len; i++) { JSText *p = JS_VALUE_GET_STRING (argv[0]); /* Re-chase each iteration */ uint32_t c = string_get (p, i); if (c >= 'a' && c <= 'z') c = c - 'a' + 'A'; b = pretext_putc (ctx, b, c); if (!b) return JS_EXCEPTION; } return pretext_end (ctx, b); } /* text.trim(str, reject) - trim whitespace or custom characters */ static JSValue js_cell_text_trim (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; if (!JS_IsText (argv[0])) return JS_NULL; JSValue str = argv[0]; int start = 0; int end = js_string_value_len (str); if (argc > 1 && !JS_IsNull (argv[1])) { /* Custom trim with reject characters */ const char *reject = JS_ToCString (ctx, argv[1]); if (!reject) return JS_EXCEPTION; size_t reject_len = strlen (reject); while (start < end) { uint32_t c = js_string_value_get (str, start); int found = 0; for (size_t i = 0; i < reject_len; i++) { if (c == (uint8_t)reject[i]) { found = 1; break; } } if (!found) break; start++; } while (end > start) { uint32_t c = js_string_value_get (str, end - 1); int found = 0; for (size_t i = 0; i < reject_len; i++) { if (c == (uint8_t)reject[i]) { found = 1; break; } } if (!found) break; end--; } JS_FreeCString (ctx, reject); } else { /* Default: trim whitespace */ while (start < end && lre_is_space (js_string_value_get (str, start))) start++; while (end > start && lre_is_space (js_string_value_get (str, end - 1))) end--; } return js_sub_string_val (ctx, str, start, end); } /* text.codepoint(str) - get first codepoint */ JSValue js_cell_text_codepoint (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; if (!JS_IsText (argv[0])) return JS_NULL; /* Handle immediate strings directly */ if (MIST_IsImmediateASCII (argv[0])) { int plen = MIST_GetImmediateASCIILen (argv[0]); if (plen == 0) return JS_NULL; uint32_t c = MIST_GetImmediateASCIIChar (argv[0], 0); return JS_NewInt32 (ctx, c); } /* Heap string */ JSText *p = JS_VALUE_GET_STRING (argv[0]); int plen = (int)JSText_len (p); if (plen == 0) { return JS_NULL; } uint32_t c = string_get (p, 0); /* Handle surrogate pairs */ if (c >= 0xD800 && c <= 0xDBFF && plen > 1) { uint32_t c2 = string_get (p, 1); if (c2 >= 0xDC00 && c2 <= 0xDFFF) { c = 0x10000 + ((c - 0xD800) << 10) + (c2 - 0xDC00); } } return JS_NewInt32 (ctx, c); } /* Helpers (C, not C++). Put these above js_cell_text_replace in the same C * file. */ static JSText *pt_concat_value_to_string_free (JSContext *ctx, JSText *b, JSValue v) { /* Root b across JS_ToString which can allocate and trigger GC */ JSGCRef b_ref, s_ref; JS_PushGCRef (ctx, &b_ref); b_ref.val = JS_MKPTR (b); JSValue s = JS_ToString (ctx, v); if (JS_IsException (s)) { JS_PopGCRef (ctx, &b_ref); return NULL; } /* Root s — pretext_concat_value can trigger GC and move the heap string */ JS_PushGCRef (ctx, &s_ref); s_ref.val = s; b = (JSText *)chase (b_ref.val); /* re-fetch after possible GC */ b = pretext_concat_value (ctx, b, s_ref.val); JS_PopGCRef (ctx, &s_ref); JS_PopGCRef (ctx, &b_ref); return b; } /* Build replacement for a match at `found`. * - If replacement is a function: call it as (match_text, found) * - Else if replacement exists: duplicate it * - Else: empty string * Returns JS_EXCEPTION on error, JS_NULL if callback returned null, or any * JSValue. This function CONSUMES match_val if it calls a function (it will * free it via args cleanup), otherwise it will free match_val before * returning. */ static JSValue make_replacement (JSContext *ctx, int argc, JSValue *argv, int found, JSValue match_val) { JSValue rep; if (argc > 2 && JS_IsFunction (argv[2])) { JSValue args[2]; args[0] = match_val; args[1] = JS_NewInt32 (ctx, found); rep = JS_Call (ctx, argv[2], JS_NULL, 2, args); return rep; } if (argc > 2) return argv[2]; return JS_KEY_empty; } static int JS_IsRegExp (JSContext *ctx, JSValue v) { if (!mist_is_gc_object (v)) return 0; JSValue exec = JS_GetPropertyStr (ctx, v, "exec"); if (JS_IsException (exec)) return -1; int ok = JS_IsFunction (exec); return ok; } /* text.replace(text, target, replacement, limit) * * Return a new text in which the target is replaced by the replacement. * * target: string (pattern support not implemented here; non-string => null) * replacement: string or function(match_text, start_pos) -> string|null * limit: max number of replacements (default unlimited). Limit includes null * matches. * * Empty target semantics: * Replace at every boundary: before first char, between chars, after last * char. Example: replace("abc", "", "-") => "-a-b-c-" Boundaries count toward * limit even if replacement returns null. */ static JSValue js_cell_text_replace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsText (argv[0])) return JS_NULL; int target_is_regex = 0; { if (JS_IsText (argv[1])) { target_is_regex = 0; } else if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) { target_is_regex = 1; } else { return JS_NULL; } } if (!JS_VALUE_IS_TEXT (argv[0])) return JS_RaiseDisrupt (ctx, "Replace must have text in arg0."); int len = js_string_value_len (argv[0]); int32_t limit = -1; if (argc > 3 && !JS_IsNull (argv[3])) { if (JS_ToInt32 (ctx, &limit, argv[3])) { return JS_NULL; } if (limit < 0) limit = -1; } JSText *b = pretext_init (ctx, len); if (!b) return JS_EXCEPTION; /* Root b across all allocating calls */ JSGCRef b_ref; JS_PushGCRef (ctx, &b_ref); b_ref.val = JS_MKPTR (b); /* Macro to re-chase b from GC ref before use */ #define B_RECHASE() b = (JSText *)chase (b_ref.val) /* Macro to update b_ref after b changes */ #define B_UPDATE(new_b) do { b = (new_b); b_ref.val = JS_MKPTR (b); } while(0) #define B_CLEANUP() JS_PopGCRef (ctx, &b_ref) if (!target_is_regex) { if (!JS_VALUE_IS_TEXT (argv[1])) { B_CLEANUP (); return JS_RaiseDisrupt ( ctx, "Second arg of replace must be pattern or text."); } int t_len = js_string_value_len (argv[1]); if (t_len == 0) { int32_t count = 0; for (int boundary = 0; boundary <= len; boundary++) { if (limit >= 0 && count >= limit) break; JSValue match = JS_KEY_empty; if (JS_IsException (match)) { B_CLEANUP (); goto fail_str_target; } JSValue rep = make_replacement (ctx, argc, argv, boundary, match); if (JS_IsException (rep)) { B_CLEANUP (); goto fail_str_target; } count++; if (!JS_IsNull (rep)) { B_RECHASE (); B_UPDATE (pt_concat_value_to_string_free (ctx, b, rep)); if (!b) { B_CLEANUP (); goto fail_str_target; } } if (boundary < len) { JSValue ch = js_sub_string_val (ctx, argv[0], boundary, boundary + 1); if (JS_IsException (ch)) { B_CLEANUP (); goto fail_str_target; } B_RECHASE (); B_UPDATE (pretext_concat_value (ctx, b, ch)); if (!b) { B_CLEANUP (); goto fail_str_target; } } } B_RECHASE (); B_CLEANUP (); return pretext_end (ctx, b); } int pos = 0; int32_t count = 0; while (pos <= len - t_len && (limit < 0 || count < limit)) { int found = -1; /* Search for pattern using character-by-character comparison */ for (int i = pos; i <= len - t_len; i++) { int match = 1; for (int j = 0; j < t_len; j++) { if (js_string_value_get (argv[0], i + j) != js_string_value_get (argv[1], j)) { match = 0; break; } } if (match) { found = i; break; } } if (found < 0) break; if (found > pos) { int sub_len = found - pos; JSText *sub_str = js_alloc_string (ctx, sub_len); if (!sub_str) { B_CLEANUP (); goto fail_str_target; } for (int i = 0; i < sub_len; i++) { string_put (sub_str, i, js_string_value_get (argv[0], pos + i)); } sub_str->length = sub_len; JSValue sub = pretext_end (ctx, sub_str); if (JS_IsException (sub)) { B_CLEANUP (); goto fail_str_target; } B_RECHASE (); B_UPDATE (pretext_concat_value (ctx, b, sub)); if (!b) { B_CLEANUP (); goto fail_str_target; } } /* Build match substring manually */ JSText *match_str = js_alloc_string (ctx, t_len); if (!match_str) { B_CLEANUP (); goto fail_str_target; } for (int i = 0; i < t_len; i++) { string_put (match_str, i, js_string_value_get (argv[0], found + i)); } match_str->length = t_len; JSValue match = pretext_end (ctx, match_str); if (JS_IsException (match)) { B_CLEANUP (); goto fail_str_target; } JSValue rep = make_replacement (ctx, argc, argv, found, match); if (JS_IsException (rep)) { B_CLEANUP (); goto fail_str_target; } count++; if (!JS_IsNull (rep)) { B_RECHASE (); B_UPDATE (pt_concat_value_to_string_free (ctx, b, rep)); if (!b) { B_CLEANUP (); goto fail_str_target; } } pos = found + t_len; } if (pos < len) { int sub_len = len - pos; JSText *sub_str = js_alloc_string (ctx, sub_len); if (!sub_str) { B_CLEANUP (); goto fail_str_target; } for (int i = 0; i < sub_len; i++) { string_put (sub_str, i, js_string_value_get (argv[0], pos + i)); } sub_str->length = sub_len; JSValue sub = pretext_end (ctx, sub_str); if (JS_IsException (sub)) { B_CLEANUP (); goto fail_str_target; } B_RECHASE (); B_UPDATE (pretext_concat_value (ctx, b, sub)); if (!b) { B_CLEANUP (); goto fail_str_target; } } B_RECHASE (); B_CLEANUP (); return pretext_end (ctx, b); fail_str_target: return JS_EXCEPTION; } /* Regex target - root rx across allocating calls */ JSGCRef rx_ref; JS_PushGCRef (ctx, &rx_ref); rx_ref.val = argv[1]; #define RX_CLEANUP() do { JS_PopGCRef (ctx, &rx_ref); B_CLEANUP (); } while(0) #define RX_VAL (rx_ref.val) JSValue orig_last_index = JS_GetPropertyStr (ctx, RX_VAL, "lastIndex"); if (JS_IsException (orig_last_index)) { RX_CLEANUP (); goto fail_rx; } int have_orig_last_index = 1; int pos = 0; int32_t count = 0; while (pos <= len && (limit < 0 || count < limit)) { if (JS_SetPropertyStr (ctx, RX_VAL, "lastIndex", JS_NewInt32 (ctx, 0)) < 0) { RX_CLEANUP (); goto fail_rx; } JSValue sub_str = js_sub_string_val (ctx, argv[0], pos, len); if (JS_IsException (sub_str)) { RX_CLEANUP (); goto fail_rx; } JSValue exec_res = JS_Invoke (ctx, RX_VAL, JS_KEY_exec, 1, (JSValue *)&sub_str); if (JS_IsException (exec_res)) { RX_CLEANUP (); goto fail_rx; } if (JS_IsNull (exec_res)) { break; } JSValue idx_val = JS_GetPropertyStr (ctx, exec_res, "index"); if (JS_IsException (idx_val)) { RX_CLEANUP (); goto fail_rx; } int32_t local_index = 0; if (JS_ToInt32 (ctx, &local_index, idx_val)) { RX_CLEANUP (); goto fail_rx; } if (local_index < 0) local_index = 0; int found = pos + local_index; if (found < pos) found = pos; if (found > len) { break; } JSValue match = JS_GetPropertyStr (ctx, exec_res, "match"); if (JS_IsException (match)) { RX_CLEANUP (); goto fail_rx; } JSValue end_val = JS_GetPropertyStr (ctx, exec_res, "end"); if (JS_IsException (end_val)) { RX_CLEANUP (); goto fail_rx; } int32_t end = 0; if (JS_ToInt32 (ctx, &end, end_val)) { RX_CLEANUP (); goto fail_rx; } int match_len = end - local_index; if (match_len < 0) match_len = 0; if (found > pos) { JSValue prefix = js_sub_string_val (ctx, argv[0], pos, found); if (JS_IsException (prefix)) { RX_CLEANUP (); goto fail_rx; } B_RECHASE (); B_UPDATE (pretext_concat_value (ctx, b, prefix)); if (!b) { RX_CLEANUP (); goto fail_rx; } } JSValue rep = make_replacement (ctx, argc, argv, found, match); if (JS_IsException (rep)) { RX_CLEANUP (); goto fail_rx; } count++; if (!JS_IsNull (rep)) { B_RECHASE (); B_UPDATE (pt_concat_value_to_string_free (ctx, b, rep)); if (!b) { RX_CLEANUP (); goto fail_rx; } } pos = found + match_len; if (match_len == 0) { if (pos < len) pos++; else break; } } if (pos < len) { JSValue tail = js_sub_string_val (ctx, argv[0], pos, len); if (JS_IsException (tail)) { RX_CLEANUP (); goto fail_rx; } B_RECHASE (); B_UPDATE (pretext_concat_value (ctx, b, tail)); if (!b) { RX_CLEANUP (); goto fail_rx; } } if (have_orig_last_index) JS_SetPropertyStr (ctx, RX_VAL, "lastIndex", orig_last_index); B_RECHASE (); RX_CLEANUP (); return pretext_end (ctx, b); fail_rx: if (!JS_IsNull (orig_last_index) && !JS_IsException (orig_last_index)) { JS_SetPropertyStr (ctx, argv[1], "lastIndex", orig_last_index); } else { } return JS_EXCEPTION; } #undef RX_CLEANUP #undef RX_VAL #undef B_RECHASE #undef B_UPDATE #undef B_CLEANUP /* text.search(str, target, from) - find substring or regex match */ static JSValue js_cell_text_search (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsText (argv[0])) return JS_NULL; int target_is_regex = 0; if (JS_IsText (argv[1])) { target_is_regex = 0; } else if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) { target_is_regex = 1; } else { return JS_NULL; } JSValue str = argv[0]; int len = js_string_value_len (str); int from = 0; if (argc > 2 && !JS_IsNull (argv[2])) { if (JS_ToInt32 (ctx, &from, argv[2])) { return JS_NULL; } if (from < 0) from += len; if (from < 0) from = 0; } if (from > len) { return JS_NULL; } if (!target_is_regex) { JSValue target = argv[1]; int t_len = js_string_value_len (target); int result = -1; if (len >= t_len) { for (int i = from; i <= len - t_len; i++) { int match = 1; for (int j = 0; j < t_len; j++) { if (js_string_value_get (str, i + j) != js_string_value_get (target, j)) { match = 0; break; } } if (match) { result = i; break; } } } if (result == -1) return JS_NULL; return JS_NewInt32 (ctx, result); } /* Regex target - root rx and str across allocating calls */ JSGCRef rx_ref, str_ref; JS_PushGCRef (ctx, &rx_ref); rx_ref.val = argv[1]; JS_PushGCRef (ctx, &str_ref); str_ref.val = str; #define SEARCH_CLEANUP() do { JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &rx_ref); } while(0) JSValue orig_last_index = JS_GetPropertyStr (ctx, rx_ref.val, "lastIndex"); if (JS_IsException (orig_last_index)) { SEARCH_CLEANUP (); return JS_EXCEPTION; } int have_orig_last_index = 1; if (JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", JS_NewInt32 (ctx, 0)) < 0) goto fail_rx_search; JSValue sub_str = js_sub_string_val (ctx, str_ref.val, from, len); if (JS_IsException (sub_str)) goto fail_rx_search; JSValue exec_res = JS_Invoke (ctx, rx_ref.val, JS_KEY_exec, 1, (JSValue *)&sub_str); if (JS_IsException (exec_res)) goto fail_rx_search; if (JS_IsNull (exec_res)) { if (have_orig_last_index) JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); SEARCH_CLEANUP (); return JS_NULL; } JSValue idx_val = JS_GetPropertyStr (ctx, exec_res, "index"); if (JS_IsException (idx_val)) { goto fail_rx_search; } int32_t local_index = 0; if (JS_ToInt32 (ctx, &local_index, idx_val)) { goto fail_rx_search; } if (local_index < 0) local_index = 0; if (have_orig_last_index) JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); SEARCH_CLEANUP (); return JS_NewInt32 (ctx, from + local_index); fail_rx_search: if (!JS_IsNull (orig_last_index) && !JS_IsException (orig_last_index)) { JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); } SEARCH_CLEANUP (); return JS_EXCEPTION; } #undef SEARCH_CLEANUP /* text_extract(text, pattern, from?, to?) - return array of matches or null - literal pattern: [match] - regexp pattern: [full_match, cap1, cap2, ...] */ static JSValue js_cell_text_extract (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsText (argv[0])) return JS_NULL; JSValue str = argv[0]; int len = js_string_value_len (str); int from = 0; if (argc >= 3 && !JS_IsNull (argv[2])) { if (JS_ToInt32 (ctx, &from, argv[2])) return JS_EXCEPTION; if (from < 0) from += len; if (from < 0) from = 0; if (from > len) from = len; } int to = len; if (argc >= 4 && !JS_IsNull (argv[3])) { if (JS_ToInt32 (ctx, &to, argv[3])) return JS_EXCEPTION; if (to < 0) to += len; if (to < 0) to = 0; if (to > len) to = len; } if (from > to) return JS_NULL; /* RegExp path: convert new exec result record -> classic array */ if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) { /* Root rx, str, out across allocating calls */ JSGCRef rx_ref, str_ref, out_ref; JS_PushGCRef (ctx, &rx_ref); rx_ref.val = argv[1]; JS_PushGCRef (ctx, &str_ref); str_ref.val = str; JS_PushGCRef (ctx, &out_ref); out_ref.val = JS_NULL; #define EXT_CLEANUP() do { JS_PopGCRef (ctx, &out_ref); JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &rx_ref); } while(0) JSValue orig_last_index = JS_GetPropertyStr (ctx, rx_ref.val, "lastIndex"); if (JS_IsException (orig_last_index)) { EXT_CLEANUP (); return JS_EXCEPTION; } if (JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", JS_NewInt32 (ctx, 0)) < 0) goto fail_rx; JSValue sub_str; if (from == 0 && to == len) { sub_str = str_ref.val; } else { sub_str = js_sub_string_val (ctx, str_ref.val, from, to); if (JS_IsException (sub_str)) goto fail_rx; } JSGCRef exec_ref; JS_PushGCRef (ctx, &exec_ref); exec_ref.val = JS_Invoke (ctx, rx_ref.val, JS_KEY_exec, 1, (JSValue *)&sub_str); if (JS_IsException (exec_ref.val)) { JS_PopGCRef (ctx, &exec_ref); goto fail_rx; } if (JS_IsNull (exec_ref.val)) { JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); JS_PopGCRef (ctx, &exec_ref); EXT_CLEANUP (); return JS_NULL; } /* Build result array */ JSValue out = JS_NewArray (ctx); if (JS_IsException (out)) { JS_PopGCRef (ctx, &exec_ref); goto fail_rx; } out_ref.val = out; /* out[0] = exec_res.match */ JSValue match0 = JS_GetPropertyStr (ctx, exec_ref.val, "match"); if (JS_IsException (match0)) { JS_PopGCRef (ctx, &exec_ref); goto fail_rx; } out = out_ref.val; if (JS_IsException (JS_SetPropertyNumber (ctx, out, 0, match0))) { JS_PopGCRef (ctx, &exec_ref); goto fail_rx; } /* Append capture groups from exec_res.captures */ JSValue caps = JS_GetPropertyStr (ctx, exec_ref.val, "captures"); JS_PopGCRef (ctx, &exec_ref); /* exec_ref no longer needed */ if (!JS_IsException (caps) && JS_IsArray (caps)) { int64_t caps_len = 0; if (js_get_length64 (ctx, &caps_len, caps) == 0 && caps_len > 0) { for (int64_t i = 0; i < caps_len; i++) { JSValue cap = JS_GetPropertyNumber (ctx, caps, i); if (JS_IsException (cap)) { goto fail_rx; } out = out_ref.val; if (JS_IsException (JS_SetPropertyNumber (ctx, out, i + 1, cap))) { goto fail_rx; } } } } JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); out = out_ref.val; EXT_CLEANUP (); return out; fail_rx: if (!JS_IsNull (orig_last_index) && !JS_IsException (orig_last_index)) { JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); } EXT_CLEANUP (); return JS_EXCEPTION; } #undef EXT_CLEANUP /* Literal text path */ JSValue needle_val = JS_ToString (ctx, argv[1]); if (JS_IsException (needle_val)) return JS_EXCEPTION; str = argv[0]; /* refresh after potential GC */ int needle_len = js_string_value_len (needle_val); /* Find needle in str[from..to) */ int pos = -1; if (needle_len == 0) { pos = from; } else if (needle_len <= (to - from)) { int limit = to - needle_len; for (int i = from; i <= limit; i++) { int j = 0; for (; j < needle_len; j++) { if (js_string_value_get (str, i + j) != js_string_value_get (needle_val, j)) break; } if (j == needle_len) { pos = i; break; } } } if (pos < 0) return JS_NULL; JSGCRef arr_ref; JS_PushGCRef (ctx, &arr_ref); arr_ref.val = JS_NewArrayLen (ctx, 1); if (JS_IsException (arr_ref.val)) { JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } str = argv[0]; /* refresh after potential GC */ JSValue match = js_sub_string_val (ctx, str, pos, pos + needle_len); if (JS_IsException (match)) { JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } JSValue arr = arr_ref.val; JS_PopGCRef (ctx, &arr_ref); if (JS_IsException (JS_SetPropertyNumber (ctx, arr, 0, match))) return JS_EXCEPTION; return arr; } /* format(text, collection, transformer) - string interpolation * Finds {name} or {name:format} patterns and substitutes from collection. * Collection can be array (index by number) or record (index by key). * Transformer can be function(value, format) or record of functions. */ static JSValue js_cell_text_format (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsText (argv[0])) return JS_NULL; JSValue text_val = argv[0]; JSValue collection = argv[1]; JSValue transformer = argc > 2 ? argv[2] : JS_NULL; int is_array = JS_IsArray (collection); int is_record = JS_IsRecord (collection); if (!is_array && !is_record) return JS_NULL; int len = js_string_value_len (text_val); /* Root text_val, collection, transformer BEFORE any allocation */ JSGCRef res_ref, text_ref, coll_ref, xform_ref; JS_PushGCRef (ctx, &res_ref); JS_PushGCRef (ctx, &text_ref); JS_PushGCRef (ctx, &coll_ref); JS_PushGCRef (ctx, &xform_ref); res_ref.val = JS_NULL; text_ref.val = text_val; coll_ref.val = collection; xform_ref.val = transformer; #define FMT_CLEANUP() do { \ JS_PopGCRef (ctx, &xform_ref); \ JS_PopGCRef (ctx, &coll_ref); \ JS_PopGCRef (ctx, &text_ref); \ JS_PopGCRef (ctx, &res_ref); \ } while(0) JSText *result = pretext_init (ctx, len); if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; } res_ref.val = JS_MKPTR (result); int pos = 0; while (pos < len) { text_val = text_ref.val; /* Find next '{' */ int brace_start = -1; for (int i = pos; i < len; i++) { if (js_string_value_get (text_val, i) == '{') { brace_start = i; break; } } if (brace_start < 0) { /* No more braces, copy rest of string */ JSValue tail = js_sub_string_val (ctx, text_ref.val, pos, len); if (JS_IsException (tail)) { FMT_CLEANUP(); return JS_EXCEPTION; } result = (JSText *)chase (res_ref.val); result = pretext_concat_value (ctx, result, tail); if (result) res_ref.val = JS_MKPTR (result); break; } /* Copy text before brace */ if (brace_start > pos) { JSValue prefix = js_sub_string_val (ctx, text_ref.val, pos, brace_start); if (JS_IsException (prefix)) { FMT_CLEANUP(); return JS_EXCEPTION; } result = (JSText *)chase (res_ref.val); result = pretext_concat_value (ctx, result, prefix); if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; } res_ref.val = JS_MKPTR (result); } /* Find closing '}' */ text_val = text_ref.val; int brace_end = -1; for (int i = brace_start + 1; i < len; i++) { if (js_string_value_get (text_val, i) == '}') { brace_end = i; break; } } if (brace_end < 0) { /* No closing brace, copy '{' and continue */ JSValue ch = js_sub_string_val (ctx, text_ref.val, brace_start, brace_start + 1); if (JS_IsException (ch)) { FMT_CLEANUP(); return JS_EXCEPTION; } result = (JSText *)chase (res_ref.val); result = pretext_concat_value (ctx, result, ch); if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; } res_ref.val = JS_MKPTR (result); pos = brace_start + 1; continue; } /* Extract content between braces */ JSValue middle = js_sub_string_val (ctx, text_ref.val, brace_start + 1, brace_end); if (JS_IsException (middle)) { FMT_CLEANUP(); return JS_EXCEPTION; } /* Split on ':' to get name and format_spec */ int middle_len = js_string_value_len (middle); int colon_pos = -1; for (int i = 0; i < middle_len; i++) { if (js_string_value_get (middle, i) == ':') { colon_pos = i; break; } } JSValue name_val, format_spec; if (colon_pos >= 0) { name_val = js_sub_string_val (ctx, middle, 0, colon_pos); format_spec = js_sub_string_val (ctx, middle, colon_pos + 1, middle_len); } else { name_val = middle; format_spec = JS_KEY_empty; } /* Get value from collection — protect with GCRef since JS_Call below can trigger GC */ JSGCRef cv_ref; JS_PushGCRef (ctx, &cv_ref); cv_ref.val = JS_NULL; if (is_array) { int name_len = js_string_value_len (name_val); int32_t idx = 0; int valid = (name_len > 0); for (int ni = 0; ni < name_len && valid; ni++) { uint32_t ch = js_string_value_get (name_val, ni); if (ch >= '0' && ch <= '9') idx = idx * 10 + (ch - '0'); else valid = 0; } if (valid && idx >= 0) { cv_ref.val = JS_GetPropertyNumber (ctx, coll_ref.val, (uint32_t)idx); } } else { cv_ref.val = JS_GetProperty (ctx, coll_ref.val, name_val); } /* Try to get substitution */ JSValue substitution = JS_NULL; int made_substitution = 0; if (!JS_IsNull (xform_ref.val)) { if (JS_IsFunction (xform_ref.val)) { JSValue args[2] = { cv_ref.val, format_spec }; JSValue result_val = JS_Call (ctx, xform_ref.val, JS_NULL, 2, args); if (JS_IsText (result_val)) { substitution = result_val; made_substitution = 1; } } else if (JS_IsRecord (xform_ref.val)) { JSValue func = JS_GetProperty (ctx, xform_ref.val, format_spec); if (JS_IsFunction (func)) { JSValue result_val = JS_Call (ctx, func, JS_NULL, 1, &cv_ref.val); if (JS_IsText (result_val)) { substitution = result_val; made_substitution = 1; } } } } if (!made_substitution && JS_IsNumber (cv_ref.val) && !JS_IsNull (format_spec)) { JSValue text_method = JS_GetPropertyStr (ctx, cv_ref.val, "text"); if (JS_IsFunction (text_method)) { JSValue result_val = JS_Call (ctx, text_method, cv_ref.val, 1, &format_spec); if (JS_IsText (result_val)) { substitution = result_val; made_substitution = 1; } } } if (!made_substitution && JS_IsNull (cv_ref.val)) { substitution = JS_NewString (ctx, "null"); made_substitution = 1; } if (!made_substitution && !JS_IsNull (cv_ref.val)) { JSValue conv_text_val = JS_ToString (ctx, cv_ref.val); if (JS_IsText (conv_text_val)) { substitution = conv_text_val; made_substitution = 1; } } JS_PopGCRef (ctx, &cv_ref); result = (JSText *)chase (res_ref.val); if (made_substitution) { result = pretext_concat_value (ctx, result, substitution); if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; } res_ref.val = JS_MKPTR (result); } else { /* No substitution — treat the '{' as a literal character and rescan from brace_start + 1 so that real placeholders like {0} inside the skipped range are still found. */ JSValue ch = js_sub_string_val (ctx, text_ref.val, brace_start, brace_start + 1); if (JS_IsException (ch)) { FMT_CLEANUP(); return JS_EXCEPTION; } result = (JSText *)chase (res_ref.val); result = pretext_concat_value (ctx, result, ch); if (!result) { FMT_CLEANUP(); return JS_EXCEPTION; } res_ref.val = JS_MKPTR (result); pos = brace_start + 1; continue; } pos = brace_end + 1; } result = (JSText *)chase (res_ref.val); FMT_CLEANUP(); #undef FMT_CLEANUP return pretext_end (ctx, result); } static JSValue js_stacktrace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { (void)this_val; (void)argc; (void)argv; JSValue stack = JS_GetStack(ctx); int64_t n = 0; JS_GetLength(ctx, stack, &n); for (int i = 0; i < (int)n; i++) { JSValue fr = JS_GetPropertyNumber(ctx, stack, i); JSValue fn_val = JS_GetPropertyStr(ctx, fr, "fn"); JSValue file_val = JS_GetPropertyStr(ctx, fr, "file"); const char *fn = JS_ToCString(ctx, fn_val); const char *file = JS_ToCString(ctx, file_val); int32_t line = 0, col = 0; JS_ToInt32(ctx, &line, JS_GetPropertyStr(ctx, fr, "line")); JS_ToInt32(ctx, &col, JS_GetPropertyStr(ctx, fr, "col")); printf(" at %s (%s:%d:%d)\n", fn ? fn : "", file ? file : "", line, col); if (fn) JS_FreeCString(ctx, fn); if (file) JS_FreeCString(ctx, file); } return JS_NULL; } static JSValue js_caller_info (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { (void)this_val; int depth = 0; if (argc > 0) JS_ToInt32(ctx, &depth, argv[0]); JSValue stack = JS_GetStack(ctx); int64_t n = 0; JS_GetLength(ctx, stack, &n); /* depth 0 = immediate caller of caller_info, which is frame index 1 (frame 0 is caller_info itself) */ int idx = depth + 1; if (idx >= (int)n) idx = (int)n - 1; if (idx < 0) idx = 0; if (n > 0) return JS_GetPropertyNumber(ctx, stack, idx); return JS_NULL; } /* ---------------------------------------------------------------------------- * array function and sub-functions * ---------------------------------------------------------------------------- */ /* array(arg, arg2, arg3, arg4) - main array function */ static JSValue js_cell_array (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { (void)this_val; if (argc < 1) return JS_NULL; JSValue arg = argv[0]; /* array(number) - create array of size */ /* array(number, initial_value) - create array with initial values */ if (JS_IsNumber (arg)) { if (!JS_IsInteger (arg)) return JS_RaiseDisrupt (ctx, "Array expected an integer."); int len = JS_VALUE_GET_INT (arg); if (len < 0) return JS_NULL; JSGCRef result_ref; JSValue result = JS_NewArrayLen (ctx, len); if (JS_IsException (result)) return result; if (argc > 1 && JS_IsFunction (argv[1])) { /* Fill with function results - GC-safe */ JSGCRef func_ref; JS_PushGCRef (ctx, &func_ref); func_ref.val = argv[1]; int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (func_ref.val))->length; if (arity >= 1) { for (int i = 0; i < len; i++) { JSValue idx_arg = JS_NewInt32 (ctx, i); JS_PUSH_VALUE (ctx, result); JSValue val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &idx_arg, 0); JS_POP_VALUE (ctx, result); if (JS_IsException (val)) { JS_PopGCRef (ctx, &func_ref); return JS_EXCEPTION; } JSArray *out = JS_VALUE_GET_ARRAY (result); /* re-chase after call */ out->values[i] = val; } } else { for (int i = 0; i < len; i++) { JS_PUSH_VALUE (ctx, result); JSValue val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 0, NULL, 0); JS_POP_VALUE (ctx, result); if (JS_IsException (val)) { JS_PopGCRef (ctx, &func_ref); return JS_EXCEPTION; } JSArray *out = JS_VALUE_GET_ARRAY (result); /* re-chase after call */ out->values[i] = val; } } JS_PopGCRef (ctx, &func_ref); } else if (argc > 1) { /* Fill with value */ JSArray *out = JS_VALUE_GET_ARRAY (result); for (int i = 0; i < len; i++) out->values[i] = argv[1]; } return result; } /* array(array) - copy */ /* array(array, function) - map */ /* array(array, another_array) - concat */ /* array(array, from, to) - slice */ if (JS_IsArray (arg)) { /* Root input array and arg1 for GC safety in this section */ JSGCRef arg0_ref, arg1_ref; JS_PushGCRef (ctx, &arg0_ref); JS_PushGCRef (ctx, &arg1_ref); arg0_ref.val = argv[0]; arg1_ref.val = argc > 1 ? argv[1] : JS_NULL; JSArray *arr = JS_VALUE_GET_ARRAY (arg0_ref.val); int len = arr->len; if (argc < 2 || JS_IsNull (argv[1])) { /* Copy */ JSValue result = JS_NewArrayLen (ctx, len); if (JS_IsException (result)) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result; } arr = JS_VALUE_GET_ARRAY (arg0_ref.val); /* re-chase after allocation */ JSArray *out = JS_VALUE_GET_ARRAY (result); for (int i = 0; i < len; i++) { out->values[i] = arr->values[i]; } out->len = len; JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result; } if (JS_IsFunction (arg1_ref.val)) { /* Map - GC-safe: root result throughout, use rooted refs for func and array */ int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (arg1_ref.val))->length; int reverse = 0; if (argc > 2 && !JS_IsNull (argv[2])) { if (!JS_IsBool (argv[2])) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return JS_RaiseDisrupt (ctx, "array: reverse must be a logical"); } reverse = JS_VALUE_GET_BOOL (argv[2]); } JSValue exit_val = argc > 3 ? argv[3] : JS_NULL; JSGCRef result_ref; JS_PushGCRef (ctx, &result_ref); result_ref.val = JS_NewArrayLen (ctx, len); if (JS_IsException (result_ref.val)) { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result_ref.val; } int out_idx = 0; #define MAP_STORE(val) do { \ JSArray *out = JS_VALUE_GET_ARRAY (result_ref.val); \ out->values[out_idx++] = (val); \ } while(0) #define MAP_ERR() do { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return JS_EXCEPTION; } while(0) if (arity >= 2) { if (reverse) { for (int i = len - 1; i >= 0; i--) { arr = JS_VALUE_GET_ARRAY (arg0_ref.val); if (i >= (int)arr->len) continue; JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, i) }; JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 2, args, 0); if (JS_IsException (val)) { MAP_ERR (); } if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break; MAP_STORE (val); } } else { for (int i = 0; i < len; i++) { arr = JS_VALUE_GET_ARRAY (arg0_ref.val); if (i >= (int)arr->len) break; JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, i) }; JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 2, args, 0); if (JS_IsException (val)) { MAP_ERR (); } if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break; MAP_STORE (val); } } } else if (arity == 1) { if (reverse) { for (int i = len - 1; i >= 0; i--) { arr = JS_VALUE_GET_ARRAY (arg0_ref.val); if (i >= (int)arr->len) continue; JSValue item = arr->values[i]; JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 1, &item, 0); if (JS_IsException (val)) { MAP_ERR (); } if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break; MAP_STORE (val); } } else { for (int i = 0; i < len; i++) { arr = JS_VALUE_GET_ARRAY (arg0_ref.val); if (i >= (int)arr->len) break; JSValue item = arr->values[i]; JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 1, &item, 0); if (JS_IsException (val)) { MAP_ERR (); } if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break; MAP_STORE (val); } } } else { if (reverse) { for (int i = len - 1; i >= 0; i--) { arr = JS_VALUE_GET_ARRAY (arg0_ref.val); if (i >= (int)arr->len) continue; JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 0, NULL, 0); if (JS_IsException (val)) { MAP_ERR (); } if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break; MAP_STORE (val); } } else { for (int i = 0; i < len; i++) { arr = JS_VALUE_GET_ARRAY (arg0_ref.val); if (i >= (int)arr->len) break; JSValue val = JS_CallInternal (ctx, arg1_ref.val, JS_NULL, 0, NULL, 0); if (JS_IsException (val)) { MAP_ERR (); } if (!JS_IsNull (exit_val) && js_strict_eq (ctx, val, exit_val)) break; MAP_STORE (val); } } } #undef MAP_STORE #undef MAP_ERR /* Truncate if early exit produced fewer elements */ JSArray *out = JS_VALUE_GET_ARRAY (result_ref.val); out->len = out_idx; JSValue result = result_ref.val; JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result; } if (JS_IsArray (arg1_ref.val)) { /* Concat */ JSArray *arr2 = JS_VALUE_GET_ARRAY (arg1_ref.val); int len2 = arr2->len; JSValue result = JS_NewArrayLen (ctx, len + len2); if (JS_IsException (result)) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result; } /* Re-chase arrays after allocation */ arr = JS_VALUE_GET_ARRAY (arg0_ref.val); arr2 = JS_VALUE_GET_ARRAY (arg1_ref.val); JSArray *out = JS_VALUE_GET_ARRAY (result); for (int i = 0; i < len; i++) { out->values[i] = arr->values[i]; } for (int i = 0; i < len2; i++) { out->values[len + i] = arr2->values[i]; } out->len = len + len2; JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result; } if (JS_IsNumber (argv[1])) { /* Slice */ if (!JS_IsInteger (argv[1])) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return JS_RaiseDisrupt (ctx, "array slice: from must be an integer"); } int from = JS_VALUE_GET_INT (argv[1]); int to; if (argc > 2 && !JS_IsNull (argv[2])) { if (!JS_IsNumber (argv[2]) || !JS_IsInteger (argv[2])) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return JS_RaiseDisrupt (ctx, "array slice: to must be an integer"); } to = JS_VALUE_GET_INT (argv[2]); } else { to = len; } if (from < 0) from += len; if (to < 0) to += len; if (from < 0 || from > len || to < 0 || to > len || from > to) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return JS_NULL; } int slice_len = to - from; JSValue result = JS_NewArrayLen (ctx, slice_len); if (JS_IsException (result)) { JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result; } /* Re-chase arrays after allocation */ arr = JS_VALUE_GET_ARRAY (arg0_ref.val); JSArray *out = JS_VALUE_GET_ARRAY (result); for (int i = 0; i < slice_len; i++) { out->values[i] = arr->values[from + i]; } out->len = slice_len; JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return result; } JS_PopGCRef (ctx, &arg1_ref); JS_PopGCRef (ctx, &arg0_ref); return JS_RaiseDisrupt (ctx, "array: invalid argument combination"); } /* array(object) - keys */ if (JS_IsRecord (arg)) { if (argc > 1 && JS_IsFunction (argv[1])) return JS_RaiseDisrupt (ctx, "array(record, fn) is not valid — use array(array(record), fn) to map over keys"); /* Return object keys */ return JS_GetOwnPropertyNames (ctx, arg); } /* array(text) - split into characters */ /* array(text, separator) - split by separator */ /* array(text, length) - dice into chunks */ if (JS_VALUE_IS_TEXT (arg)) { int len = js_string_value_len (arg); if (argc < 2 || JS_IsNull (argv[1])) { /* Split into characters */ JSGCRef arr_ref, str_ref; JS_PushGCRef (ctx, &arr_ref); JS_PushGCRef (ctx, &str_ref); str_ref.val = arg; arr_ref.val = JS_NewArrayLen (ctx, len); if (JS_IsException (arr_ref.val)) { JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } for (int i = 0; i < len; i++) { JSValue ch = js_sub_string_val (ctx, str_ref.val, i, i + 1); if (JS_IsException (ch)) { JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } JS_SetPropertyNumber (ctx, arr_ref.val, i, ch); } JSValue result = arr_ref.val; JS_PopGCRef (ctx, &str_ref); JS_PopGCRef (ctx, &arr_ref); return result; } if (JS_VALUE_IS_TEXT (argv[1])) { /* Split by separator */ const char *cstr = JS_ToCString (ctx, arg); const char *sep = JS_ToCString (ctx, argv[1]); if (!cstr || !sep) { if (cstr) JS_FreeCString (ctx, cstr); if (sep) JS_FreeCString (ctx, sep); return JS_EXCEPTION; } size_t sep_len = strlen (sep); /* Count the number of parts first */ int64_t count = 0; if (sep_len == 0) { count = len; } else { const char *pos = cstr; const char *found; count = 1; while ((found = strstr (pos, sep)) != NULL) { count++; pos = found + sep_len; } } /* GC-protect result and arg across allocating calls */ JSGCRef result_ref, arg_ref; JS_PushGCRef (ctx, &result_ref); JS_PushGCRef (ctx, &arg_ref); arg_ref.val = arg; result_ref.val = JS_NewArrayLen (ctx, count); if (JS_IsException (result_ref.val)) { JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &result_ref); JS_FreeCString (ctx, cstr); JS_FreeCString (ctx, sep); return result_ref.val; } int64_t idx = 0; const char *pos = cstr; const char *found; if (sep_len == 0) { for (int i = 0; i < len; i++) { JSValue ch = js_sub_string_val (ctx, arg_ref.val, i, i + 1); JS_SetPropertyNumber (ctx, result_ref.val, idx++, ch); } } else { while ((found = strstr (pos, sep)) != NULL) { JSValue part = JS_NewStringLen (ctx, pos, found - pos); JS_SetPropertyNumber (ctx, result_ref.val, idx++, part); pos = found + sep_len; } JSValue part = JS_NewString (ctx, pos); JS_SetPropertyNumber (ctx, result_ref.val, idx++, part); } JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &result_ref); JS_FreeCString (ctx, cstr); JS_FreeCString (ctx, sep); return result_ref.val; } if (mist_is_gc_object (argv[1]) && JS_IsRegExp (ctx, argv[1])) { /* Split by regex (manual "global" iteration; ignore g flag semantics) */ /* Root rx, result, arg across allocating calls */ JSGCRef rx_ref, res_ref, arg_ref; JS_PushGCRef (ctx, &rx_ref); rx_ref.val = argv[1]; JS_PushGCRef (ctx, &res_ref); res_ref.val = JS_NULL; JS_PushGCRef (ctx, &arg_ref); arg_ref.val = arg; #define RXS_CLEANUP() do { JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &rx_ref); } while(0) JSValue result = JS_NewArray (ctx); if (JS_IsException (result)) { RXS_CLEANUP (); return result; } res_ref.val = result; /* Save & restore lastIndex to avoid mutating caller-visible state */ JSValue orig_last_index = JS_GetPropertyStr (ctx, rx_ref.val, "lastIndex"); if (JS_IsException (orig_last_index)) { RXS_CLEANUP (); return JS_EXCEPTION; } int pos = 0; int64_t out_idx = 0; while (pos <= len) { if (JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", JS_NewInt32 (ctx, 0)) < 0) goto fail_rx_split; JSValue sub_str = js_sub_string_val (ctx, arg_ref.val, pos, len); if (JS_IsException (sub_str)) goto fail_rx_split; JSValue exec_res = JS_Invoke (ctx, rx_ref.val, JS_KEY_exec, 1, (JSValue *)&sub_str); if (JS_IsException (exec_res)) goto fail_rx_split; if (JS_IsNull (exec_res)) { JSValue tail = js_sub_string_val (ctx, arg_ref.val, pos, len); if (JS_IsException (tail)) goto fail_rx_split; result = res_ref.val; if (JS_ArrayPush (ctx, &result, tail) < 0) { res_ref.val = result; goto fail_rx_split; } res_ref.val = result; break; } JSValue idx_val = JS_GetPropertyStr (ctx, exec_res, "index"); if (JS_IsException (idx_val)) goto fail_rx_split; int32_t local_index = 0; if (JS_ToInt32 (ctx, &local_index, idx_val)) goto fail_rx_split; if (local_index < 0) local_index = 0; int found = pos + local_index; if (found < pos) found = pos; if (found > len) { JSValue tail = js_sub_string_val (ctx, arg_ref.val, pos, len); if (JS_IsException (tail)) goto fail_rx_split; result = res_ref.val; JS_SetPropertyNumber (ctx, result, out_idx++, tail); break; } JSValue end_val = JS_GetPropertyStr (ctx, exec_res, "end"); if (JS_IsException (end_val)) goto fail_rx_split; int32_t end = 0; if (JS_ToInt32 (ctx, &end, end_val)) goto fail_rx_split; int match_len = end - local_index; if (match_len < 0) match_len = 0; JSValue part = js_sub_string_val (ctx, arg_ref.val, pos, found); if (JS_IsException (part)) goto fail_rx_split; result = res_ref.val; if (JS_ArrayPush (ctx, &result, part) < 0) { res_ref.val = result; goto fail_rx_split; } res_ref.val = result; pos = found + match_len; if (match_len == 0) { if (found >= len) { JSValue empty = JS_NewStringLen (ctx, "", 0); if (JS_IsException (empty)) goto fail_rx_split; result = res_ref.val; if (JS_ArrayPush (ctx, &result, empty) < 0) { res_ref.val = result; goto fail_rx_split; } res_ref.val = result; break; } pos = found + 1; } } JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); result = res_ref.val; RXS_CLEANUP (); return result; fail_rx_split: if (!JS_IsException (orig_last_index)) { JS_SetPropertyStr (ctx, rx_ref.val, "lastIndex", orig_last_index); } RXS_CLEANUP (); return JS_EXCEPTION; } #undef RXS_CLEANUP if (JS_VALUE_IS_NUMBER (argv[1])) { /* Dice into chunks */ int chunk_len; if (JS_ToInt32 (ctx, &chunk_len, argv[1])) return JS_NULL; if (chunk_len <= 0) return JS_NULL; int64_t count = (len + chunk_len - 1) / chunk_len; /* GC-protect result and arg across allocating calls */ JSGCRef result_ref, arg_ref; JS_PushGCRef (ctx, &result_ref); JS_PushGCRef (ctx, &arg_ref); arg_ref.val = arg; result_ref.val = JS_NewArrayLen (ctx, count); if (JS_IsException (result_ref.val)) { JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &result_ref); return result_ref.val; } int64_t idx = 0; for (int i = 0; i < len; i += chunk_len) { int end = i + chunk_len; if (end > len) end = len; JSValue chunk = js_sub_string_val (ctx, arg_ref.val, i, end); if (JS_IsException (chunk)) { JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &result_ref); return JS_EXCEPTION; } JS_SetPropertyNumber (ctx, result_ref.val, idx++, chunk); } JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &result_ref); return result_ref.val; } return JS_NULL; } return JS_NULL; } /* array.reduce(arr, fn, initial, reverse) */ static JSValue js_cell_array_reduce (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; if (!JS_IsFunction (argv[1])) return JS_NULL; JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); word_t len = arr->len; JSValue fn = argv[1]; int reverse = 0; if (argc > 3 && !JS_IsNull (argv[3])) { if (!JS_IsBool (argv[3])) return JS_RaiseDisrupt (ctx, "reduce: reverse must be a logical"); reverse = JS_VALUE_GET_BOOL (argv[3]); } JSGCRef acc_ref; JSValue acc; if (argc < 3 || JS_IsNull (argv[2])) { if (len == 0) return JS_NULL; if (len == 1) return arr->values[0]; if (reverse) { acc = arr->values[len - 1]; for (word_t i = len - 1; i > 0; i--) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i - 1 >= arr->len) continue; JSValue args[2] = { acc, arr->values[i - 1] }; JS_PUSH_VALUE (ctx, acc); JSValue new_acc = js_call_internal_capped (ctx, fn, JS_NULL, 2, args); JS_POP_VALUE (ctx, acc); if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } else { acc = arr->values[0]; for (word_t i = 1; i < len; i++) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue args[2] = { acc, arr->values[i] }; JS_PUSH_VALUE (ctx, acc); JSValue new_acc = js_call_internal_capped (ctx, fn, JS_NULL, 2, args); JS_POP_VALUE (ctx, acc); if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } } else { if (len == 0) return argv[2]; acc = argv[2]; if (reverse) { for (word_t i = len; i > 0; i--) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i - 1 >= arr->len) continue; JSValue args[2] = { acc, arr->values[i - 1] }; JS_PUSH_VALUE (ctx, acc); JSValue new_acc = js_call_internal_capped (ctx, fn, JS_NULL, 2, args); JS_POP_VALUE (ctx, acc); if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } else { for (word_t i = 0; i < len; i++) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue args[2] = { acc, arr->values[i] }; JS_PUSH_VALUE (ctx, acc); JSValue new_acc = js_call_internal_capped (ctx, fn, JS_NULL, 2, args); JS_POP_VALUE (ctx, acc); if (JS_IsException (new_acc)) return JS_EXCEPTION; acc = new_acc; } } } return acc; } /* array.for(arr, fn, reverse, exit) - GC-safe */ static JSValue js_cell_array_for (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; if (!JS_IsFunction (argv[1])) return JS_NULL; JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); word_t len = arr->len; if (len == 0) return JS_NULL; int reverse = 0; if (argc > 2 && !JS_IsNull (argv[2])) { if (!JS_IsBool (argv[2])) return JS_RaiseDisrupt (ctx, "arrfor: reverse must be a logical"); reverse = JS_VALUE_GET_BOOL (argv[2]); } JSValue exit_val = argc > 3 ? argv[3] : JS_NULL; if (reverse) { for (word_t i = len; i > 0; i--) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i - 1 >= arr->len) continue; JSValue args[2]; args[0] = arr->values[i - 1]; args[1] = JS_NewInt32 (ctx, (int32_t)(i - 1)); JSValue result = js_call_internal_capped (ctx, argv[1], JS_NULL, 2, args); if (JS_IsException (result)) return JS_EXCEPTION; if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val)) return result; } } else { for (word_t i = 0; i < len; i++) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue args[2]; args[0] = arr->values[i]; args[1] = JS_NewInt32 (ctx, (int32_t)i); JSValue result = js_call_internal_capped (ctx, argv[1], JS_NULL, 2, args); if (JS_IsException (result)) return JS_EXCEPTION; if (!JS_IsNull (exit_val) && js_strict_eq (ctx, result, exit_val)) return result; } } return JS_NULL; } /* array.find(arr, fn_or_value, reverse, from) */ static JSValue js_cell_array_find (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); word_t len = arr->len; int reverse = 0; if (argc > 2 && !JS_IsNull (argv[2])) { if (!JS_IsBool (argv[2])) return JS_RaiseDisrupt (ctx, "find: reverse must be a logical"); reverse = JS_VALUE_GET_BOOL (argv[2]); } int32_t from; if (argc > 3 && !JS_IsNull (argv[3])) { if (JS_ToInt32 (ctx, &from, argv[3])) return JS_NULL; } else { from = reverse ? (int32_t)(len - 1) : 0; } if (!JS_IsFunction (argv[1])) { JSValue target = argv[1]; if (reverse) { for (int32_t i = from; i >= 0; i--) { arr = JS_VALUE_GET_ARRAY (argv[0]); if ((word_t)i >= arr->len) continue; if (js_strict_eq (ctx, arr->values[i], target)) return JS_NewInt32 (ctx, i); } } else { for (word_t i = (word_t)from; i < len; i++) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; if (js_strict_eq (ctx, arr->values[i], target)) return JS_NewInt32 (ctx, (int32_t)i); } } return JS_NULL; } /* Use function predicate — re-chase after each call */ if (reverse) { for (int32_t i = from; i >= 0; i--) { arr = JS_VALUE_GET_ARRAY (argv[0]); if ((word_t)i >= arr->len) continue; JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, i) }; JSValue result = js_call_internal_capped (ctx, argv[1], JS_NULL, 2, args); if (JS_IsException (result)) return JS_EXCEPTION; if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, i); } } else { for (word_t i = (word_t)from; i < len; i++) { arr = JS_VALUE_GET_ARRAY (argv[0]); if (i >= arr->len) break; JSValue args[2] = { arr->values[i], JS_NewInt32 (ctx, (int32_t)i) }; JSValue result = js_call_internal_capped (ctx, argv[1], JS_NULL, 2, args); if (JS_IsException (result)) return JS_EXCEPTION; if (JS_ToBool (ctx, result)) return JS_NewInt32 (ctx, (int32_t)i); } } return JS_NULL; } /* array.filter(arr, fn) - GC-safe */ static JSValue js_cell_array_filter (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; if (!JS_IsFunction (argv[1])) return JS_NULL; /* Protect input array and function throughout the loop */ JSGCRef input_ref, func_ref, result_ref; JS_PushGCRef (ctx, &input_ref); JS_PushGCRef (ctx, &func_ref); JS_PushGCRef (ctx, &result_ref); input_ref.val = argv[0]; func_ref.val = argv[1]; JSArray *arr = JS_VALUE_GET_ARRAY (input_ref.val); word_t len = arr->len; result_ref.val = JS_NewArray (ctx); if (JS_IsException (result_ref.val)) { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &input_ref); return JS_EXCEPTION; } int arity = ((JSFunction *)JS_VALUE_GET_FUNCTION (func_ref.val))->length; for (word_t i = 0; i < len; i++) { /* Re-chase input array each iteration (it may have moved) */ arr = JS_VALUE_GET_ARRAY (input_ref.val); if (i >= arr->len) break; JSValue item = arr->values[i]; JSValue val; if (arity >= 2) { JSValue args[2] = { item, JS_NewInt32 (ctx, (int32_t)i) }; val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 2, args, 0); } else if (arity == 1) { val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 1, &item, 0); } else { val = JS_CallInternal (ctx, func_ref.val, JS_NULL, 0, NULL, 0); } if (JS_IsException (val)) { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &input_ref); return JS_EXCEPTION; } if (JS_VALUE_GET_TAG (val) == JS_TAG_BOOL) { if (JS_VALUE_GET_BOOL (val)) { /* Re-read item after the call (GC may have moved the input array) */ arr = JS_VALUE_GET_ARRAY (input_ref.val); if (i < arr->len) { item = arr->values[i]; if (js_intrinsic_array_push (ctx, &result_ref.val, item) < 0) { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &input_ref); return JS_EXCEPTION; } } } } else { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &input_ref); return JS_NULL; } } JSValue result = result_ref.val; JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &input_ref); return result; } /* array.sort(arr, select) - GC-safe */ static JSValue js_cell_array_sort (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { (void)this_val; if (argc < 1) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; /* GC-safe: root argv[0] */ JSGCRef arr_ref; JS_PushGCRef (ctx, &arr_ref); arr_ref.val = argv[0]; JSArray *arr = JS_VALUE_GET_ARRAY (arr_ref.val); word_t len = arr->len; JSValue result = JS_NewArrayLen (ctx, len); if (JS_IsException (result)) { JS_PopGCRef (ctx, &arr_ref); return result; } if (len == 0) { JS_PopGCRef (ctx, &arr_ref); return result; } /* Root result across allocating calls */ JSGCRef result_ref; JS_PushGCRef (ctx, &result_ref); result_ref.val = result; /* Re-chase arr after allocation */ arr = JS_VALUE_GET_ARRAY (arr_ref.val); len = arr->len; /* Use alloca for temporary working arrays - they won't move during GC */ JSValue *items = alloca (sizeof (JSValue) * len); double *keys = alloca (sizeof (double) * len); char **str_keys = NULL; int is_string = 0; /* Extract items and keys - re-chase arrays as needed */ for (word_t i = 0; i < len; i++) { /* Re-chase input and key arrays each iteration */ arr = JS_VALUE_GET_ARRAY (arr_ref.val); if (i >= arr->len) break; items[i] = arr->values[i]; JSValue key; if (argc < 2 || JS_IsNull (argv[1])) { key = items[i]; } else if (JS_VALUE_GET_TAG (argv[1]) == JS_TAG_INT) { /* Numeric index - use for nested arrays */ int32_t idx; JS_ToInt32 (ctx, &idx, argv[1]); if (JS_IsArray (items[i])) { JSArray *nested = JS_VALUE_GET_ARRAY (items[i]); if (idx >= 0 && (word_t)idx < nested->len) key = nested->values[idx]; else key = JS_NULL; } else { key = JS_GetPropertyNumber (ctx, items[i], idx); /* Re-read items[i] after potential GC */ arr = JS_VALUE_GET_ARRAY (arr_ref.val); items[i] = arr->values[i]; } } else if (JS_VALUE_IS_TEXT (argv[1])) { JSValue prop_key = js_key_from_string (ctx, argv[1]); /* Re-read items[i] (js_key_from_string no longer allocates, but re-read is harmless) */ arr = JS_VALUE_GET_ARRAY (arr_ref.val); items[i] = arr->values[i]; key = JS_GetProperty (ctx, items[i], prop_key); } else if (argc >= 2 && JS_IsArray (argv[1])) { /* Re-chase key array */ JSArray *key_arr = JS_VALUE_GET_ARRAY (argv[1]); if (i < key_arr->len) key = key_arr->values[i]; else key = JS_NULL; } else { key = items[i]; } if (JS_IsException (key)) { if (str_keys) { for (word_t j = 0; j < i; j++) JS_FreeCString (ctx, str_keys[j]); } JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } int key_tag = JS_VALUE_GET_TAG (key); if (key_tag == JS_TAG_INT || key_tag == JS_TAG_FLOAT64 || key_tag == JS_TAG_SHORT_FLOAT) { JS_ToFloat64 (ctx, &keys[i], key); if (i == 0) is_string = 0; } else if (JS_VALUE_IS_TEXT (key)) { if (i == 0) { is_string = 1; str_keys = alloca (sizeof (char *) * len); } if (is_string) { str_keys[i] = (char *)JS_ToCString (ctx, key); } } else { if (str_keys) { for (word_t j = 0; j < i; j++) JS_FreeCString (ctx, str_keys[j]); } JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &arr_ref); return JS_NULL; } } /* Re-read all items from GC-safe source after key extraction (GC may have moved them) */ arr = JS_VALUE_GET_ARRAY (arr_ref.val); for (word_t i = 0; i < len && i < arr->len; i++) items[i] = arr->values[i]; /* Create index array using alloca */ int *indices = alloca (sizeof (int) * len); for (word_t i = 0; i < len; i++) indices[i] = (int)i; /* Simple insertion sort (stable) */ for (word_t i = 1; i < len; i++) { int temp = indices[i]; int j = (int)i - 1; while (j >= 0) { int cmp; if (is_string) { cmp = strcmp (str_keys[indices[j]], str_keys[temp]); } else { double a = keys[indices[j]], b = keys[temp]; cmp = (a > b) - (a < b); } if (cmp <= 0) break; indices[j + 1] = indices[j]; j--; } indices[j + 1] = temp; } /* Build sorted array directly into output - re-chase result */ JSArray *out = JS_VALUE_GET_ARRAY (result_ref.val); for (word_t i = 0; i < len; i++) { out->values[i] = items[indices[i]]; } out->len = len; /* Cleanup string keys only (alloca frees automatically) */ if (str_keys) { for (word_t i = 0; i < len; i++) JS_FreeCString (ctx, str_keys[i]); } result = result_ref.val; JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &arr_ref); return result; } /* ---------------------------------------------------------------------------- * object function and sub-functions * ---------------------------------------------------------------------------- */ /* object(arg, arg2) - main object function */ static JSValue js_cell_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue arg = argv[0]; /* object(object) - shallow mutable copy */ if (mist_is_gc_object (arg) && !JS_IsArray (arg) && !JS_IsFunction (arg)) { if (argc < 2 || JS_IsNull (argv[1])) { /* Shallow copy - root arg, result, keys across allocating calls */ JSGCRef arg_ref, res_ref, keys_ref; JS_PushGCRef (ctx, &arg_ref); arg_ref.val = arg; JS_PushGCRef (ctx, &res_ref); res_ref.val = JS_NewObject (ctx); JS_PushGCRef (ctx, &keys_ref); keys_ref.val = JS_NULL; #define OBJ_COPY_CLEANUP() do { JS_PopGCRef (ctx, &keys_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &arg_ref); } while(0) if (JS_IsException (res_ref.val)) { OBJ_COPY_CLEANUP (); return JS_EXCEPTION; } keys_ref.val = JS_GetOwnPropertyNames (ctx, arg_ref.val); if (JS_IsException (keys_ref.val)) { OBJ_COPY_CLEANUP (); return JS_EXCEPTION; } uint32_t len; if (js_get_length32 (ctx, &len, keys_ref.val)) { OBJ_COPY_CLEANUP (); return JS_EXCEPTION; } for (uint32_t i = 0; i < len; i++) { JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i); JSValue val = JS_GetProperty (ctx, arg_ref.val, key); if (JS_IsException (val)) { OBJ_COPY_CLEANUP (); return JS_EXCEPTION; } JS_SetProperty (ctx, res_ref.val, key, val); } JSValue result = res_ref.val; OBJ_COPY_CLEANUP (); return result; } #undef OBJ_COPY_CLEANUP /* object(object, another_object) - combine */ if (mist_is_gc_object (argv[1]) && !JS_IsArray (argv[1])) { JSGCRef arg_ref, arg2_ref, res_ref, keys_ref; JS_PushGCRef (ctx, &arg_ref); arg_ref.val = arg; JS_PushGCRef (ctx, &arg2_ref); arg2_ref.val = argv[1]; JS_PushGCRef (ctx, &res_ref); res_ref.val = JS_NewObject (ctx); JS_PushGCRef (ctx, &keys_ref); keys_ref.val = JS_NULL; #define OBJ_COMBINE_CLEANUP() do { JS_PopGCRef (ctx, &keys_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &arg2_ref); JS_PopGCRef (ctx, &arg_ref); } while(0) if (JS_IsException (res_ref.val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; } /* Copy from first object */ keys_ref.val = JS_GetOwnPropertyNames (ctx, arg_ref.val); if (JS_IsException (keys_ref.val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; } uint32_t len; if (js_get_length32 (ctx, &len, keys_ref.val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; } for (uint32_t i = 0; i < len; i++) { JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i); JSValue val = JS_GetProperty (ctx, arg_ref.val, key); if (JS_IsException (val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; } JS_SetProperty (ctx, res_ref.val, key, val); } /* Copy from second object */ keys_ref.val = JS_GetOwnPropertyNames (ctx, arg2_ref.val); if (JS_IsException (keys_ref.val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; } if (js_get_length32 (ctx, &len, keys_ref.val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; } for (uint32_t i = 0; i < len; i++) { JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i); JSValue val = JS_GetProperty (ctx, arg2_ref.val, key); if (JS_IsException (val)) { OBJ_COMBINE_CLEANUP (); return JS_EXCEPTION; } JS_SetProperty (ctx, res_ref.val, key, val); } JSValue result = res_ref.val; OBJ_COMBINE_CLEANUP (); return result; } #undef OBJ_COMBINE_CLEANUP /* object(object, array_of_keys) - select */ if (JS_IsArray (argv[1])) { JSGCRef arg_ref, res_ref, karr_ref; JS_PushGCRef (ctx, &arg_ref); arg_ref.val = arg; JS_PushGCRef (ctx, &res_ref); res_ref.val = JS_NewObject (ctx); JS_PushGCRef (ctx, &karr_ref); karr_ref.val = argv[1]; #define OBJ_SEL_CLEANUP() do { JS_PopGCRef (ctx, &karr_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &arg_ref); } while(0) if (JS_IsException (res_ref.val)) { OBJ_SEL_CLEANUP (); return JS_EXCEPTION; } JSArray *keys = JS_VALUE_GET_ARRAY (karr_ref.val); int len = keys->len; for (int i = 0; i < len; i++) { keys = JS_VALUE_GET_ARRAY (karr_ref.val); /* re-chase each iteration */ if (i >= (int)keys->len) break; JSValue key = keys->values[i]; if (JS_IsText (key)) { JSValue prop_key = js_key_from_string (ctx, key); int has = JS_HasProperty (ctx, arg_ref.val, prop_key); if (has > 0) { JSValue val = JS_GetProperty (ctx, arg_ref.val, prop_key); if (!JS_IsException (val)) { JS_SetProperty (ctx, res_ref.val, prop_key, val); } } } } JSValue result = res_ref.val; OBJ_SEL_CLEANUP (); return result; } #undef OBJ_SEL_CLEANUP } /* object(array_of_keys) - set with true values */ /* object(array_of_keys, value) - value set */ /* object(array_of_keys, function) - functional value set */ if (JS_IsArray (arg)) { JSArray *keys = JS_VALUE_GET_ARRAY (arg); int len = keys->len; int is_func = argc >= 2 && JS_IsFunction (argv[1]); /* Root keys array and func/value BEFORE JS_NewObject which may trigger GC. argv[] is on the C stack and is NOT a GC root, so after any allocation that triggers GC, argv[] values become dangling pointers. */ JSGCRef keys_ref, func_ref, result_ref; JS_PushGCRef (ctx, &keys_ref); keys_ref.val = arg; /* use already-read arg, not argv[0] */ JS_PushGCRef (ctx, &func_ref); func_ref.val = argc >= 2 ? argv[1] : JS_NULL; JS_PushGCRef (ctx, &result_ref); result_ref.val = JS_NULL; JSValue result = JS_NewObject (ctx); if (JS_IsException (result)) { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &keys_ref); return result; } result_ref.val = result; for (int i = 0; i < len; i++) { keys = JS_VALUE_GET_ARRAY (keys_ref.val); if (i >= (int)keys->len) break; JSValue key = keys->values[i]; if (JS_IsText (key)) { /* Use text directly as key */ JSValue prop_key = js_key_from_string (ctx, key); JSValue val; if (argc < 2) { val = JS_TRUE; } else if (is_func) { JSValue arg_key = key; val = js_call_internal_capped (ctx, func_ref.val, JS_NULL, 1, &arg_key); if (JS_IsException (val)) { JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &keys_ref); return JS_EXCEPTION; } } else { val = func_ref.val; } JS_SetProperty (ctx, result_ref.val, prop_key, val); /* prop_key is interned, no need to free */ } } result = JS_PopGCRef (ctx, &result_ref); JS_PopGCRef (ctx, &func_ref); JS_PopGCRef (ctx, &keys_ref); return result; } return JS_NULL; } /* ---------------------------------------------------------------------------- * fn function and sub-functions * ---------------------------------------------------------------------------- */ /* fn.apply(func, args) - arity is enforced in JS_CallInternal */ static JSValue js_cell_fn_apply (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; if (!JS_IsFunction (argv[0])) return argv[0]; JSFunction *fn = JS_VALUE_GET_FUNCTION (argv[0]); if (argc < 2) return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0); if (!JS_IsArray (argv[1])) { if (fn->length >= 0 && 1 > fn->length) return JS_RaiseDisrupt (ctx, "too many arguments for apply: expected %d, got 1", fn->length); return JS_CallInternal (ctx, argv[0], JS_NULL, 1, &argv[1], 0); } JSArray *arr = JS_VALUE_GET_ARRAY (argv[1]); int len = arr->len; if (fn->length >= 0 && len > fn->length) return JS_RaiseDisrupt (ctx, "too many arguments for apply: expected %d, got %d", fn->length, len); if (len == 0) return JS_CallInternal (ctx, argv[0], JS_NULL, 0, NULL, 0); JSValue *args = alloca (sizeof (JSValue) * len); for (int i = 0; i < len; i++) { args[i] = arr->values[i]; } return JS_CallInternal (ctx, argv[0], JS_NULL, len, args, 0); } /* ============================================================================ * Blob Intrinsic Type * ============================================================================ */ /* Get JSBlob* from a JSValue, chasing forwards. Returns NULL if not a blob. */ static JSBlob *checked_get_blob (JSContext *ctx, JSValue val) { if (!mist_is_blob (val)) return NULL; return (JSBlob *)chase (val); } /* Allocate a new JSBlob on the GC heap with given capacity in bits. */ static JSValue js_new_heap_blob (JSContext *ctx, size_t capacity_bits) { size_t word_count = (capacity_bits + 63) / 64; size_t total = sizeof (JSBlob) + word_count * sizeof (word_t); JSBlob *bd = js_mallocz (ctx, total); if (!bd) return JS_EXCEPTION; bd->mist_hdr = objhdr_make (capacity_bits, OBJ_BLOB, false, false, false, false); bd->length = 0; return JS_MKPTR (bd); } /* Grow a blob using the forward-pointer pattern. *pblob is updated to point to the new, larger blob. */ static int blob_grow (JSContext *ctx, JSValue *pblob, size_t need_bits) { JSGCRef blob_ref; JS_PushGCRef (ctx, &blob_ref); blob_ref.val = *pblob; /* Growth: double until enough, minimum 64 bits */ JSBlob *old = (JSBlob *)chase (blob_ref.val); size_t old_cap = objhdr_cap56 (old->mist_hdr); size_t new_cap = old_cap == 0 ? 64 : old_cap * 2; while (new_cap < need_bits) new_cap *= 2; /* Allocate new blob — may trigger GC */ size_t word_count = (new_cap + 63) / 64; size_t total = sizeof (JSBlob) + word_count * sizeof (word_t); JSBlob *nb = js_mallocz (ctx, total); if (!nb) { JS_PopGCRef (ctx, &blob_ref); return -1; } /* Re-derive old after potential GC */ old = (JSBlob *)chase (blob_ref.val); /* Copy header, length, and bit data */ nb->mist_hdr = objhdr_make (new_cap, OBJ_BLOB, false, false, false, false); nb->length = old->length; size_t old_words = (old_cap + 63) / 64; if (old_words > 0) memcpy (nb->bits, old->bits, old_words * sizeof (word_t)); /* Install forward pointer at old location */ size_t old_blob_size = gc_object_size (old); old->mist_hdr = objhdr_make_fwd (nb); *((size_t *)((uint8_t *)old + sizeof (objhdr_t))) = old_blob_size; /* Update caller's JSValue */ *pblob = JS_MKPTR (nb); JS_PopGCRef (ctx, &blob_ref); return 0; } /* Ensure blob has capacity for total_need bits. May grow and update *pblob. */ static int blob_ensure_cap (JSContext *ctx, JSValue *pblob, size_t total_need) { JSBlob *bd = (JSBlob *)chase (*pblob); size_t cap = objhdr_cap56 (bd->mist_hdr); if (total_need <= cap) return 0; return blob_grow (ctx, pblob, total_need); } /* blob() constructor */ static JSValue js_blob_constructor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { /* blob() - empty blob */ if (argc == 0) { return js_new_heap_blob (ctx, 0); } /* blob(capacity) - blob with initial capacity in bits */ if (argc == 1 && JS_IsNumber (argv[0])) { int64_t capacity_bits; if (JS_ToInt64 (ctx, &capacity_bits, argv[0]) < 0) return JS_EXCEPTION; if (capacity_bits < 0) capacity_bits = 0; return js_new_heap_blob (ctx, (size_t)capacity_bits); } /* blob(length, logical/random) - blob with fill or random */ if (argc == 2 && JS_IsNumber (argv[0])) { int64_t length_bits; if (JS_ToInt64 (ctx, &length_bits, argv[0]) < 0) return JS_EXCEPTION; if (length_bits < 0) length_bits = 0; if (JS_IsBool (argv[1])) { int is_one = JS_ToBool (ctx, argv[1]); JSValue bv = js_new_heap_blob (ctx, (size_t)length_bits); if (JS_IsException (bv)) return bv; JSBlob *bd = (JSBlob *)chase (bv); bd->length = length_bits; if (is_one && length_bits > 0) { size_t bytes = (length_bits + 7) / 8; memset (bd->bits, 0xFF, bytes); size_t trail = length_bits & 7; if (trail) ((uint8_t *)bd->bits)[bytes - 1] &= (1 << trail) - 1; } return bv; } if (JS_IsFunction (argv[1])) { JSGCRef bv_ref; JS_PushGCRef (ctx, &bv_ref); bv_ref.val = js_new_heap_blob (ctx, (size_t)length_bits); if (JS_IsException (bv_ref.val)) { JS_PopGCRef (ctx, &bv_ref); return JS_EXCEPTION; } JSBlob *bd = (JSBlob *)chase (bv_ref.val); bd->length = length_bits; size_t bits_written = 0; while (bits_written < (size_t)length_bits) { JSValue randval = JS_Call (ctx, argv[1], JS_NULL, 0, NULL); if (JS_IsException (randval)) { JS_PopGCRef (ctx, &bv_ref); return JS_EXCEPTION; } int64_t fitval; JS_ToInt64 (ctx, &fitval, randval); size_t bits_to_use = length_bits - bits_written; if (bits_to_use > 52) bits_to_use = 52; bd = (JSBlob *)chase (bv_ref.val); /* re-derive after JS_Call */ uint8_t *data = (uint8_t *)bd->bits; for (size_t j = 0; j < bits_to_use; j++) { size_t bit_pos = bits_written + j; size_t byte_idx = bit_pos / 8; size_t bit_idx = bit_pos % 8; if (fitval & (1LL << j)) data[byte_idx] |= (uint8_t)(1 << bit_idx); else data[byte_idx] &= (uint8_t)~(1 << bit_idx); } bits_written += bits_to_use; } JSValue ret = bv_ref.val; JS_PopGCRef (ctx, &bv_ref); return ret; } return JS_RaiseDisrupt (ctx, "Second argument must be boolean or random function"); } /* blob(blob, from, to) - copy from another blob */ if (argc >= 1 && mist_is_blob (argv[0])) { JSBlob *src = checked_get_blob (ctx, argv[0]); if (!src) return JS_RaiseDisrupt (ctx, "blob constructor: argument 1 not a blob"); int64_t from = 0, to = (int64_t)src->length; if (argc >= 2 && JS_IsNumber (argv[1])) { JS_ToInt64 (ctx, &from, argv[1]); if (from < 0) from = 0; } if (argc >= 3 && JS_IsNumber (argv[2])) { JS_ToInt64 (ctx, &to, argv[2]); if (to < from) to = from; if (to > (int64_t)src->length) to = (int64_t)src->length; } size_t copy_bits = (size_t)(to - from); JSGCRef src_ref; JS_PushGCRef (ctx, &src_ref); src_ref.val = argv[0]; JSValue bv = js_new_heap_blob (ctx, copy_bits); if (JS_IsException (bv)) { JS_PopGCRef (ctx, &src_ref); return JS_EXCEPTION; } src = (JSBlob *)chase (src_ref.val); /* re-derive after alloc */ JSBlob *dst = (JSBlob *)chase (bv); if (copy_bits > 0) copy_bits_fast (src->bits, dst->bits, (size_t)from, (size_t)to - 1, 0); dst->length = copy_bits; JS_PopGCRef (ctx, &src_ref); return bv; } /* blob(text) - create blob from UTF-8 string */ if (argc == 1 && JS_IsText (argv[0])) { const char *str = JS_ToCString (ctx, argv[0]); if (!str) return JS_EXCEPTION; size_t len = strlen (str); JSValue bv = js_new_heap_blob (ctx, len * 8); if (JS_IsException (bv)) { JS_FreeCString (ctx, str); return bv; } JSBlob *bd = (JSBlob *)chase (bv); if (len > 0) memcpy (bd->bits, str, len); bd->length = len * 8; JS_FreeCString (ctx, str); return bv; } return JS_RaiseDisrupt (ctx, "blob constructor: invalid arguments"); } /* blob.write_bit(logical) */ static JSValue js_blob_write_bit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "write_bit(logical) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "write_bit: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "write_bit: cannot write (maybe stone or OOM)"); int bit_val; if (JS_IsNumber (argv[0])) { int32_t num; JS_ToInt32 (ctx, &num, argv[0]); if (num != 0 && num != 1) return JS_RaiseDisrupt (ctx, "write_bit: value must be true, false, 0, or 1"); bit_val = num; } else { bit_val = JS_ToBool (ctx, argv[0]); } if (blob_ensure_cap (ctx, &this_val, bd->length + 1) < 0) return JS_RaiseDisrupt (ctx, "write_bit: cannot write (maybe stone or OOM)"); bd = (JSBlob *)chase (this_val); uint8_t *data = (uint8_t *)bd->bits; size_t idx = bd->length; if (bit_val) data[idx >> 3] |= (1 << (idx & 7)); else data[idx >> 3] &= ~(1 << (idx & 7)); bd->length++; return JS_NULL; } /* blob.write_blob(second_blob) */ static JSValue js_blob_write_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "write_blob(second_blob) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "write_blob: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "write_blob: cannot write to stone blob or OOM"); JSBlob *second = checked_get_blob (ctx, argv[0]); if (!second) return JS_RaiseDisrupt (ctx, "write_blob: argument must be a blob"); size_t src_len = second->length; if (src_len == 0) return JS_NULL; /* Root both blobs across potential growth allocation */ JSGCRef this_ref, arg_ref; JS_PushGCRef (ctx, &this_ref); JS_PushGCRef (ctx, &arg_ref); this_ref.val = this_val; arg_ref.val = argv[0]; bd = (JSBlob *)chase (this_ref.val); if (blob_ensure_cap (ctx, &this_ref.val, bd->length + src_len) < 0) { JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &this_ref); return JS_RaiseDisrupt (ctx, "write_blob: cannot write to stone blob or OOM"); } bd = (JSBlob *)chase (this_ref.val); second = (JSBlob *)chase (arg_ref.val); copy_bits_fast (second->bits, bd->bits, 0, src_len - 1, bd->length); bd->length += src_len; JS_PopGCRef (ctx, &arg_ref); JS_PopGCRef (ctx, &this_ref); return JS_NULL; } /* blob.write_number(number) - write dec64 */ static JSValue js_blob_write_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "write_number(number) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "write_number: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "write_number: cannot write to stone blob or OOM"); double d; if (JS_ToFloat64 (ctx, &d, argv[0]) < 0) return JS_EXCEPTION; if (blob_ensure_cap (ctx, &this_val, bd->length + 64) < 0) return JS_RaiseDisrupt (ctx, "write_number: cannot write to stone blob or OOM"); bd = (JSBlob *)chase (this_val); copy_bits_fast (&d, bd->bits, 0, 63, bd->length); bd->length += 64; return JS_NULL; } /* blob.write_fit(value, len) */ static JSValue js_blob_write_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_RaiseDisrupt (ctx, "write_fit(value, len) requires 2 arguments"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "write_fit: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "write_fit: value doesn't fit or stone blob"); int64_t value; int32_t len; if (JS_ToInt64 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION; if (JS_ToInt32 (ctx, &len, argv[1]) < 0) return JS_EXCEPTION; if (len < 1 || len > 64) return JS_RaiseDisrupt (ctx, "write_fit: value doesn't fit or stone blob"); /* Check if value fits in len bits with sign */ if (len < 64) { int64_t max = (1LL << (len - 1)) - 1; int64_t min = -(1LL << (len - 1)); if (value < min || value > max) return JS_RaiseDisrupt (ctx, "write_fit: value doesn't fit or stone blob"); } if (blob_ensure_cap (ctx, &this_val, bd->length + len) < 0) return JS_RaiseDisrupt (ctx, "write_fit: value doesn't fit or stone blob"); bd = (JSBlob *)chase (this_val); copy_bits_fast (&value, bd->bits, 0, len - 1, bd->length); bd->length += len; return JS_NULL; } /* blob.write_text(text) */ static JSValue js_blob_write_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "write_text(text) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "write_text: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "write_text: cannot write to stone blob or OOM"); const char *str = JS_ToCString (ctx, argv[0]); if (!str) return JS_EXCEPTION; size_t slen = strlen (str); size_t need = 64 + slen * 8; if (blob_ensure_cap (ctx, &this_val, bd->length + need) < 0) { JS_FreeCString (ctx, str); return JS_RaiseDisrupt (ctx, "write_text: cannot write to stone blob or OOM"); } bd = (JSBlob *)chase (this_val); /* Write 64-bit length prefix */ int64_t text_len = (int64_t)slen; copy_bits_fast (&text_len, bd->bits, 0, 63, bd->length); bd->length += 64; /* Write raw text bytes */ if (slen > 0) { copy_bits_fast (str, bd->bits, 0, slen * 8 - 1, bd->length); bd->length += slen * 8; } JS_FreeCString (ctx, str); return JS_NULL; } /* blob.write_pad(block_size) */ static JSValue js_blob_write_pad (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "write_pad(block_size) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "write_pad: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "write_pad: cannot write"); int32_t block_size; if (JS_ToInt32 (ctx, &block_size, argv[0]) < 0) return JS_EXCEPTION; if (block_size <= 0) return JS_RaiseDisrupt (ctx, "write_pad: cannot write"); /* Write a 1 bit, then zeros to align to block_size */ size_t after_one = bd->length + 1; size_t rem = after_one % block_size; size_t pad_zeros = rem > 0 ? block_size - rem : 0; size_t total_pad = 1 + pad_zeros; if (blob_ensure_cap (ctx, &this_val, bd->length + total_pad) < 0) return JS_RaiseDisrupt (ctx, "write_pad: cannot write"); bd = (JSBlob *)chase (this_val); uint8_t *data = (uint8_t *)bd->bits; /* Write the 1 bit */ size_t idx = bd->length; data[idx >> 3] |= (1 << (idx & 7)); bd->length++; /* Zero bits are already 0 from js_mallocz, but if we grew we need to be safe */ for (size_t i = 0; i < pad_zeros; i++) { idx = bd->length; data[idx >> 3] &= ~(1 << (idx & 7)); bd->length++; } return JS_NULL; } /* blob.w16(value) - write 16-bit value */ static JSValue js_blob_w16 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "w16(value) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "w16: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "w16: cannot write"); int32_t value; if (JS_ToInt32 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION; int16_t short_val = (int16_t)value; if (blob_ensure_cap (ctx, &this_val, bd->length + 16) < 0) return JS_RaiseDisrupt (ctx, "w16: cannot write"); bd = (JSBlob *)chase (this_val); uint8_t *data = (uint8_t *)bd->bits; size_t bit_off = bd->length; if ((bit_off & 7) == 0) { memcpy (data + (bit_off >> 3), &short_val, sizeof (int16_t)); } else { copy_bits_fast (&short_val, bd->bits, 0, 15, bit_off); } bd->length += 16; return JS_NULL; } /* blob.w32(value) - write 32-bit value */ static JSValue js_blob_w32 (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "w32(value) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "w32: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "w32: cannot write"); int32_t value; if (JS_ToInt32 (ctx, &value, argv[0]) < 0) return JS_EXCEPTION; if (blob_ensure_cap (ctx, &this_val, bd->length + 32) < 0) return JS_RaiseDisrupt (ctx, "w32: cannot write"); bd = (JSBlob *)chase (this_val); uint8_t *data = (uint8_t *)bd->bits; size_t bit_off = bd->length; if ((bit_off & 7) == 0) { memcpy (data + (bit_off >> 3), &value, sizeof (int32_t)); } else { copy_bits_fast (&value, bd->bits, 0, 31, bit_off); } bd->length += 32; return JS_NULL; } /* blob.wf(value) - write float */ static JSValue js_blob_wf (JSContext *ctx, JSValue this_val, JSValue arg0) { if (JS_IsNull (arg0)) return JS_RaiseDisrupt (ctx, "wf(value) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "wf: not called on a blob"); if (objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "wf: cannot write"); float f; double d; if (JS_ToFloat64 (ctx, &d, arg0) < 0) return JS_EXCEPTION; f = (float)d; if (blob_ensure_cap (ctx, &this_val, bd->length + 32) < 0) return JS_RaiseDisrupt (ctx, "wf: cannot write"); bd = (JSBlob *)chase (this_val); uint8_t *data = (uint8_t *)bd->bits; size_t bit_off = bd->length; if ((bit_off & 7) == 0) { memcpy (data + (bit_off >> 3), &f, sizeof (f)); } else { copy_bits_fast (&f, bd->bits, 0, 31, bit_off); } bd->length += 32; return JS_NULL; } /* blob.read_logical(from) */ static JSValue js_blob_read_logical (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "read_logical(from) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "read_logical: not called on a blob"); if (!objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "read_logical: blob must be stone"); int64_t pos; if (JS_ToInt64 (ctx, &pos, argv[0]) < 0) return JS_RaiseDisrupt (ctx, "must provide a positive bit"); if (pos < 0 || (size_t)pos >= bd->length) return JS_RaiseDisrupt (ctx, "read_logical: position must be non-negative"); uint8_t *data = (uint8_t *)bd->bits; int bit_val = (data[pos >> 3] >> (pos & 7)) & 1; return JS_NewBool (ctx, bit_val); } /* blob.read_blob(from, to) */ JSValue js_blob_read_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "read_blob: not called on a blob"); if (!objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "read_blob: blob must be stone"); int64_t from = 0; int64_t to = (int64_t)bd->length; if (argc >= 1) { if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; if (from < 0) from = 0; } if (argc >= 2) { if (JS_ToInt64 (ctx, &to, argv[1]) < 0) return JS_EXCEPTION; if (to > (int64_t)bd->length) to = (int64_t)bd->length; } if (from >= to) return js_new_heap_blob (ctx, 0); size_t copy_bits = (size_t)(to - from); /* Root source blob across new allocation */ JSGCRef src_ref; JS_PushGCRef (ctx, &src_ref); src_ref.val = this_val; JSValue bv = js_new_heap_blob (ctx, copy_bits); if (JS_IsException (bv)) { JS_PopGCRef (ctx, &src_ref); return bv; } bd = (JSBlob *)chase (src_ref.val); JSBlob *dst = (JSBlob *)chase (bv); copy_bits_fast (bd->bits, dst->bits, (size_t)from, (size_t)to - 1, 0); dst->length = copy_bits; JS_PopGCRef (ctx, &src_ref); return bv; } /* blob.read_number(from) */ static JSValue js_blob_read_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "read_number(from) requires 1 argument"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "read_number: not called on a blob"); if (!objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "read_number: blob must be stone"); double from_d; if (JS_ToFloat64 (ctx, &from_d, argv[0]) < 0) return JS_EXCEPTION; size_t from = (size_t)from_d; if (from_d < 0 || from + 64 > bd->length) return JS_RaiseDisrupt (ctx, "read_number: out of range"); double d; copy_bits_fast (bd->bits, &d, from, from + 63, 0); return JS_NewFloat64 (ctx, d); } /* blob.read_fit(from, len) */ static JSValue js_blob_read_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_RaiseDisrupt (ctx, "read_fit(from, len) requires 2 arguments"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "read_fit: not called on a blob"); if (!objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "read_fit: blob must be stone"); int64_t from; int32_t len; if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; if (JS_ToInt32 (ctx, &len, argv[1]) < 0) return JS_EXCEPTION; if (from < 0) return JS_RaiseDisrupt (ctx, "read_fit: position must be non-negative"); if (len < 1 || len > 64 || from + len > (int64_t)bd->length) return JS_RaiseDisrupt (ctx, "read_fit: out of range or invalid length"); int64_t value = 0; copy_bits_fast (bd->bits, &value, (size_t)from, (size_t)(from + len - 1), 0); /* Sign extend if necessary */ if (len < 64 && (value & (1LL << (len - 1)))) { int64_t mask = ~((1LL << len) - 1); value |= mask; } return JS_NewInt64 (ctx, value); } /* blob.read_text(from) */ JSValue js_blob_read_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "read_text: not called on a blob"); if (!objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "read_text: blob must be stone"); int64_t from = 0; if (argc >= 1) { if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; } /* Need at least 64 bits for length prefix */ if (from < 0 || (size_t)from + 64 > bd->length) return JS_RaiseDisrupt (ctx, "read_text: out of range or invalid encoding"); int64_t raw_len = 0; copy_bits_fast (bd->bits, &raw_len, (size_t)from, (size_t)from + 63, 0); if (raw_len < 0) return JS_RaiseDisrupt (ctx, "read_text: out of range or invalid encoding"); size_t slen = (size_t)raw_len; size_t after_len = (size_t)from + 64; if (after_len + slen * 8 > bd->length) return JS_RaiseDisrupt (ctx, "read_text: out of range or invalid encoding"); char *str = sys_malloc (slen + 1); if (!str) return JS_RaiseOOM(ctx); if (slen > 0) copy_bits_fast (bd->bits, str, after_len, after_len + slen * 8 - 1, 0); str[slen] = '\0'; JSValue result = JS_NewString (ctx, str); sys_free (str); return result; } /* blob.pad?(from, block_size) */ static JSValue js_blob_pad_q (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_RaiseDisrupt (ctx, "pad?(from, block_size) requires 2 arguments"); JSBlob *bd = checked_get_blob (ctx, this_val); if (!bd) return JS_RaiseDisrupt (ctx, "pad?: not called on a blob"); if (!objhdr_s (bd->mist_hdr)) return JS_RaiseDisrupt (ctx, "pad?: blob must be stone"); int64_t from; int32_t block_size; if (JS_ToInt64 (ctx, &from, argv[0]) < 0) return JS_EXCEPTION; if (JS_ToInt32 (ctx, &block_size, argv[1]) < 0) return JS_EXCEPTION; if (block_size <= 0) return JS_FALSE; /* Check: length is aligned to block_size, distance from..length is valid */ if (bd->length % block_size != 0) return JS_FALSE; int64_t diff = (int64_t)bd->length - from; if (diff <= 0 || diff > block_size) return JS_FALSE; /* First bit must be 1, rest must be 0 */ uint8_t *data = (uint8_t *)bd->bits; int bit = (data[from >> 3] >> (from & 7)) & 1; if (!bit) return JS_FALSE; for (size_t i = (size_t)from + 1; i < bd->length; i++) { bit = (data[i >> 3] >> (i & 7)) & 1; if (bit) return JS_FALSE; } return JS_TRUE; } static const JSCFunctionListEntry js_blob_proto_funcs[] = { /* Write methods */ JS_CFUNC_DEF ("write_bit", 1, js_blob_write_bit), JS_CFUNC_DEF ("write_blob", 1, js_blob_write_blob), JS_CFUNC_DEF ("write_number", 1, js_blob_write_number), JS_CFUNC_DEF ("write_fit", 2, js_blob_write_fit), JS_CFUNC_DEF ("write_text", 1, js_blob_write_text), JS_CFUNC_DEF ("write_pad", 1, js_blob_write_pad), JS_CFUNC1_DEF ("wf", js_blob_wf), JS_CFUNC_DEF ("w16", 1, js_blob_w16), JS_CFUNC_DEF ("w32", 1, js_blob_w32), /* Read methods */ JS_CFUNC_DEF ("read_logical", 1, js_blob_read_logical), JS_CFUNC_DEF ("read_blob", 2, js_blob_read_blob), JS_CFUNC_DEF ("read_number", 1, js_blob_read_number), JS_CFUNC_DEF ("read_fit", 2, js_blob_read_fit), JS_CFUNC_DEF ("read_text", 1, js_blob_read_text), JS_CFUNC_DEF ("pad?", 2, js_blob_pad_q), }; /* ============================================================================ * Blob external API functions (called from other files via cell.h) * ============================================================================ */ /* Initialize blob - called during context setup (but we do it in * JS_AddIntrinsicBaseObjects now) */ JSValue js_core_blob_use (JSContext *js) { return JS_GetPropertyStr (js, js->global_obj, "blob"); } /* Allocate a mutable blob. *out receives writable pointer to data area. WARNING: *out is invalidated by ANY GC-triggering operation. Caller fills data, then calls js_blob_stone(). */ JSValue js_new_blob_alloc (JSContext *js, size_t bytes, void **out) { size_t bits = bytes * 8; JSValue bv = js_new_heap_blob (js, bits); if (JS_IsException (bv)) { *out = NULL; return bv; } JSBlob *bd = (JSBlob *)chase (bv); bd->length = bits; *out = bd->bits; return bv; } /* Set actual length and stone the blob. actual_bytes <= allocated bytes. Does NOT allocate — cannot trigger GC. */ void js_blob_stone (JSValue blob, size_t actual_bytes) { JSBlob *bd = (JSBlob *)chase (blob); bd->length = actual_bytes * 8; bd->mist_hdr = objhdr_set_s (bd->mist_hdr, true); } /* Create a new blob from raw data, stone it, and return as JSValue */ JSValue js_new_blob_stoned_copy (JSContext *js, void *data, size_t bytes) { void *out; JSValue bv = js_new_blob_alloc (js, bytes, &out); if (JS_IsException (bv)) return bv; if (bytes > 0) memcpy (out, data, bytes); js_blob_stone (bv, bytes); return bv; } /* Get raw data pointer from a blob (must be stone) - returns byte count. WARNING: returned pointer is into GC heap — do NOT hold across allocations. */ void *js_get_blob_data (JSContext *js, size_t *size, JSValue v) { JSBlob *bd = checked_get_blob (js, v); if (!bd) { JS_RaiseDisrupt (js, "get_blob_data: not called on a blob"); return NULL; } *size = (bd->length + 7) / 8; if (!objhdr_s (bd->mist_hdr)) { JS_RaiseDisrupt (js, "attempted to read data from a non-stone blob"); return NULL; } if (bd->length % 8 != 0) { JS_RaiseDisrupt ( js, "attempted to read data from a non-byte aligned blob [length is %llu]", (unsigned long long)bd->length); return NULL; } return (void *)bd->bits; } /* Get raw data pointer from a blob (must be stone) - returns bit count. WARNING: returned pointer is into GC heap — do NOT hold across allocations. */ void *js_get_blob_data_bits (JSContext *js, size_t *bits, JSValue v) { JSBlob *bd = checked_get_blob (js, v); if (!bd) { JS_RaiseDisrupt (js, "get_blob_data_bits: not called on a blob"); return NULL; } if (!objhdr_s (bd->mist_hdr)) { JS_RaiseDisrupt (js, "attempted to read data from a non-stone blob"); return NULL; } if (bd->length % 8 != 0) { JS_RaiseDisrupt (js, "attempted to read data from a non-byte aligned blob"); return NULL; } if (bd->length == 0) { JS_RaiseDisrupt (js, "attempted to read data from an empty blob"); return NULL; } *bits = bd->length; return (void *)bd->bits; } /* Check if a value is a blob */ int js_is_blob (JSContext *js, JSValue v) { return mist_is_blob (v); } /* ============================================================================ * eval() function - compile and execute code with environment * ============================================================================ */ /* mach_load(blob, env?) - deserialize and execute binary blob */ static JSValue js_mach_load (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "mach_load requires a blob argument"); size_t data_size; void *data = js_get_blob_data (ctx, &data_size, argv[0]); if (!data) return JS_EXCEPTION; MachCode *mc = JS_DeserializeMachCode ((const uint8_t *)data, data_size); if (!mc) return JS_RaiseDisrupt (ctx, "mach_load: failed to deserialize bytecode"); JSValue env = (argc >= 2 && mist_is_gc_object (argv[1])) ? argv[1] : JS_NULL; JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env); JS_FreeMachCode (mc); return JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env, JS_NULL); } /* mach_eval_mcode(name, mcode_json, env?) - compile mcode IR and run via register VM */ static JSValue js_mach_eval_mcode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2 || !JS_IsText (argv[0]) || !JS_IsText (argv[1])) return JS_RaiseDisrupt (ctx, "mach_eval_mcode requires (name, mcode_json) text arguments"); const char *name = JS_ToCString (ctx, argv[0]); if (!name) return JS_EXCEPTION; const char *json_str = JS_ToCString (ctx, argv[1]); if (!json_str) { JS_FreeCString (ctx, name); return JS_EXCEPTION; } cJSON *mcode = cJSON_Parse (json_str); JS_FreeCString (ctx, json_str); if (!mcode) { JS_FreeCString (ctx, name); return JS_RaiseDisrupt (ctx, "mach_eval_mcode: failed to parse mcode JSON"); } /* Set filename on the mcode root if not present */ if (!cJSON_GetObjectItemCaseSensitive (mcode, "filename")) cJSON_AddStringToObject (mcode, "filename", name); /* Compile mcode IR → MachCode binary */ MachCode *mc = mach_compile_mcode (mcode); cJSON_Delete (mcode); if (!mc) { JS_FreeCString (ctx, name); return JS_RaiseDisrupt (ctx, "mach_eval_mcode: compilation failed"); } JSValue env = (argc >= 3 && mist_is_gc_object (argv[2])) ? argv[2] : JS_NULL; JSCodeRegister *code = JS_LoadMachCode (ctx, mc, env); JS_FreeMachCode (mc); JSValue result = JS_CallRegisterVM (ctx, code, ctx->global_obj, 0, NULL, env, JS_NULL); JS_FreeCString (ctx, name); return result; } /* mach_dump_mcode(name, mcode_json, env?) - compile mcode IR and dump bytecode disassembly */ static JSValue js_mach_dump_mcode (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2 || !JS_IsText (argv[0]) || !JS_IsText (argv[1])) return JS_RaiseDisrupt (ctx, "mach_dump_mcode requires (name, mcode_json) text arguments"); const char *name = JS_ToCString (ctx, argv[0]); if (!name) return JS_EXCEPTION; const char *json_str = JS_ToCString (ctx, argv[1]); if (!json_str) { JS_FreeCString (ctx, name); return JS_EXCEPTION; } cJSON *mcode = cJSON_Parse (json_str); JS_FreeCString (ctx, json_str); if (!mcode) { JS_FreeCString (ctx, name); return JS_RaiseDisrupt (ctx, "mach_dump_mcode: failed to parse mcode JSON"); } if (!cJSON_GetObjectItemCaseSensitive (mcode, "filename")) cJSON_AddStringToObject (mcode, "filename", name); MachCode *mc = mach_compile_mcode (mcode); cJSON_Delete (mcode); if (!mc) { JS_FreeCString (ctx, name); return JS_RaiseDisrupt (ctx, "mach_dump_mcode: compilation failed"); } JSValue env = (argc >= 3 && mist_is_gc_object (argv[2])) ? argv[2] : JS_NULL; /* Serialize to binary then dump */ size_t bin_size; uint8_t *bin = JS_SerializeMachCode (mc, &bin_size); JS_FreeMachCode (mc); JS_DumpMachBin (ctx, bin, bin_size, env); sys_free (bin); JS_FreeCString (ctx, name); return JS_NULL; } /* gc_stats() — return {count, bytes_copied, heap_size, ct_pages} and reset counters */ static JSValue js_gc_stats (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSValue obj = JS_NewObject (ctx); if (JS_IsException (obj)) return obj; JS_SetPropertyStr (ctx, obj, "count", JS_NewInt64 (ctx, (int64_t)ctx->gc_count)); JS_SetPropertyStr (ctx, obj, "bytes_copied", JS_NewInt64 (ctx, (int64_t)ctx->gc_bytes_copied)); JS_SetPropertyStr (ctx, obj, "heap_size", JS_NewInt64 (ctx, (int64_t)ctx->current_block_size)); /* Count CT overflow pages */ int ct_page_count = 0; for (CTPage *p = (CTPage *)ctx->ct_pages; p; p = p->next) ct_page_count++; JS_SetPropertyStr (ctx, obj, "ct_pages", JS_NewInt32 (ctx, ct_page_count)); ctx->gc_count = 0; ctx->gc_bytes_copied = 0; return obj; } /* mach_compile_mcode_bin(name, mcode_json) - compile mcode IR to serialized binary blob */ static JSValue js_mach_compile_mcode_bin (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2 || !JS_IsText (argv[0]) || !JS_IsText (argv[1])) return JS_RaiseDisrupt (ctx, "mach_compile_mcode_bin requires (name, mcode_json) text arguments"); const char *name = JS_ToCString (ctx, argv[0]); if (!name) return JS_EXCEPTION; const char *json_str = JS_ToCString (ctx, argv[1]); if (!json_str) { JS_FreeCString (ctx, name); return JS_EXCEPTION; } cJSON *mcode = cJSON_Parse (json_str); JS_FreeCString (ctx, json_str); if (!mcode) { JS_FreeCString (ctx, name); return JS_RaiseDisrupt (ctx, "mach_compile_mcode_bin: failed to parse mcode JSON"); } if (!cJSON_GetObjectItemCaseSensitive (mcode, "filename")) cJSON_AddStringToObject (mcode, "filename", name); MachCode *mc = mach_compile_mcode (mcode); cJSON_Delete (mcode); JS_FreeCString (ctx, name); if (!mc) return JS_RaiseDisrupt (ctx, "mach_compile_mcode_bin: compilation failed"); size_t size = 0; uint8_t *data = JS_SerializeMachCode (mc, &size); JS_FreeMachCode (mc); if (!data) return JS_RaiseDisrupt (ctx, "mach_compile_mcode_bin: serialization failed"); JSValue blob = js_new_blob_stoned_copy (ctx, data, size); sys_free (data); return blob; } /* ============================================================================ * stone() function - deep freeze with blob support * ============================================================================ */ /* stone(object) - deep freeze an object */ static JSValue js_cell_stone (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue obj = argv[0]; if (mist_is_blob (obj)) { JSBlob *bd = (JSBlob *)chase (obj); bd->mist_hdr = objhdr_set_s (bd->mist_hdr, true); return obj; } if (mist_is_gc_object (obj)) { JSRecord *rec = JS_VALUE_GET_RECORD (obj); obj_set_stone (rec); return obj; } if (JS_IsArray (obj)) { JSArray *arr = JS_VALUE_GET_ARRAY (obj); arr->mist_hdr = objhdr_set_s (arr->mist_hdr, true); return obj; } return JS_NULL; } /* ============================================================================ * reverse() function - reverse an array * ============================================================================ */ static JSValue js_cell_reverse (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue value = argv[0]; /* Handle arrays */ if (JS_IsArray (value)) { /* GC-safe: root argv[0] */ JSGCRef arr_ref; JS_PushGCRef (ctx, &arr_ref); arr_ref.val = argv[0]; JSArray *arr = JS_VALUE_GET_ARRAY (arr_ref.val); int len = arr->len; JSValue result = JS_NewArrayLen (ctx, len); if (JS_IsException (result)) { JS_PopGCRef (ctx, &arr_ref); return result; } arr = JS_VALUE_GET_ARRAY (arr_ref.val); /* re-chase after allocation */ JSArray *out = JS_VALUE_GET_ARRAY (result); for (int i = len - 1, j = 0; i >= 0; i--, j++) { out->values[j] = arr->values[i]; } out->len = len; JS_PopGCRef (ctx, &arr_ref); return result; } /* Handle strings */ if (JS_IsText (value)) { int len = js_string_value_len (value); if (len == 0) return JS_NewString (ctx, ""); JSText *str = js_alloc_string (ctx, len); if (!str) return JS_EXCEPTION; for (int i = 0; i < len; i++) { string_put (str, i, js_string_value_get (value, len - 1 - i)); } str->length = len; return pretext_end (ctx, str); } /* Handle blobs */ if (mist_is_blob (value)) { /* Blobs need proper blob reversal support - return null for now */ return JS_NULL; } return JS_NULL; } /* ============================================================================ * proto() function - get prototype of an object * ============================================================================ */ static JSValue js_cell_pop (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue obj = argv[0]; if (!JS_IsArray (obj)) return JS_NULL; JSArray *arr = JS_VALUE_GET_ARRAY (obj); if (objhdr_s (arr->mist_hdr)) return JS_RaiseDisrupt (ctx, "cannot pop from a stoned array"); if (arr->len == 0) return JS_NULL; /* Transfer ownership: take value without dup, clear slot, decrement len */ JSValue last = arr->values[arr->len - 1]; arr->values[arr->len - 1] = JS_NULL; arr->len--; return last; } JSValue JS_Stone (JSContext *ctx, JSValue this_val) { return js_cell_stone (ctx, this_val, 1, &this_val); } /* GC-safe push: takes pointer to array JSValue, updates it if array grows. Returns 0 on success, -1 on error. */ int JS_ArrayPush (JSContext *ctx, JSValue *arr_ptr, JSValue val) { if (!JS_IsArray (*arr_ptr)) { JS_RaiseDisrupt (ctx, "not an array"); return -1; } return js_intrinsic_array_push (ctx, arr_ptr, val); } JSValue JS_ArrayPop (JSContext *ctx, JSValue obj) { if (!JS_IsArray (obj)) return JS_RaiseDisrupt (ctx, "not an array"); return js_cell_pop (ctx, JS_NULL, 1, &obj); } /* C API: array(arg0, arg1, arg2, arg3) - array(number) or array(number, fill_value_or_fn) - create array - array(array) - copy - array(array, fn, reverse, exit) - map - array(array, array2) - concat - array(array, from, to) - slice - array(object) - keys - array(text) or array(text, sep_or_len) - split */ JSValue JS_Array (JSContext *ctx, JSValue arg0, JSValue arg1, JSValue arg2, JSValue arg3) { JSValue argv[4] = { arg0, arg1, arg2, arg3 }; int argc = 4; if (JS_IsNull (arg3)) argc = 3; if (JS_IsNull (arg2)) argc = 2; if (JS_IsNull (arg1)) argc = 1; return js_cell_array (ctx, JS_NULL, argc, argv); } /* C API: filter(arr, fn) - returns new filtered array */ JSValue JS_ArrayFilter (JSContext *ctx, JSValue arr, JSValue fn) { JSValue argv[2] = { arr, fn }; return js_cell_array_filter (ctx, JS_NULL, 2, argv); } /* C API: sort(arr, selector) - returns new sorted array */ JSValue JS_ArraySort (JSContext *ctx, JSValue arr, JSValue selector) { JSValue argv[2] = { arr, selector }; return js_cell_array_sort (ctx, JS_NULL, 2, argv); } /* C API: find(arr, target_or_fn, reverse, from) - returns index or null */ JSValue JS_ArrayFind (JSContext *ctx, JSValue arr, JSValue target_or_fn, JSValue reverse, JSValue from) { JSValue argv[4] = { arr, target_or_fn, reverse, from }; int argc = 4; if (JS_IsNull (from)) argc = 3; if (JS_IsNull (reverse)) argc = 2; return js_cell_array_find (ctx, JS_NULL, argc, argv); } /* C API: arrfor(arr, fn, reverse, exit) - iterate array */ JSValue JS_ArrFor (JSContext *ctx, JSValue arr, JSValue fn, JSValue reverse, JSValue exit_val) { JSValue argv[4] = { arr, fn, reverse, exit_val }; int argc = 4; if (JS_IsNull (exit_val)) argc = 3; if (JS_IsNull (reverse)) argc = 2; return js_cell_array_for (ctx, JS_NULL, argc, argv); } /* C API: reduce(arr, fn, initial, reverse) - reduce array */ JSValue JS_ArrayReduce (JSContext *ctx, JSValue arr, JSValue fn, JSValue initial, JSValue reverse) { JSValue argv[4] = { arr, fn, initial, reverse }; int argc = 4; if (JS_IsNull (reverse)) argc = 3; if (JS_IsNull (initial)) argc = 2; return js_cell_array_reduce (ctx, JS_NULL, argc, argv); } /* ============================================================ C API Wrappers for Cell Intrinsic Functions ============================================================ */ /* C API: stone(val) - make value immutable */ JSValue JS_CellStone (JSContext *ctx, JSValue val) { return js_cell_stone (ctx, JS_NULL, 1, &val); } /* C API: length(val) - get length of array/text/blob */ JSValue JS_CellLength (JSContext *ctx, JSValue val) { return js_cell_length (ctx, JS_NULL, 1, &val); } /* C API: reverse(val) - reverse array or text */ JSValue JS_CellReverse (JSContext *ctx, JSValue val) { return js_cell_reverse (ctx, JS_NULL, 1, &val); } /* C API: proto(obj) - get prototype */ JSValue JS_CellProto (JSContext *ctx, JSValue obj) { return js_cell_proto (ctx, JS_NULL, 1, &obj); } /* C API: splat(val) - convert to array */ JSValue JS_CellSplat (JSContext *ctx, JSValue val) { return js_cell_splat (ctx, JS_NULL, 1, &val); } /* C API: meme(obj, deep) - clone object */ JSValue JS_CellMeme (JSContext *ctx, JSValue obj, JSValue deep) { JSValue argv[2] = { obj, deep }; int argc = JS_IsNull (deep) ? 1 : 2; return js_cell_meme (ctx, JS_NULL, argc, argv); } /* C API: apply(fn, args) - apply function to array of args */ JSValue JS_CellApply (JSContext *ctx, JSValue fn, JSValue args) { JSValue argv[2] = { fn, args }; return js_cell_fn_apply (ctx, JS_NULL, 2, argv); } /* C API: call(fn, this, args...) - call function */ JSValue JS_CellCall (JSContext *ctx, JSValue fn, JSValue this_val, JSValue args) { JSValue argv[3] = { fn, this_val, args }; int argc = JS_IsNull (args) ? 2 : 3; return js_cell_call (ctx, JS_NULL, argc, argv); } static int js_cell_read_number_strict (JSValue val, double *out) { uint32_t tag = JS_VALUE_GET_TAG (val); if (tag == JS_TAG_INT) { *out = (double)JS_VALUE_GET_INT (val); return 0; } if (JS_TAG_IS_FLOAT64 (tag)) { *out = JS_VALUE_GET_FLOAT64 (val); return 0; } return -1; } static JSValue js_cell_number_from_double (JSContext *ctx, double d) { if (d >= INT32_MIN && d <= INT32_MAX) { int32_t i = (int32_t)d; if ((double)i == d) return JS_NewInt32 (ctx, i); } return JS_NewFloat64 (ctx, d); } /* C API: modulo(a, b) - modulo operation */ JSValue JS_CellModulo (JSContext *ctx, JSValue a, JSValue b) { double dividend, divisor; if (js_cell_read_number_strict (a, ÷nd) < 0) return JS_NULL; if (js_cell_read_number_strict (b, &divisor) < 0) return JS_NULL; if (isnan (dividend) || isnan (divisor)) return JS_NULL; if (divisor == 0.0) return JS_NULL; if (dividend == 0.0) return JS_NewFloat64 (ctx, 0.0); return js_cell_number_from_double (ctx, dividend - (divisor * floor (dividend / divisor))); } /* C API: neg(val) - negate number */ JSValue JS_CellNeg (JSContext *ctx, JSValue val) { double d; if (js_cell_read_number_strict (val, &d) < 0) return JS_NULL; if (isnan (d)) return JS_NULL; return js_cell_number_from_double (ctx, -d); } /* C API: not(val) - logical not */ JSValue JS_CellNot (JSContext *ctx, JSValue val) { return js_cell_not (ctx, JS_NULL, 1, &val); } /* Text functions */ /* C API: text(val) - convert to text */ JSValue JS_CellText (JSContext *ctx, JSValue val) { return js_cell_text (ctx, JS_NULL, 1, &val); } /* C API: lower(text) - convert to lowercase */ JSValue JS_CellLower (JSContext *ctx, JSValue text) { return js_cell_text_lower (ctx, JS_NULL, 1, &text); } /* C API: upper(text) - convert to uppercase */ JSValue JS_CellUpper (JSContext *ctx, JSValue text) { return js_cell_text_upper (ctx, JS_NULL, 1, &text); } /* C API: trim(text, chars) - trim whitespace or specified chars */ JSValue JS_CellTrim (JSContext *ctx, JSValue text, JSValue chars) { JSValue argv[2] = { text, chars }; int argc = JS_IsNull (chars) ? 1 : 2; return js_cell_text_trim (ctx, JS_NULL, argc, argv); } /* C API: codepoint(text, idx) - get codepoint at index */ JSValue JS_CellCodepoint (JSContext *ctx, JSValue text, JSValue idx) { JSValue argv[2] = { text, idx }; int argc = JS_IsNull (idx) ? 1 : 2; return js_cell_text_codepoint (ctx, JS_NULL, argc, argv); } /* C API: replace(text, pattern, replacement) - replace in text */ JSValue JS_CellReplace (JSContext *ctx, JSValue text, JSValue pattern, JSValue replacement) { JSValue argv[3] = { text, pattern, replacement }; return js_cell_text_replace (ctx, JS_NULL, 3, argv); } /* C API: search(text, pattern, from) - search in text */ JSValue JS_CellSearch (JSContext *ctx, JSValue text, JSValue pattern, JSValue from) { JSValue argv[3] = { text, pattern, from }; int argc = JS_IsNull (from) ? 2 : 3; return js_cell_text_search (ctx, JS_NULL, argc, argv); } /* C API: extract(text, from, to) - extract substring Internally, js_cell_text_extract expects (text, pattern, from, to) but for simple substring extraction we don't need a pattern */ JSValue JS_CellExtract (JSContext *ctx, JSValue text, JSValue from, JSValue to) { if (!JS_IsText (text)) return JS_NULL; JSGCRef text_ref; JS_PushGCRef (ctx, &text_ref); text_ref.val = text; int len = js_string_value_len (text_ref.val); int from_idx = 0; int to_idx = len; if (!JS_IsNull (from)) { if (JS_ToInt32 (ctx, &from_idx, from)) { JS_PopGCRef (ctx, &text_ref); return JS_EXCEPTION; } if (from_idx < 0) from_idx += len; if (from_idx < 0) from_idx = 0; if (from_idx > len) from_idx = len; } if (!JS_IsNull (to)) { if (JS_ToInt32 (ctx, &to_idx, to)) { JS_PopGCRef (ctx, &text_ref); return JS_EXCEPTION; } if (to_idx < 0) to_idx += len; if (to_idx < 0) to_idx = 0; if (to_idx > len) to_idx = len; } if (from_idx > to_idx) { JS_PopGCRef (ctx, &text_ref); return JS_NULL; } if (from_idx == to_idx) { JS_PopGCRef (ctx, &text_ref); return JS_NewString (ctx, ""); } /* Create result string */ int result_len = to_idx - from_idx; JSText *str = js_alloc_string (ctx, result_len); if (!str) { JS_PopGCRef (ctx, &text_ref); return JS_EXCEPTION; } for (int i = 0; i < result_len; i++) { string_put (str, i, js_string_value_get (text_ref.val, from_idx + i)); } str->length = result_len; JS_PopGCRef (ctx, &text_ref); return pretext_end (ctx, str); } /* C API: character(codepoint) - create single character text */ JSValue JS_CellCharacter (JSContext *ctx, JSValue codepoint) { return js_cell_character (ctx, JS_NULL, 1, &codepoint); } /* Number functions */ /* C API: number(val) - convert to number */ JSValue JS_CellNumber (JSContext *ctx, JSValue val) { return js_cell_number (ctx, JS_NULL, 1, &val); } /* C API: abs(num) - absolute value */ JSValue JS_CellAbs (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; return js_cell_number_from_double (ctx, fabs (d)); } /* C API: sign(num) - sign of number (-1, 0, 1) */ JSValue JS_CellSign (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; if (d < 0) return JS_NewInt32 (ctx, -1); if (d > 0) return JS_NewInt32 (ctx, 1); return JS_NewInt32 (ctx, 0); } /* C API: floor(num) - floor */ JSValue JS_CellFloor (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; return js_cell_number_from_double (ctx, floor (d)); } /* C API: ceiling(num) - ceiling */ JSValue JS_CellCeiling (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; return js_cell_number_from_double (ctx, ceil (d)); } /* C API: round(num) - round to nearest integer */ JSValue JS_CellRound (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; return js_cell_number_from_double (ctx, round (d)); } /* C API: trunc(num) - truncate towards zero */ JSValue JS_CellTrunc (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; return js_cell_number_from_double (ctx, trunc (d)); } /* C API: whole(num) - integer part */ JSValue JS_CellWhole (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; return js_cell_number_from_double (ctx, trunc (d)); } /* C API: fraction(num) - fractional part */ JSValue JS_CellFraction (JSContext *ctx, JSValue num) { double d; if (js_cell_read_number_strict (num, &d) < 0) return JS_NULL; return js_cell_number_from_double (ctx, d - trunc (d)); } /* C API: min(a, b) - minimum of two numbers */ JSValue JS_CellMin (JSContext *ctx, JSValue a, JSValue b) { double da, db; if (js_cell_read_number_strict (a, &da) < 0) return JS_NULL; if (js_cell_read_number_strict (b, &db) < 0) return JS_NULL; return js_cell_number_from_double (ctx, da < db ? da : db); } /* C API: max(a, b) - maximum of two numbers */ JSValue JS_CellMax (JSContext *ctx, JSValue a, JSValue b) { double da, db; if (js_cell_read_number_strict (a, &da) < 0) return JS_NULL; if (js_cell_read_number_strict (b, &db) < 0) return JS_NULL; return js_cell_number_from_double (ctx, da > db ? da : db); } /* C API: remainder(a, b) - remainder after division */ JSValue JS_CellRemainder (JSContext *ctx, JSValue a, JSValue b) { double dividend, divisor; if (js_cell_read_number_strict (a, ÷nd) < 0) return JS_NULL; if (js_cell_read_number_strict (b, &divisor) < 0) return JS_NULL; if (divisor == 0.0) return JS_NULL; return js_cell_number_from_double (ctx, dividend - (trunc (dividend / divisor) * divisor)); } /* Object functions */ /* C API: object(proto, props) - create object */ JSValue JS_CellObject (JSContext *ctx, JSValue proto, JSValue props) { JSValue argv[2] = { proto, props }; int argc = JS_IsNull (props) ? 1 : 2; if (JS_IsNull (proto)) argc = 0; return js_cell_object (ctx, JS_NULL, argc, argv); } /* C API: format(text, collection, transformer) - string interpolation */ JSValue JS_CellFormat (JSContext *ctx, JSValue text, JSValue collection, JSValue transformer) { JSValue argv[3] = { text, collection, transformer }; int argc = JS_IsNull (transformer) ? 2 : 3; return js_cell_text_format (ctx, JS_NULL, argc, argv); } /* ============================================================ Helper Functions for C API ============================================================ */ /* Create an array from a list of JSValues */ JSValue JS_NewArrayFrom (JSContext *ctx, int count, JSValue *values) { JSGCRef arr_ref; JS_PushGCRef (ctx, &arr_ref); arr_ref.val = JS_NewArrayLen (ctx, count); if (JS_IsException (arr_ref.val)) { JS_PopGCRef (ctx, &arr_ref); return JS_EXCEPTION; } for (int i = 0; i < count; i++) { JS_SetPropertyNumber (ctx, arr_ref.val, i, values[i]); } JSValue result = arr_ref.val; JS_PopGCRef (ctx, &arr_ref); return result; } /* Print a JSValue text to stdout */ void JS_PrintText (JSContext *ctx, JSValue val) { if (!JS_IsText (val)) { /* Try to convert to string first */ val = JS_ToString (ctx, val); if (JS_IsException (val) || !JS_IsText (val)) { printf ("[non-text value]"); return; } } const char *str = JS_ToCString (ctx, val); if (str) { printf ("%s", str); JS_FreeCString (ctx, str); } } /* Print a JSValue text to stdout with newline */ void JS_PrintTextLn (JSContext *ctx, JSValue val) { JS_PrintText (ctx, val); printf ("\n"); } /* Format and print - convenience function */ void JS_PrintFormatted (JSContext *ctx, const char *fmt, int count, JSValue *values) { JSValue fmt_str = JS_NewString (ctx, fmt); JSValue arr = JS_NewArrayFrom (ctx, count, values); JSValue result = JS_CellFormat (ctx, fmt_str, arr, JS_NULL); JS_PrintText (ctx, result); } static JSValue js_cell_push (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; if (!JS_IsArray (argv[0])) return JS_NULL; for (int i = 1; i < argc; i++) { if (js_intrinsic_array_push (ctx, &argv[0], argv[i]) < 0) return JS_EXCEPTION; } return JS_NULL; } static JSValue js_cell_proto (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { (void)this_val; if (argc < 1) return JS_NULL; JSValue obj = argv[0]; if (JS_IsArray (obj)) { return JS_RaiseDisrupt (ctx, "arrays do not have prototypes"); } if (!mist_is_gc_object (obj)) return JS_NULL; JSValue proto = JS_GetPrototype (ctx, obj); if (JS_IsException (proto)) return JS_NULL; /* If prototype is Object.prototype, return null */ if (mist_is_gc_object (proto)) { JSValue obj_proto = ctx->class_proto[JS_CLASS_OBJECT]; if (mist_is_gc_object (obj_proto) && JS_VALUE_GET_OBJ (proto) == JS_VALUE_GET_OBJ (obj_proto)) { return JS_NULL; } } return proto; } static JSValue js_cell_meme (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { JSValue proto = JS_NULL; if (argc > 0 && !JS_IsNull (argv[0])) proto = argv[0]; JSValue result = JS_NewObjectProto (ctx, proto); if (JS_IsException (result)) return result; if (argc < 2) return result; /* Root result across allocating calls */ JSGCRef result_ref; JS_PushGCRef (ctx, &result_ref); result_ref.val = result; /* Helper function to apply a single mixin */ #define APPLY_MIXIN(mix_val) \ do { \ if (!mist_is_gc_object (mix_val) || JS_IsNull (mix_val) || JS_IsArray (mix_val)) \ break; \ JSGCRef _mix_ref; \ JS_PushGCRef (ctx, &_mix_ref); \ _mix_ref.val = mix_val; \ JSValue _keys = JS_GetOwnPropertyNames (ctx, _mix_ref.val); \ if (JS_IsException (_keys)) { \ JS_PopGCRef (ctx, &_mix_ref); \ JS_PopGCRef (ctx, &result_ref); \ return JS_EXCEPTION; \ } \ uint32_t _len; \ if (js_get_length32 (ctx, &_len, _keys)) { \ JS_PopGCRef (ctx, &_mix_ref); \ JS_PopGCRef (ctx, &result_ref); \ return JS_EXCEPTION; \ } \ for (uint32_t j = 0; j < _len; j++) { \ JSValue _key = JS_GetPropertyNumber (ctx, _keys, j); \ JSValue val = JS_GetProperty (ctx, _mix_ref.val, _key); \ if (JS_IsException (val)) { \ JS_PopGCRef (ctx, &_mix_ref); \ JS_PopGCRef (ctx, &result_ref); \ return JS_EXCEPTION; \ } \ JS_SetProperty (ctx, result_ref.val, _key, val); \ } \ JS_PopGCRef (ctx, &_mix_ref); \ } while (0) /* Process all arguments starting from argv[1] as mixins */ for (int i = 1; i < argc; i++) { JSValue mixins = argv[i]; if (JS_IsArray (mixins)) { /* Array of mixins - root the array across calls */ JSGCRef mixins_ref; JS_PushGCRef (ctx, &mixins_ref); mixins_ref.val = mixins; int64_t len; if (js_get_length64 (ctx, &len, mixins_ref.val)) { JS_PopGCRef (ctx, &mixins_ref); JS_PopGCRef (ctx, &result_ref); return JS_EXCEPTION; } for (int64_t j = 0; j < len; j++) { JSValue mix = JS_GetPropertyNumber (ctx, mixins_ref.val, j); if (JS_IsException (mix)) { JS_PopGCRef (ctx, &mixins_ref); JS_PopGCRef (ctx, &result_ref); return JS_EXCEPTION; } APPLY_MIXIN (mix); } JS_PopGCRef (ctx, &mixins_ref); } else if (mist_is_gc_object (mixins) && !JS_IsNull (mixins)) { /* Single mixin object */ APPLY_MIXIN (mixins); } } #undef APPLY_MIXIN result = result_ref.val; JS_PopGCRef (ctx, &result_ref); return result; } /* ============================================================================ * splat() function - flatten object with prototype chain * ============================================================================ */ static JSValue js_cell_splat (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue obj = argv[0]; if (!mist_is_gc_object (obj) || JS_IsNull (obj)) return JS_NULL; /* Root obj, result, current, keys across allocating calls */ JSGCRef obj_ref, res_ref, cur_ref, keys_ref; JS_PushGCRef (ctx, &obj_ref); obj_ref.val = obj; JS_PushGCRef (ctx, &res_ref); res_ref.val = JS_NewObject (ctx); JS_PushGCRef (ctx, &cur_ref); cur_ref.val = obj_ref.val; /* use rooted value, not stale local */ JS_PushGCRef (ctx, &keys_ref); keys_ref.val = JS_NULL; #define SPLAT_CLEANUP() do { JS_PopGCRef (ctx, &keys_ref); JS_PopGCRef (ctx, &cur_ref); JS_PopGCRef (ctx, &res_ref); JS_PopGCRef (ctx, &obj_ref); } while(0) if (JS_IsException (res_ref.val)) { SPLAT_CLEANUP (); return JS_EXCEPTION; } /* Walk prototype chain and collect text keys */ while (!JS_IsNull (cur_ref.val)) { keys_ref.val = JS_GetOwnPropertyNames (ctx, cur_ref.val); if (JS_IsException (keys_ref.val)) { SPLAT_CLEANUP (); return JS_EXCEPTION; } uint32_t len; if (js_get_length32 (ctx, &len, keys_ref.val)) { SPLAT_CLEANUP (); return JS_EXCEPTION; } for (uint32_t i = 0; i < len; i++) { JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i); int has = JS_HasProperty (ctx, res_ref.val, key); if (has < 0) { SPLAT_CLEANUP (); return JS_EXCEPTION; } if (!has) { JSValue val = JS_GetProperty (ctx, cur_ref.val, key); if (JS_IsException (val)) { SPLAT_CLEANUP (); return JS_EXCEPTION; } int tag = JS_VALUE_GET_TAG (val); if (mist_is_gc_object (val) || JS_IsNumber (val) || tag == JS_TAG_STRING || tag == JS_TAG_STRING_IMM || tag == JS_TAG_BOOL) { JS_SetProperty (ctx, res_ref.val, key, val); } } } cur_ref.val = JS_GetPrototype (ctx, cur_ref.val); } /* Call to_data if present */ JSValue to_data = JS_GetPropertyStr (ctx, obj_ref.val, "to_data"); if (JS_IsFunction (to_data)) { JSValue args[1] = { res_ref.val }; JSValue extra = JS_Call (ctx, to_data, obj_ref.val, 1, args); if (!JS_IsException (extra) && mist_is_gc_object (extra)) { keys_ref.val = JS_GetOwnPropertyNames (ctx, extra); if (!JS_IsException (keys_ref.val)) { uint32_t len; if (!js_get_length32 (ctx, &len, keys_ref.val)) { for (uint32_t i = 0; i < len; i++) { JSValue key = JS_GetPropertyNumber (ctx, keys_ref.val, i); JSValue val = JS_GetProperty (ctx, extra, key); JS_SetProperty (ctx, res_ref.val, key, val); } } } } } JSValue result = res_ref.val; SPLAT_CLEANUP (); return result; } #undef SPLAT_CLEANUP /* ============================================================================ * length() function * ============================================================================ */ static JSValue js_cell_length (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue val = argv[0]; /* null returns null */ if (JS_IsNull (val)) return JS_NULL; /* Functions return arity (accessed directly, not via properties) */ if (JS_IsFunction (val)) { JSFunction *f = JS_VALUE_GET_FUNCTION (val); return JS_NewInt32 (ctx, f->length); } /* Strings return codepoint count */ if (MIST_IsImmediateASCII (val)) { return JS_NewInt32 (ctx, MIST_GetImmediateASCIILen (val)); } if (JS_IsPtr (val) && objhdr_type (*chase (val)) == OBJ_TEXT) { JSText *p = (JSText *)chase (val); return JS_NewInt32 (ctx, (int)JSText_len (p)); } /* Check for blob */ if (mist_is_blob (val)) { JSBlob *bd = (JSBlob *)chase (val); return JS_NewInt64 (ctx, bd->length); } /* Arrays return element count */ if (JS_IsArray (val)) { JSArray *arr = JS_VALUE_GET_ARRAY (val); return JS_NewInt32 (ctx, arr->len); } /* Objects with length property */ if (mist_is_gc_object (val)) { JSValue len = JS_GetPropertyStr (ctx, val, "length"); if (!JS_IsException (len) && !JS_IsNull (len)) { if (JS_IsFunction (len)) { JSValue result = JS_Call (ctx, len, val, 0, NULL); return result; } if (JS_VALUE_IS_NUMBER (len)) return len; } else if (JS_IsException (len)) { return len; } } return JS_NULL; } /* ============================================================================ * call() function - call a function with explicit this and arguments * ============================================================================ */ /* call(func, this_val, args_array) */ static JSValue js_cell_call (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_RaiseDisrupt (ctx, "call requires a function argument"); if (!JS_IsFunction (argv[0])) return JS_RaiseDisrupt (ctx, "first argument must be a function"); JSGCRef this_ref; JS_PushGCRef (ctx, &this_ref); this_ref.val = argc >= 2 ? argv[1] : JS_NULL; if (argc < 3 || JS_IsNull (argv[2])) { JSValue ret = JS_CallInternal (ctx, argv[0], this_ref.val, 0, NULL, 0); JS_PopGCRef (ctx, &this_ref); return ret; } if (!JS_IsArray (argv[2])) { JS_PopGCRef (ctx, &this_ref); return JS_RaiseDisrupt (ctx, "third argument must be an array"); } uint32_t len; JSValue *tab = build_arg_list (ctx, &len, &argv[2]); if (!tab) { JS_PopGCRef (ctx, &this_ref); return JS_EXCEPTION; } JSValue ret = JS_CallInternal (ctx, argv[0], this_ref.val, len, tab, 0); free_arg_list (ctx, tab, len); JS_PopGCRef (ctx, &this_ref); return ret; } /* ============================================================================ * is_* type checking functions * ============================================================================ */ /* is_array(val) */ static JSValue js_cell_is_array (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, JS_IsArray (argv[0])); } /* is_blob(val) */ static JSValue js_cell_is_blob (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, mist_is_blob (argv[0])); } /* is_data(val) - check if object is a plain object (data record) */ static JSValue js_cell_is_data (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; /* data is text, number, logical, array, blob, or record — not function, null */ if (JS_IsNull (val)) return JS_FALSE; if (JS_IsFunction (val)) return JS_FALSE; return JS_TRUE; } /* is_function(val) */ static JSValue js_cell_is_function (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, JS_IsFunction (argv[0])); } /* is_logical(val) - check if value is a boolean (true or false) */ static JSValue js_cell_is_logical (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, JS_VALUE_GET_TAG (argv[0]) == JS_TAG_BOOL); } /* is_integer(val) */ static JSValue js_cell_is_integer (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; int tag = JS_VALUE_GET_TAG (val); if (tag == JS_TAG_INT) return JS_TRUE; if (tag == JS_TAG_FLOAT64) { double d = JS_VALUE_GET_FLOAT64 (val); return JS_NewBool (ctx, isfinite (d) && trunc (d) == d); } return JS_FALSE; } /* is_null(val) */ static JSValue js_cell_is_null (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, JS_IsNull (argv[0])); } /* is_number(val) */ static JSValue js_cell_is_number (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, JS_IsNumber (argv[0])); } /* is_object(val) - true for records */ static JSValue js_cell_is_object (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, mist_is_record (argv[0])); } /* is_actor(val) - true for actor objects (have actor_sym property) */ static JSValue js_cell_is_actor (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!mist_is_record (val)) return JS_FALSE; if (JS_IsNull (ctx->actor_sym)) return JS_FALSE; int has = JS_HasPropertyKey (ctx, val, ctx->actor_sym); return JS_NewBool (ctx, has > 0); } /* is_stone(val) - check if value is immutable */ static JSValue js_cell_is_stone (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { (void)this_val; if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, JS_IsStone (argv[0])); } /* is_text(val) */ static JSValue js_cell_is_text (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, JS_IsText (argv[0])); } static JSValue js_cell_is_letter (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!JS_IsText (val)) return JS_FALSE; if (js_string_value_len (val) != 1) return JS_FALSE; uint32_t c = js_string_value_get (val, 0); return JS_NewBool (ctx, (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')); } /* is_true(val) - check if value is exactly true */ static JSValue js_cell_is_true (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, argv[0] == JS_TRUE); } /* is_false(val) - check if value is exactly false */ static JSValue js_cell_is_false (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; return JS_NewBool (ctx, argv[0] == JS_FALSE); } /* is_fit(val) - check if value is a safe integer (int32 or float with integer value <= 2^53) */ static JSValue js_cell_is_fit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (JS_IsInt (val)) return JS_TRUE; if (JS_IsShortFloat (val)) { double d = JS_VALUE_GET_FLOAT64 (val); return JS_NewBool (ctx, isfinite (d) && trunc (d) == d && fabs (d) <= 9007199254740992.0); } return JS_FALSE; } /* is_character(val) - check if value is a single character text */ static JSValue js_cell_is_character (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!JS_IsText (val)) return JS_FALSE; return JS_NewBool (ctx, js_string_value_len (val) == 1); } /* is_digit(val) - check if value is a single digit character */ static JSValue js_cell_is_digit (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!JS_IsText (val)) return JS_FALSE; if (js_string_value_len (val) != 1) return JS_FALSE; uint32_t c = js_string_value_get (val, 0); return JS_NewBool (ctx, c >= '0' && c <= '9'); } /* is_lower(val) - check if value is a single lowercase letter */ static JSValue js_cell_is_lower (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!JS_IsText (val)) return JS_FALSE; if (js_string_value_len (val) != 1) return JS_FALSE; uint32_t c = js_string_value_get (val, 0); return JS_NewBool (ctx, c >= 'a' && c <= 'z'); } /* is_upper(val) - check if value is a single uppercase letter */ static JSValue js_cell_is_upper (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!JS_IsText (val)) return JS_FALSE; if (js_string_value_len (val) != 1) return JS_FALSE; uint32_t c = js_string_value_get (val, 0); return JS_NewBool (ctx, c >= 'A' && c <= 'Z'); } /* is_whitespace(val) - check if all characters are whitespace (non-empty) */ static JSValue js_cell_is_whitespace (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_FALSE; JSValue val = argv[0]; if (!JS_IsText (val)) return JS_FALSE; int len = js_string_value_len (val); if (len == 0) return JS_FALSE; for (int i = 0; i < len; i++) { uint32_t c = js_string_value_get (val, i); if (!(c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v')) return JS_FALSE; } return JS_TRUE; } /* is_proto(val, master) - check if val has master in prototype chain */ static JSValue js_cell_is_proto (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_FALSE; JSValue val = argv[0]; JSValue master = argv[1]; if (!mist_is_gc_object (val) || JS_IsNull (master)) return JS_FALSE; /* Walk prototype chain */ JSValue proto = JS_GetPrototype (ctx, val); while (!JS_IsNull (proto) && !JS_IsException (proto)) { /* If master is a function with prototype property, check that */ if (JS_IsFunction (master)) { JSValue master_proto = JS_GetPropertyStr (ctx, master, "prototype"); if (!JS_IsException (master_proto) && !JS_IsNull (master_proto)) { JSRecord *p1 = JS_VALUE_GET_OBJ (proto); JSRecord *p2 = JS_VALUE_GET_OBJ (master_proto); if (p1 == p2) { return JS_TRUE; } } else if (!JS_IsException (master_proto)) { } } /* Also check if proto == master directly */ if (mist_is_gc_object (master)) { JSRecord *p1 = JS_VALUE_GET_OBJ (proto); JSRecord *p2 = JS_VALUE_GET_OBJ (master); if (p1 == p2) { return JS_TRUE; } } JSValue next = JS_GetPrototype (ctx, proto); proto = next; } if (JS_IsException (proto)) return proto; return JS_FALSE; } /* logical(val) — false for 0/false/"false"/null, true for 1/true/"true", null otherwise */ static JSValue js_cell_logical(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue v = argv[0]; if (JS_IsNull(v) || (JS_IsInt(v) && JS_VALUE_GET_INT(v) == 0) || (JS_IsBool(v) && !JS_VALUE_GET_BOOL(v))) return JS_FALSE; if ((JS_IsInt(v) && JS_VALUE_GET_INT(v) == 1) || (JS_IsBool(v) && JS_VALUE_GET_BOOL(v))) return JS_TRUE; if (JS_IsText(v)) { char buf[8]; JS_KeyGetStr(ctx, buf, sizeof(buf), v); if (strcmp(buf, "false") == 0) return JS_FALSE; if (strcmp(buf, "true") == 0) return JS_TRUE; } return JS_NULL; } /* realloc wrapper for unicode_normalize */ static void *normalize_realloc(void *opaque, void *ptr, size_t size) { (void)opaque; if (size == 0) { pjs_free(ptr); return NULL; } return pjs_realloc(ptr, size); } /* normalize(text) — NFC normalization */ static JSValue js_cell_normalize(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 1) return JS_NULL; JSValue val = argv[0]; if (!JS_IsText(val)) return JS_NULL; int len = js_string_value_len(val); if (len == 0) return JS_NewString(ctx, ""); uint32_t *src = pjs_malloc(len * sizeof(uint32_t)); if (!src) return JS_EXCEPTION; for (int i = 0; i < len; i++) src[i] = js_string_value_get(val, i); uint32_t *dst = NULL; int dst_len = unicode_normalize(&dst, src, len, UNICODE_NFC, NULL, normalize_realloc); pjs_free(src); if (dst_len < 0) { pjs_free(dst); return JS_NULL; } JSText *str = js_alloc_string(ctx, dst_len); if (!str) { pjs_free(dst); return JS_EXCEPTION; } for (int i = 0; i < dst_len; i++) string_put(str, i, dst[i]); str->length = dst_len; pjs_free(dst); return pretext_end(ctx, str); } /* starts_with(str, prefix) — search(str, prefix) == 0 */ static JSValue js_cell_starts_with(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; JSValue args[3] = { argv[0], argv[1], JS_NULL }; JSValue pos = js_cell_text_search(ctx, JS_NULL, 2, args); if (JS_IsInt(pos) && JS_VALUE_GET_INT(pos) == 0) return JS_TRUE; return JS_FALSE; } /* ends_with(str, suffix) — search(str, suffix, -length(suffix)) != null */ static JSValue js_cell_ends_with(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; JSValue len_val = js_cell_length(ctx, JS_NULL, 1, &argv[1]); int slen = JS_IsInt(len_val) ? JS_VALUE_GET_INT(len_val) : 0; JSValue offset = JS_NewInt32(ctx, -slen); JSValue args[3] = { argv[0], argv[1], offset }; JSValue pos = js_cell_text_search(ctx, JS_NULL, 3, args); if (!JS_IsNull(pos)) return JS_TRUE; return JS_FALSE; } /* every(arr, pred) — find(arr, x => !pred(x)) == null */ static JSValue js_cell_every (JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; if (!JS_IsArray (argv[0]) || !JS_IsFunction (argv[1])) return JS_NULL; JSArray *arr = JS_VALUE_GET_ARRAY (argv[0]); for (int i = 0; i < arr->len; i++) { JSValue elem = arr->values[i]; JSValue r = js_call_internal_capped (ctx, argv[1], JS_NULL, 1, &elem); arr = JS_VALUE_GET_ARRAY (argv[0]); if (JS_IsException (r)) return r; if (!JS_ToBool (ctx, r)) return JS_FALSE; } return JS_TRUE; } /* some(arr, pred) — find(arr, pred) != null */ static JSValue js_cell_some(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) { if (argc < 2) return JS_NULL; JSValue r = js_cell_array_find(ctx, JS_NULL, argc, argv); if (JS_IsException(r)) return r; if (!JS_IsNull(r)) return JS_TRUE; return JS_FALSE; } /* C API: Type-check wrappers for sensory functions */ JS_BOOL JS_IsDigit (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_digit (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } JS_BOOL JS_IsLetter (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_letter (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } JS_BOOL JS_IsLower (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_lower (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } JS_BOOL JS_IsUpper (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_upper (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } JS_BOOL JS_IsWhitespace (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_whitespace (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } JS_BOOL JS_IsCharacter (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_character (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } JS_BOOL JS_IsFit (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_fit (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } JS_BOOL JS_IsData (JSValue val) { return !JS_IsNull (val) && !JS_IsFunction (val); } JS_BOOL JS_IsActor (JSContext *ctx, JSValue val) { JSValue r = js_cell_is_actor (ctx, JS_NULL, 1, &val); return JS_VALUE_GET_BOOL (r); } /* C API: logical(val) */ JSValue JS_CellLogical (JSContext *ctx, JSValue val) { return js_cell_logical (ctx, JS_NULL, 1, &val); } /* C API: every(arr, pred) */ JSValue JS_CellEvery (JSContext *ctx, JSValue arr, JSValue pred) { JSValue argv[2] = { arr, pred }; return js_cell_every (ctx, JS_NULL, 2, argv); } /* C API: some(arr, pred) */ JSValue JS_CellSome (JSContext *ctx, JSValue arr, JSValue pred) { JSValue argv[2] = { arr, pred }; return js_cell_some (ctx, JS_NULL, 2, argv); } /* C API: starts_with(text, prefix) */ JSValue JS_CellStartsWith (JSContext *ctx, JSValue text, JSValue prefix) { JSValue argv[2] = { text, prefix }; return js_cell_starts_with (ctx, JS_NULL, 2, argv); } /* C API: ends_with(text, suffix) */ JSValue JS_CellEndsWith (JSContext *ctx, JSValue text, JSValue suffix) { JSValue argv[2] = { text, suffix }; return js_cell_ends_with (ctx, JS_NULL, 2, argv); } /* C API: normalize(text) */ JSValue JS_CellNormalize (JSContext *ctx, JSValue text) { return js_cell_normalize (ctx, JS_NULL, 1, &text); } static void js_set_global_cfunc(JSContext *ctx, const char *name, JSCFunction *func, int length) { JSGCRef ref; JS_PushGCRef(ctx, &ref); ref.val = JS_NewCFunction(ctx, func, name, length); JS_SetPropertyStr(ctx, ctx->global_obj, name, ref.val); JS_PopGCRef(ctx, &ref); } static void JS_AddIntrinsicBaseObjects (JSContext *ctx) { ctx->throw_type_error = JS_NewCFunction (ctx, js_throw_type_error, NULL, 0); ctx->global_obj = JS_NewObject (ctx); /* Cell Script global functions: text, number, array, object, fn */ { JSValue text_func = JS_NewCFunction (ctx, js_cell_text, "text", 3); JS_SetPropertyStr (ctx, ctx->global_obj, "text", text_func); JSValue number_func = JS_NewCFunction (ctx, js_cell_number, "number", 2); JS_SetPropertyStr (ctx, ctx->global_obj, "number", number_func); JSValue array_func = JS_NewCFunction (ctx, js_cell_array, "array", 4); JS_SetPropertyStr (ctx, ctx->global_obj, "array", array_func); JSValue object_func = JS_NewCFunction (ctx, js_cell_object, "object", 2); JS_SetPropertyStr (ctx, ctx->global_obj, "object", object_func); /* Blob intrinsic type */ { JSClassDef blob_class = { .class_name = "blob", .finalizer = NULL, }; JS_NewClass (ctx, JS_CLASS_BLOB, &blob_class); ctx->class_proto[JS_CLASS_BLOB] = JS_NewObject (ctx); JS_SetPropertyFunctionList (ctx, ctx->class_proto[JS_CLASS_BLOB], js_blob_proto_funcs, countof (js_blob_proto_funcs)); JSValue blob_ctor = JS_NewCFunction2 (ctx, js_blob_constructor, "blob", 3, JS_CFUNC_generic, 0); JS_SetPropertyStr (ctx, ctx->global_obj, "blob", blob_ctor); } /* Core functions - using GC-safe helper */ js_set_global_cfunc(ctx, "mach_load", js_mach_load, 2); js_set_global_cfunc(ctx, "mach_eval_mcode", js_mach_eval_mcode, 3); js_set_global_cfunc(ctx, "mach_dump_mcode", js_mach_dump_mcode, 3); js_set_global_cfunc(ctx, "mach_compile_mcode_bin", js_mach_compile_mcode_bin, 2); js_set_global_cfunc(ctx, "gc_stats", js_gc_stats, 0); js_set_global_cfunc(ctx, "stone", js_cell_stone, 1); js_set_global_cfunc(ctx, "length", js_cell_length, 1); js_set_global_cfunc(ctx, "call", js_cell_call, 3); /* is_* type checking functions */ js_set_global_cfunc(ctx, "is_array", js_cell_is_array, 1); js_set_global_cfunc(ctx, "is_blob", js_cell_is_blob, 1); js_set_global_cfunc(ctx, "is_data", js_cell_is_data, 1); js_set_global_cfunc(ctx, "is_function", js_cell_is_function, 1); js_set_global_cfunc(ctx, "is_logical", js_cell_is_logical, 1); js_set_global_cfunc(ctx, "is_integer", js_cell_is_integer, 1); js_set_global_cfunc(ctx, "is_null", js_cell_is_null, 1); js_set_global_cfunc(ctx, "is_number", js_cell_is_number, 1); js_set_global_cfunc(ctx, "is_object", js_cell_is_object, 1); js_set_global_cfunc(ctx, "is_actor", js_cell_is_actor, 1); js_set_global_cfunc(ctx, "is_stone", js_cell_is_stone, 1); js_set_global_cfunc(ctx, "is_text", js_cell_is_text, 1); js_set_global_cfunc(ctx, "is_proto", js_cell_is_proto, 2); js_set_global_cfunc(ctx, "is_letter", js_cell_is_letter, 1); js_set_global_cfunc(ctx, "is_true", js_cell_is_true, 1); js_set_global_cfunc(ctx, "is_false", js_cell_is_false, 1); js_set_global_cfunc(ctx, "is_fit", js_cell_is_fit, 1); js_set_global_cfunc(ctx, "is_character", js_cell_is_character, 1); js_set_global_cfunc(ctx, "is_digit", js_cell_is_digit, 1); js_set_global_cfunc(ctx, "is_lower", js_cell_is_lower, 1); js_set_global_cfunc(ctx, "is_upper", js_cell_is_upper, 1); js_set_global_cfunc(ctx, "is_whitespace", js_cell_is_whitespace, 1); /* Utility functions */ js_set_global_cfunc(ctx, "apply", js_cell_fn_apply, 2); js_set_global_cfunc(ctx, "replace", js_cell_text_replace, 4); js_set_global_cfunc(ctx, "lower", js_cell_text_lower, 1); js_set_global_cfunc(ctx, "upper", js_cell_text_upper, 1); js_set_global_cfunc(ctx, "trim", js_cell_text_trim, 2); js_set_global_cfunc(ctx, "codepoint", js_cell_text_codepoint, 1); js_set_global_cfunc(ctx, "search", js_cell_text_search, 3); js_set_global_cfunc(ctx, "extract", js_cell_text_extract, 4); js_set_global_cfunc(ctx, "format", js_cell_text_format, 3); js_set_global_cfunc(ctx, "reduce", js_cell_array_reduce, 4); js_set_global_cfunc(ctx, "arrfor", js_cell_array_for, 4); js_set_global_cfunc(ctx, "find", js_cell_array_find, 4); js_set_global_cfunc(ctx, "filter", js_cell_array_filter, 2); js_set_global_cfunc(ctx, "sort", js_cell_array_sort, 2); /* Number intrinsics: direct calls lower to mcode; globals remain for first-class use. */ js_set_global_cfunc(ctx, "whole", js_cell_number_whole, 1); js_set_global_cfunc(ctx, "fraction", js_cell_number_fraction, 1); js_set_global_cfunc(ctx, "floor", js_cell_number_floor, 2); js_set_global_cfunc(ctx, "ceiling", js_cell_number_ceiling, 2); js_set_global_cfunc(ctx, "abs", js_cell_number_abs, 1); js_set_global_cfunc(ctx, "round", js_cell_number_round, 2); js_set_global_cfunc(ctx, "sign", js_cell_number_sign, 1); js_set_global_cfunc(ctx, "trunc", js_cell_number_trunc, 2); js_set_global_cfunc(ctx, "min", js_cell_number_min, 2); js_set_global_cfunc(ctx, "max", js_cell_number_max, 2); js_set_global_cfunc(ctx, "remainder", js_cell_number_remainder, 2); js_set_global_cfunc(ctx, "character", js_cell_character, 2); js_set_global_cfunc(ctx, "modulo", js_cell_modulo, 2); js_set_global_cfunc(ctx, "neg", js_cell_neg, 1); js_set_global_cfunc(ctx, "not", js_cell_not, 1); js_set_global_cfunc(ctx, "reverse", js_cell_reverse, 1); js_set_global_cfunc(ctx, "proto", js_cell_proto, 1); js_set_global_cfunc(ctx, "splat", js_cell_splat, 1); /* pi - mathematical constant (no GC concern for immediate float) */ JS_SetPropertyStr(ctx, ctx->global_obj, "pi", JS_NewFloat64(ctx, 3.14159265358979323846264338327950288419716939937510)); js_set_global_cfunc(ctx, "push", js_cell_push, 2); js_set_global_cfunc(ctx, "pop", js_cell_pop, 1); js_set_global_cfunc(ctx, "meme", js_cell_meme, 2); /* Additional builtins */ js_set_global_cfunc(ctx, "logical", js_cell_logical, 1); js_set_global_cfunc(ctx, "starts_with", js_cell_starts_with, 2); js_set_global_cfunc(ctx, "ends_with", js_cell_ends_with, 2); js_set_global_cfunc(ctx, "every", js_cell_every, 2); js_set_global_cfunc(ctx, "some", js_cell_some, 2); js_set_global_cfunc(ctx, "normalize", js_cell_normalize, 1); /* fn record with apply property */ { JSGCRef fn_ref; JS_PushGCRef(ctx, &fn_ref); fn_ref.val = JS_NewObject(ctx); JSValue apply_fn = JS_NewCFunction(ctx, js_cell_fn_apply, "apply", 2); JS_SetPropertyStr(ctx, fn_ref.val, "apply", apply_fn); JS_SetPropertyStr(ctx, ctx->global_obj, "fn", fn_ref.val); JS_PopGCRef(ctx, &fn_ref); } /* I/O functions */ js_set_global_cfunc(ctx, "stacktrace", js_stacktrace, 0); js_set_global_cfunc(ctx, "caller_info", js_caller_info, 1); } } #define STRLEN(s) (sizeof (s) / sizeof (s[0])) #define CSTR "" static inline void key_to_buf (JSContext *ctx, JSValue key, char *dst, int cap, const char *fallback) { if (JS_IsNull (key)) { strncpy (dst, fallback, cap); dst[cap - 1] = 0; return; } JS_KeyGetStr (ctx, dst, cap, key); if (dst[0] == 0) { strncpy (dst, fallback, cap); dst[cap - 1] = 0; } } void js_debug_info (JSContext *js, JSValue fn, js_debug *dbg) { *dbg = (js_debug){ 0 }; if (!JS_IsFunction (fn)) return; JSFunction *f = JS_VALUE_GET_FUNCTION (fn); dbg->unique = (int)(uintptr_t)f; JSValue name_key = JS_NULL; if (!JS_IsNull (f->name)) name_key = f->name; key_to_buf (js, name_key, dbg->name, sizeof (dbg->name), ""); if (f->kind == JS_FUNC_KIND_C || f->kind == JS_FUNC_KIND_C_DATA) { strncpy (dbg->filename, "", sizeof (dbg->filename)); dbg->filename[sizeof (dbg->filename) - 1] = 0; dbg->what = "C"; dbg->param_n = f->length; dbg->vararg = 1; dbg->line = 0; dbg->source = (const uint8_t *)CSTR; dbg->srclen = STRLEN (CSTR); } } void js_debug_sethook (JSContext *ctx, js_hook hook, int type, void *user) { ctx->trace_hook = hook; ctx->trace_type = type; ctx->trace_data = user; } void js_debug_gethook (JSContext *ctx, js_hook *hook, int *type, void **user) { if (hook) *hook = ctx->trace_hook; if (type) *type = ctx->trace_type; if (user) *user = ctx->trace_data; } size_t cell_ctx_heap_used (JSContext *ctx) { return (size_t)(ctx->heap_free - ctx->heap_base); } /* Public API: get stack trace as JS array of {fn, file, line, col} objects. Does NOT clear reg_current_frame — caller is responsible if needed. Two-phase approach for GC safety: Phase 1 collects raw C data on the C stack (pointers into stable JSCodeRegister fields), Phase 2 allocates JS objects. */ JSValue JS_GetStack(JSContext *ctx) { if (JS_IsNull(ctx->reg_current_frame)) return JS_NewArray(ctx); /* Phase 1: collect raw frame data (no JS allocations) */ struct { const char *fn; const char *file; uint16_t line, col; } frames[64]; int count = 0; JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame); uint32_t cur_pc = ctx->current_register_pc; int is_first = 1; while (frame && count < 64) { if (!JS_IsFunction(frame->function)) break; JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code) { JSCodeRegister *code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; uint32_t pc = is_first ? cur_pc : (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16); frames[count].fn = code->name_cstr; frames[count].file = code->filename_cstr; frames[count].line = 0; frames[count].col = 0; if (code->line_table && pc < code->instr_count) { frames[count].line = code->line_table[pc].line; frames[count].col = code->line_table[pc].col; } count++; } if (JS_IsNull(frame->caller)) break; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); is_first = 0; } /* Phase 2: create JS objects (frame data is on C stack, safe across GC) */ JSGCRef arr_ref; JS_PushGCRef(ctx, &arr_ref); arr_ref.val = JS_NewArray(ctx); for (int i = 0; i < count; i++) { JSGCRef item_ref; JS_PushGCRef(ctx, &item_ref); item_ref.val = JS_NewObject(ctx); JSValue fn_str = JS_NewString(ctx, frames[i].fn ? frames[i].fn : ""); JS_SetPropertyStr(ctx, item_ref.val, "fn", fn_str); JSValue file_str = JS_NewString(ctx, frames[i].file ? frames[i].file : ""); JS_SetPropertyStr(ctx, item_ref.val, "file", file_str); JS_SetPropertyStr(ctx, item_ref.val, "line", JS_NewInt32(ctx, frames[i].line)); JS_SetPropertyStr(ctx, item_ref.val, "col", JS_NewInt32(ctx, frames[i].col)); JS_SetPropertyNumber(ctx, arr_ref.val, i, item_ref.val); JS_PopGCRef(ctx, &item_ref); } JSValue result = arr_ref.val; JS_PopGCRef(ctx, &arr_ref); return result; } /* Crash-safe stack printer: walks JS frames and writes to stderr using only write() (async-signal-safe). No GC allocations. Best-effort: if the heap is corrupted, we may double-fault. */ void JS_CrashPrintStack(JSContext *ctx) { if (!ctx) return; if (JS_IsNull(ctx->reg_current_frame)) return; static const char hdr[] = "\n--- JS Stack ---\n"; write(STDERR_FILENO, hdr, sizeof(hdr) - 1); JSFrameRegister *frame = (JSFrameRegister *)JS_VALUE_GET_PTR(ctx->reg_current_frame); uint32_t cur_pc = ctx->current_register_pc; int is_first = 1; int depth = 0; while (frame && depth < 32) { if (!JS_IsFunction(frame->function)) break; JSFunction *fn = JS_VALUE_GET_FUNCTION(frame->function); if (fn->kind == JS_FUNC_KIND_REGISTER && JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code) { JSCodeRegister *code = JS_VALUE_GET_CODE(FN_READ_CODE(fn))->u.reg.code; uint32_t pc = is_first ? cur_pc : (uint32_t)(JS_VALUE_GET_INT(frame->address) >> 16); const char *name = code->name_cstr ? code->name_cstr : ""; const char *file = code->filename_cstr ? code->filename_cstr : ""; int line = 0; if (code->line_table && pc < code->instr_count) line = code->line_table[pc].line; /* Format: " at name (file:line)\n" using only write() */ char buf[512]; int pos = 0; buf[pos++] = ' '; buf[pos++] = ' '; buf[pos++] = ' '; buf[pos++] = ' '; buf[pos++] = 'a'; buf[pos++] = 't'; buf[pos++] = ' '; for (const char *p = name; *p && pos < 200; p++) buf[pos++] = *p; buf[pos++] = ' '; buf[pos++] = '('; for (const char *p = file; *p && pos < 440; p++) buf[pos++] = *p; buf[pos++] = ':'; /* itoa for line number */ if (line == 0) { buf[pos++] = '0'; } else { char digits[12]; int d = 0; int n = line; while (n > 0) { digits[d++] = '0' + (n % 10); n /= 10; } for (int i = d - 1; i >= 0; i--) buf[pos++] = digits[i]; } buf[pos++] = ')'; buf[pos++] = '\n'; write(STDERR_FILENO, buf, pos); } if (JS_IsNull(frame->caller)) break; frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame->caller); is_first = 0; depth++; } static const char ftr[] = "--- End JS Stack ---\n"; write(STDERR_FILENO, ftr, sizeof(ftr) - 1); }