Skip to content

Exceptions and priorities

Real rules have exceptions, and exceptions collide. The eligibility rule of Northbridge gets three of them: a permit is not issued while the applicant’s previous permit is suspended; an applicant with outstanding parking fines is refused; and a holder of a disability badge is eligible whether or not a vehicle is registered. What happens when a badge holder also has fines? The package below compiles, and every answer on this page is a scenario the checks run.

Open this package in the playground → — edit the rules and the scenarios of this page and run them in your browser.

language "law.core" version "0.2";
package demo.parking version "0.1.0";
namespace "urn:law:demo:parking";
entity Applicant;
relation resident(a: Applicant) kind institutional;
relation vehicle_registered(a: Applicant) kind institutional;
relation permit_suspended(a: Applicant) kind institutional;
relation outstanding_fines(a: Applicant) kind institutional;
relation disability_badge(a: Applicant) kind institutional;
relation permit_eligible(a: Applicant) kind institutional;

The base rule is now defeasible: a conclusion that can be defeated. A strict rule admits no exception at all, so a rule that is meant to have one must say so — an unless clause on a strict rule is refused by the check with LDC-E4110. (The older spelling default is still accepted for defeasible, with warning LDC-E1308.) The unless clause is the first exception — a proviso that withdraws the conclusion without asserting the opposite.

rule PermitEligibility defeasible {
for a: Applicant;
when resident(a) and vehicle_registered(a);
then permit_eligible(a);
unless permit_suspended(a);
}

The second exception is a refusal: a rule whose conclusion is the negation. Two defeasible rules with opposite conclusions are, on their own, equal — the answer would keep both. A priority declaration says which one wins and names the ground.

rule FinesRefusal defeasible {
for a: Applicant;
when outstanding_fines(a);
then not permit_eligible(a);
}
priority RefusalOverEligibility {
prefer FinesRefusal over PermitEligibility;
reason lex_specialis;
}

The third rule is another route to eligibility. No priority relates it to the refusal — deliberately, to show what the engine does with a conflict nobody has resolved.

rule BadgeEligibility defeasible {
for a: Applicant;
when disability_badge(a);
then permit_eligible(a);
}

The base case: Ann is a resident with a registered vehicle.

test "no exception applies" {
given {
context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; }
assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; }
assert vehicle_registered(entity_ref("urn:demo:parking:ann")) { id "ann-vehicle"; origin case_input; }
}
evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann")));
expect truth_status == TRUE_ONLY;
expect evaluation_status == COMPUTED;
}

The proviso: her previous permit is suspended. The unless clause blocks the conclusion, and nothing asserts the opposite — the answer is NEITHER, not a refusal.

test "the unless clause withdraws the conclusion" {
given {
context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; }
assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; }
assert vehicle_registered(entity_ref("urn:demo:parking:ann")) { id "ann-vehicle"; origin case_input; }
assert permit_suspended(entity_ref("urn:demo:parking:ann")) { id "ann-suspended"; origin case_input; }
}
evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann")));
expect truth_status == NEITHER;
expect evaluation_status == COMPUTED;
}

The refusal: outstanding fines. Both rules fire; the priority makes the refusal win, and the answer is FALSE_ONLY.

test "the refusal wins by priority" {
given {
context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; }
assert resident(entity_ref("urn:demo:parking:ann")) { id "ann-resident"; origin case_input; }
assert vehicle_registered(entity_ref("urn:demo:parking:ann")) { id "ann-vehicle"; origin case_input; }
assert outstanding_fines(entity_ref("urn:demo:parking:ann")) { id "ann-fines"; origin case_input; }
}
evaluate truth(permit_eligible(entity_ref("urn:demo:parking:ann")));
expect truth_status == FALSE_ONLY;
expect evaluation_status == COMPUTED;
}

The unresolved conflict: Bob holds a disability badge and has outstanding fines. BadgeEligibility supports eligibility, FinesRefusal supports its negation, and no priority ranks them. The engine keeps both sides — the answer is BOTH.

test "two grounds, no priority: the conflict is preserved" {
given {
context { legal_time @2026-03-01; decision_time @2026-03-01T09:00:00Z; knowledge_time @2026-03-01T09:00:00Z; timezone "UTC"; }
assert disability_badge(entity_ref("urn:demo:parking:bob")) { id "bob-badge"; origin case_input; }
assert outstanding_fines(entity_ref("urn:demo:parking:bob")) { id "bob-fines"; origin case_input; }
}
evaluate truth(permit_eligible(entity_ref("urn:demo:parking:bob")));
expect truth_status == BOTH;
expect evaluation_status == COMPUTED;
}

BOTH is an answer, not an error. It tells the author that the package has two applicable norms and no rule for choosing between them — exactly what a lawyer would want flagged rather than silently resolved by the order the rules happen to be written in.

Terminal window
law engine check parking.law
law engine test tests/exceptions.lawtest --program parking.law

Expected output:

check OK: parking.law
test PASS: no exception applies
test PASS: the unless clause withdraws the conclusion
test PASS: the refusal wins by priority
test PASS: two grounds, no priority: the conflict is preserved

Delete the priority RefusalOverEligibility { … } declaration and run the scenarios again. The third scenario now ends in BOTH — the refusal and the eligibility rule are equals, and the priority was the only thing that ranked them:

test FAIL: the refusal wins by priority
truth_status == FALSE_ONLY: в документе BOTH

Then put the priority back and add a second one that prefers FinesRefusal over BadgeEligibility. Bob’s answer becomes FALSE_ONLY.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.