Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
38e7932
test(rfl): consolidate non-duplicate coverage from spec migration
ser-vasilich Apr 27, 2026
153e9e9
fix(lang): raise arity on wrong-arg-count for UNARY/BINARY builtins
ser-vasilich Apr 27, 2026
072e040
fix(cmp): lexicographic ordering for SYM atoms (>, <, >=, <=)
ser-vasilich Apr 27, 2026
9ff6e80
fix(arith): neg preserves narrow-int type; pin all type-promotion rules
ser-vasilich Apr 27, 2026
ea281ef
fix(arith): abs preserves narrow-int type (was widening to i64)
ser-vasilich Apr 27, 2026
3df78bb
fix(eval): call_fn1 routes atomic UNARY builtins through atomic_map_u…
ser-vasilich Apr 27, 2026
ac0e7ca
docs(repl): pin ray_repl_run_file return-code contract
ser-vasilich Apr 27, 2026
ab2074c
fix(like): single iterative matcher, no catastrophic backtracking
ser-vasilich Apr 27, 2026
d20284c
fix(store): mkdir -p for set-splayed; tolerate missing root sym in ge…
ser-vasilich Apr 27, 2026
52cf5df
chore: ignore IDE state and gcov / lcov artifacts
ser-vasilich Apr 27, 2026
89f37a9
test: salvage radix-boundary + null sort coverage from scratch files
ser-vasilich Apr 27, 2026
6a7ffa4
test: targeted coverage for fold-right/scan-right/retract-fact/scan-e…
ser-vasilich Apr 27, 2026
6f32db0
test: per-type and list-form coverage for reverse/union/except/alter
ser-vasilich Apr 28, 2026
c4961bb
test(table): pivot avg/min/max + multi-key + f64 value; add union-all
ser-vasilich Apr 28, 2026
e368634
test(integration): DAG executor binary ops via select-with-derived-cols
ser-vasilich Apr 28, 2026
6e0da6f
test(integration): cross-type workout — single 200-row, 11-column fix…
ser-vasilich Apr 28, 2026
3872960
fix(agg): first/last preserve type for DATE/TIME/TIMESTAMP/BOOL/U8
ser-vasilich Apr 28, 2026
43df6d3
test(integration): groupby + per-key-type + diverse aggregators
ser-vasilich Apr 28, 2026
7c58f50
test(table): pivot multi-key, I64/DATE keys, missing-cell semantics
ser-vasilich Apr 28, 2026
f3fc330
test(datalog): recursive ancestor rule + multi-clause body forms
ser-vasilich Apr 28, 2026
82fae9d
review: fix three blockers + bonus tests / docs
ser-vasilich Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@ rf_test_*.csv

CLAUDE.md
docs/plans/

# IDE state
.idea/
.vscode/

