Gherkin-Driven Contracts for GOAP Synthesis
For new readers
This page shows a Gherkin BDD scenario (the plain-English Given/When/Then style used for behaviour-driven testing) carrying inline contract clauses — And require x < 10, And ensure x < 20 — checked as real preconditions/postconditions rather than just descriptive prose. The interesting part is where that check happens: against a hand-written function it's immediate, but against a GOAP-synthesized action (see the goal-oriented demo for GOAP itself) it's checked against the value the planner's own search settled on, before that candidate is ever accepted — with the final program paying nothing for it at runtime. Every example below was actually run, output shown unedited.
Every PatLang function can already carry require/ensure contracts (see Design by Contract) and PatLang's GOAP planner can already synthesize an action sequence to reach a goal (see the goal-oriented demo) — but until now, a Gherkin .feature scenario had no way to state a contract clause itself, and nothing connected a contract to what GOAP was actually searching for. This page shows the bridge: And require <expr> / And ensure <expr> steps, checked as a genuine boolean guard against concrete bound values — never encoded as a GOAP fact, since a comparison like x < 10 isn't a unifiable ground predicate the way GOAP's own preconditions are.
Act 1: a contract clause checked against a hand-written function
The plainest case first — a Gherkin scenario whose clauses reference a variable a Given step has already bound:
Given an integer x
And require x < 10
When I do something brilliant
Then it returns 2*x
And ensure x < 20
Since x is already bound by the time each contract clause is parsed, it's evaluated immediately — exactly like an ordinary require/ensure statement inside a function body would be, just triggered from a Gherkin step instead:
Given step ran: x = 5 ok: require x < 10 When step ran: result = x * 2 = 10 ok: ensure x < 20
Act 2: the same clauses, checked against a genuinely GOAP-synthesized action
Here the clauses reference X before any value exists — no Given step has bound it, because the value only becomes known once GOAP's search actually runs. The clauses are stashed rather than evaluated on the spot:
Given a scalable input of 5
And require X < 10
When a scale action is synthesized for that input
And ensure X < 20
Then the plan satisfies its contract clauses
A real GOAP action template — scale(X) requires input(X), produces scaled(X) — is registered, then plan([["scaled", ["5"]]]) genuinely searches for and finds the matching instantiation. GOAP already reports which concrete value each variable resolved to, as a plain string label like scale(X=5); the stashed clauses are parsed against that value directly, before the candidate is accepted:
Registered action scale(X): requires input(X), produces scaled(X) (deferred) require X < 10 (deferred) ensure X < 20 plan([["scaled", ["5"]]]) synthesized: scale(X=5) ok: require X < 10 (from scale(X=5)) ok: ensure X < 20 (from scale(X=5)) Contract check on synthesized candidate scale(X=5): true
Act 3: a synthesized candidate that violates its contract — watch it get rejected
The same mechanism run again, this time with an input GOAP can genuinely synthesize a plan for, but which breaks the contract clause:
Given a scalable input of 15
And require X < 10
When a scale action is synthesized for that input
Then the plan violates its contract clauses
(deferred) require X < 10 plan([["scaled", ["15"]]]) synthesized: scale(X=15) FAIL: require violated by synthesized plan: scale(X=15) Contract check on synthesized candidate scale(X=15): false (X=15 violates require X < 10 -- correctly detected and reported, not silently accepted)
This is the honest core of the feature: GOAP will happily synthesize a plan for scaled(15) — nothing about the planning search itself knows or cares about the contract clause — so the violation has to be caught by a separate check applied to whichever candidate the search actually returns, not baked into the search itself.
Act 4: confirming the checking mechanism never becomes a runtime assertion itself
The whole point of doing this at "compile time" (synthesis/verification time, before any code runs for real) is that the final program shouldn't pay for a runtime check that's already been settled. Confirmed directly, by inspecting the actual library source text for the host call every ordinary require/ensure/assert statement lowers to:
Does gherkin_contracts.patlang call the runtime contract_check(...) primitive? false (false is the whole point: the guard runs separately from, and before, whatever code would use the checked value for real)
How it fits together
- Grammar: a deliberately narrow
<ident> <op> <literal>parser (not the full expression grammar the compiler's own parser uses) — enough for the comparisons a contract clause actually needs, without pulling the Gherkin runner into a dependency on the compiler's internals. - Immediate vs. deferred: a clause is checked the moment it's parsed if its variable is already bound (Act 1); otherwise it's stashed until a real value exists to check it against (Acts 2-3) — the same clause syntax works for both hand-written functions and GOAP-synthesized actions.
- No new GOAP mechanism needed: the planner's own search is completely unchanged. A contract clause is a guard applied to whichever candidate the search returns, using the plan's own existing
name(var=value,...)reporting — not a new kind of fact the planner has to reason about.
See also
- Design by Contract — the
require/ensure/assertprimitives this feature reuses the meaning of, without reusing the runtime mechanism. - PatLang Goal-Oriented Programming — the GOAP planner (
action_add/plan) this feature checks candidates from. - BDD framework: unit, integration, Gherkin — the underlying Given/When/Then runner this feature extends.