Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
66 changes: 66 additions & 0 deletions tests/test_wirelog_advanced.c
Original file line number Diff line number Diff line change
Expand Up @@ -1174,6 +1174,70 @@ test_negated_side_compound_body_pattern_rejected(void)
return rc;
}

/* Parity (#785): mirrors test_unsafe_negation_variable_rejected (issue #920).
* A variable bound only inside a negated atom is unsafe; IR lowering must
* reject it. */
static int
test_unsafe_negation_variable_rejected(void)
{
const char *src
= ".decl a(x: symbol)\n"
".decl b(x: symbol, y: symbol)\n"
".decl c(x: symbol)\n"
"c(X) :- a(X), !b(X, Y).\n";
wirelog_error_t parse_err = WIRELOG_OK;
wirelog_program_t *prog = wirelog_parse_string(src, &parse_err);
if (!prog)
return parse_err == WIRELOG_OK ? 1 : 0;

wirelog_session_t *s = NULL;
wirelog_error_t err
= wirelog_session_create(prog, WIRELOG_BACKEND_DEFAULT, 1, &s);
int rc = 0;
if (err != WIRELOG_ERR_INVALID_IR || s) {
fprintf(stderr,
"expected invalid IR for unsafe negation variable, got %d\n", err);
wirelog_session_destroy(s);
rc = 1;
}
wirelog_program_free(prog);
return rc;
}

/* Parity (#785): mirrors test_safe_negation_via_projection_accepted (#920).
* The projected-key form and wildcard columns are safe and must be accepted. */
static int
test_safe_negation_via_projection_accepted(void)
{
const char *src
= ".decl a(x: symbol)\n"
".decl b(x: symbol, y: symbol)\n"
".decl b_key(x: symbol)\n"
".decl c(x: symbol)\n"
".decl d(x: symbol)\n"
"b_key(X) :- b(X, Y).\n"
"c(X) :- a(X), !b_key(X).\n"
"d(X) :- a(X), !b(X, _).\n";
wirelog_error_t parse_err = WIRELOG_OK;
wirelog_program_t *prog = wirelog_parse_string(src, &parse_err);
if (!prog) {
fprintf(stderr, "safe negation: parse failed, got %d\n", parse_err);
return 1;
}

wirelog_session_t *s = NULL;
wirelog_error_t err
= wirelog_session_create(prog, WIRELOG_BACKEND_DEFAULT, 1, &s);
int rc = 0;
if (err != WIRELOG_OK || !s) {
fprintf(stderr, "safe negation: expected valid IR, got %d\n", err);
rc = 1;
}
wirelog_session_destroy(s);
wirelog_program_free(prog);
return rc;
}

/* Parity (#785): mirrors test_wirelog_easy.c::test_intern_returns_same_id.
* Interning the same string twice through wl_intern_put must return
* the same int64 id; the advanced API exposes the intern table via
Expand Down Expand Up @@ -1570,6 +1634,8 @@ main(void)
failures += test_side_compound_wrong_functor_handle_no_match();
failures += test_side_compound_nested_child_no_match();
failures += test_negated_side_compound_body_pattern_rejected();
failures += test_unsafe_negation_variable_rejected();
failures += test_safe_negation_via_projection_accepted();
failures += test_intern_returns_same_id();
failures += test_snapshot_filter();
failures += test_cleanup_order_no_use_after_free();
Expand Down
51 changes: 51 additions & 0 deletions tests/test_wirelog_easy.c
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,55 @@ test_negated_side_compound_body_pattern_rejected(void)
PASS();
}

/* Issue #920: a variable that appears only inside a negated body atom has an
* unbounded range and must be rejected (range-restriction / safety). */
static void
test_unsafe_negation_variable_rejected(void)
{
TEST("unsafe variable in negated atom is rejected");

const char *src
= ".decl a(x: symbol)\n"
".decl b(x: symbol, y: symbol)\n"
".decl c(x: symbol)\n"
"c(X) :- a(X), !b(X, Y).\n"; /* Y bound only under negation */

wirelog_easy_session_t *s = NULL;
if (wirelog_easy_open(src, &s) == WIRELOG_OK || s) {
wirelog_easy_close(s);
FAIL("unsafe negation rule should fail open");
return;
}
PASS();
}

