diff --git a/src/ops/expr.c b/src/ops/expr.c index 08061f65..b4aa5cce 100644 --- a/src/ops/expr.c +++ b/src/ops/expr.c @@ -508,7 +508,7 @@ bool ray_expr_disable; /* test knob: force the fallback path */ static void expr_stats_dump(void) { static const char* names[EXPR_BAIL__N] = { "root-shape", "graph-size", "depth", "regs", "ins", - "mapcommon", "str", "nulls", "slice", "sym-domain", + "mapcommon", "str", "guid", "nulls", "slice", "sym-domain", "const", "null-shape", "other", }; fprintf(stderr, "expr_compile ok=%llu\n", @@ -646,6 +646,12 @@ bool expr_compile(ray_graph_t* g, ray_t* tbl, ray_op_t* root, ray_expr_t* out) { if (!col) EXPR_BAIL(EXPR_BAIL_OTHER); if (col->type == RAY_MAPCOMMON) EXPR_BAIL(EXPR_BAIL_MAPCOMMON); if (col->type == RAY_STR) EXPR_BAIL(EXPR_BAIL_STR); /* RAY_STR needs string comparison path */ + /* GUID cells are 16-byte blobs the fused program has no + * loads or compares for — without this bail the compiled + * expression read garbage and predicates silently + * matched nothing. The unfused executor compares GUIDs + * correctly. */ + if (col->type == RAY_GUID) EXPR_BAIL(EXPR_BAIL_GUID); if (col->attrs & RAY_ATTR_SLICE) EXPR_BAIL(EXPR_BAIL_SLICE); /* Length-1 columns used as scalar broadcasts are handled by the * fallback's exec_elementwise_binary (which has vec/scalar routing). @@ -3395,6 +3401,44 @@ ray_t* exec_elementwise_binary(ray_graph_t* g, ray_op_t* op, ray_t* lhs, ray_t* } } + /* GUID comparison: 16-byte cells, memcmp equality. Without this + * branch a guid predicate fell into the numeric loops, which read + * the cells at the wrong width and silently matched nothing. Only + * == and != are defined for guid operands here (cmp.c orders guid + * ATOMS, but a ranged guid predicate over a column has no use). */ + { + bool l_guid = (l_scalar ? (lhs->type == -RAY_GUID || lhs->type == RAY_GUID) + : lhs->type == RAY_GUID); + bool r_guid = (r_scalar ? (rhs->type == -RAY_GUID || rhs->type == RAY_GUID) + : rhs->type == RAY_GUID); + if (l_guid || r_guid) { + uint16_t opc = op->opcode; + if (!l_guid || !r_guid) { + ray_release(result); + return ray_error("type", "expr eval: cannot compare %s and %s", + ray_type_name(lhs->type), ray_type_name(rhs->type)); + } + if (opc != OP_EQ && opc != OP_NE) { + ray_release(result); + return ray_error("type", "expr eval: guid operands support only == and !="); + } + uint8_t* out = (uint8_t*)ray_data(result); + const uint8_t* lb = (lhs->type == -RAY_GUID) + ? (const uint8_t*)ray_data(lhs->obj) + : (const uint8_t*)ray_data(lhs); + const uint8_t* rb = (rhs->type == -RAY_GUID) + ? (const uint8_t*)ray_data(rhs->obj) + : (const uint8_t*)ray_data(rhs); + for (int64_t i = 0; i < len; i++) { + const uint8_t* lp = lb + (l_scalar ? 0 : i * 16); + const uint8_t* rp = rb + (r_scalar ? 0 : i * 16); + uint8_t eq = (memcmp(lp, rp, 16) == 0) ? 1 : 0; + out[i] = (opc == OP_EQ) ? eq : (uint8_t)!eq; + } + return result; + } + } + /* Reject string atom in arithmetic context (only comparisons are valid). */ { bool l_atom_str = (l_scalar && lhs->type == -RAY_STR); diff --git a/src/ops/internal.h b/src/ops/internal.h index 9699add5..6f7ccb66 100644 --- a/src/ops/internal.h +++ b/src/ops/internal.h @@ -771,7 +771,8 @@ typedef struct { typedef enum { EXPR_BAIL_ROOT = 0, EXPR_BAIL_SIZE, EXPR_BAIL_DEPTH, EXPR_BAIL_REGS, - EXPR_BAIL_INS, EXPR_BAIL_MAPCOMMON, EXPR_BAIL_STR, EXPR_BAIL_NULLS, + EXPR_BAIL_INS, EXPR_BAIL_MAPCOMMON, EXPR_BAIL_STR, EXPR_BAIL_GUID, + EXPR_BAIL_NULLS, EXPR_BAIL_SLICE, EXPR_BAIL_SYM_DOMAIN, EXPR_BAIL_CONST, EXPR_BAIL_NULL_SHAPE, /* nullable program hit a not-yet-null-capable instruction */ EXPR_BAIL_OTHER, EXPR_BAIL__N diff --git a/src/ops/string.c b/src/ops/string.c index f7c1e16c..e76ea8d3 100644 --- a/src/ops/string.c +++ b/src/ops/string.c @@ -27,6 +27,7 @@ #include "ops/rowsel.h" #include "core/pool.h" #include "lang/format.h" /* ray_type_name (error context) */ +#include "lang/internal.h" /* ray_like_fn (list-of-strings delegate) */ /* ============================================================================ * OP_LIKE: glob pattern matching on STR / SYM columns. See ops/glob.[ch]. @@ -738,8 +739,21 @@ ray_t* exec_like(ray_graph_t* g, ray_op_t* op) { : ray_glob_match(sp, sl, pat_str, pat_len)) ? 1 : 0; } } + } else if (in_type == RAY_LIST) { + /* List of string/symbol atoms — the shape splayed string columns + * load as (col_load_str_list). Delegate to the builtin, which + * owns the list branch. */ + ray_release(result); + ray_t* r = ray_like_fn(input, pat_v); + ray_release(input); ray_release(pat_v); + return r; } else { - memset(dst, 0, (size_t)len); + /* Previously memset-zero: a predicate over an unsupported column + * type silently matched nothing. Surface the type error the + * direct builtin raises instead. */ + ray_release(result); ray_release(input); ray_release(pat_v); + return ray_error("type", "like: expects a string or symbol column, got %s", + ray_type_name(in_type)); } ray_release(input); ray_release(pat_v); diff --git a/src/ops/strop.c b/src/ops/strop.c index 831286c2..eed3db13 100644 --- a/src/ops/strop.c +++ b/src/ops/strop.c @@ -1079,6 +1079,31 @@ ray_t* ray_like_fn(ray_t* x, ray_t* pattern) { return result; } + /* List of string/symbol atoms — the shape splayed string columns + * load as (col_load_str_list). Mirrors str-find's list branch. */ + if (x->type == RAY_LIST) { + int64_t n = x->len; + ray_t* result = ray_vec_new(RAY_BOOL, n); + if (RAY_IS_ERR(result)) return result; + result->len = n; + uint8_t* out = (uint8_t*)ray_data(result); + ray_t** items = (ray_t**)ray_data(x); + for (int64_t i = 0; i < n; i++) { + if ((i & (RAY_MORSEL_ELEMS - 1)) == 0 && pool_cancelled(NULL)) { + ray_release(result); + return ray_error("cancel", NULL); + } + const char* s; size_t sl; + if (!str_atom_bytes(items[i], &s, &sl, NULL)) { + ray_release(result); + return ray_error("type", "like: list items must be string or symbol atoms"); + } + out[i] = (use_simple ? ray_glob_match_compiled(&pc, s, sl) + : ray_glob_match(s, sl, pat, pat_len)) ? 1 : 0; + } + return result; + } + return ray_error("type", "like: expects string or symbol"); } diff --git a/test/rfl/query/guid_like_predicates.rfl b/test/rfl/query/guid_like_predicates.rfl new file mode 100644 index 00000000..3af51d01 --- /dev/null +++ b/test/rfl/query/guid_like_predicates.rfl @@ -0,0 +1,32 @@ +;; select-where predicates that used to fail silently. +;; +;; GUID: the fused expression program has no 16-byte loads, and the +;; elementwise fallback routed guid cells through the numeric loops — +;; both silently matched nothing. The compiler now bails GUID columns +;; (EXPR_BAIL_GUID) and the fallback has a memcmp branch, so a guid +;; equality predicate returns the matching rows. +;; +;; like: an unsupported input type in exec_like memset the result to +;; all-false. A list column (splayed string load shape) now delegates +;; to the builtin's list branch; a non-string column raises. + +(set gs (guid 3)) +(set t (table [g v] (list gs [10 20 30]))) +(set g1 (at gs 1)) + +;; guid equality predicate finds its row +(get (select {from: t where: (== g g1)}) 'v) -- [20] +(count (get (select {from: t where: (!= g g1)}) 'v)) -- 2 + +;; guid predicate composes with aggregation +(at (get (select {from: t where: (== g g1) s: (sum v)}) 's) 0) -- 20 + +;; ordering on guid operands is undefined — loud, not silent +(select {from: t where: (< g g1)}) !- type + +;; like over a list-of-strings column inside select-where +(set ts (table [s v] (list (list "keep me" "drop" "also keep") [1 2 3]))) +(get (select {from: ts where: (like s "*keep*")}) 'v) -- [1 3] + +;; like over a non-string column raises instead of matching nothing +(select {from: ts where: (like v "*1*")}) !- type diff --git a/test/rfl/strop/like.rfl b/test/rfl/strop/like.rfl index 6b57a98d..1232612f 100644 --- a/test/rfl/strop/like.rfl +++ b/test/rfl/strop/like.rfl @@ -180,3 +180,11 @@ ;; SHAPE_PREFIX with empty lit ("`*`" alone is SHAPE_ANY, not PREFIX, so use trailing only) ;; — covered by `(like "" "*")` above (SHAPE_ANY). + +;; ────────────── list of string/symbol atoms ────────────── +;; The shape splayed string columns load as (col_load_str_list) — like +;; used to reject it with a type error while str-find accepted it. +(like (list "abc" "has Pending mark" "xyz") "*Pending*") -- [false true false] +(like (list "alpha" "beta") "a*") -- [true false] +(like (list 'alpha 'beta) "a*") -- [true false] +(like (list "a" 1 "b") "a*") !- type