From 9e196613f69d577d33c705550f7b638a8369ea08 Mon Sep 17 00:00:00 2001 From: Morsel Date: Tue, 20 Oct 2026 22:23:34 +0000 Subject: [PATCH 1/2] fix(cast): reject container-valued STR cells Addresses #733. Keep flat element-wise casts and reject recursive vector results before string-atom access. --- docs/docs/guides/types.md | 5 +++++ src/ops/builtins.c | 8 ++++++++ test/rfl/type/as_nested_str.rfl | 16 ++++++++++++++++ test/rfl/type/as_nested_str_parted.rfl | 14 ++++++++++++++ 4 files changed, 43 insertions(+) create mode 100644 test/rfl/type/as_nested_str.rfl create mode 100644 test/rfl/type/as_nested_str_parted.rfl diff --git a/docs/docs/guides/types.md b/docs/docs/guides/types.md index dbda83f63..e5db5cc94 100644 --- a/docs/docs/guides/types.md +++ b/docs/docs/guides/types.md @@ -101,6 +101,11 @@ significant digits so casting the string back to F32 preserves the value. **Vector casts:** `as` maps element-wise over vectors. +Casting to `STR` produces a flat string vector from a vector or a LIST of +atoms. Each outer element must convert to one string atom. A nested vector or +LIST cell is rejected with a `type` error; this cast does not implicitly format +or flatten a whole inner collection into a string cell. + ```lisp ; Cast an integer vector to float (as 'f64 [1 2 3]) ; [1.0 2.0 3.0] diff --git a/src/ops/builtins.c b/src/ops/builtins.c index a2fe6201e..24f4f3139 100644 --- a/src/ops/builtins.c +++ b/src/ops/builtins.c @@ -1700,6 +1700,14 @@ ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) { ray_t* cast = ray_cast_fn(type_sym, elem); if (alloc) ray_release(elem); if (RAY_IS_ERR(cast)) { ray_release(vec); return cast; } + if (!cast) { ray_release(vec); return ray_error("oom", NULL); } + /* Each output cell needs a string atom. A nested input can + * recursively produce a STR vector, not an atom to inspect. */ + if (cast->type != -RAY_STR) { + ray_release(cast); + ray_release(vec); + return ray_error("type", "as: STR vector elements must cast to string atoms"); + } const char* sp = ray_str_ptr(cast); size_t slen = ray_str_len(cast); vec = ray_str_vec_append(vec, sp ? sp : "", sp ? slen : 0); diff --git a/test/rfl/type/as_nested_str.rfl b/test/rfl/type/as_nested_str.rfl new file mode 100644 index 000000000..7fef3d543 --- /dev/null +++ b/test/rfl/type/as_nested_str.rfl @@ -0,0 +1,16 @@ +;; A flat STR result needs one string atom per input cell, not a nested vector. +(set NestedStrSource (list [1 2] [3 4])) +(as 'STR NestedStrSource) !- type +NestedStrSource -- (list [1 2] [3 4]) +(as 'STR (list 7 [1 2])) !- type +(as 'STR (list "a long first string cell" [1 2])) !- type +(as 'STR (list (list 1 2))) !- type +(as 'STR (list [])) !- type + +(as 'STR 42) -- "42" +(as 'STR [1 2]) -- ["1" "2"] +(as 'STR (list 1 2)) -- ["1" "2"] +(as 'STR (list 1 true 'x)) -- ["1" "true" "x"] +(as 'STR (list "" "text")) -- ["" "text"] +(type (as 'STR [])) -- 'STR +(count (as 'STR [])) -- 0 diff --git a/test/rfl/type/as_nested_str_parted.rfl b/test/rfl/type/as_nested_str_parted.rfl new file mode 100644 index 000000000..6ed9b4bcb --- /dev/null +++ b/test/rfl/type/as_nested_str_parted.rfl @@ -0,0 +1,14 @@ +;; @requires: posix +;; Original #733 shape: grouped parted output holds I64 vectors in a LIST. +(.db.splayed.set "__cast733/2024.01.01/t/" (table [s v] (list ['b 'a 'c] [10 20 30]))) +(.db.splayed.set "__cast733/2024.01.02/t/" (table [s v] (list ['a 'b 'd] [40 50 60]))) +(set Cast733 (.db.parted.get "__cast733/" 't)) +(set Cast733Vals (at (select {a: v by: s from: Cast733}) 'a)) +(type Cast733Vals) -- 'LIST +(count Cast733Vals) -- 4 +(type (at Cast733Vals 0)) -- 'I64 +(as 'STR Cast733Vals) !- type +(count Cast733Vals) -- 4 +(set Cast733Vals 0) +(set Cast733 0) +(.sys.exec "rm -rf __cast733") From 82a9840e693b158a01691bf478cfb4db6e3cedf9 Mon Sep 17 00:00:00 2001 From: Morsel Date: Wed, 21 Oct 2026 17:08:50 +0000 Subject: [PATCH 2/2] perf(cast): resolve STR once for flat collections --- src/ops/builtins.c | 165 ++++++++++++++++++++++++++------------------- 1 file changed, 95 insertions(+), 70 deletions(-) diff --git a/src/ops/builtins.c b/src/ops/builtins.c index 24f4f3139..5fd1aee35 100644 --- a/src/ops/builtins.c +++ b/src/ops/builtins.c @@ -1379,6 +1379,98 @@ static ray_t* cast_vec_numeric(ray_t* type_sym, ray_t* val, int8_t out_type) { return vec; } +/* STR has already been resolved. Successful scalar formatting produces + * atoms by construction; the flat collection kernel needs no kind check. */ +static inline __attribute__((always_inline)) +ray_t* cast_str_atom(ray_t* val) { + if (val->type == -RAY_STR) { ray_retain(val); return val; } + if (val->type == -RAY_SYM) { + ray_t* sym_str = ray_sym_str(val->i64); + return sym_str ? sym_str : ray_str("", 0); + } + if (val->type == -RAY_I64) { + char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%lld", (long long)val->i64); + return ray_str(buf, (size_t)n2); + } + if (val->type == -RAY_I32) { + char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%d", (int)val->i32); + return ray_str(buf, (size_t)n2); + } + if (val->type == -RAY_I16) { + char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%d", (int)val->i16); + return ray_str(buf, (size_t)n2); + } + if (val->type == -RAY_F64) { + double fv = val->f64; + fv = clear_neg_zero(fv); + char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%g", fv); + return ray_str(buf, (size_t)n2); + } + if (val->type == -RAY_F32) { + /* Data conversion uses plain decimal text, independent of the + * literal printer and its configured display precision. */ + double fv = clear_neg_zero((double)(float)val->f64); + char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%.9g", fv); + return ray_str(buf, (size_t)n2); + } + if (val->type == -RAY_BOOL) { + return val->b8 ? ray_str("true", 4) : ray_str("false", 5); + } + /* Fallback: use ray_fmt for any other atom type */ + if (ray_is_atom(val)) { + ray_t* formatted = ray_fmt(val, 0); + if (formatted && !RAY_IS_ERR(formatted)) return formatted; + if (formatted) ray_release(formatted); + } + return ray_error("type", "as: cannot cast %s to str", ray_type_name(val->type)); +} + +/* Choose the collection contract once. A boxed flat-vector element can + * only produce a string atom on success; LIST children may recurse to vectors. + * Keep the collection loop outside scalar dispatch's register frame. */ +static inline __attribute__((always_inline)) +ray_t* cast_str_cells(ray_t* type_sym, ray_t* val, bool may_nest) { + int64_t n2 = val->len; + ray_t* vec = ray_vec_new(RAY_STR, n2); + if (!vec || RAY_IS_ERR(vec)) return vec ? vec : ray_error("oom", NULL); + for (int64_t i = 0; i < n2; i++) { + int alloc = 0; + ray_t* elem = collection_elem(val, i, &alloc); + if (RAY_IS_ERR(elem)) { ray_release(vec); return elem; } + ray_t* cast = may_nest ? ray_cast_fn(type_sym, elem) + : RAY_ATOM_IS_NULL(elem) ? ray_str("", 0) + : cast_str_atom(elem); + if (alloc) ray_release(elem); + if (RAY_IS_ERR(cast)) { ray_release(vec); return cast; } + if (!cast) { ray_release(vec); return ray_error("oom", NULL); } + /* Each output cell needs a string atom. A nested input can + * recursively produce a STR vector, not an atom to inspect. */ + if (may_nest && cast->type != -RAY_STR) { + ray_release(cast); + ray_release(vec); + return ray_error("type", "as: STR vector elements must cast to string atoms"); + } + const char* sp = ray_str_ptr(cast); + size_t slen = ray_str_len(cast); + vec = ray_str_vec_append(vec, sp ? sp : "", sp ? slen : 0); + ray_release(cast); + if (RAY_IS_ERR(vec)) return vec; + } + ray_t* result = cast_vec_copy_nulls(vec, val); + if (RAY_IS_ERR(result)) return result; + return vec; +} + +static __attribute__((noinline)) +ray_t* cast_str_flat(ray_t* type_sym, ray_t* val) { + return cast_str_cells(type_sym, val, false); +} + +static __attribute__((noinline)) +ray_t* cast_str_list(ray_t* type_sym, ray_t* val) { + return cast_str_cells(type_sym, val, true); +} + ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) { if (type_sym->type != -RAY_SYM) return ray_error("type", "as: target type must be sym, got %s", ray_type_name(type_sym->type)); /* Nullable targets preserve nulls; BOOL/U8 produce ordinary values. */ @@ -1649,76 +1741,9 @@ ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) { /* Cast to STR/str */ if (cast_match(tname, tlen, "STR") || cast_match(tname, tlen, "str")) { ray_release(s); - if (val->type == -RAY_STR) { ray_retain(val); return val; } - if (val->type == -RAY_SYM) { - ray_t* sym_str = ray_sym_str(val->i64); - return sym_str ? sym_str : ray_str("", 0); - } - if (val->type == -RAY_I64) { - char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%lld", (long long)val->i64); - return ray_str(buf, (size_t)n2); - } - if (val->type == -RAY_I32) { - char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%d", (int)val->i32); - return ray_str(buf, (size_t)n2); - } - if (val->type == -RAY_I16) { - char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%d", (int)val->i16); - return ray_str(buf, (size_t)n2); - } - if (val->type == -RAY_F64) { - double fv = val->f64; - fv = clear_neg_zero(fv); - char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%g", fv); - return ray_str(buf, (size_t)n2); - } - if (val->type == -RAY_F32) { - /* Data conversion uses plain decimal text, independent of the - * literal printer and its configured display precision. */ - double fv = clear_neg_zero((double)(float)val->f64); - char buf[32]; int n2 = snprintf(buf, sizeof(buf), "%.9g", fv); - return ray_str(buf, (size_t)n2); - } - if (val->type == -RAY_BOOL) { - return val->b8 ? ray_str("true", 4) : ray_str("false", 5); - } - /* Fallback: use ray_fmt for any other atom type */ - if (ray_is_atom(val)) { - ray_t* formatted = ray_fmt(val, 0); - if (formatted && !RAY_IS_ERR(formatted)) return formatted; - if (formatted) ray_release(formatted); - } - /* Vector/list -> STR vector: cast each element to string */ - if (ray_is_vec(val) || val->type == RAY_LIST) { - int64_t n2 = val->len; - ray_t* vec = ray_vec_new(RAY_STR, n2); - if (!vec || RAY_IS_ERR(vec)) return vec ? vec : ray_error("oom", NULL); - for (int64_t i = 0; i < n2; i++) { - int alloc = 0; - ray_t* elem = collection_elem(val, i, &alloc); - if (RAY_IS_ERR(elem)) { ray_release(vec); return elem; } - ray_t* cast = ray_cast_fn(type_sym, elem); - if (alloc) ray_release(elem); - if (RAY_IS_ERR(cast)) { ray_release(vec); return cast; } - if (!cast) { ray_release(vec); return ray_error("oom", NULL); } - /* Each output cell needs a string atom. A nested input can - * recursively produce a STR vector, not an atom to inspect. */ - if (cast->type != -RAY_STR) { - ray_release(cast); - ray_release(vec); - return ray_error("type", "as: STR vector elements must cast to string atoms"); - } - const char* sp = ray_str_ptr(cast); - size_t slen = ray_str_len(cast); - vec = ray_str_vec_append(vec, sp ? sp : "", sp ? slen : 0); - ray_release(cast); - if (RAY_IS_ERR(vec)) return vec; - } - ray_t* result = cast_vec_copy_nulls(vec, val); - if (RAY_IS_ERR(result)) return result; - return vec; - } - return ray_error("type", "as: cannot cast %s to str", ray_type_name(val->type)); + if (val->type == RAY_LIST) return cast_str_list(type_sym, val); + if (ray_is_vec(val)) return cast_str_flat(type_sym, val); + return cast_str_atom(val); } /* Cast to SYMBOL/sym */ if (cast_match(tname, tlen, "SYMBOL") || cast_match(tname, tlen, "sym") || cast_match(tname, tlen, "symbol")) {