/* Issue #920: the safe form, where the negated relation is projected to a key
* relation first so every negated variable is positively bound, is accepted.
* A wildcard under negation is likewise safe. */
static void
test_safe_negation_via_projection_accepted(void)
{
TEST("safe negation (projected key / wildcard) is accepted");

const char *src
= ".decl a(x: symbol)\n"
".decl b(x: symbol, y: symbol)\n"
".decl b_key(x: symbol)\n"
".decl c(x: symbol)\n"
".decl d(x: symbol)\n"
"b_key(X) :- b(X, Y).\n" /* project away Y */
"c(X) :- a(X), !b_key(X).\n" /* X positively bound */
"d(X) :- a(X), !b(X, _).\n"; /* wildcard column is safe */

wirelog_easy_session_t *s = NULL;
if (wirelog_easy_open(src, &s) != WIRELOG_OK || !s) {
FAIL("safe negation rule should open");
return;
}
wirelog_easy_close(s);
PASS();
}

static void
test_side_compound_public_allocation_saturates(void)
{
Expand Down Expand Up @@ -1790,6 +1839,8 @@ main(void)
test_side_compound_wrong_functor_handle_no_match();
test_side_compound_nested_child_no_match();
test_negated_side_compound_body_pattern_rejected();
test_unsafe_negation_variable_rejected();
test_safe_negation_via_projection_accepted();
test_side_compound_public_allocation_saturates();
test_insert_sym_variadic();
test_remove_sym();
Expand Down
40 changes: 40 additions & 0 deletions wirelog/ir/program.c
Original file line number Diff line number Diff line change
Expand Up @@ -1864,6 +1864,46 @@ convert_rule(const wl_parser_ast_node_t *rule_node,
wirelog_ir_node_t *neg_scan
= build_atom_scan(neg_atom, prog, &neg_vars, &neg_vcount);

/* Safety / range restriction (issue #920): every named variable in
* a negated atom must also be bound by a positive body atom. A
* variable that occurs only under negation has an unbounded range,
* so the rule is unsafe and is rejected. build_atom_scan normalizes
* wildcards and constants to NULL, so only non-NULL entries are
* named variables that require a positive binding. */
for (uint32_t j = 0; j < neg_vcount; j++) {
if (!neg_vars[j])
continue;
bool bound = false;
for (uint32_t c = 0; c < cur_vcount; c++) {
if (cur_vars[c]
&& strcmp(cur_vars[c], neg_vars[j]) == 0) {
bound = true;
break;
}
}
if (bound)
continue;

WL_LOG(WL_LOG_SEC_PARSER, WL_LOG_ERROR,
"unsafe variable '%s' appears only in negated atom '%s'; "
"bind it in a positive body atom (e.g. project the negated "
"relation first: key(...) :- %s(...); "
"head :- ..., !key(...))",
neg_vars[j], neg_atom->name ? neg_atom->name : "?",
neg_atom->name ? neg_atom->name : "rel");
free_var_names(neg_vars, neg_vcount);
wl_ir_node_free(neg_scan);
for (uint32_t s = 0; s < scan_count; s++)
free_var_names(scan_vars[s], scan_vcounts[s]);
if (cur_vars_is_merged)
free_var_names(cur_vars, cur_vcount);
wl_ir_node_free(current);
free(scans);
free(scan_vars);
free(scan_vcounts);
return NULL;
}

if (neg_scan) {
wirelog_ir_node_t *aj = wl_ir_node_create(WIRELOG_IR_ANTIJOIN);
if (aj) {
Expand Down
Loading