Goal-Oriented Programming: Backward Chaining + GOAP + goal/pursue/activate
For new readers
Two related ideas share this page. "Backward chaining" is a way of answering questions like "is X true?" by working backwards from X through a set of if-then rules until it either finds a chain of facts that supports it or runs out of options — the same underlying idea Prolog is built on. "GOAP" (goal-oriented action planning) is a different but related idea from game AI: given a goal and a list of possible actions (each with a cost, and requirements for when it can be used), find the cheapest ordered sequence of actions that reaches the goal. Both are demonstrated below with real, runnable examples rather than just described. The Paradigms Guide covers the same ground with less code and more explanation, if that's a better starting point.
A worked example of PatLang's goal-oriented paradigm, run live below — see the Paradigms Guide for how rule_add/solve and action_add/plan fit together, the Grammar reference for the declarative rule ... :- ... syntax this demo could equally be written in, and the Standard Library reference for the full function list.
rule_add/solve do actual SLD-style backward-chaining resolution with backtracking (a ground fact is just a rule with an empty body — the standard Prolog trick), and action_add/plan is a real GOAP planner, doing uniform-cost forward search over actions with preconditions/effects/cost to find the cheapest ordered plan — not just picking the first path that works. A successful require/ensure check also asserts a contract_holds fact, so design-by-contract checks feed directly into either engine. This first demo derives whether a build target is buildable via a genuine multi-hop dependency chain, and separately produces a real 3-step build plan that correctly prefers a cheaper path over a pricier shortcut.
The second demo below shows the newer goal/pursue/activate surface syntax built directly on top of that same engine — no new planner logic, just a real DSL over it. goal NAME { dep1(args), ... } names a target state as a list of fact-terms; pursue NAME is an expression that plans against a declared goal; activate PLAN actually runs the plan, by calling each step's bound closure (registered with action_bind) in order. Each bound closure receives its step's bound argument values as a single List, since a plan step's real arity is only known once the planner has actually run — not splatted positionally. Critically, activate does not assume a found plan always succeeds: a dependency being satisfiable by a function doesn't guarantee that function actually succeeds when it runs, so the moment any bound closure returns false, activate stops immediately and reports failure rather than silently continuing or claiming success — the second run below deliberately demonstrates exactly that.
PatLang source
# A genuine goal-oriented build-system demo -- proving PatLang's
# backward-chaining resolver (A1), GOAP planner (A2), and design-by-contract
# both work AND compose, unlike the old fact/goal/query trio (still present,
# unchanged, for backward compatibility) which was flat and write-only.
#
# A1 answers "is target X buildable" by deriving it from dependency/changed
# facts through real multi-hop rule chaining and backtracking -- not a
# single-predicate lookup.
# A2 answers "what's the actual build plan" -- an ordered, cost-aware
# sequence of actions to reach a goal state, the natural shape of a real
# build's output.
# A DbC contract's successful check becomes a fact usable by either.
# ---- dependency graph: a -> b -> c (c is unchanged; a and b are not) ----
rule_add("dep", ["a", "b"], [])
rule_add("dep", ["b", "c"], [])
rule_add("unchanged", ["c"], [])
# buildable(X) :- unchanged(X).
# buildable(X) :- dep(X, Y), buildable(Y).
# A target is buildable if it's unchanged itself, or everything it
# transitively depends on is -- real multi-hop backward chaining, not a
# flat lookup: proving "a" buildable requires walking a -> b -> c.
rule_add("buildable", ["X"], [["unchanged", ["X"]]])
rule_add("buildable", ["X"], [["dep", ["X", "Y"]], ["buildable", ["Y"]]])
let sols_a = solve("buildable", ["a"])
print("buildable(a) via a->b->c, c unchanged: " + list_len(sols_a) + " solution(s)")
# A genuinely unbuildable case: "d" depends on "e", which is never declared
# unchanged anywhere -- the resolver must actually search and correctly fail,
# not just return the first thing that looks plausible.
rule_add("dep", ["d", "e"], [])
let sols_d = solve("buildable", ["d"])
print("buildable(d) via d->e, e never unchanged: " + list_len(sols_d) + " solution(s) (should be 0)")
# ---- design-by-contract: verify the resolver itself, contract becomes a fact ----
make a function called hop_count takes chain_len returns result
require chain_len >= 0
let result = chain_len
ensure result >= 0
return result
end
let hc = hop_count(2)
let contract_sols = solve("contract_holds", ["hop_count", "X"])
print("contract_holds facts recorded for hop_count: " + list_len(contract_sols))
# ---- A2: GOAP planner produces the actual ordered, cost-aware build plan ----
action_add("compile_b", [], [["compiled", ["b"]]], [], 5)
action_add("compile_a", [["compiled", ["b"]]], [["compiled", ["a"]]], [], 3)
action_add("link", [["compiled", ["a"]], ["compiled", ["b"]]], [["shipped", []]], [], 2)
# A pricier shortcut that skips straight to "shipped" without building
# anything -- the planner must find the CHEAPER real path (5+3+2=10) is
# actually the only valid one here since this shortcut needs "compiled b"
# too, proving cost-based search isn't just picking the first match.
action_add("direct_ship", [["compiled", ["b"]]], [["shipped", []]], [], 100)
let build_plan = plan([["shipped", []]])
print("build plan (should be the 3-step compile_b/compile_a/link path, not the pricier shortcut): " + list_len(build_plan) + " step(s)")
let mut i = 0
let n = to_num(list_len(build_plan))
while i < n do
print(" step " + (i + 1) + ": " + build_plan[i])
i = i + 1
end
print("DONE")
(not run yet)
Native run on the build machine:
buildable(a) via a->b->c, c unchanged: 1 solution(s) buildable(d) via d->e, e never unchanged: 0 solution(s) (should be 0) contract_holds facts recorded for hop_count: 2 build plan (should be the 3-step compile_b/compile_a/link path, not the pricier shortcut): 3 step(s) step 1: compile_b step 2: compile_a step 3: link DONE
goal / pursue / activate: the surface syntax over the same planner
PatLang source
# The `goal`/`pursue`/`activate` surface syntax over the existing GOAP
# planner (see the first demo above for the underlying
# rule_add/solve/action_add/plan engine this sits on top of, unchanged).
#
# `goal NAME { dep1(args), ... }` names a target state as a list of
# fact-terms. `pursue NAME` plans a path to it (the same A2 uniform-cost
# search `plan(...)` already does). `activate PLAN` actually RUNS each
# planned step's bound closure (action_bind), in order, stopping the
# moment one fails -- a dependency being met by a function doesn't
# guarantee that function succeeds just because the planner found a path
# through it.
# ---- dependency graph + a two-shaped build action ----
rule_add("dep", ["house", "walls"], [])
rule_add("dep", ["walls", "foundation"], [])
rule_add("built", ["foundation_base"], [])
action_add("build_from_base", [["built", ["foundation_base"]]], [["built", ["foundation"]]], [], 1)
action_add("build_from_dep", [["dep", ["X", "Y"]], ["built", ["Y"]]], [["built", ["X"]]], [], 1)
# Bind each action name to the actual work it does. A bound closure
# always receives ONE argument: the list of bound values for that plan
# step (e.g. build_from_dep(X=walls,Y=foundation) -> args = ["walls",
# "foundation"]) -- not splatted positionally, since a plan step's real
# arg count is only known once the planner has run.
action_bind("build_from_base", |args| {
print("pouring the foundation base")
return true
})
action_bind("build_from_dep", |args| {
print("building " + args[0] + " (needs " + args[1] + ")")
return true
})
goal need_a_house {
built(house)
}
let plan = pursue need_a_house
print("plan:")
print(plan)
let ok = activate plan
print("activate succeeded: " + ok)
# ---- a plan step that fails partway through ----
# Not every dependency-satisfying function is guaranteed to succeed just
# because the planner found a path through it -- activate must stop
# immediately and report failure, not silently continue or claim success.
action_bind("build_from_dep", |args| {
if args[0] == "house" then
print("uh oh -- can't actually build the house today")
return false
end
print("building " + args[0] + " (needs " + args[1] + ")")
return true
})
let plan2 = pursue need_a_house
let ok2 = activate plan2
print("second attempt succeeded: " + ok2)
(not run yet)
Native run on the build machine:
plan: [build_from_base, build_from_dep(X=walls,Y=foundation), build_from_dep(X=house,Y=walls)] pouring the foundation base building walls (needs foundation) building house (needs walls) activate succeeded: true pouring the foundation base building walls (needs foundation) uh oh -- can't actually build the house today second attempt succeeded: false