Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit bb8884e

Browse files
authored
Merge pull request #8 from ser-vasilich/fixes/seven-bug-probes
fix(8 bugs) + test(coverage +1.3pp): probes, type-preservation, broad workout
2 parents e3d3297 + 82fae9d commit bb8884e

81 files changed

Lines changed: 3079 additions & 175 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,15 @@ rf_test_*.csv
1919

2020
CLAUDE.md
2121
docs/plans/
22+
23+
# IDE state
24+
.idea/
25+
.vscode/
26+
27+
# gcov / lcov artifacts
28+
*.gcda
29+
*.gcno
30+
*.gcov
31+
coverage*.info
32+
coverage_html/
33+
rayforce.cov

src/app/repl.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ typedef struct ray_repl {
4040
ray_repl_t* ray_repl_create(ray_poll_t* poll);
4141
void ray_repl_destroy(ray_repl_t* repl);
4242
void ray_repl_run(ray_repl_t* repl);
43+
44+
/* Run a Rayfall script file in batch (script) mode. Contract:
45+
* - returns 0 on success
46+
* - returns 1 on any eval error (script execution stops at first
47+
* error; subsequent forms are not run)
48+
* Distinct from ray_repl_run / stdin pipe which use REPL semantics
49+
* (errors are printed but do not terminate the loop). */
4350
int ray_repl_run_file(const char* path);
4451

4552
#endif /* RAY_IO_REPL_H */

src/lang/eval.c

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,8 @@ ray_t* call_fn1(ray_t* fn, ray_t* arg) {
740740
if (fn_is_restricted(fn)) return ray_error("access", "restricted");
741741
if (fn->type == RAY_UNARY) {
742742
ray_unary_fn f = (ray_unary_fn)(uintptr_t)fn->i64;
743+
if ((fn->attrs & RAY_FN_ATOMIC) && is_collection(arg))
744+
return atomic_map_unary(f, arg);
743745
return f(arg);
744746
}
745747
if (fn->type == RAY_LAMBDA) {
@@ -1661,18 +1663,16 @@ op_callf: {
16611663
switch (fn_obj->type) {
16621664
case RAY_UNARY:
16631665
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; }
1664-
if (n < 1) { result = ray_error("arity", "expected 1 arg, got 0"); break; }
1666+
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; }
16651667
result = ((ray_unary_fn)(uintptr_t)fn_obj->i64)(fn_args[0]);
16661668
ray_release(fn_args[0]);
1667-
for (int32_t i = 1; i < n; i++) ray_release(fn_args[i]);
16681669
break;
16691670
case RAY_BINARY:
16701671
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; }
1671-
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; }
1672+
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; }
16721673
result = ((ray_binary_fn)(uintptr_t)fn_obj->i64)(fn_args[0], fn_args[1]);
16731674
ray_release(fn_args[0]);
16741675
ray_release(fn_args[1]);
1675-
for (int32_t i = 2; i < n; i++) ray_release(fn_args[i]);
16761676
break;
16771677
case RAY_VARY:
16781678
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; }
@@ -2021,8 +2021,8 @@ static void ray_register_builtins(void) {
20212021
register_binary_op("<=", RAY_FN_ATOMIC, ray_lte_fn, OP_LE);
20222022
register_binary_op("==", RAY_FN_ATOMIC, ray_eq_fn, OP_EQ);
20232023
register_binary_op("!=", RAY_FN_ATOMIC, ray_neq_fn, OP_NE);
2024-
register_binary_op("and", RAY_FN_NONE, ray_and_fn, OP_AND);
2025-
register_binary_op("or", RAY_FN_NONE, ray_or_fn, OP_OR);
2024+
register_vary("and", RAY_FN_NONE, ray_and_vary_fn);
2025+
register_vary("or", RAY_FN_NONE, ray_or_vary_fn);
20262026
register_unary_op("not", RAY_FN_NONE, ray_not_fn, OP_NOT);
20272027
register_unary_op("neg", RAY_FN_ATOMIC, ray_neg_fn, OP_NEG);
20282028
register_unary("round", RAY_FN_ATOMIC, ray_round_fn);
@@ -2392,7 +2392,7 @@ ray_t* ray_eval(ray_t* obj) {
23922392

23932393
switch (head->type) {
23942394
case RAY_UNARY: {
2395-
if (n < 2) { ray_release(head); ret = ray_error("domain", NULL); goto out; }
2395+
if (n != 2) { ray_release(head); ret = ray_error("arity", "expected 1 arg, got %d", (int)(n-1)); goto out; }
23962396
if (fn_is_restricted(head)) { ray_release(head); ret = ray_error("access", "restricted"); goto out; }
23972397
ray_unary_fn fn = (ray_unary_fn)(uintptr_t)head->i64;
23982398
uint8_t fn_attrs = head->attrs;
@@ -2412,7 +2412,7 @@ ray_t* ray_eval(ray_t* obj) {
24122412
ret = result; goto out;
24132413
}
24142414
case RAY_BINARY: {
2415-
if (n < 3) { ray_release(head); ret = ray_error("domain", NULL); goto out; }
2415+
if (n != 3) { ray_release(head); ret = ray_error("arity", "expected 2 args, got %d", (int)(n-1)); goto out; }
24162416
if (fn_is_restricted(head)) { ray_release(head); ret = ray_error("access", "restricted"); goto out; }
24172417
ray_binary_fn fn = (ray_binary_fn)(uintptr_t)head->i64;
24182418
uint8_t fn_attrs = head->attrs;

src/lang/eval.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,8 @@ ray_t* ray_neq_fn(ray_t* a, ray_t* b);
201201
/* Logic */
202202
ray_t* ray_and_fn(ray_t* a, ray_t* b);
203203
ray_t* ray_or_fn(ray_t* a, ray_t* b);
204+
ray_t* ray_and_vary_fn(ray_t** args, int64_t n);
205+
ray_t* ray_or_vary_fn(ray_t** args, int64_t n);
204206
ray_t* ray_not_fn(ray_t* x);
205207
ray_t* ray_neg_fn(ray_t* x);
206208

src/ops/agg.c

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,13 @@ ray_t* ray_first_fn(ray_t* x) {
239239
}
240240
if (ray_is_vec(x)) {
241241
if (ray_len(x) == 0) return ray_typed_null(-x->type);
242-
/* For SYM, GUID, STR and other non-numeric types, use collection_elem directly */
243-
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
244-
x->type == RAY_GUID || x->type == RAY_STR) {
242+
/* For non-I64/F64 types route through collection_elem which
243+
* preserves the element type. The DAG path widens to i64 for
244+
* DATE/TIME/TIMESTAMP/BOOL/U8 — bypass it. */
245+
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
246+
x->type == RAY_GUID || x->type == RAY_STR || x->type == RAY_BOOL ||
247+
x->type == RAY_U8 || x->type == RAY_DATE || x->type == RAY_TIME ||
248+
x->type == RAY_TIMESTAMP) {
245249
int alloc = 0;
246250
return collection_elem(x, 0, &alloc);
247251
}
@@ -275,8 +279,11 @@ ray_t* ray_last_fn(ray_t* x) {
275279
}
276280
if (ray_is_vec(x)) {
277281
if (ray_len(x) == 0) return ray_typed_null(-x->type);
278-
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
279-
x->type == RAY_GUID || x->type == RAY_STR) {
282+
/* See ray_first_fn for rationale on the type whitelist. */
283+
if (x->type == RAY_SYM || x->type == RAY_I32 || x->type == RAY_I16 ||
284+
x->type == RAY_GUID || x->type == RAY_STR || x->type == RAY_BOOL ||
285+
x->type == RAY_U8 || x->type == RAY_DATE || x->type == RAY_TIME ||
286+
x->type == RAY_TIMESTAMP) {
280287
int alloc = 0;
281288
return collection_elem(x, ray_len(x) - 1, &alloc);
282289
}

src/ops/arith.c

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,15 @@ ray_t* ray_mod_fn(ray_t* a, ray_t* b) {
330330

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

@@ -359,13 +366,15 @@ ray_t* ray_ceil_fn(ray_t* x) {
359366
return ray_error("type", NULL);
360367
}
361368

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

src/ops/cmp.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,37 @@ int char_str_cmp(ray_t* a, ray_t* b, int *out) {
4040
return 0;
4141
}
4242

43+
/* Lexicographic compare of two SYM atoms. Fast path: equal interned
44+
* ids ⇒ identical text ⇒ 0, no global-table lookup. Slow path: pull
45+
* the backing STR via ray_sym_str and delegate to ray_str_cmp, which
46+
* uses the 12-byte SSO inline path for short symbols.
47+
*
48+
* If a sym_str lookup fails (NULL — e.g. corrupted intern table or
49+
* uninitialised state) we fall back to comparing the raw interned ids
50+
* rather than declaring the unequal symbols equal. Stable, never
51+
* silently collapses distinct symbols. */
52+
int sym_atom_cmp(ray_t* a, ray_t* b) {
53+
if (a->i64 == b->i64) return 0;
54+
ray_t* sa = ray_sym_str(a->i64);
55+
ray_t* sb = ray_sym_str(b->i64);
56+
int r;
57+
if (sa && sb) {
58+
r = ray_str_cmp(sa, sb);
59+
} else {
60+
/* Fallback: order by interned id (stable, total). Same sign
61+
* convention as memcmp: negative if a < b, positive if a > b. */
62+
r = (a->i64 < b->i64) ? -1 : 1;
63+
}
64+
if (sa) ray_release(sa);
65+
if (sb) ray_release(sb);
66+
return r;
67+
}
68+
4369
/* Comparison */
4470
ray_t* ray_gt_fn(ray_t* a, ray_t* b) {
4571
{ int c; if (char_str_cmp(a, b, &c) == 0) return make_bool(c > 0 ? 1 : 0); }
72+
if (a->type == -RAY_SYM && b->type == -RAY_SYM)
73+
return make_bool(sym_atom_cmp(a, b) > 0 ? 1 : 0);
4674
if (a->type == -RAY_GUID && b->type == -RAY_GUID)
4775
return make_bool(memcmp(ray_data(a->obj), ray_data(b->obj), 16) > 0 ? 1 : 0);
4876
/* Temporal comparison (same or cross-temporal via nanosecond conversion) */
@@ -63,6 +91,8 @@ ray_t* ray_gt_fn(ray_t* a, ray_t* b) {
6391

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

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

103135
ray_t* ray_lte_fn(ray_t* a, ray_t* b) {
104136
{ int c; if (char_str_cmp(a, b, &c) == 0) return make_bool(c <= 0 ? 1 : 0); }
137+
if (a->type == -RAY_SYM && b->type == -RAY_SYM)
138+
return make_bool(sym_atom_cmp(a, b) <= 0 ? 1 : 0);
105139
if (a->type == -RAY_GUID && b->type == -RAY_GUID)
106140
return make_bool(memcmp(ray_data(a->obj), ray_data(b->obj), 16) <= 0 ? 1 : 0);
107141
if (is_temporal(a) && is_temporal(b)) {
@@ -215,6 +249,34 @@ ray_t* ray_or_fn(ray_t* a, ray_t* b) {
215249
return make_bool((is_truthy(a) || is_truthy(b)) ? 1 : 0);
216250
}
217251

252+
/* Variadic left-fold over the binary kernels. (and a b c) folds as
253+
* (and (and a b) c) — same shape Lisp/Clojure use. */
254+
ray_t* ray_and_vary_fn(ray_t** args, int64_t n) {
255+
if (n < 2) return ray_error("arity", "expected at least 2 args, got %lld", (long long)n);
256+
ray_t* acc = ray_and_fn(args[0], args[1]);
257+
if (!acc || RAY_IS_ERR(acc)) return acc;
258+
for (int64_t i = 2; i < n; i++) {
259+
ray_t* next = ray_and_fn(acc, args[i]);
260+
ray_release(acc);
261+
if (!next || RAY_IS_ERR(next)) return next;
262+
acc = next;
263+
}
264+
return acc;
265+
}
266+
267+
ray_t* ray_or_vary_fn(ray_t** args, int64_t n) {
268+
if (n < 2) return ray_error("arity", "expected at least 2 args, got %lld", (long long)n);
269+
ray_t* acc = ray_or_fn(args[0], args[1]);
270+
if (!acc || RAY_IS_ERR(acc)) return acc;
271+
for (int64_t i = 2; i < n; i++) {
272+
ray_t* next = ray_or_fn(acc, args[i]);
273+
ray_release(acc);
274+
if (!next || RAY_IS_ERR(next)) return next;
275+
acc = next;
276+
}
277+
return acc;
278+
}
279+
218280
/* Unary */
219281
ray_t* ray_not_fn(ray_t* x) {
220282
/* Element-wise for bool vectors */

src/ops/glob.c

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
3+
* All rights reserved.
4+
*/
5+
6+
/*
7+
* Iterative glob matcher. Replaces three pre-existing implementations
8+
* that diverged in syntax (eval used *,?,[abc]; DAG used SQL %,_) and
9+
* one of which (strop.c::str_glob) blew up exponentially on patterns
10+
* like "a*a*a*…a*b" against an a-only string. This single file is
11+
* the only matcher; both call sites delegate here.
12+
*/
13+
14+
#include "ops/glob.h"
15+
16+
/* Lowercase an ASCII byte; non-ASCII passes through unchanged. */
17+
static inline char to_lower(char c) {
18+
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
19+
}
20+
21+
/* Match a single character against a class `[ ... ]`. On entry *pi
22+
* points at the byte after `[`. On return *pi points one past `]`.
23+
* Recognises `[abc]`, `[a-z]`, leading `!` for negation, embedded
24+
* `]` is allowed as the first char (after optional `!`). */
25+
static bool match_class(const char* p, size_t pn, size_t* pi, char c, bool ci) {
26+
size_t i = *pi;
27+
bool neg = false;
28+
if (i < pn && p[i] == '!') { neg = true; i++; }
29+
bool matched = false;
30+
bool first = true;
31+
char ch = ci ? to_lower(c) : c;
32+
while (i < pn && (first || p[i] != ']')) {
33+
char lo = ci ? to_lower(p[i]) : p[i];
34+
if (i + 2 < pn && p[i + 1] == '-' && p[i + 2] != ']') {
35+
char hi = ci ? to_lower(p[i + 2]) : p[i + 2];
36+
if (ch >= lo && ch <= hi) matched = true;
37+
i += 3;
38+
} else {
39+
if (ch == lo) matched = true;
40+
i++;
41+
}
42+
first = false;
43+
}
44+
if (i < pn && p[i] == ']') i++; /* consume closing bracket */
45+
*pi = i;
46+
return neg ? !matched : matched;
47+
}
48+
49+
static bool glob_impl(const char* s, size_t sn,
50+
const char* p, size_t pn, bool ci) {
51+
size_t si = 0, pi = 0;
52+
size_t star_pi = (size_t)-1, star_si = 0;
53+
54+
while (si < sn) {
55+
if (pi < pn && p[pi] == '*') {
56+
star_pi = pi++; /* remember star, skip it */
57+
star_si = si;
58+
} else if (pi < pn && p[pi] == '?') {
59+
pi++;
60+
si++;
61+
} else if (pi < pn && p[pi] == '[') {
62+
size_t cls_pi = pi + 1;
63+
if (match_class(p, pn, &cls_pi, s[si], ci)) {
64+
pi = cls_pi;
65+
si++;
66+
} else if (star_pi != (size_t)-1) {
67+
pi = star_pi + 1;
68+
si = ++star_si;
69+
} else {
70+
return false;
71+
}
72+
} else if (pi < pn) {
73+
char a = ci ? to_lower(s[si]) : s[si];
74+
char b = ci ? to_lower(p[pi]) : p[pi];
75+
if (a == b) {
76+
pi++;
77+
si++;
78+
} else if (star_pi != (size_t)-1) {
79+
pi = star_pi + 1;
80+
si = ++star_si;
81+
} else {
82+
return false;
83+
}
84+
} else if (star_pi != (size_t)-1) {
85+
pi = star_pi + 1;
86+
si = ++star_si;
87+
} else {
88+
return false;
89+
}
90+
}
91+
/* Consumed all of input — pattern must be at end, modulo trailing stars. */
92+
while (pi < pn && p[pi] == '*') pi++;
93+
return pi == pn;
94+
}
95+
96+
bool ray_glob_match(const char* s, size_t sn, const char* p, size_t pn) {
97+
return glob_impl(s, sn, p, pn, false);
98+
}
99+
100+
bool ray_glob_match_ci(const char* s, size_t sn, const char* p, size_t pn) {
101+
return glob_impl(s, sn, p, pn, true);
102+
}

0 commit comments

Comments
 (0)