11904 lines
372 KiB
C
11904 lines
372 KiB
C
/*
|
|
* QuickJS Javascript Engine
|
|
*
|
|
* Copyright (c) 2017-2025 Fabrice Bellard
|
|
* Copyright (c) 2017-2025 Charlie Gordon
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to
|
|
* deal in the Software without restriction, including without limitation the
|
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
* sell copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in
|
|
* all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
* IN THE SOFTWARE.
|
|
*/
|
|
|
|
#define BLOB_IMPLEMENTATION
|
|
#include "quickjs-internal.h"
|
|
#include <unistd.h>
|
|
|
|
// #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 : "<anonymous>",
|
|
file ? file : "<unknown>", 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 quickjs-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 <execinfo.h>
|
|
#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);
|
|
}
|
|
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
|
|
|
|
/* Check memory limit — kill actor if heap exceeds cap */
|
|
if (ctx->heap_memory_limit > 0 && ctx->current_block_size > ctx->heap_memory_limit) {
|
|
#ifdef ACTOR_TRACE
|
|
void *crt = ctx->user_opaque;
|
|
if (crt)
|
|
fprintf(stderr, "[ACTOR_TRACE] heap %zu > limit %zu, OOM\n",
|
|
ctx->current_block_size, ctx->heap_memory_limit);
|
|
#endif
|
|
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;
|
|
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;
|
|
|
|
/* 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_GetContextOpaque (JSContext *ctx) { return ctx->user_opaque; }
|
|
|
|
void JS_SetContextOpaque (JSContext *ctx, void *opaque) {
|
|
ctx->user_opaque = opaque;
|
|
}
|
|
|
|
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);
|
|
|
|
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 ("<null>");
|
|
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);
|
|
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). */
|
|
JSValue JS_RaiseOOM (JSContext *ctx) {
|
|
fprintf (stderr, "out of memory\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, "<invalid string tag %d>", (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 : "<anonymous>", file ? file : "<unknown>", 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 = argc > 2 && JS_ToBool (ctx, 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_NULL;
|
|
}
|
|
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_NULL;
|
|
}
|
|
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_NULL;
|
|
}
|
|
|
|
/* 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 = argc > 3 && JS_ToBool (ctx, 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 = argc > 2 && JS_ToBool (ctx, 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 = argc > 2 && JS_ToBool (ctx, 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 || JS_IsNull (func_ref.val)) {
|
|
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];
|
|
if (!mist_is_gc_object (val)) return JS_FALSE;
|
|
if (JS_IsArray (val)) return JS_FALSE;
|
|
if (JS_IsFunction (val)) return JS_FALSE;
|
|
if (mist_is_blob (val)) return JS_FALSE;
|
|
/* Check if it's a plain object (prototype is Object.prototype or null) */
|
|
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 value is a single whitespace character */
|
|
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;
|
|
if (js_string_value_len (val) != 1) return JS_FALSE;
|
|
uint32_t c = js_string_value_get (val, 0);
|
|
return JS_NewBool (ctx, c == ' ' || c == '\t' || c == '\n'
|
|
|| c == '\r' || c == '\f' || c == '\v');
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
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);
|
|
|
|
/* 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 "<native C>"
|
|
|
|
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), "<anonymous>");
|
|
|
|
if (f->kind == JS_FUNC_KIND_C || f->kind == JS_FUNC_KIND_C_DATA) {
|
|
strncpy (dbg->filename, "<native C>", 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;
|
|
}
|
|
|
|
|
|
/* 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 : "<anonymous>");
|
|
JS_SetPropertyStr(ctx, item_ref.val, "fn", fn_str);
|
|
JSValue file_str = JS_NewString(ctx, frames[i].file ? frames[i].file : "<unknown>");
|
|
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 at crash ---\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 : "<anonymous>";
|
|
const char *file = code->filename_cstr ? code->filename_cstr : "<unknown>";
|
|
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);
|
|
}
|