# gcov / lcov artifacts
*.gcda
*.gcno
*.gcov
coverage*.info
coverage_html/
rayforce.cov
7 changes: 7 additions & 0 deletions src/app/repl.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ typedef struct ray_repl {
ray_repl_t* ray_repl_create(ray_poll_t* poll);
void ray_repl_destroy(ray_repl_t* repl);
void ray_repl_run(ray_repl_t* repl);

/* Run a Rayfall script file in batch (script) mode. Contract:
* - returns 0 on success
* - returns 1 on any eval error (script execution stops at first
* error; subsequent forms are not run)
* Distinct from ray_repl_run / stdin pipe which use REPL semantics
* (errors are printed but do not terminate the loop). */
int ray_repl_run_file(const char* path);

#endif /* RAY_IO_REPL_H */
16 changes: 8 additions & 8 deletions src/lang/eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ ray_t* call_fn1(ray_t* fn, ray_t* arg) {
if (fn_is_restricted(fn)) return ray_error("access", "restricted");
if (fn->type == RAY_UNARY) {
ray_unary_fn f = (ray_unary_fn)(uintptr_t)fn->i64;
if ((fn->attrs & RAY_FN_ATOMIC) && is_collection(arg))
return atomic_map_unary(f, arg);
return f(arg);
}
if (fn->type == RAY_LAMBDA) {
Expand Down Expand Up @@ -1661,18 +1663,16 @@ op_callf: {
switch (fn_obj->type) {
case RAY_UNARY:
if (fn_is_restricted(fn_obj)) { for (int32_t i = 0; i < n; i++) ray_release(fn_args[i]); result = ray_error("access", "restricted"); break; }
if (n < 1) { result = ray_error("arity", "expected 1 arg, got 0"); break; }
if (n != 1) { for (int32_t i = 0; i < n; i++) ray_release(fn_args[i]); result = ray_error("arity", "expected 1 arg, got %d", n); break; }
result = ((ray_unary_fn)(uintptr_t)fn_obj->i64)(fn_args[0]);
ray_release(fn_args[0]);
for (int32_t i = 1; i < n; i++) ray_release(fn_args[i]);
break;
case RAY_BINARY:
if (fn_is_restricted(fn_obj)) { for (int32_t i = 0; i < n; i++) ray_release(fn_args[i]); result = ray_error("access", "restricted"); break; }
if (n < 2) { for (int32_t i = 0; i < n; i++) ray_release(fn_args[i]); result = ray_error("arity", "expected 2 args, got %d", n); break; }
if (n != 2) { for (int32_t i = 0; i < n; i++) ray_release(fn_args[i]); result = ray_error("arity", "expected 2 args, got %d", n); break; }
result = ((ray_binary_fn)(uintptr_t)fn_obj->i64)(fn_args[0], fn_args[1]);
ray_release(fn_args[0]);
ray_release(fn_args[1]);
for (int32_t i = 2; i < n; i++) ray_release(fn_args[i]);
break;
case RAY_VARY:
if (fn_is_restricted(fn_obj)) { for (int32_t i = 0; i < n; i++) ray_release(fn_args[i]); result = ray_error("access", "restricted"); break; }
Expand Down Expand Up @@ -2021,8 +2021,8 @@ static void ray_register_builtins(void) {
register_binary_op("<=", RAY_FN_ATOMIC, ray_lte_fn, OP_LE);
register_binary_op("==", RAY_FN_ATOMIC, ray_eq_fn, OP_EQ);
register_binary_op("!=", RAY_FN_ATOMIC, ray_neq_fn, OP_NE);
register_binary_op("and", RAY_FN_NONE, ray_and_fn, OP_AND);
register_binary_op("or", RAY_FN_NONE, ray_or_fn, OP_OR);
register_vary("and", RAY_FN_NONE, ray_and_vary_fn);
register_vary("or", RAY_FN_NONE, ray_or_vary_fn);
register_unary_op("not", RAY_FN_NONE, ray_not_fn, OP_NOT);
register_unary_op("neg", RAY_FN_ATOMIC, ray_neg_fn, OP_NEG);
register_unary("round", RAY_FN_ATOMIC, ray_round_fn);
Expand Down Expand Up @@ -2392,7 +2392,7 @@ ray_t* ray_eval(ray_t* obj) {

switch (head->type) {
case RAY_UNARY: {
if (n < 2) { ray_release(head); ret = ray_error("domain", NULL); goto out; }
if (n != 2) { ray_release(head); ret = ray_error("arity", "expected 1 arg, got %d", (int)(n-1)); goto out; }
if (fn_is_restricted(head)) { ray_release(head); ret = ray_error("access", "restricted"); goto out; }
ray_unary_fn fn = (ray_unary_fn)(uintptr_t)head->i64;
uint8_t fn_attrs = head->attrs;
Expand All @@ -2412,7 +2412,7 @@ ray_t* ray_eval(ray_t* obj) {
ret = result; goto out;
}
case RAY_BINARY: {
if (n < 3) { ray_release(head); ret = ray_error("domain", NULL); goto out; }
if (n != 3) { ray_release(head); ret = ray_error("arity", "expected 2 args, got %d", (int)(n-1)); goto out; }
if (fn_is_restricted(head)) { ray_release(head); ret = ray_error("access", "restricted"); goto out; }
ray_binary_fn fn = (ray_binary_fn)(uintptr_t)head->i64;
uint8_t fn_attrs = head->attrs;
Expand Down
2 changes: 2 additions & 0 deletions src/lang/eval.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,8 @@ ray_t* ray_neq_fn(ray_t* a, ray_t* b);
/* Logic */
ray_t* ray_and_fn(ray_t* a, ray_t* b);
ray_t* ray_or_fn(ray_t* a, ray_t* b);
ray_t* ray_and_vary_fn(ray_t** args, int64_t n);
ray_t* ray_or_vary_fn(ray_t** args, int64_t n);
ray_t* ray_not_fn(ray_t* x);
ray_t* ray_neg_fn(ray_t* x);

Expand Down
17 changes: 12 additions & 5 deletions src/ops/agg.c
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,13 @@ ray_t* ray_first_fn(ray_t* x) {
}
if (ray_is_vec(x)) {
if (ray_len(x) == 0) return ray_typed_null(-x->type);
/* For SYM, GUID, STR and other non-numeric types, use collection_elem directly */
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
x->type == RAY_GUID || x->type == RAY_STR) {
/* For non-I64/F64 types route through collection_elem which
* preserves the element type. The DAG path widens to i64 for
* DATE/TIME/TIMESTAMP/BOOL/U8 — bypass it. */
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
x->type == RAY_GUID || x->type == RAY_STR || x->type == RAY_BOOL ||
x->type == RAY_U8 || x->type == RAY_DATE || x->type == RAY_TIME ||
x->type == RAY_TIMESTAMP) {
int alloc = 0;
return collection_elem(x, 0, &alloc);
}
Expand Down Expand Up @@ -275,8 +279,11 @@ ray_t* ray_last_fn(ray_t* x) {
}
if (ray_is_vec(x)) {
if (ray_len(x) == 0) return ray_typed_null(-x->type);
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
x->type == RAY_GUID || x->type == RAY_STR) {
/* See ray_first_fn for rationale on the type whitelist. */
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
x->type == RAY_GUID || x->type == RAY_STR || x->type == RAY_BOOL ||
x->type == RAY_U8 || x->type == RAY_DATE || x->type == RAY_TIME ||
x->type == RAY_TIMESTAMP) {
int alloc = 0;
return collection_elem(x, ray_len(x) - 1, &alloc);
}
Expand Down
19 changes: 14 additions & 5 deletions src/ops/arith.c
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,15 @@ ray_t* ray_mod_fn(ray_t* a, ray_t* b) {

ray_t* ray_neg_fn(ray_t* x) {
if (RAY_ATOM_IS_NULL(x)) { ray_retain(x); return x; }
if (x->type == -RAY_I64) return make_i64(-x->i64);
if (x->type == -RAY_F64) return make_f64(-x->f64);
/* Negate via unsigned to avoid signed-overflow UB on INT_MIN.
* Wraparound is defined for unsigned types; (T)(uT)(-(uT)x) yields
* the same wrapped value the corresponding two's-complement
* arithmetic would produce — so (neg INT_MIN) returns INT_MIN
* (overflow-wrap) consistently with binary `(- 0 INT_MIN)`. */
if (x->type == -RAY_I64) return make_i64((int64_t)(-(uint64_t)x->i64));
if (x->type == -RAY_I32) return make_i32((int32_t)(-(uint32_t)x->i32));
if (x->type == -RAY_I16) return make_i16((int16_t)(-(uint16_t)x->i16));
return ray_error("type", NULL);
}

Expand Down Expand Up @@ -359,13 +366,15 @@ ray_t* ray_ceil_fn(ray_t* x) {
return ray_error("type", NULL);
}

/* abs: absolute value, preserves type */
/* abs: absolute value, preserves type. Uses unsigned-wrap negation
* for the negative branch — same overflow-wrap semantics as `neg`,
* so (abs INT_MIN) returns INT_MIN rather than UB. */
ray_t* ray_abs_fn(ray_t* x) {
if (RAY_ATOM_IS_NULL(x)) { ray_retain(x); return x; }
if (x->type == -RAY_F64) return make_f64(fabs(x->f64));
if (x->type == -RAY_I64) return make_i64(x->i64 < 0 ? -x->i64 : x->i64);
if (x->type == -RAY_I32) return make_i64(x->i32 < 0 ? -(int64_t)x->i32 : x->i32);
if (x->type == -RAY_I16) return make_i64(x->i16 < 0 ? -(int64_t)x->i16 : x->i16);
if (x->type == -RAY_I64) return make_i64(x->i64 < 0 ? (int64_t)(-(uint64_t)x->i64) : x->i64);
if (x->type == -RAY_I32) return make_i32(x->i32 < 0 ? (int32_t)(-(uint32_t)x->i32) : x->i32);
if (x->type == -RAY_I16) return make_i16(x->i16 < 0 ? (int16_t)(-(uint16_t)x->i16) : x->i16);
return ray_error("type", NULL);
}

Expand Down
62 changes: 62 additions & 0 deletions src/ops/cmp.c
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,37 @@ int char_str_cmp(ray_t* a, ray_t* b, int *out) {
return 0;
}

/* Lexicographic compare of two SYM atoms. Fast path: equal interned
* ids ⇒ identical text ⇒ 0, no global-table lookup. Slow path: pull
* the backing STR via ray_sym_str and delegate to ray_str_cmp, which
* uses the 12-byte SSO inline path for short symbols.
*
* If a sym_str lookup fails (NULL — e.g. corrupted intern table or
* uninitialised state) we fall back to comparing the raw interned ids
* rather than declaring the unequal symbols equal. Stable, never
* silently collapses distinct symbols. */
int sym_atom_cmp(ray_t* a, ray_t* b) {
if (a->i64 == b->i64) return 0;
ray_t* sa = ray_sym_str(a->i64);
ray_t* sb = ray_sym_str(b->i64);
int r;
if (sa && sb) {
r = ray_str_cmp(sa, sb);
} else {
/* Fallback: order by interned id (stable, total). Same sign
* convention as memcmp: negative if a < b, positive if a > b. */
r = (a->i64 < b->i64) ? -1 : 1;
}
if (sa) ray_release(sa);
if (sb) ray_release(sb);
return r;
}

/* Comparison */
ray_t* ray_gt_fn(ray_t* a, ray_t* b) {
{ int c; if (char_str_cmp(a, b, &c) == 0) return make_bool(c > 0 ? 1 : 0); }
if (a->type == -RAY_SYM && b->type == -RAY_SYM)
return make_bool(sym_atom_cmp(a, b) > 0 ? 1 : 0);
if (a->type == -RAY_GUID && b->type == -RAY_GUID)
return make_bool(memcmp(ray_data(a->obj), ray_data(b->obj), 16) > 0 ? 1 : 0);
/* Temporal comparison (same or cross-temporal via nanosecond conversion) */
Expand All @@ -63,6 +91,8 @@ ray_t* ray_gt_fn(ray_t* a, ray_t* b) {

ray_t* ray_lt_fn(ray_t* a, ray_t* b) {
{ int c; if (char_str_cmp(a, b, &c) == 0) return make_bool(c < 0 ? 1 : 0); }
if (a->type == -RAY_SYM && b->type == -RAY_SYM)
return make_bool(sym_atom_cmp(a, b) < 0 ? 1 : 0);
if (a->type == -RAY_GUID && b->type == -RAY_GUID)
return make_bool(memcmp(ray_data(a->obj), ray_data(b->obj), 16) < 0 ? 1 : 0);
if (is_temporal(a) && is_temporal(b)) {
Expand All @@ -82,6 +112,8 @@ ray_t* ray_lt_fn(ray_t* a, ray_t* b) {

ray_t* ray_gte_fn(ray_t* a, ray_t* b) {
{ int c; if (char_str_cmp(a, b, &c) == 0) return make_bool(c >= 0 ? 1 : 0); }
if (a->type == -RAY_SYM && b->type == -RAY_SYM)
return make_bool(sym_atom_cmp(a, b) >= 0 ? 1 : 0);
if (a->type == -RAY_GUID && b->type == -RAY_GUID)
return make_bool(memcmp(ray_data(a->obj), ray_data(b->obj), 16) >= 0 ? 1 : 0);
if (is_temporal(a) && is_temporal(b)) {
Expand All @@ -102,6 +134,8 @@ ray_t* ray_gte_fn(ray_t* a, ray_t* b) {

ray_t* ray_lte_fn(ray_t* a, ray_t* b) {
{ int c; if (char_str_cmp(a, b, &c) == 0) return make_bool(c <= 0 ? 1 : 0); }
if (a->type == -RAY_SYM && b->type == -RAY_SYM)
return make_bool(sym_atom_cmp(a, b) <= 0 ? 1 : 0);
if (a->type == -RAY_GUID && b->type == -RAY_GUID)
return make_bool(memcmp(ray_data(a->obj), ray_data(b->obj), 16) <= 0 ? 1 : 0);
if (is_temporal(a) && is_temporal(b)) {
Expand Down Expand Up @@ -215,6 +249,34 @@ ray_t* ray_or_fn(ray_t* a, ray_t* b) {
return make_bool((is_truthy(a) || is_truthy(b)) ? 1 : 0);
}

/* Variadic left-fold over the binary kernels. (and a b c) folds as
* (and (and a b) c) — same shape Lisp/Clojure use. */
ray_t* ray_and_vary_fn(ray_t** args, int64_t n) {
if (n < 2) return ray_error("arity", "expected at least 2 args, got %lld", (long long)n);
ray_t* acc = ray_and_fn(args[0], args[1]);
if (!acc || RAY_IS_ERR(acc)) return acc;
for (int64_t i = 2; i < n; i++) {
ray_t* next = ray_and_fn(acc, args[i]);
ray_release(acc);
if (!next || RAY_IS_ERR(next)) return next;
acc = next;
}
return acc;
}

ray_t* ray_or_vary_fn(ray_t** args, int64_t n) {
if (n < 2) return ray_error("arity", "expected at least 2 args, got %lld", (long long)n);
ray_t* acc = ray_or_fn(args[0], args[1]);
if (!acc || RAY_IS_ERR(acc)) return acc;
for (int64_t i = 2; i < n; i++) {
ray_t* next = ray_or_fn(acc, args[i]);
ray_release(acc);
if (!next || RAY_IS_ERR(next)) return next;
acc = next;
}
return acc;
}

/* Unary */
ray_t* ray_not_fn(ray_t* x) {
/* Element-wise for bool vectors */
Expand Down
102 changes: 102 additions & 0 deletions src/ops/glob.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
* All rights reserved.
*/

/*
* Iterative glob matcher. Replaces three pre-existing implementations
* that diverged in syntax (eval used *,?,[abc]; DAG used SQL %,_) and
* one of which (strop.c::str_glob) blew up exponentially on patterns
* like "a*a*a*…a*b" against an a-only string. This single file is
* the only matcher; both call sites delegate here.
*/

#include "ops/glob.h"

/* Lowercase an ASCII byte; non-ASCII passes through unchanged. */
static inline char to_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}

/* Match a single character against a class `[ ... ]`. On entry *pi
* points at the byte after `[`. On return *pi points one past `]`.
* Recognises `[abc]`, `[a-z]`, leading `!` for negation, embedded
* `]` is allowed as the first char (after optional `!`). */
static bool match_class(const char* p, size_t pn, size_t* pi, char c, bool ci) {
size_t i = *pi;
bool neg = false;
if (i < pn && p[i] == '!') { neg = true; i++; }
bool matched = false;
bool first = true;
char ch = ci ? to_lower(c) : c;
while (i < pn && (first || p[i] != ']')) {
char lo = ci ? to_lower(p[i]) : p[i];
if (i + 2 < pn && p[i + 1] == '-' && p[i + 2] != ']') {
char hi = ci ? to_lower(p[i + 2]) : p[i + 2];
if (ch >= lo && ch <= hi) matched = true;
i += 3;
} else {
if (ch == lo) matched = true;
i++;
}
first = false;
}
if (i < pn && p[i] == ']') i++; /* consume closing bracket */
*pi = i;
return neg ? !matched : matched;
}

static bool glob_impl(const char* s, size_t sn,
const char* p, size_t pn, bool ci) {
size_t si = 0, pi = 0;
size_t star_pi = (size_t)-1, star_si = 0;

while (si < sn) {
if (pi < pn && p[pi] == '*') {
star_pi = pi++; /* remember star, skip it */
star_si = si;
} else if (pi < pn && p[pi] == '?') {
pi++;
si++;
} else if (pi < pn && p[pi] == '[') {
size_t cls_pi = pi + 1;
if (match_class(p, pn, &cls_pi, s[si], ci)) {
pi = cls_pi;
si++;
} else if (star_pi != (size_t)-1) {
pi = star_pi + 1;
si = ++star_si;
} else {
return false;
}
} else if (pi < pn) {
char a = ci ? to_lower(s[si]) : s[si];
char b = ci ? to_lower(p[pi]) : p[pi];
if (a == b) {
pi++;
si++;
} else if (star_pi != (size_t)-1) {
pi = star_pi + 1;
si = ++star_si;
} else {
return false;
}
} else if (star_pi != (size_t)-1) {
pi = star_pi + 1;
si = ++star_si;
} else {
return false;
}
}
/* Consumed all of input — pattern must be at end, modulo trailing stars. */
while (pi < pn && p[pi] == '*') pi++;
return pi == pn;
}

bool ray_glob_match(const char* s, size_t sn, const char* p, size_t pn) {
return glob_impl(s, sn, p, pn, false);
}

bool ray_glob_match_ci(const char* s, size_t sn, const char* p, size_t pn) {
return glob_impl(s, sn, p, pn, true);
}
Loading
Loading