diff --git a/include/rayforce.h b/include/rayforce.h index a126d0f..e7fa59d 100644 --- a/include/rayforce.h +++ b/include/rayforce.h @@ -246,6 +246,15 @@ bool ray_sym_ensure_cap(uint32_t needed); ray_err_t ray_sym_save(const char* path); ray_err_t ray_sym_load(const char* path); +/* ===== Environment API ===== + * + * Thread-safety: the environment is shared global state. Concurrent calls + * to ray_env_get() and ray_env_set() require external synchronization by + * the caller. */ + +ray_t* ray_env_get(int64_t sym_id); +ray_err_t ray_env_set(int64_t sym_id, ray_t* val); + /* ===== Table API ===== */ ray_t* ray_table_new(int64_t ncols); diff --git a/src/ops/datalog.c b/src/ops/datalog.c index 25bbdd1..e3a9713 100644 --- a/src/ops/datalog.c +++ b/src/ops/datalog.c @@ -38,6 +38,10 @@ void dl_program_free(dl_program_t* prog) { ray_release(prog->rels[i].table); if (prog->rels[i].prov_col && !RAY_IS_ERR(prog->rels[i].prov_col)) ray_release(prog->rels[i].prov_col); + if (prog->rels[i].prov_src_offsets && !RAY_IS_ERR(prog->rels[i].prov_src_offsets)) + ray_release(prog->rels[i].prov_src_offsets); + if (prog->rels[i].prov_src_data && !RAY_IS_ERR(prog->rels[i].prov_src_data)) + ray_release(prog->rels[i].prov_src_data); } ray_free(dl_prog_block(prog)); } @@ -1327,8 +1331,123 @@ static bool dl_row_in_table(ray_t* tbl, int64_t row, ray_t* ref) { return false; } +/* Build source provenance for one IDB relation in CSR format. + * + * For each derived row, extracts head variable bindings from the firing rule + * and scans each positive body atom's relation for rows consistent with those + * bindings. Results are stored as two parallel vectors on the relation: + * + * prov_src_offsets — I64[nrows+1]: offsets[i] = start index in prov_src_data + * for derived row i. offsets[nrows] = total entry count. + * prov_src_data — I64[total]: each entry = (rel_idx << 32) | row_idx, + * packed reference to the contributing source row. + * Row indices are truncated to 32 bits (max ~4 billion rows + * per relation). + * + * Body-only variables (not appearing in the head) are unconstrained during + * source lookup, so the entry set may be a superset of the true proof. */ +static void dl_build_source_prov(dl_program_t* prog, dl_rel_t* rel, + int64_t nrows, int64_t* pd) { + ray_t* off_vec = ray_vec_new(RAY_I64, nrows + 1); + if (!off_vec || RAY_IS_ERR(off_vec)) return; + off_vec->len = nrows + 1; + int64_t* off = (int64_t*)ray_data(off_vec); + + int64_t buf_cap = (nrows < 16) ? 64 : nrows * 4; + ray_t* buf_block = ray_alloc((size_t)buf_cap * sizeof(int64_t)); + if (!buf_block) { ray_release(off_vec); return; } + int64_t* buf = (int64_t*)ray_data(buf_block); + int64_t buf_len = 0; + + for (int64_t row = 0; row < nrows; row++) { + off[row] = buf_len; + if (pd[row] < 0) continue; + + dl_rule_t* rule = &prog->rules[pd[row]]; + + int64_t var_vals[DL_MAX_ARITY * DL_MAX_BODY]; + bool var_set [DL_MAX_ARITY * DL_MAX_BODY]; + memset(var_set, 0, sizeof(var_set)); + + /* Extract head variable bindings from this derived row */ + for (int h = 0; h < rule->head_arity; h++) { + int v = rule->head_vars[h]; + if (v == DL_CONST) continue; + ray_t* col = ray_table_get_col_idx(rel->table, h); + if (!col) continue; + var_vals[v] = ((int64_t*)ray_data(col))[row]; + var_set[v] = true; + } + + /* For each positive body atom, find matching source rows */ + for (int b = 0; b < rule->n_body; b++) { + dl_body_t* body = &rule->body[b]; + if (body->type != DL_POS) continue; + + int bri = dl_find_rel(prog, body->pred); + if (bri < 0) continue; + dl_rel_t* brel = &prog->rels[bri]; + int64_t bnrows = ray_table_nrows(brel->table); + + for (int64_t br = 0; br < bnrows; br++) { + bool match = true; + for (int c = 0; c < body->arity; c++) { + ray_t* bcol = ray_table_get_col_idx(brel->table, c); + if (!bcol) { match = false; break; } + int64_t cell = ((int64_t*)ray_data(bcol))[br]; + int v = body->vars[c]; + if (v == DL_CONST) { + if (cell != body->const_vals[c]) { match = false; break; } + } else if (var_set[v]) { + if (cell != var_vals[v]) { match = false; break; } + } + /* body-only variable: unconstrained, always matches */ + } + if (!match) continue; + + if (buf_len >= buf_cap) { + int64_t new_cap = buf_cap * 2; + ray_t* new_block = ray_alloc((size_t)new_cap * sizeof(int64_t)); + if (!new_block) goto oom; + memcpy(ray_data(new_block), buf, (size_t)buf_len * sizeof(int64_t)); + ray_free(buf_block); + buf_block = new_block; + buf = (int64_t*)ray_data(new_block); + buf_cap = new_cap; + } + buf[buf_len++] = ((int64_t)bri << 32) | (int64_t)(uint32_t)br; + } + } + } + + /* Success path: finalize CSR */ + off[nrows] = buf_len; + { + ray_t* data_vec = ray_vec_new(RAY_I64, buf_len > 0 ? buf_len : 1); + if (!data_vec || RAY_IS_ERR(data_vec)) goto oom; + data_vec->len = buf_len; + if (buf_len > 0) + memcpy(ray_data(data_vec), buf, (size_t)buf_len * sizeof(int64_t)); + ray_free(buf_block); + + if (rel->prov_src_offsets) ray_release(rel->prov_src_offsets); + if (rel->prov_src_data) ray_release(rel->prov_src_data); + rel->prov_src_offsets = off_vec; + rel->prov_src_data = data_vec; + return; + } + +oom: + /* Allocation failed — discard partial results, leave both fields NULL */ + ray_free(buf_block); + ray_release(off_vec); + if (rel->prov_src_offsets) { ray_release(rel->prov_src_offsets); rel->prov_src_offsets = NULL; } + if (rel->prov_src_data) { ray_release(rel->prov_src_data); rel->prov_src_data = NULL; } +} + /* Build provenance for all IDB relations. - * For each rule, compile with final tables and mark matching tuples. */ + * For each rule, compile with final tables and mark matching tuples. + * Then build deep source provenance (CSR offsets + packed source refs). */ static void dl_build_provenance(dl_program_t* prog) { for (int ri = 0; ri < prog->n_rels; ri++) { dl_rel_t* rel = &prog->rels[ri]; @@ -1376,6 +1495,8 @@ static void dl_build_provenance(dl_program_t* prog) { if (rel->prov_col) ray_release(rel->prov_col); rel->prov_col = prov; + + dl_build_source_prov(prog, rel, nrows, pd); } } @@ -1630,18 +1751,20 @@ ray_t* dl_get_provenance(dl_program_t* prog, const char* pred_name) { return prog->rels[idx].prov_col; } -/* Stub: deep provenance source offsets (not yet implemented in C engine). - * Returns NULL; Rust side treats this as "no deep provenance available". */ ray_t* dl_get_provenance_src_offsets(dl_program_t* prog, const char* pred_name) { - (void)prog; (void)pred_name; - return NULL; + if (!prog || !pred_name) return NULL; + if (!(prog->flags & DL_FLAG_PROVENANCE)) return NULL; + int idx = dl_find_rel(prog, pred_name); + if (idx < 0) return NULL; + return prog->rels[idx].prov_src_offsets; } -/* Stub: deep provenance source data (not yet implemented in C engine). - * Returns NULL; Rust side treats this as "no deep provenance available". */ ray_t* dl_get_provenance_src_data(dl_program_t* prog, const char* pred_name) { - (void)prog; (void)pred_name; - return NULL; + if (!prog || !pred_name) return NULL; + if (!(prog->flags & DL_FLAG_PROVENANCE)) return NULL; + int idx = dl_find_rel(prog, pred_name); + if (idx < 0) return NULL; + return prog->rels[idx].prov_src_data; } /* ── Builtins ── */ @@ -2311,22 +2434,16 @@ static ray_t* dl_parse_body_clause(dl_rule_t* rule, ray_t* clause, return ray_error("type", "rule/query: unrecognized body clause form"); } -/* (rule (head-name ?v1 ?v2 ...) clause1 clause2 ...) - * Special form: args are NOT evaluated. - * Parses the head and body into a dl_rule_t and stores it globally. */ -ray_t* ray_rule_fn(ray_t** args, int64_t n) { - if (n < 2) - return ray_error("arity", "rule expects at least a head and one body clause"); - - /* First arg: head -- must be a list (head-name ?v1 ?v2 ...) */ - ray_t* head = args[0]; +/* Parse head + body clauses into out (shared by rule and query inline rules). */ +static ray_t* dl_parse_rule_from_head_and_body(dl_rule_t* out, ray_t* head, + ray_t** body_args, int64_t n_body, + dl_var_map_t* vars) { if (!is_list(head) || ray_len(head) < 1) return ray_error("type", "rule: head must be (name ?var ...)"); ray_t** hd = (ray_t**)ray_data(head); int64_t hlen = ray_len(head); - /* Head name */ if (hd[0]->type != -RAY_SYM) return ray_error("type", "rule: head name must be a symbol"); @@ -2334,56 +2451,75 @@ ray_t* ray_rule_fn(ray_t** args, int64_t n) { if (!head_name_str) return ray_error("type", "rule: cannot resolve head name"); - /* _ is reserved as wildcard -- cannot be a rule predicate name */ if (ray_str_len(head_name_str) == 1 && ray_str_ptr(head_name_str)[0] == '_') return ray_error("domain", "rule: _ is reserved as wildcard"); - if (g_dl_n_rules >= DL_MAX_RULES) - return ray_error("domain", "rule: too many rules (max 128)"); - - /* Build variable map */ - dl_var_map_t vars; - memset(&vars, 0, sizeof(vars)); - int head_arity = (int)(hlen - 1); - dl_rule_t rule; - dl_rule_init(&rule, ray_str_ptr(head_name_str), head_arity); + dl_rule_init(out, ray_str_ptr(head_name_str), head_arity); - /* Head variables */ for (int i = 0; i < head_arity; i++) { ray_t* harg = hd[i + 1]; if (is_dl_var(harg)) { - int vi = dl_var_get_or_create(&vars, harg->i64); - dl_rule_head_var(&rule, i, vi); + int vi = dl_var_get_or_create(vars, harg->i64); + dl_rule_head_var(out, i, vi); } else if (harg->type == -RAY_I64) { - dl_rule_head_const(&rule, i, harg->i64); + dl_rule_head_const(out, i, harg->i64); } else if (harg->type == -RAY_SYM) { - dl_rule_head_const(&rule, i, harg->i64); + dl_rule_head_const(out, i, harg->i64); } else { return ray_error("type", "rule: head arguments must be ?variables or constants"); } } - /* Body clauses */ - for (int64_t i = 1; i < n; i++) { - ray_t* err = dl_parse_body_clause(&rule, args[i], &vars); + for (int64_t i = 0; i < n_body; i++) { + ray_t* err = dl_parse_body_clause(out, body_args[i], vars); if (err) return err; } - rule.n_vars = vars.n; + out->n_vars = vars->n; + return NULL; +} + +/* One inline rule: ((head-name ?a ...) body1 body2 ...) */ +static ray_t* dl_parse_inline_rule(dl_rule_t* out, ray_t* rule_list) { + if (!is_list(rule_list) || ray_len(rule_list) < 1) + return ray_error("type", "query: each (rules ...) entry must be a non-empty list"); + + ray_t** re = (ray_t**)ray_data(rule_list); + int64_t rlen = ray_len(rule_list); + dl_var_map_t vars; + memset(&vars, 0, sizeof(vars)); + return dl_parse_rule_from_head_and_body(out, re[0], &re[1], rlen - 1, &vars); +} + +/* (rule (head-name ?v1 ?v2 ...) clause1 clause2 ...) + * Special form: args are NOT evaluated. + * Parses the head and body into a dl_rule_t and stores it globally. */ +ray_t* ray_rule_fn(ray_t** args, int64_t n) { + if (n < 2) + return ray_error("arity", "rule expects at least a head and one body clause"); + + if (g_dl_n_rules >= DL_MAX_RULES) + return ray_error("domain", "rule: too many rules (max 128)"); + + dl_var_map_t vars; + memset(&vars, 0, sizeof(vars)); + dl_rule_t rule; + ray_t* perr = dl_parse_rule_from_head_and_body(&rule, args[0], &args[1], n - 1, &vars); + if (perr) return perr; - /* Store globally */ memcpy(&g_dl_rules[g_dl_n_rules++], &rule, sizeof(dl_rule_t)); return ray_bool(true); } -/* (query db (find ?a ?b ...) (where clause1 clause2 ...)) +/* (query db (find ?a ?b ...) (where clause1 clause2 ...) [(rules ...)]) + * Optional fourth arg (rules ...) supplies inline rules only (globals ignored). * Special form: db is evaluated, find/where are NOT evaluated. * Creates a temporary dl_program_t, registers the EAV table, - * copies global rules, builds a synthetic query rule, and evaluates. */ + * copies global rules (unless inline rules), builds a synthetic query rule, and evaluates. */ ray_t* ray_query_fn(ray_t** args, int64_t n) { - if (n < 3) - return ray_error("arity", "query expects: db (find ...) (where ...)"); + if (n < 3 || n > 4) + return ray_error("arity", "query expects: db (find ...) (where ...) [(rules ...)]"); /* Evaluate db (first arg) */ ray_t* db = ray_eval(args[0]); @@ -2441,6 +2577,27 @@ ray_t* ray_query_fn(ray_t** args, int64_t n) { return ray_error("type", "query: expected (where ...) as third argument"); } + /* Optional 4th arg must be (rules ...) — inline rules override globals */ + ray_t* rules_clause = NULL; + if (n == 4) { + ray_t* fourth = args[3]; + if (!is_list(fourth) || ray_len(fourth) < 1) { + ray_release(db); + return ray_error("type", "query: fourth argument must be (rules ...)"); + } + ray_t** re4 = (ray_t**)ray_data(fourth); + if (re4[0]->type != -RAY_SYM) { + ray_release(db); + return ray_error("type", "query: fourth argument must be (rules ...)"); + } + ray_t* rname = ray_sym_str(re4[0]->i64); + if (!rname || strcmp(ray_str_ptr(rname), "rules") != 0) { + ray_release(db); + return ray_error("type", "query: fourth argument must be (rules ...)"); + } + rules_clause = fourth; + } + /* Build variable map for the query */ dl_var_map_t vars; memset(&vars, 0, sizeof(vars)); @@ -2495,9 +2652,27 @@ ray_t* ray_query_fn(ray_t** args, int64_t n) { ray_release(eav_tbl); } - /* Copy all global rules into the program */ - for (int i = 0; i < g_dl_n_rules; i++) - dl_add_rule(prog, &g_dl_rules[i]); + if (rules_clause) { + ray_t** re = (ray_t**)ray_data(rules_clause); + int64_t rlen = ray_len(rules_clause); + for (int64_t i = 1; i < rlen; i++) { + dl_rule_t irule; + ray_t* rerr = dl_parse_inline_rule(&irule, re[i]); + if (rerr) { + dl_program_free(prog); + ray_release(db); + return rerr; + } + if (dl_add_rule(prog, &irule) < 0) { + dl_program_free(prog); + ray_release(db); + return ray_error("domain", "query: too many rules"); + } + } + } else { + for (int i = 0; i < g_dl_n_rules; i++) + dl_add_rule(prog, &g_dl_rules[i]); + } /* Add the synthetic query rule */ dl_add_rule(prog, &qrule); diff --git a/src/ops/datalog.h b/src/ops/datalog.h index 61a47d3..ae17238 100644 --- a/src/ops/datalog.h +++ b/src/ops/datalog.h @@ -115,6 +115,8 @@ typedef struct { bool is_idb; /* true = derived (intensional) */ int64_t col_names[DL_MAX_ARITY]; /* interned column name symbols */ ray_t* prov_col; /* provenance column (when DL_FLAG_PROVENANCE) */ + ray_t* prov_src_offsets; /* CSR offsets into prov_src_data, length nrows+1 */ + ray_t* prov_src_data; /* packed source refs: (rel_idx << 32) | row_idx */ } dl_rel_t; /* ===== Datalog program ===== */ @@ -162,10 +164,24 @@ ray_t* dl_query(dl_program_t* prog, const char* pred_name); * of rule indices, or NULL if provenance not enabled/available. */ ray_t* dl_get_provenance(dl_program_t* prog, const char* pred_name); -/* Stub: deep provenance source offsets (not yet implemented). Returns NULL. */ +/* Retrieve deep provenance source offsets for a derived relation. + * Returns an I64 vector of length nrows+1 in CSR format: offsets[i] is the + * start index in the source-data vector for derived row i. + * Only valid when DL_FLAG_PROVENANCE is set. Returns NULL if unavailable. */ ray_t* dl_get_provenance_src_offsets(dl_program_t* prog, const char* pred_name); -/* Stub: deep provenance source data (not yet implemented). Returns NULL. */ +/* Retrieve deep provenance source data for a derived relation. + * Returns a flat I64 vector of packed source references. Each entry encodes + * (relation_index << 32) | row_index, identifying which EDB or IDB relation + * and row contributed to deriving a given output tuple. Row indices are + * truncated to 32 bits (max ~4 billion rows per relation). + * + * For rules with body-only variables (variables appearing in body atoms but + * not in the head), source entries include all body rows consistent with + * head-visible bindings. Cross-body join constraints are not re-enforced + * during source lookup, so entries may be a superset of the true derivation. + * + * Only valid when DL_FLAG_PROVENANCE is set. Returns NULL if unavailable. */ ray_t* dl_get_provenance_src_data(dl_program_t* prog, const char* pred_name); /* ===== Rule builder helpers ===== */ diff --git a/test/test_datalog.c b/test/test_datalog.c new file mode 100644 index 0000000..90cdf2c --- /dev/null +++ b/test/test_datalog.c @@ -0,0 +1,162 @@ +/* + * test_datalog.c — Tests for the Datalog engine (src/ops/datalog.h) + * + * Covers: deep source provenance (CSR offsets + packed source refs). + */ +#include "munit.h" +#include +#include "mem/heap.h" +#include "ops/datalog.h" +#include + +static void* datalog_setup(const void* params, void* user_data) { + (void)params; (void)user_data; + ray_heap_init(); + (void)ray_sym_init(); + return NULL; +} + +static void datalog_teardown(void* fixture) { + (void)fixture; + ray_sym_destroy(); + ray_heap_destroy(); +} + +/* Verify that dl_get_provenance_src_offsets and dl_get_provenance_src_data + * are populated correctly for a simple one-rule derivation. + * + * Program: + * EDB: edge(1,2), edge(2,3), edge(3,4) + * Rule: path(X,Y) :- edge(X,Y) + * + * Expected after eval with DL_FLAG_PROVENANCE: + * path has 3 rows (one per edge row). + * prov_col[i] = 0 (rule index 0 fired for all rows) + * prov_src_offsets = [0, 1, 2, 3] (one source entry per derived row) + * prov_src_data[i] = (edge_rel_idx << 32) | i + */ +static MunitResult test_source_provenance(const void* params, void* fixture) { + (void)params; (void)fixture; + + int64_t src_vals[] = {1, 2, 3}; + int64_t dst_vals[] = {2, 3, 4}; + ray_t* src = ray_vec_from_raw(RAY_I64, src_vals, 3); + ray_t* dst = ray_vec_from_raw(RAY_I64, dst_vals, 3); + munit_assert_ptr_not_null(src); + munit_assert_ptr_not_null(dst); + + ray_t* edge = ray_table_new(2); + munit_assert_ptr_not_null(edge); + edge = ray_table_add_col(edge, ray_sym_intern("edge__c0", 8), src); + munit_assert_false(RAY_IS_ERR(edge)); + edge = ray_table_add_col(edge, ray_sym_intern("edge__c1", 8), dst); + munit_assert_false(RAY_IS_ERR(edge)); + + dl_program_t* prog = dl_program_new(); + munit_assert_ptr_not_null(prog); + prog->flags |= DL_FLAG_PROVENANCE; + + int edge_idx = dl_add_edb(prog, "edge", edge, 2); + munit_assert_int(edge_idx, ==, 0); + + /* path(X,Y) :- edge(X,Y) */ + dl_rule_t rule; + dl_rule_init(&rule, "path", 2); + dl_rule_head_var(&rule, 0, 0); + dl_rule_head_var(&rule, 1, 1); + int body = dl_rule_add_atom(&rule, "edge", 2); + munit_assert_int(body, ==, 0); + dl_body_set_var(&rule, body, 0, 0); + dl_body_set_var(&rule, body, 1, 1); + munit_assert_int(dl_add_rule(prog, &rule), ==, 0); + + munit_assert_int(dl_eval(prog), ==, 0); + + ray_t* out = dl_query(prog, "path"); + munit_assert_ptr_not_null(out); + munit_assert_int((int)ray_table_nrows(out), ==, 3); + + /* Rule-level provenance: all rows attributed to rule 0 */ + ray_t* prov = dl_get_provenance(prog, "path"); + munit_assert_ptr_not_null(prov); + munit_assert_int((int)ray_len(prov), ==, 3); + int64_t* pv = (int64_t*)ray_data(prov); + munit_assert_int((int)pv[0], ==, 0); + munit_assert_int((int)pv[1], ==, 0); + munit_assert_int((int)pv[2], ==, 0); + + /* Deep source provenance: CSR offsets and packed source refs */ + ray_t* offsets = dl_get_provenance_src_offsets(prog, "path"); + ray_t* data = dl_get_provenance_src_data(prog, "path"); + munit_assert_ptr_not_null(offsets); + munit_assert_ptr_not_null(data); + + /* offsets: length nrows+1 = 4; each derived row has exactly 1 source */ + munit_assert_int((int)ray_len(offsets), ==, 4); + munit_assert_int((int)ray_len(data), ==, 3); + + int64_t* off = (int64_t*)ray_data(offsets); + int64_t* src_data = (int64_t*)ray_data(data); + + munit_assert_int((int)off[0], ==, 0); + munit_assert_int((int)off[1], ==, 1); + munit_assert_int((int)off[2], ==, 2); + munit_assert_int((int)off[3], ==, 3); + + /* Each entry encodes (rel_idx << 32) | row_idx */ + for (int i = 0; i < 3; i++) { + int64_t expected = ((int64_t)edge_idx << 32) | (int64_t)i; + munit_assert(src_data[i] == expected); + } + + dl_program_free(prog); + ray_release(edge); + ray_release(src); + ray_release(dst); + return MUNIT_OK; +} + +/* Deep provenance is only populated when DL_FLAG_PROVENANCE is set. + * Without the flag both getters must return NULL. */ +static MunitResult test_source_prov_requires_flag(const void* params, void* fixture) { + (void)params; (void)fixture; + + int64_t vals[] = {1, 2}; + ray_t* v = ray_vec_from_raw(RAY_I64, vals, 2); + munit_assert_ptr_not_null(v); + + ray_t* tbl = ray_table_new(1); + tbl = ray_table_add_col(tbl, ray_sym_intern("p__c0", 5), v); + munit_assert_false(RAY_IS_ERR(tbl)); + + dl_program_t* prog = dl_program_new(); + munit_assert_ptr_not_null(prog); + /* DL_FLAG_PROVENANCE intentionally NOT set */ + + dl_add_edb(prog, "p", tbl, 1); + + dl_rule_t rule; + dl_rule_init(&rule, "q", 1); + dl_rule_head_var(&rule, 0, 0); + int body = dl_rule_add_atom(&rule, "p", 1); + dl_body_set_var(&rule, body, 0, 0); + dl_add_rule(prog, &rule); + + munit_assert_int(dl_eval(prog), ==, 0); + + munit_assert_null(dl_get_provenance_src_offsets(prog, "q")); + munit_assert_null(dl_get_provenance_src_data(prog, "q")); + + dl_program_free(prog); + ray_release(tbl); + ray_release(v); + return MUNIT_OK; +} + +static MunitTest datalog_tests[] = { + { "/source_provenance", test_source_provenance, datalog_setup, datalog_teardown, 0, NULL }, + { "/source_prov_requires_flag", test_source_prov_requires_flag, datalog_setup, datalog_teardown, 0, NULL }, + { NULL, NULL, NULL, NULL, 0, NULL }, +}; + +MunitSuite test_datalog_suite = { "/datalog", datalog_tests, NULL, 1, 0 }; diff --git a/test/test_lang.c b/test/test_lang.c index b495ea6..49dae1c 100644 --- a/test/test_lang.c +++ b/test/test_lang.c @@ -1703,6 +1703,55 @@ static MunitResult test_datalog_fixpoint(const void* params, void* fixture) { return MUNIT_OK; } +static MunitResult test_datalog_query_inline_rules(const void* params, void* fixture) { + (void)params; (void)fixture; + + ray_t* r_inline = ray_eval_str( + "(do" + " (set db (datoms))" + " (set db (assert-fact db 1 'edge 2))" + " (set db (assert-fact db 2 'edge 3))" + " (set db (assert-fact db 3 'edge 4))" + " (query db (find ?x ?y) (where (path ?x ?y))" + " (rules" + " ((path ?x ?y) (?x :edge ?y))" + " ((path ?x ?z) (?x :edge ?y) (path ?y ?z)))))" + ); + munit_assert(r_inline != NULL); + munit_assert(!RAY_IS_ERR(r_inline)); + munit_assert_int(r_inline->type, ==, RAY_TABLE); + munit_assert_int((int)ray_table_nrows(r_inline), ==, 6); + ray_release(r_inline); + + /* Global foo rule — inline rules omit it; foo yields no rows */ + ray_t* r_foo = ray_eval_str( + "(do" + " (set db (datoms))" + " (set db (assert-fact db 1 'edge 2))" + " (rule (foo ?x) (?x :edge 2))" + " (query db (find ?x) (where (foo ?x))" + " (rules ((path ?x ?y) (?x :edge ?y)))))" + ); + munit_assert(r_foo != NULL); + munit_assert(!RAY_IS_ERR(r_foo)); + munit_assert_int((int)ray_table_nrows(r_foo), ==, 0); + ray_release(r_foo); + + ray_t* r_global = ray_eval_str( + "(do" + " (set db (datoms))" + " (set db (assert-fact db 1 'edge 2))" + " (rule (foo ?x) (?x :edge 2))" + " (query db (find ?x) (where (foo ?x))))" + ); + munit_assert(r_global != NULL); + munit_assert(!RAY_IS_ERR(r_global)); + munit_assert_int((int)ray_table_nrows(r_global), ==, 1); + ray_release(r_global); + + return MUNIT_OK; +} + /* ═══════════════════════════════════════════════════════════════ * Ported rayforce lang tests (41 functions, ~3800 assertions) * ═══════════════════════════════════════════════════════════════ */ @@ -1851,6 +1900,7 @@ static MunitTest lang_tests[] = { { "/rf/safety", test_rf_safety, lang_setup, lang_teardown, 0, NULL }, { "/rf/read_csv", test_rf_read_csv, lang_setup, lang_teardown, 0, NULL }, { "/datalog/fixpoint", test_datalog_fixpoint, lang_setup, lang_teardown, 0, NULL }, + { "/datalog/query_inline_rules", test_datalog_query_inline_rules, lang_setup, lang_teardown, 0, NULL }, { NULL, NULL, NULL, NULL, 0, NULL }, }; diff --git a/test/test_main.c b/test/test_main.c index 4dd5ad3..d2bbdd2 100644 --- a/test/test_main.c +++ b/test/test_main.c @@ -64,6 +64,7 @@ extern MunitSuite test_audit_suite; extern MunitSuite test_arena_suite; extern MunitSuite test_lang_suite; extern MunitSuite test_format_suite; +extern MunitSuite test_datalog_suite; static MunitSuite child_suites[] = { /* { .prefix, .tests, .suites, .iterations, .options } */ @@ -97,6 +98,7 @@ static MunitSuite child_suites[] = { { "/arena", NULL, NULL, 0, 0 }, { "/lang", NULL, NULL, 0, 0 }, { "/format", NULL, NULL, 0, 0 }, + { "/datalog", NULL, NULL, 0, 0 }, { NULL, NULL, NULL, 0, 0 }, /* terminator */ }; @@ -140,6 +142,7 @@ int main(int argc, char* argv[]) { child_suites[27] = test_arena_suite; child_suites[28] = test_lang_suite; child_suites[29] = test_format_suite; + child_suites[30] = test_datalog_suite; return munit_suite_main(&root_suite, NULL, argc, argv); }