Conversation
|
PR Review posted via API |
|
Updated review -- see below for findings. |
PR Review -- CLI refactoring + engine auditThe PR itself (deleting build.rs, simplifying main.rs to a compile-only CLI) is clean. Findings below, numbered by severity. |
1. Fragile exit-code detection -- src/main.rs (introduced by this PR)The new main.rs detects parse errors with: This string must exactly match the prefix produced in src/compiler.rs:22 (format!(Parse error: {}, e)). If that wording changes, the exit-code contract silently breaks. Recommend a typed error enum (ParseError / CompileError) from compiler::compile instead of string matching. |
2. HIGH: Semicolon/comma precedence inverted in clause bodies -- parser.rs:124-147parse_goal_list (comma) wraps parse_goal_disjunction (semicolon). This inverts ISO precedences:
Consequence: p :- a, b ; c. is parsed as a, (b ; c) -- if a fails the whole clause fails and c is never tried. ISO requires (a, b) ; c -- c is tried when a,b fail together. Code in explicit parentheses like (a, b ; c) parses correctly via parse_paren_body. Only bare clause bodies are affected. No integration test exercises this pattern so the bug is silent. Fix: add an outer parse_body_disjunction layer handling semicolons, which calls parse_body_conjunction (current parse_goal_list logic) for each operand. |
3. O(n) cycle detection in apply_impl -- unify.rs:78seen.contains(id) does a linear scan over Vec. For a cyclic term (e.g. created by X = f(X) without occurs check) apply becomes O(d^2) in nesting depth. Replacing Vec with FnvHashSet makes each lookup O(1). |
4. Missing integer-division and rem arithmetic operators -- builtins.rs eval_arithISO 6.7.2 requires:
Any program using X is N // D or X is A rem B currently throws Unknown arithmetic operator. Note that / on two integers already truncates (ISO-correct), but // is the unambiguous ISO form and rem has genuinely different semantics from mod. |
5. atom_concat/3 throws runtime error in reverse mode -- solver.rs:421ISO allows atom_concat(X, Y, foo) with X or Y unbound, enumerating splits on backtracking. The current code returns SolveResult::Error when either first arg is not an atom, making atom_concat forward-mode only. Same pattern repeats in try_solve_once and try_solve_collecting. |
6. unify_with_occurs_check/2 implemented but never exposed -- unify.rs:163occurs_in is correct but marked allow(dead_code). ISO 8.2.2 requires unify_with_occurs_check/2 as a builtin. Wiring it into is_builtin and exec_builtin needs only a few lines. |
7. Minor: between/3 clones continuation on every iteration -- solver.rs:1438remaining.clone() is called inside the for-val loop. For large ranges with complex continuations this is O(N x goals) allocations. Not a correctness issue. Summary: 1-Medium src/main.rs fragile string match; 2-High parser.rs:124 semicolon/comma precedence inverted in clause bodies; 3-Low unify.rs:78 O(n^2) apply on cyclic terms; 4-Medium builtins.rs // and rem missing; 5-Medium solver.rs:421 atom_concat reverse mode; 6-Low unify.rs:163 unify_with_occurs_check unreachable; 7-Low solver.rs:1438 redundant clone. |
┌─────┬─────────────────────────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ # │ Issue │ Fix │
├─────┼─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 1 │ Fragile starts_with("Parse error") string │ Replaced with CompileError enum (Parse / Build variants) │
│ │ match │ │
├─────┼─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 2 │ Semicolon/comma precedence inverted in │ Added parse_goal_conjunction layer — semicolon is now outer (1100), comma inner (1000). a, b ; │
│ │ clause bodies │ c correctly parses as (a, b) ; c │
├─────┼─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 3 │ O(n) cycle detection in apply_impl │ Replaced Vec<VarId> with FnvHashSet<VarId> for O(1) lookups │
├─────┼─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 4 │ Missing // and rem operators │ Added IntDiv and Rem tokens, parser support, arith_int_div and arith_rem functions │
├─────┼─────────────────────────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ 6 │ unify_with_occurs_check/2 not wired up │ Added unify_with_occurs_check method + registered as builtin │
└─────┴─────────────────────────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────┘
Not fixed:
- Issue 5 (atom_concat/3 reverse mode) — feature addition, not a bug
- Issue 7 (between/3 clone per iteration) — minor perf, same as previous reviews
9 regression tests added. 290 total tests now.
|
PR 5 code review - three issues found |
|
PR 5 Review - Three issues found. BUG 1: occurs_in infinite loop on circular terms (unify.rs lines 207-222) occurs_in follows variable bindings via self.lookup but has no visited-set. Previously dead code with allow(dead_code), this PR makes it reachable via unify_with_occurs_check/2. If a variable is circularly bound via =/2, a subsequent unify_with_occurs_check call on a term containing that variable loops forever, bypassing the step limit entirely. Reproducer: X = f(X) followed by unify_with_occurs_check(Y, g(X)). After the first goal binds X circularly, occurs_in(Y_id, g(X)) follows X -> f(X) -> X -> ... indefinitely. Fix: add a FnvHashSet visited guard to occurs_in, analogous to the cycle guard already in apply_impl. BUG 2: rem reserved as keyword, unusable as atom or functor (tokenizer.rs line 515) rem is now always TokenKind::Rem at scan time. parse_primary has no arm for TokenKind::Rem, so rem in atom argument position or as a predicate name is a parse error. For example, foo(rem). and a rule with rem as the head functor both fail to parse. mod and is have the same pre-existing issue, but this PR adds a new restricted name. Fix: handle TokenKind::Rem (and Mod, Is) in parse_primary with atom fallback when not in infix operator position. MISSING TESTS: error paths for // and rem (tests/integration.rs) The tests cover normal and sign cases but leave these paths uncovered: X is 7 // 0, X is 7 rem 0 (both should produce runtime errors, not panics), and the overflow cases X is -9223372036854775808 // -1 and X is -9223372036854775808 rem -1. The implementations handle all four correctly via checked_div and checked_rem; the paths are simply unexercised. No other issues. The semicolon/comma precedence fix is correct (old parser produced wrong associativity). The right-assoc conjunction tree, checked_div/checked_rem overflow handling, unify_with_occurs_check core logic (occurs check on variable-binding step), and the apply_impl switch from Vec to FnvHashSet are all correct. |
No description provided.