-
Notifications
You must be signed in to change notification settings - Fork 1
fix(sdk): correct i64 parameter legalization and dict key decoding #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||||||
| */ | ||||||||||||||||
|
|
||||||||||||||||
| #include <emscripten.h> | ||||||||||||||||
| #include <math.h> | ||||||||||||||||
| #include <stdio.h> | ||||||||||||||||
| #include <stdlib.h> | ||||||||||||||||
| #include <string.h> | ||||||||||||||||
|
|
@@ -368,6 +369,52 @@ EMSCRIPTEN_KEEPALIVE int64_t get_data_byte_size(ray_t* obj) { | |||||||||||||||
| * Atom constructors | ||||||||||||||||
| * ============================================================================ */ | ||||||||||||||||
|
|
||||||||||||||||
| /* Index, length and symbol-id parameters that cross the JS boundary are | ||||||||||||||||
| * declared `ray_jsidx_t` (double), never int64_t. | ||||||||||||||||
| * | ||||||||||||||||
| * The build links with `-s WASM_BIGINT=0`, which legalizes every i64 parameter | ||||||||||||||||
| * into two i32 words (lo, hi). The SDK's cwrap arg lists declare one 'number' | ||||||||||||||||
| * per C parameter, so an i64 parameter that is not last shifts every parameter | ||||||||||||||||
| * after it: `vec_set_idx(obj, idx, val)` received `val` as idx's high word and | ||||||||||||||||
| * 0 as `val`, which tripped the `if (!obj || !val)` guard and returned | ||||||||||||||||
| * RAY_NULL_OBJ. A trailing i64 appeared to work only because the missing high | ||||||||||||||||
| * word defaults to 0 -- it still truncated at 2^32. | ||||||||||||||||
| * | ||||||||||||||||
| * A double is passed as one f64, needs no legalization, matches the JS Number | ||||||||||||||||
| * domain exactly, and represents every integer up to 2^53 -- far beyond any | ||||||||||||||||
| * index reachable in a 4 GiB wasm32 heap. Call sites in JS stay unchanged. */ | ||||||||||||||||
| typedef double ray_jsidx_t; | ||||||||||||||||
|
|
||||||||||||||||
| /* Values arriving as f64 are untrusted: JS (or a raw `_fill_i32_vec(p, d, -1)` | ||||||||||||||||
| * probe) can hand us NaN, Infinity, a fraction or a negative. Casting those | ||||||||||||||||
| * straight to int64_t is undefined for NaN/Inf and, for a negative, yields a | ||||||||||||||||
| * negative count whose byte size wraps to nearly UINT32_MAX once memcpy's | ||||||||||||||||
| * 32-bit size_t truncates it on wasm32. Narrow only after proving the value | ||||||||||||||||
| * is finite, integral, non-negative and inside the exact-integer range of a | ||||||||||||||||
| * double. */ | ||||||||||||||||
| #define RAY_JSIDX_MAX 9007199254740991.0 /* 2^53 - 1 */ | ||||||||||||||||
|
|
||||||||||||||||
| static bool jsidx_to_i64(ray_jsidx_t v, int64_t* out) { | ||||||||||||||||
| if (!isfinite(v) || v < 0.0 || v > RAY_JSIDX_MAX || v != floor(v)) return false; | ||||||||||||||||
| *out = (int64_t)v; | ||||||||||||||||
| return true; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /* String-length arguments cross the boundary as f64 as well. Every JS call | ||||||||||||||||
| * site marshals its argument through cwrap's 'string', which hands us a | ||||||||||||||||
| * NUL-terminated copy, so we can do better than trusting the number: validate | ||||||||||||||||
| * it, then clamp to the buffer's actual extent. A bogus length then neither | ||||||||||||||||
| * wraps the size_t cast nor reads past the copy. */ | ||||||||||||||||
| static bool jsidx_to_strlen(const char* s, ray_jsidx_t len_f, size_t* out) { | ||||||||||||||||
| int64_t n; | ||||||||||||||||
| if (!s || !jsidx_to_i64(len_f, &n) || (uint64_t)n > (uint64_t)SIZE_MAX) return false; | ||||||||||||||||
| /* Open-coded strnlen: that is POSIX, and this TU builds with -std=c17. */ | ||||||||||||||||
| size_t max = (size_t)n, i = 0; | ||||||||||||||||
| while (i < max && s[i]) i++; | ||||||||||||||||
| *out = i; | ||||||||||||||||
| return true; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_b8(bool val) { return ray_bool(val); } | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_u8(uint8_t val) { return ray_u8(val); } | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_i16(int16_t val) { return ray_i16(val); } | ||||||||||||||||
|
|
@@ -379,12 +426,16 @@ EMSCRIPTEN_KEEPALIVE ray_t* init_date(int64_t days) { return ray_date(days | |||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_time(int64_t ms) { return ray_time(ms); } | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_timestamp(int64_t ns) { return ray_timestamp(ns); } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_symbol_str(const char* s, int64_t len) { | ||||||||||||||||
| return ray_sym(ray_sym_intern(s, (size_t)len)); | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_symbol_str(const char* s, ray_jsidx_t len) { | ||||||||||||||||
| size_t n; | ||||||||||||||||
| if (!jsidx_to_strlen(s, len, &n)) return ray_error("length", "init_symbol_str: invalid length"); | ||||||||||||||||
| return ray_sym(ray_sym_intern(s, n)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_string_str(const char* s, int64_t len) { | ||||||||||||||||
| return ray_str(s, (size_t)len); | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_string_str(const char* s, ray_jsidx_t len) { | ||||||||||||||||
| size_t n; | ||||||||||||||||
| if (!jsidx_to_strlen(s, len, &n)) return ray_error("length", "init_string_str: invalid length"); | ||||||||||||||||
| return ray_str(s, n); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /* ============================================================================ | ||||||||||||||||
|
|
@@ -419,7 +470,12 @@ EMSCRIPTEN_KEEPALIVE int64_t read_symbol_id(ray_t* obj) { | |||||||||||||||
| * thread-local scratch buffer so the returned pointer survives until the | ||||||||||||||||
| * next call (Emscripten's UTF8ToString already copies on the JS side | ||||||||||||||||
| * before the next call lands). */ | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE const char* symbol_to_str(int64_t id) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE const char* symbol_to_str(ray_jsidx_t id_f) { | ||||||||||||||||
| int64_t id; | ||||||||||||||||
| if (!jsidx_to_i64(id_f, &id)) { | ||||||||||||||||
| g_sym_to_str_buf[0] = '\0'; | ||||||||||||||||
| return g_sym_to_str_buf; | ||||||||||||||||
| } | ||||||||||||||||
| ray_t* s = ray_sym_str(id); | ||||||||||||||||
| if (!s) { | ||||||||||||||||
| g_sym_to_str_buf[0] = '\0'; | ||||||||||||||||
|
|
@@ -436,12 +492,13 @@ EMSCRIPTEN_KEEPALIVE const char* symbol_to_str(int64_t id) { | |||||||||||||||
| /* Resolve a symbol vector cell through the vector's own domain. CSV/splayed | ||||||||||||||||
| * columns may use a file-local dictionary whose positions are not runtime | ||||||||||||||||
| * symbol IDs. */ | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE const char* symbol_vec_get(ray_t* vec, int64_t idx) { | ||||||||||||||||
| if (!vec || ray_type(vec) != RAY_SYM || idx < 0 || idx >= ray_len(vec)) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE const char* symbol_vec_get(ray_t* vec, ray_jsidx_t idx) { | ||||||||||||||||
| int64_t i; | ||||||||||||||||
| if (!vec || ray_type(vec) != RAY_SYM || !jsidx_to_i64(idx, &i) || i >= ray_len(vec)) { | ||||||||||||||||
| g_sym_to_str_buf[0] = '\0'; | ||||||||||||||||
| return g_sym_to_str_buf; | ||||||||||||||||
| } | ||||||||||||||||
| ray_t* s = ray_sym_vec_cell(vec, idx); /* borrowed from the domain */ | ||||||||||||||||
| ray_t* s = ray_sym_vec_cell(vec, i); /* borrowed from the domain */ | ||||||||||||||||
| if (!s) { | ||||||||||||||||
| g_sym_to_str_buf[0] = '\0'; | ||||||||||||||||
| return g_sym_to_str_buf; | ||||||||||||||||
|
|
@@ -467,13 +524,14 @@ EMSCRIPTEN_KEEPALIVE int64_t str_atom_len(ray_t* s) { | |||||||||||||||
|
|
||||||||||||||||
| /* Per-cell read of a RAY_STR vector. Copies into a thread-local scratch | ||||||||||||||||
| * buffer; lifetime as for symbol_to_str. */ | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE const char* str_vec_get(ray_t* vec, int64_t idx) { | ||||||||||||||||
| if (!vec) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE const char* str_vec_get(ray_t* vec, ray_jsidx_t idx) { | ||||||||||||||||
| int64_t i; | ||||||||||||||||
| if (!vec || !jsidx_to_i64(idx, &i)) { | ||||||||||||||||
| g_str_vec_buf[0] = '\0'; | ||||||||||||||||
| return g_str_vec_buf; | ||||||||||||||||
| } | ||||||||||||||||
| size_t n = 0; | ||||||||||||||||
| const char* p = ray_str_vec_get(vec, idx, &n); | ||||||||||||||||
| const char* p = ray_str_vec_get(vec, i, &n); | ||||||||||||||||
| if (!p) { | ||||||||||||||||
| g_str_vec_buf[0] = '\0'; | ||||||||||||||||
| return g_str_vec_buf; | ||||||||||||||||
|
|
@@ -494,13 +552,17 @@ EMSCRIPTEN_KEEPALIVE const char* str_vec_get(ray_t* vec, int64_t idx) { | |||||||||||||||
| * typed-array view and have the engine see the elements. We bridge that | ||||||||||||||||
| * by setting v->len = capacity after allocation; data starts zero-filled | ||||||||||||||||
| * because mmap'd pages are zero-init. */ | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_vector(int8_t type, int64_t len) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_vector(int8_t type, ray_jsidx_t len_f) { | ||||||||||||||||
| int64_t len; | ||||||||||||||||
| if (!jsidx_to_i64(len_f, &len)) return ray_error("length", "init_vector: invalid length"); | ||||||||||||||||
| ray_t* v = (type == RAY_SYM) ? ray_sym_vec_new(RAY_SYM_W64, len) : ray_vec_new(type, len); | ||||||||||||||||
| if (v && !RAY_IS_ERR(v)) v->len = len; | ||||||||||||||||
| return v; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_list(int64_t len) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_list(ray_jsidx_t len_f) { | ||||||||||||||||
| int64_t len; | ||||||||||||||||
| if (!jsidx_to_i64(len_f, &len)) return ray_error("length", "init_list: invalid length"); | ||||||||||||||||
| ray_t* l = ray_list_new(len); | ||||||||||||||||
| if (l && !RAY_IS_ERR(l)) { | ||||||||||||||||
| /* Slots default to RAY_NULL_OBJ so iteration / drop is safe before | ||||||||||||||||
|
|
@@ -564,8 +626,10 @@ static ray_t* box_vec_element(int8_t vec_type, const void* p) { | |||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* vec_at_idx(ray_t* obj, int64_t idx) { | ||||||||||||||||
| if (!obj) return RAY_NULL_OBJ; | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* vec_at_idx(ray_t* obj, ray_jsidx_t idx_f) { | ||||||||||||||||
| int64_t idx; | ||||||||||||||||
| /* An unusable index reads like an out-of-range one: RAY_NULL_OBJ. */ | ||||||||||||||||
| if (!obj || !jsidx_to_i64(idx_f, &idx)) return RAY_NULL_OBJ; | ||||||||||||||||
| int8_t t = ray_type(obj); | ||||||||||||||||
| if (t == RAY_LIST) { | ||||||||||||||||
| ray_t* item = ray_list_get(obj, idx); | ||||||||||||||||
|
|
@@ -581,11 +645,14 @@ EMSCRIPTEN_KEEPALIVE ray_t* vec_at_idx(ray_t* obj, int64_t idx) { | |||||||||||||||
| return box_vec_element(t, p); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* vec_set_idx(ray_t* obj, int64_t idx, ray_t* val) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* vec_set_idx(ray_t* obj, ray_jsidx_t idx_f, ray_t* val) { | ||||||||||||||||
| int64_t idx; | ||||||||||||||||
| if (!obj || !val) return RAY_NULL_OBJ; | ||||||||||||||||
| if (!jsidx_to_i64(idx_f, &idx)) return ray_error("index", "vec_set_idx: invalid index"); | ||||||||||||||||
| int8_t t = ray_type(obj); | ||||||||||||||||
| if (t == RAY_LIST) { | ||||||||||||||||
| ray_retain(val); | ||||||||||||||||
| /* ray_list_set retains `val` itself — the caller's ref stays its own. | ||||||||||||||||
| * Retaining here too would strand one ref per element. */ | ||||||||||||||||
| return ray_list_set(obj, idx, val); | ||||||||||||||||
| } | ||||||||||||||||
| if (t == RAY_STR) { | ||||||||||||||||
|
|
@@ -601,7 +668,7 @@ EMSCRIPTEN_KEEPALIVE ray_t* vec_push(ray_t* obj, ray_t* val) { | |||||||||||||||
| if (!obj || !val) return RAY_NULL_OBJ; | ||||||||||||||||
| int8_t t = ray_type(obj); | ||||||||||||||||
| if (t == RAY_LIST) { | ||||||||||||||||
| ray_retain(val); | ||||||||||||||||
| /* ray_list_append retains `val` itself — see vec_set_idx. */ | ||||||||||||||||
| return ray_list_append(obj, val); | ||||||||||||||||
| } | ||||||||||||||||
| if (t == RAY_STR) { | ||||||||||||||||
|
|
@@ -613,11 +680,13 @@ EMSCRIPTEN_KEEPALIVE ray_t* vec_push(ray_t* obj, ray_t* val) { | |||||||||||||||
| return ray_vec_append(obj, sp); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* vec_insert(ray_t* obj, int64_t idx, ray_t* val) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* vec_insert(ray_t* obj, ray_jsidx_t idx_f, ray_t* val) { | ||||||||||||||||
| int64_t idx; | ||||||||||||||||
| if (!obj || !val) return RAY_NULL_OBJ; | ||||||||||||||||
| if (!jsidx_to_i64(idx_f, &idx)) return ray_error("index", "vec_insert: invalid index"); | ||||||||||||||||
| int8_t t = ray_type(obj); | ||||||||||||||||
| if (t == RAY_LIST) { | ||||||||||||||||
| ray_retain(val); | ||||||||||||||||
| /* ray_list_insert_at retains `val` itself — see vec_set_idx. */ | ||||||||||||||||
| return ray_list_insert_at(obj, idx, val); | ||||||||||||||||
| } | ||||||||||||||||
| if (t == RAY_STR) { | ||||||||||||||||
|
|
@@ -637,22 +706,28 @@ EMSCRIPTEN_KEEPALIVE ray_t* vec_insert(ray_t* obj, int64_t idx, ray_t* val) { | |||||||||||||||
| * the JS wrappers must build vectors fresh before filling. | ||||||||||||||||
| * ============================================================================ */ | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE void fill_i64_vec(ray_t* obj, int64_t* data, int64_t len) { | ||||||||||||||||
| if (!obj || !data || obj->type != RAY_I64) return; | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE void fill_i64_vec(ray_t* obj, int64_t* data, ray_jsidx_t len_f) { | ||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [P1] Please validate
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed and fixed. The narrowing
The fixTwo validators next to the static bool jsidx_to_i64(ray_jsidx_t v, int64_t* out) {
if (!isfinite(v) || v < 0.0 || v > RAY_JSIDX_MAX || v != floor(v)) return false;
*out = (int64_t)v;
return true;
}
Rest of the sweepThe other thirteen sites were the same latent bug. Each one now fails the way that function already fails for bad input, rather than introducing a new convention:
Three notes on the details: String lengths clamp to the buffer, not just the number. All four string sites are reached through cwrap's
Negative indices still work. The SDK normalizes and bounds-checks them in JS before calling in ( TestsNew Verified diagnostic the same way as the last round: reverting The two that pass pre-fix are marked Ran locally after a clean Separate defect this turned up — not fixed hereWhile auditing the parameters I checked the ones still typed
I left it alone deliberately, because the |
||||||||||||||||
| int64_t len; | ||||||||||||||||
| if (!obj || !data || obj->type != RAY_I64 || !jsidx_to_i64(len_f, &len)) return; | ||||||||||||||||
| int64_t copy_len = len < ray_len(obj) ? len : ray_len(obj); | ||||||||||||||||
| memcpy(ray_data(obj), data, copy_len * sizeof(int64_t)); | ||||||||||||||||
| if (copy_len <= 0) return; | ||||||||||||||||
| memcpy(ray_data(obj), data, (size_t)copy_len * sizeof(int64_t)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE void fill_i32_vec(ray_t* obj, int32_t* data, int64_t len) { | ||||||||||||||||
| if (!obj || !data || obj->type != RAY_I32) return; | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE void fill_i32_vec(ray_t* obj, int32_t* data, ray_jsidx_t len_f) { | ||||||||||||||||
| int64_t len; | ||||||||||||||||
| if (!obj || !data || obj->type != RAY_I32 || !jsidx_to_i64(len_f, &len)) return; | ||||||||||||||||
| int64_t copy_len = len < ray_len(obj) ? len : ray_len(obj); | ||||||||||||||||
| memcpy(ray_data(obj), data, copy_len * sizeof(int32_t)); | ||||||||||||||||
| if (copy_len <= 0) return; | ||||||||||||||||
| memcpy(ray_data(obj), data, (size_t)copy_len * sizeof(int32_t)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE void fill_f64_vec(ray_t* obj, double* data, int64_t len) { | ||||||||||||||||
| if (!obj || !data || obj->type != RAY_F64) return; | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE void fill_f64_vec(ray_t* obj, double* data, ray_jsidx_t len_f) { | ||||||||||||||||
| int64_t len; | ||||||||||||||||
| if (!obj || !data || obj->type != RAY_F64 || !jsidx_to_i64(len_f, &len)) return; | ||||||||||||||||
| int64_t copy_len = len < ray_len(obj) ? len : ray_len(obj); | ||||||||||||||||
| memcpy(ray_data(obj), data, copy_len * sizeof(double)); | ||||||||||||||||
| if (copy_len <= 0) return; | ||||||||||||||||
| memcpy(ray_data(obj), data, (size_t)copy_len * sizeof(double)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /* ============================================================================ | ||||||||||||||||
|
|
@@ -694,8 +769,8 @@ EMSCRIPTEN_KEEPALIVE ray_t* dict_get(ray_t* d, ray_t* key) { | |||||||||||||||
| * | ||||||||||||||||
| * v2 builds tables column-by-column; init_table replays the v1 contract by | ||||||||||||||||
| * iterating the JS-supplied (sym-vec, list-of-cols) and chaining add_col. | ||||||||||||||||
| * Each col is retained once because ray_table_add_col → ray_list_append | ||||||||||||||||
| * consumes the ref. | ||||||||||||||||
| * ray_table_add_col retains each col itself, so the caller's refs (held by | ||||||||||||||||
| * the JS wrappers) survive the call untouched. | ||||||||||||||||
| * ============================================================================ */ | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* init_table(ray_t* keys, ray_t* vals) { | ||||||||||||||||
|
|
@@ -711,9 +786,10 @@ EMSCRIPTEN_KEEPALIVE ray_t* init_table(ray_t* keys, ray_t* vals) { | |||||||||||||||
|
|
||||||||||||||||
| int64_t* key_ids = (int64_t*)ray_data(keys); | ||||||||||||||||
| for (int64_t i = 0; i < n; i++) { | ||||||||||||||||
| ray_t* col = ray_list_get(vals, i); | ||||||||||||||||
| ray_t* col = ray_list_get(vals, i); /* borrowed */ | ||||||||||||||||
| if (!col) continue; | ||||||||||||||||
| ray_retain(col); | ||||||||||||||||
| /* ray_table_add_col retains col_vec itself; an extra retain here would | ||||||||||||||||
| * strand one ref per column. */ | ||||||||||||||||
| tbl = ray_table_add_col(tbl, key_ids[i], col); | ||||||||||||||||
| if (RAY_IS_ERR(tbl)) return tbl; | ||||||||||||||||
| } | ||||||||||||||||
|
|
@@ -737,23 +813,30 @@ EMSCRIPTEN_KEEPALIVE ray_t* table_vals(ray_t* t) { | |||||||||||||||
| ray_t* lst = ray_list_new(n); | ||||||||||||||||
| for (int64_t i = 0; i < n; i++) { | ||||||||||||||||
| ray_t* col = ray_table_get_col_idx(t, i); /* borrowed */ | ||||||||||||||||
| if (col) ray_retain(col); | ||||||||||||||||
| /* ray_list_append takes the ref the returned list needs; retaining here | ||||||||||||||||
| * too would strand one per column on every .values() call. */ | ||||||||||||||||
| lst = ray_list_append(lst, col ? col : RAY_NULL_OBJ); | ||||||||||||||||
| } | ||||||||||||||||
| return lst; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* table_col(ray_t* t, const char* name, int64_t len) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* table_col(ray_t* t, const char* name, ray_jsidx_t len) { | ||||||||||||||||
| size_t n; | ||||||||||||||||
| if (!t || ray_type(t) != RAY_TABLE) return RAY_NULL_OBJ; | ||||||||||||||||
| int64_t id = ray_sym_intern(name, (size_t)len); | ||||||||||||||||
| if (!jsidx_to_strlen(name, len, &n)) return RAY_NULL_OBJ; | ||||||||||||||||
| int64_t id = ray_sym_intern(name, n); | ||||||||||||||||
| ray_t* col = ray_table_get_col(t, id); /* borrowed */ | ||||||||||||||||
| if (col) ray_retain(col); | ||||||||||||||||
| return col ? col : RAY_NULL_OBJ; | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| /* Build a {col_name: col[idx]} dict for one row. */ | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* table_row(ray_t* t, int64_t idx) { | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* table_row(ray_t* t, ray_jsidx_t idx) { | ||||||||||||||||
| int64_t row; | ||||||||||||||||
| if (!t || ray_type(t) != RAY_TABLE) return RAY_NULL_OBJ; | ||||||||||||||||
| /* Checked here rather than left to vec_at_idx, so a bad index fails as a | ||||||||||||||||
| * row instead of yielding a dict of nulls. */ | ||||||||||||||||
| if (!jsidx_to_i64(idx, &row)) return RAY_NULL_OBJ; | ||||||||||||||||
| int64_t n = ray_table_ncols(t); | ||||||||||||||||
| ray_t* keys = ray_sym_vec_new(RAY_SYM_W64, n); | ||||||||||||||||
| ray_t* vals = ray_list_new(n); | ||||||||||||||||
|
|
@@ -811,8 +894,11 @@ EMSCRIPTEN_KEEPALIVE ray_t* table_upsert(ray_t* t, ray_t* match_count, ray_t* da | |||||||||||||||
| * Symbol / global env / type name | ||||||||||||||||
| * ============================================================================ */ | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE int64_t intern_symbol(const char* s, int64_t len) { | ||||||||||||||||
| return ray_sym_intern(s, (size_t)len); | ||||||||||||||||
| /* Returns -1 for an invalid length; interned IDs are non-negative slots. */ | ||||||||||||||||
| EMSCRIPTEN_KEEPALIVE int64_t intern_symbol(const char* s, ray_jsidx_t len) { | ||||||||||||||||
| size_t n; | ||||||||||||||||
| if (!jsidx_to_strlen(s, len, &n)) return -1; | ||||||||||||||||
| return ray_sym_intern(s, n); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| EMSCRIPTEN_KEEPALIVE ray_t* global_set(ray_t* name, ray_t* val) { | ||||||||||||||||
|
|
||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[P1] This now makes the
rf.list()/rf.dict()set path reachable, but the list branch below still callsray_retain(val)beforeray_list_set, which retains the item itself. The same double retain exists invec_pushandvec_insert. I reproducedrc 1 -> 3; after dropping both the list and the caller's atom, one reference remained permanently. Please remove the redundant wrapper retains (and release SDK-created temporary wrappers after transferring ownership), otherwise normal list/dict construction leaks every element.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed all of them; details below.
The root cause
The engine's list API is borrow semantics —
ray_list_append/_set/_insert_atretain the item themselves (src/vec/list.c:107,147), leaving the caller's ref alone.main.cretained as well, so an element went1 → 3exactly as you measured: caller's ref, our retain, the list's retain. Dropping the list and the caller's handle returned two, stranding one permanently.It was dormant before this PR: the mangled i64 index made
valarrive as0, so the list branch was unreachable fromrf.list()/rf.dict(). Legalizing the parameter is what exposed it.Worth noting this isn't a blanket rule —
init_dictis correct to retain, becauseray_dict_newdocuments "consumes one ref each." The contract is per-function, so I audited everyray_retaininmain.cagainst its callee rather than removing them by pattern.C fixes (
src/main.c)The three you flagged:
vec_set_idx(:604),vec_push(:622),vec_insert(:638) — redundant retain removed from theRAY_LISTbranch.Two more with the identical bug, found by that audit:
init_table(:727) —ray_table_add_coldocuments "Retainscol_vecinternally so the caller keeps its own ref," so the retain stranded one ref per column on everyrf.table(). The header comment above it asserted the opposite of the real contract, which is likely how this got in; corrected.table_vals(:761) — retained a borrowed column and then passed it toray_list_append, which retains again: one stranded ref per column on every.values()call, including the onestoJS()/toRows()make internally.Checked and left alone as already correct:
vec_at_idx,dict_keys,dict_vals,dict_get(ray_dict_getis documented as returning an owned ref),table_col,init_dict.SDK fixes (
src/rayforce.sdk.js)Temporary wrappers, per your second point.
List.set/pushnow drop the wrapper when the SDK minted it for a raw JS value, and leave caller-suppliedRayObjects alone. Call sites that pre-converted (dict(),_arrayToVector's mixed path,Table.insert) now pass raw values soList.setowns the temporaries;dict(),table(), andTable.insertdrop the intermediates once ownership has transferred.rf.set()(:618) leaked both its symbol wrapper and, for non-RayObjectvalues, its value wrapper.ray_env_setretains into the binding (env.c:356,377), so both were ours to drop.Discarded owned handles. These readers each hand back an owned ref; the calling code read one field off it and dropped it on the floor:
Dict.get()(:1354)Dict.has()(:1372)ray_dict_getreturnsDict.toJS()(:1385) / iterator (:1414)keys+vals+ one per valueList.toJS()(:1221)Table.columnNames()(:1445)colsvectorTable.toJS()(:1535)vals+ one per columnTable.toRows()(:1560)toRows()was callingthis.col(name)inside the row loop; hoisting it out fixes the leak and stops re-resolving each name rows × columns times. Iterators drop their containers in afinally, so abandoning the loop early still releases. Yielded elements remain the consumer's to drop — existing contract, now documented rather than changed.Vector,StrVector, andRayStringare clean — they read through typed arrays or return strings without minting wrappers.One of these wasn't a leak — it was breaking queries
SelectQuery.execute()bound the table to a fresh__rfq_Nglobal per call and never unbound it. The old comment called this "leave the binding for the caller to clean up," but nothing ever did. The global env is a fixed 1024-slot table (env.c:90), so this pinned every queried table in memory and then failed outright:About 700 queries into a session and the builder stops working. The misleading error is the second half:
ray_env_setreturned OOM, butrf.set()discardedglobal_set's return — which on failure is an error block — so a failed bind surfaced later as a nonsense "name undefined" from whatever read it next.Fixed by adding
rf.unset()(global_setwith a NULL value hitsray_env_set's documented delete path) and unbinding in afinally;set()now throws on a failed bind instead of swallowing it.execute()'s body moved to_run()so thefinallywraps the whole thing.Two behavior changes worth your attention
__rfq_Nbindings no longer linger after a query. They were never documented, so nothing should depend on inspecting them — flagging it since it's observable.rf.set()now throws on a failed bind where it previously returned silently.rf.unset()is a new public method, so it also needed a declaration inrayforce.sdk.d.ts—test-contract.mjscaught that omission on its own.Tests
Added
bug3(double retain) andbug4(discarded handles) sections totest-bugs.mjs— 38/38 passing.I checked every new test is actually diagnostic by reverting the fix in the built output and re-running: all of them fail pre-fix and pass post-fix.
That verification mattered, because my first attempt at the
bug4tests passed against the unfixed SDK. A heap probe is the wrong instrument for those: the readers hand back refs to objects that already exist, so a leaked handle strands a refcount without allocating anything. Those tests now probe the refcount of the underlying object (take a handle, readrefCount, drop it), and heap-growth checks are kept only for paths that genuinely allocate (table_keys/table_valsbuild fresh objects). Thebug3heap tests needed similar calibration — 20k cycles showed nothing even while leaking, since the engine grows the heap in large steps; at 400k cycles the unfixed build goes 68 MB → 320 MB. That stronger version is what caught the residualinit_tableleak after the first round of fixes.Full run after a clean
make wasm:npm run test:bugs— 38/38npm test— smoke + contract passnpm run test:examples— 15 expression examples, 11 interactive demos, CDN example pass in Chrome