prolog: compile clauses to SX closures (+17)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Has been cancelled
This commit is contained in:
157
lib/prolog/compiler.sx
Normal file
157
lib/prolog/compiler.sx
Normal file
@@ -0,0 +1,157 @@
|
||||
;; lib/prolog/compiler.sx — clause compiler: parse-AST clauses → SX closures
|
||||
;;
|
||||
;; Each compiled clause is a lambda (fn (goal trail db cut-box k) bool)
|
||||
;; that creates fresh vars, builds the instantiated head/body, and calls
|
||||
;; pl-unify! + pl-solve! directly — no AST walk at solve time.
|
||||
;;
|
||||
;; Usage:
|
||||
;; (pl-db-load! db (pl-parse src))
|
||||
;; (pl-compile-db! db)
|
||||
;; ; pl-solve-user! in runtime.sx automatically prefers compiled clauses
|
||||
;; (pl-solve-once! db goal trail)
|
||||
|
||||
;; Collect unique variable names from a parse-AST clause into a dict.
|
||||
(define
|
||||
pl-cmp-vars-into!
|
||||
(fn
|
||||
(ast seen)
|
||||
(cond
|
||||
((not (list? ast)) nil)
|
||||
((empty? ast) nil)
|
||||
((= (first ast) "var")
|
||||
(let
|
||||
((name (nth ast 1)))
|
||||
(when
|
||||
(and (not (= name "_")) (not (dict-has? seen name)))
|
||||
(dict-set! seen name true))))
|
||||
((= (first ast) "compound")
|
||||
(for-each (fn (a) (pl-cmp-vars-into! a seen)) (nth ast 2)))
|
||||
((= (first ast) "clause")
|
||||
(begin
|
||||
(pl-cmp-vars-into! (nth ast 1) seen)
|
||||
(pl-cmp-vars-into! (nth ast 2) seen))))))
|
||||
|
||||
;; Return list of unique var names in a clause (head + body, excluding _).
|
||||
(define
|
||||
pl-cmp-collect-vars
|
||||
(fn
|
||||
(clause)
|
||||
(let ((seen {})) (pl-cmp-vars-into! clause seen) (keys seen))))
|
||||
|
||||
;; Create a fresh runtime var for each name in the list; return name->var dict.
|
||||
(define
|
||||
pl-cmp-make-var-map
|
||||
(fn
|
||||
(var-names)
|
||||
(let
|
||||
((m {}))
|
||||
(for-each
|
||||
(fn (name) (dict-set! m name (pl-mk-rt-var name)))
|
||||
var-names)
|
||||
m)))
|
||||
|
||||
;; Instantiate a parse-AST term using a pre-built var-map.
|
||||
;; ("var" "_") always gets a fresh anonymous var.
|
||||
(define
|
||||
pl-cmp-build-term
|
||||
(fn
|
||||
(ast var-map)
|
||||
(cond
|
||||
((pl-var? ast) ast)
|
||||
((not (list? ast)) ast)
|
||||
((empty? ast) ast)
|
||||
((= (first ast) "var")
|
||||
(let
|
||||
((name (nth ast 1)))
|
||||
(if (= name "_") (pl-mk-rt-var "_") (dict-get var-map name))))
|
||||
((or (= (first ast) "atom") (= (first ast) "num") (= (first ast) "str"))
|
||||
ast)
|
||||
((= (first ast) "compound")
|
||||
(list
|
||||
"compound"
|
||||
(nth ast 1)
|
||||
(map (fn (a) (pl-cmp-build-term a var-map)) (nth ast 2))))
|
||||
((= (first ast) "clause")
|
||||
(list
|
||||
"clause"
|
||||
(pl-cmp-build-term (nth ast 1) var-map)
|
||||
(pl-cmp-build-term (nth ast 2) var-map)))
|
||||
(true ast))))
|
||||
|
||||
;; Compile one parse-AST clause to a lambda.
|
||||
;; Pre-computes var names at compile time; creates fresh vars per call.
|
||||
(define
|
||||
pl-compile-clause
|
||||
(fn
|
||||
(clause)
|
||||
(let
|
||||
((var-names (pl-cmp-collect-vars clause))
|
||||
(head-ast (nth clause 1))
|
||||
(body-ast (nth clause 2)))
|
||||
(fn
|
||||
(goal trail db cut-box k)
|
||||
(let
|
||||
((var-map (pl-cmp-make-var-map var-names)))
|
||||
(let
|
||||
((fresh-head (pl-cmp-build-term head-ast var-map))
|
||||
(fresh-body (pl-cmp-build-term body-ast var-map)))
|
||||
(let
|
||||
((mark (pl-trail-mark trail)))
|
||||
(if
|
||||
(pl-unify! goal fresh-head trail)
|
||||
(let
|
||||
((r (pl-solve! db fresh-body trail cut-box k)))
|
||||
(if r true (begin (pl-trail-undo-to! trail mark) false)))
|
||||
(begin (pl-trail-undo-to! trail mark) false)))))))))
|
||||
|
||||
;; Try a list of compiled clause lambdas — same cut semantics as pl-try-clauses!.
|
||||
(define
|
||||
pl-try-compiled-clauses!
|
||||
(fn
|
||||
(db
|
||||
goal
|
||||
trail
|
||||
compiled-clauses
|
||||
outer-cut-box
|
||||
outer-was-cut
|
||||
inner-cut-box
|
||||
k)
|
||||
(cond
|
||||
((empty? compiled-clauses) false)
|
||||
(true
|
||||
(let
|
||||
((r ((first compiled-clauses) goal trail db inner-cut-box k)))
|
||||
(cond
|
||||
(r true)
|
||||
((dict-get inner-cut-box :cut) false)
|
||||
((and (not outer-was-cut) (dict-get outer-cut-box :cut)) false)
|
||||
(true
|
||||
(pl-try-compiled-clauses!
|
||||
db
|
||||
goal
|
||||
trail
|
||||
(rest compiled-clauses)
|
||||
outer-cut-box
|
||||
outer-was-cut
|
||||
inner-cut-box
|
||||
k))))))))
|
||||
|
||||
;; Compile all clauses in DB and store in :compiled table.
|
||||
;; After this call, pl-solve-user! will dispatch via compiled lambdas.
|
||||
;; Note: clauses assert!-ed after this call are not compiled.
|
||||
(define
|
||||
pl-compile-db!
|
||||
(fn
|
||||
(db)
|
||||
(let
|
||||
((src-table (dict-get db :clauses)) (compiled-table {}))
|
||||
(for-each
|
||||
(fn
|
||||
(key)
|
||||
(dict-set!
|
||||
compiled-table
|
||||
key
|
||||
(map pl-compile-clause (dict-get src-table key))))
|
||||
(keys src-table))
|
||||
(dict-set! db :compiled compiled-table)
|
||||
db)))
|
||||
@@ -41,13 +41,15 @@ SUITES=(
|
||||
"assert_rules:lib/prolog/tests/assert_rules.sx:pl-assert-rules-tests-run!"
|
||||
"string_agg:lib/prolog/tests/string_agg.sx:pl-string-agg-tests-run!"
|
||||
"advanced:lib/prolog/tests/advanced.sx:pl-advanced-tests-run!"
|
||||
"compiler:lib/prolog/tests/compiler.sx:pl-compiler-tests-run!"
|
||||
)
|
||||
|
||||
SCRIPT='(epoch 1)
|
||||
(load "lib/prolog/tokenizer.sx")
|
||||
(load "lib/prolog/parser.sx")
|
||||
(load "lib/prolog/runtime.sx")
|
||||
(load "lib/prolog/query.sx")'
|
||||
(load "lib/prolog/query.sx")
|
||||
(load "lib/prolog/compiler.sx")'
|
||||
for entry in "${SUITES[@]}"; do
|
||||
IFS=: read -r _ file _ <<< "$entry"
|
||||
SCRIPT+=$'\n(load "'"$file"$'")'
|
||||
|
||||
@@ -2704,15 +2704,28 @@
|
||||
((inner-cut-box {:cut false}))
|
||||
(let
|
||||
((outer-was-cut (dict-get outer-cut-box :cut)))
|
||||
(pl-try-clauses!
|
||||
db
|
||||
goal
|
||||
trail
|
||||
(pl-db-lookup-goal db goal)
|
||||
outer-cut-box
|
||||
outer-was-cut
|
||||
inner-cut-box
|
||||
k)))))
|
||||
(let
|
||||
((compiled (when (dict-has? db :compiled) (dict-get db :compiled))))
|
||||
(if
|
||||
(and compiled (dict-has? compiled (pl-goal-key goal)))
|
||||
(pl-try-compiled-clauses!
|
||||
db
|
||||
goal
|
||||
trail
|
||||
(dict-get compiled (pl-goal-key goal))
|
||||
outer-cut-box
|
||||
outer-was-cut
|
||||
inner-cut-box
|
||||
k)
|
||||
(pl-try-clauses!
|
||||
db
|
||||
goal
|
||||
trail
|
||||
(pl-db-lookup-goal db goal)
|
||||
outer-cut-box
|
||||
outer-was-cut
|
||||
inner-cut-box
|
||||
k)))))))
|
||||
|
||||
(define
|
||||
pl-try-clauses!
|
||||
|
||||
185
lib/prolog/tests/compiler.sx
Normal file
185
lib/prolog/tests/compiler.sx
Normal file
@@ -0,0 +1,185 @@
|
||||
;; lib/prolog/tests/compiler.sx — compiled clause dispatch tests
|
||||
|
||||
(define pl-cmp-test-count 0)
|
||||
(define pl-cmp-test-pass 0)
|
||||
(define pl-cmp-test-fail 0)
|
||||
(define pl-cmp-test-failures (list))
|
||||
|
||||
(define
|
||||
pl-cmp-test!
|
||||
(fn
|
||||
(name got expected)
|
||||
(set! pl-cmp-test-count (+ pl-cmp-test-count 1))
|
||||
(if
|
||||
(= got expected)
|
||||
(set! pl-cmp-test-pass (+ pl-cmp-test-pass 1))
|
||||
(begin
|
||||
(set! pl-cmp-test-fail (+ pl-cmp-test-fail 1))
|
||||
(append! pl-cmp-test-failures name)))))
|
||||
|
||||
;; Load src, compile, return DB.
|
||||
(define
|
||||
pl-cmp-mk
|
||||
(fn
|
||||
(src)
|
||||
(let
|
||||
((db (pl-mk-db)))
|
||||
(pl-db-load! db (pl-parse src))
|
||||
(pl-compile-db! db)
|
||||
db)))
|
||||
|
||||
;; Run goal string against compiled DB; return bool (instantiates vars).
|
||||
(define
|
||||
pl-cmp-once
|
||||
(fn
|
||||
(db src)
|
||||
(pl-solve-once!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal src) {})
|
||||
(pl-mk-trail))))
|
||||
|
||||
;; Count solutions for goal string against compiled DB.
|
||||
(define
|
||||
pl-cmp-count
|
||||
(fn
|
||||
(db src)
|
||||
(pl-solve-count!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal src) {})
|
||||
(pl-mk-trail))))
|
||||
|
||||
;; ── 1. Simple facts ──────────────────────────────────────────────
|
||||
|
||||
(define pl-cmp-db1 (pl-cmp-mk "color(red). color(green). color(blue)."))
|
||||
|
||||
(pl-cmp-test! "compiled fact hit" (pl-cmp-once pl-cmp-db1 "color(red)") true)
|
||||
(pl-cmp-test!
|
||||
"compiled fact miss"
|
||||
(pl-cmp-once pl-cmp-db1 "color(yellow)")
|
||||
false)
|
||||
(pl-cmp-test! "compiled fact count" (pl-cmp-count pl-cmp-db1 "color(X)") 3)
|
||||
|
||||
;; ── 2. Recursive rule: append ────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db2
|
||||
(pl-cmp-mk "append([], L, L). append([H|T], L, [H|R]) :- append(T, L, R)."))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled append build"
|
||||
(pl-cmp-once pl-cmp-db2 "append([1,2],[3],[1,2,3])")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled append fail"
|
||||
(pl-cmp-once pl-cmp-db2 "append([1,2],[3],[1,2])")
|
||||
false)
|
||||
(pl-cmp-test!
|
||||
"compiled append split count"
|
||||
(pl-cmp-count pl-cmp-db2 "append(X, Y, [a,b])")
|
||||
3)
|
||||
|
||||
;; ── 3. Cut ───────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db3
|
||||
(pl-cmp-mk "first(X, [X|_]) :- !. first(X, [_|T]) :- first(X, T)."))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled cut: only one solution"
|
||||
(pl-cmp-count pl-cmp-db3 "first(X, [a,b,c])")
|
||||
1)
|
||||
|
||||
(let
|
||||
((db pl-cmp-db3) (trail (pl-mk-trail)) (env {}))
|
||||
(let
|
||||
((x (pl-mk-rt-var "X")))
|
||||
(dict-set! env "X" x)
|
||||
(pl-solve-once!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal "first(X, [a,b,c])") env)
|
||||
trail)
|
||||
(pl-cmp-test!
|
||||
"compiled cut: correct binding"
|
||||
(pl-atom-name (pl-walk x))
|
||||
"a")))
|
||||
|
||||
;; ── 4. member ────────────────────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db4
|
||||
(pl-cmp-mk "member(X, [X|_]). member(X, [_|T]) :- member(X, T)."))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled member hit"
|
||||
(pl-cmp-once pl-cmp-db4 "member(b, [a,b,c])")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled member miss"
|
||||
(pl-cmp-once pl-cmp-db4 "member(d, [a,b,c])")
|
||||
false)
|
||||
(pl-cmp-test!
|
||||
"compiled member count"
|
||||
(pl-cmp-count pl-cmp-db4 "member(X, [a,b,c])")
|
||||
3)
|
||||
|
||||
;; ── 5. Arithmetic in body ────────────────────────────────────────
|
||||
|
||||
(define pl-cmp-db5 (pl-cmp-mk "double(X, Y) :- Y is X * 2."))
|
||||
|
||||
(let
|
||||
((db pl-cmp-db5) (trail (pl-mk-trail)) (env {}))
|
||||
(let
|
||||
((y (pl-mk-rt-var "Y")))
|
||||
(dict-set! env "Y" y)
|
||||
(pl-solve-once!
|
||||
db
|
||||
(pl-instantiate (pl-parse-goal "double(5, Y)") env)
|
||||
trail)
|
||||
(pl-cmp-test! "compiled arithmetic in body" (pl-num-val (pl-walk y)) 10)))
|
||||
|
||||
;; ── 6. Transitive ancestor ───────────────────────────────────────
|
||||
|
||||
(define
|
||||
pl-cmp-db6
|
||||
(pl-cmp-mk
|
||||
(str
|
||||
"parent(a,b). parent(b,c). parent(c,d)."
|
||||
"ancestor(X,Y) :- parent(X,Y)."
|
||||
"ancestor(X,Y) :- parent(X,Z), ancestor(Z,Y).")))
|
||||
|
||||
(pl-cmp-test!
|
||||
"compiled ancestor direct"
|
||||
(pl-cmp-once pl-cmp-db6 "ancestor(a,b)")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled ancestor 3-step"
|
||||
(pl-cmp-once pl-cmp-db6 "ancestor(a,d)")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"compiled ancestor fail"
|
||||
(pl-cmp-once pl-cmp-db6 "ancestor(d,a)")
|
||||
false)
|
||||
|
||||
;; ── 7. Fallback: uncompiled predicate calls compiled sub-predicate
|
||||
|
||||
(define
|
||||
pl-cmp-db7
|
||||
(let
|
||||
((db (pl-mk-db)))
|
||||
(pl-db-load! db (pl-parse "q(1). q(2)."))
|
||||
(pl-compile-db! db)
|
||||
(pl-db-load! db (pl-parse "r(X) :- q(X)."))
|
||||
db))
|
||||
|
||||
(pl-cmp-test!
|
||||
"uncompiled predicate resolves"
|
||||
(pl-cmp-once pl-cmp-db7 "r(1)")
|
||||
true)
|
||||
(pl-cmp-test!
|
||||
"uncompiled calls compiled sub-pred count"
|
||||
(pl-cmp-count pl-cmp-db7 "r(X)")
|
||||
2)
|
||||
|
||||
;; ── Runner ───────────────────────────────────────────────────────
|
||||
|
||||
(define pl-compiler-tests-run! (fn () {:failed pl-cmp-test-fail :passed pl-cmp-test-pass :total pl-cmp-test-count :failures pl-cmp-test-failures}))
|
||||
Reference in New Issue
Block a user