diff --git a/conformance/README.md b/conformance/README.md index b25a61a..d082121 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -257,6 +257,11 @@ version, and grow that record only by verify-then-add. predicator or a misreading of `docs/isa.md`, and either way it gets sorted out in review, not silently. +The `library/*` cases are the family's shared example world - a library's +loans, holds, renewals and patron standing - and its definition lives in the +statifier engine repository's `conformance/README.md`, so a new library row is +authored against that world rather than against a parallel one invented here. + ## Known uncovered ### Opcodes excluded from the coverage rule diff --git a/conformance/cases/library.json b/conformance/cases/library.json new file mode 100644 index 0000000..3a37efe --- /dev/null +++ b/conformance/cases/library.json @@ -0,0 +1,180 @@ +[ + { + "id": "library/renew-allowed", + "source": "renewals < max_renewals and not holds_pending and patron.standing == \"good\"", + "context": { + "renewals": 1, + "max_renewals": 2, + "holds_pending": false, + "patron": { "id": "p-1", "standing": "good" } + }, + "expected": { "result": true }, + "tier": 3, + "notes": "the renewal guard of the library example world, in full: under the renewal limit, no hold waiting on the copy, and the patron in good standing. The three refusal rows below flip one conjunct each and change nothing else" + }, + { + "id": "library/renew-refused-by-hold", + "source": "renewals < max_renewals and not holds_pending and patron.standing == \"good\"", + "context": { + "renewals": 1, + "max_renewals": 2, + "holds_pending": true, + "patron": { "id": "p-1", "standing": "good" } + }, + "expected": { "result": false }, + "tier": 3 + }, + { + "id": "library/renew-refused-at-max", + "source": "renewals < max_renewals and not holds_pending and patron.standing == \"good\"", + "context": { + "renewals": 2, + "max_renewals": 2, + "holds_pending": false, + "patron": { "id": "p-1", "standing": "good" } + }, + "expected": { "result": false }, + "tier": 3 + }, + { + "id": "library/renew-refused-blocked-standing", + "source": "renewals < max_renewals and not holds_pending and patron.standing == \"good\"", + "context": { + "renewals": 1, + "max_renewals": 2, + "holds_pending": false, + "patron": { "id": "p-2", "standing": "blocked" } + }, + "expected": { "result": false }, + "tier": 3 + }, + { + "id": "library/standing-good-no-fines", + "source": "patron.standing == \"good\" and fines_owed == 0", + "context": { + "patron": { "id": "p-1", "standing": "good" }, + "fines_owed": 0 + }, + "expected": { "result": true }, + "tier": 3 + }, + { + "id": "library/fine-owed", + "source": "patron.standing == \"good\" and fines_owed == 0", + "context": { + "patron": { "id": "p-1", "standing": "good" }, + "fines_owed": 1 + }, + "expected": { "result": false }, + "tier": 3, + "notes": "the same guard as the row above with one unpaid fine against the patron. fines_owed counts unpaid fines; the example world fixes no currency and no amount" + }, + { + "id": "library/due-soon-window", + "source": "loan_period.days - 3 == 18", + "context": { + "loan_period": { + "$type": "duration", + "value": { + "years": 0, + "months": 0, + "weeks": 0, + "days": 21, + "hours": 0, + "minutes": 0, + "seconds": 0 + } + } + }, + "expected": { "result": true }, + "tier": 3, + "notes": "hand-decode: the due-soon reminder sits three days before the end of the 21-day loan period, at 18 days from checkout. The subtraction is over the duration's days field, not over two durations: docs/isa.md section 5 gives subtract clauses for number-number, Date-Date, DateTime-DateTime and Date/DateTime-duration only, so a duration-duration subtraction is a TypeMismatchError and no window can be computed that way. A duration context value is tagged-value encoded; see conformance/README.md" + }, + { + "id": "library/pickup-window", + "source": "7d", + "expected": { + "result": { + "$type": "duration", + "value": { + "years": 0, + "months": 0, + "weeks": 0, + "days": 7, + "hours": 0, + "minutes": 0, + "seconds": 0 + } + } + }, + "tier": 4, + "notes": "the pickup window of the example world: a hold that becomes available is held for seven days" + }, + { + "id": "library/overdue-days", + "source": "overdue.days < lost_after.days", + "context": { + "overdue": { + "$type": "duration", + "value": { + "years": 0, + "months": 0, + "weeks": 0, + "days": 7, + "hours": 0, + "minutes": 0, + "seconds": 0 + } + }, + "lost_after": { + "$type": "duration", + "value": { + "years": 0, + "months": 0, + "weeks": 0, + "days": 28, + "hours": 0, + "minutes": 0, + "seconds": 0 + } + } + }, + "expected": { "result": true }, + "tier": 3, + "notes": "hand-decode: a copy seven days overdue has not yet reached the 28-day point at which the example world declares it lost. The comparison reads the days field of each duration rather than comparing the two durations: docs/isa.md section 5 leaves ordering between two maps, which is what a duration is at the value level, unspecified for siblings" + }, + { + "id": "library/max-renewals-boundary", + "source": "renewals < max_renewals", + "context": { + "renewals": 2, + "max_renewals": 2 + }, + "expected": { "result": false }, + "tier": 1, + "notes": "the renewal limit is exclusive at the boundary: the second renewal is the last one allowed, and a third is refused" + }, + { + "id": "library/nested-patron", + "source": "loan.patron.standing == \"good\"", + "context": { + "loan": { + "copy": "c-1", + "branch": "b-1", + "patron": { "id": "p-1", "standing": "good" } + } + }, + "expected": { "result": true }, + "tier": 3, + "notes": "hand-decode: two chained property reads over a nested context - the loan carries the patron, and the patron carries the standing. The keys a host supplies are strings; docs/isa.md section 5 makes a missing key undefined rather than an error" + }, + { + "id": "library/holds-pending-negation", + "source": "not holds_pending", + "context": { + "holds_pending": true + }, + "expected": { "result": false }, + "tier": 1 + } +] diff --git a/conformance/corpus/tier-1.json b/conformance/corpus/tier-1.json index c1f83b3..29dd82f 100644 --- a/conformance/corpus/tier-1.json +++ b/conformance/corpus/tier-1.json @@ -51,6 +51,8 @@ {"context":{},"expected_result":true,"features":["legacy_logical","retired"],"id":"legacy/and-true-true","instructions":[["lit",true],["lit",true],["and"]],"notes":"the bare and/or opcodes (ADR-0001): retired at ISA v3 (4.0.0), so this build refuses them with a retired_opcode error, but never emitted by the compiler, which always compiles source-level and/or to jump_if_falsy_or_pop/jump_if_true_or_pop instead (see short_circuit.json). The case stays in the corpus with its expectation frozen, not recomputed, because a sibling claiming ISA v1 or v2 must still run it. Tier 1 by opcode; a v1 sibling must still run these.","source":null,"tier":1} {"context":{},"expected_result":true,"features":["legacy_logical","retired"],"id":"legacy/or-false-true","instructions":[["lit",false],["lit",true],["or"]],"source":null,"tier":1} {"context":{},"expected_error":{"reason":"logical_or","type":"TypeMismatchError"},"features":["errors","legacy_logical","retired"],"id":"legacy/or-rejects-non-boolean","instructions":[["lit",1],["lit",true],["or"]],"source":null,"tier":1} +{"context":{"holds_pending":true},"expected_result":false,"features":[],"id":"library/holds-pending-negation","instructions":[["load","holds_pending"],["not"]],"source":"not holds_pending","tier":1} +{"context":{"max_renewals":2,"renewals":2},"expected_result":false,"features":["comparison"],"id":"library/max-renewals-boundary","instructions":[["load","renewals"],["load","max_renewals"],["compare","LT"]],"notes":"the renewal limit is exclusive at the boundary: the second renewal is the last one allowed, and a third is refused","source":"renewals < max_renewals","tier":1} {"context":{},"expected_result":false,"features":["short_circuit"],"id":"logical/and-short-circuit-false","instructions":[["lit",false],["jump_if_falsy_or_pop",2],["lit",true]],"source":"false and true","tier":1} {"context":{},"expected_result":false,"features":[],"id":"logical/not-true","instructions":[["lit",true],["not"]],"source":"not true","tier":1} {"context":{},"expected_result":true,"features":["short_circuit"],"id":"logical/or-short-circuit-true","instructions":[["lit",true],["jump_if_true_or_pop",2],["lit",false]],"source":"true or false","tier":1} diff --git a/conformance/corpus/tier-3.json b/conformance/corpus/tier-3.json index 320343a..97216b4 100644 --- a/conformance/corpus/tier-3.json +++ b/conformance/corpus/tier-3.json @@ -18,6 +18,15 @@ {"context":{"user":{}},"expected_error":{"reason":"add","type":"TypeMismatchError"},"features":["access","arithmetic","errors"],"id":"arithmetic/add-type-mismatch","instructions":[["load","user"],["access","missing"],["lit",1],["add"]],"notes":":undefined is rejected by add, unlike compare/in/contains which propagate it","source":"user.missing + 1","tier":3} {"context":{"user":{}},"expected_result":{"$type":"undefined"},"features":["access","comparison","undefined"],"id":"comparison/undefined-propagates-through-non-strict-compare","instructions":[["load","user"],["access","missing"],["lit",1],["compare","GT"]],"notes":"user.missing is a genuine :undefined produced by access on an absent key, not a load of an absent root (which errors under default options - see errors.json)","source":"user.missing > 1","tier":3} {"context":{},"expected_error":{"reason":"insufficient_operands","type":"EvaluationError"},"features":["access","errors"],"id":"errors/insufficient-operands-bracket-access","instructions":[["lit",1],["bracket_access"]],"source":null,"tier":3} +{"context":{"loan_period":{"$type":"duration","value":{"days":21,"hours":0,"minutes":0,"months":0,"seconds":0,"weeks":0,"years":0}}},"expected_result":true,"features":["access","arithmetic","comparison","durations"],"id":"library/due-soon-window","instructions":[["load","loan_period"],["access","days"],["lit",3],["subtract"],["lit",18],["compare","EQ"]],"notes":"hand-decode: the due-soon reminder sits three days before the end of the 21-day loan period, at 18 days from checkout. The subtraction is over the duration's days field, not over two durations: docs/isa.md section 5 gives subtract clauses for number-number, Date-Date, DateTime-DateTime and Date/DateTime-duration only, so a duration-duration subtraction is a TypeMismatchError and no window can be computed that way. A duration context value is tagged-value encoded; see conformance/README.md","source":"loan_period.days - 3 == 18","tier":3} +{"context":{"fines_owed":1,"patron":{"id":"p-1","standing":"good"}},"expected_result":false,"features":["access","comparison","short_circuit"],"id":"library/fine-owed","instructions":[["load","patron"],["access","standing"],["lit","good"],["compare","EQ"],["jump_if_falsy_or_pop",4],["load","fines_owed"],["lit",0],["compare","EQ"]],"notes":"the same guard as the row above with one unpaid fine against the patron. fines_owed counts unpaid fines; the example world fixes no currency and no amount","source":"patron.standing == \"good\" and fines_owed == 0","tier":3} +{"context":{"loan":{"branch":"b-1","copy":"c-1","patron":{"id":"p-1","standing":"good"}}},"expected_result":true,"features":["access","comparison"],"id":"library/nested-patron","instructions":[["load","loan"],["access","patron"],["access","standing"],["lit","good"],["compare","EQ"]],"notes":"hand-decode: two chained property reads over a nested context - the loan carries the patron, and the patron carries the standing. The keys a host supplies are strings; docs/isa.md section 5 makes a missing key undefined rather than an error","source":"loan.patron.standing == \"good\"","tier":3} +{"context":{"lost_after":{"$type":"duration","value":{"days":28,"hours":0,"minutes":0,"months":0,"seconds":0,"weeks":0,"years":0}},"overdue":{"$type":"duration","value":{"days":7,"hours":0,"minutes":0,"months":0,"seconds":0,"weeks":0,"years":0}}},"expected_result":true,"features":["access","comparison","durations"],"id":"library/overdue-days","instructions":[["load","overdue"],["access","days"],["load","lost_after"],["access","days"],["compare","LT"]],"notes":"hand-decode: a copy seven days overdue has not yet reached the 28-day point at which the example world declares it lost. The comparison reads the days field of each duration rather than comparing the two durations: docs/isa.md section 5 leaves ordering between two maps, which is what a duration is at the value level, unspecified for siblings","source":"overdue.days < lost_after.days","tier":3} +{"context":{"holds_pending":false,"max_renewals":2,"patron":{"id":"p-1","standing":"good"},"renewals":1},"expected_result":true,"features":["access","comparison","short_circuit"],"id":"library/renew-allowed","instructions":[["load","renewals"],["load","max_renewals"],["compare","LT"],["jump_if_falsy_or_pop",3],["load","holds_pending"],["not"],["jump_if_falsy_or_pop",5],["load","patron"],["access","standing"],["lit","good"],["compare","EQ"]],"notes":"the renewal guard of the library example world, in full: under the renewal limit, no hold waiting on the copy, and the patron in good standing. The three refusal rows below flip one conjunct each and change nothing else","source":"renewals < max_renewals and not holds_pending and patron.standing == \"good\"","tier":3} +{"context":{"holds_pending":false,"max_renewals":2,"patron":{"id":"p-1","standing":"good"},"renewals":2},"expected_result":false,"features":["access","comparison","short_circuit"],"id":"library/renew-refused-at-max","instructions":[["load","renewals"],["load","max_renewals"],["compare","LT"],["jump_if_falsy_or_pop",3],["load","holds_pending"],["not"],["jump_if_falsy_or_pop",5],["load","patron"],["access","standing"],["lit","good"],["compare","EQ"]],"source":"renewals < max_renewals and not holds_pending and patron.standing == \"good\"","tier":3} +{"context":{"holds_pending":false,"max_renewals":2,"patron":{"id":"p-2","standing":"blocked"},"renewals":1},"expected_result":false,"features":["access","comparison","short_circuit"],"id":"library/renew-refused-blocked-standing","instructions":[["load","renewals"],["load","max_renewals"],["compare","LT"],["jump_if_falsy_or_pop",3],["load","holds_pending"],["not"],["jump_if_falsy_or_pop",5],["load","patron"],["access","standing"],["lit","good"],["compare","EQ"]],"source":"renewals < max_renewals and not holds_pending and patron.standing == \"good\"","tier":3} +{"context":{"holds_pending":true,"max_renewals":2,"patron":{"id":"p-1","standing":"good"},"renewals":1},"expected_result":false,"features":["access","comparison","short_circuit"],"id":"library/renew-refused-by-hold","instructions":[["load","renewals"],["load","max_renewals"],["compare","LT"],["jump_if_falsy_or_pop",3],["load","holds_pending"],["not"],["jump_if_falsy_or_pop",5],["load","patron"],["access","standing"],["lit","good"],["compare","EQ"]],"source":"renewals < max_renewals and not holds_pending and patron.standing == \"good\"","tier":3} +{"context":{"fines_owed":0,"patron":{"id":"p-1","standing":"good"}},"expected_result":true,"features":["access","comparison","short_circuit"],"id":"library/standing-good-no-fines","instructions":[["load","patron"],["access","standing"],["lit","good"],["compare","EQ"],["jump_if_falsy_or_pop",4],["load","fines_owed"],["lit",0],["compare","EQ"]],"source":"patron.standing == \"good\" and fines_owed == 0","tier":3} {"context":{},"expected_result":false,"features":["membership"],"id":"membership/contains-false","instructions":[["lit",[1,2,3]],["lit",9],["contains"]],"source":"[1, 2, 3] contains 9","tier":3} {"context":{},"expected_error":{"reason":"contains","type":"TypeMismatchError"},"features":["errors","membership"],"id":"membership/contains-left-must-be-list","instructions":[["lit","x"],["lit",1],["contains"]],"notes":"contains is the mirror of in - the left operand must be the list","source":"\"x\" contains 1","tier":3} {"context":{},"expected_result":true,"features":["membership"],"id":"membership/contains-true","instructions":[["lit",[1,2,3]],["lit",2],["contains"]],"source":"[1, 2, 3] contains 2","tier":3} diff --git a/conformance/corpus/tier-4.json b/conformance/corpus/tier-4.json index 30efe9f..0f23ed3 100644 --- a/conformance/corpus/tier-4.json +++ b/conformance/corpus/tier-4.json @@ -10,6 +10,7 @@ {"context":{},"expected_result":{"$type":"duration","value":{"days":3,"hours":0,"minutes":0,"months":0,"seconds":0,"weeks":2,"years":0}},"features":["durations"],"id":"durations/multi-unit","instructions":[["duration",[[2,"w"],[3,"d"]]]],"source":"2w3d","tier":4} {"context":{},"expected_result":{"$type":"duration","value":{"days":3,"hours":0,"minutes":0,"months":0,"seconds":0,"weeks":0,"years":0}},"features":["durations"],"id":"durations/single-unit","instructions":[["duration",[[3,"d"]]]],"notes":"hand-decode: the duration value has eight keys, all present, defaulting to 0 - this is the normative shape from docs/isa.md section 3. The JSON omits a zero milliseconds as a compaction rule of the codec; see conformance/README.md","source":"3d","tier":4} {"context":{},"expected_error":{"reason":"invalid_stack_value","type":"EvaluationError"},"features":["errors","objects"],"id":"errors/object-set-non-map-target","instructions":[["lit",5],["lit",1],["object_set","key"]],"notes":"docs/isa.md section 5: object_set's target must be a map. Reachable only from a hand-built instruction list - the compiler always emits object_new immediately before object_set - but specified, not undefined. Shares the invalid_stack_value reason with relative_date's non-duration case.","source":null,"tier":4} +{"context":{},"expected_result":{"$type":"duration","value":{"days":7,"hours":0,"minutes":0,"months":0,"seconds":0,"weeks":0,"years":0}},"features":["durations"],"id":"library/pickup-window","instructions":[["duration",[[7,"d"]]]],"notes":"the pickup window of the example world: a hold that becomes available is held for seven days","source":"7d","tier":4} {"context":{},"expected_result":true,"features":["comparison","objects"],"id":"objects/equality-by-structure","instructions":[["object_new"],["lit",1],["object_set","a"],["lit",2],["object_set","b"],["object_new"],["lit",1],["object_set","a"],["lit",2],["object_set","b"],["compare","EQ"]],"notes":"docs/isa.md: EQ/NE/STRICT_EQ/STRICT_NE on two maps are well-defined and portable - only GT/LT/GTE/LTE between two maps are unspecified, and the conformance corpus does not exercise those","source":"{a: 1, b: 2} == {a: 1, b: 2}","tier":4} {"context":{},"expected_result":{"age":30,"name":"John"},"features":["objects"],"id":"objects/literal-multi-key","instructions":[["object_new"],["lit","John"],["object_set","name"],["lit",30],["object_set","age"]],"source":"{name: \"John\", age: 30}","tier":4} {"context":{},"expected_result":{"user":{"role":"admin"}},"features":["objects"],"id":"objects/literal-nested","instructions":[["object_new"],["object_new"],["lit","admin"],["object_set","role"],["object_set","user"]],"source":"{user: {role: \"admin\"}}","tier":4} diff --git a/conformance/examples/registry.example.json b/conformance/examples/registry.example.json index ab54c4e..d1a8682 100644 --- a/conformance/examples/registry.example.json +++ b/conformance/examples/registry.example.json @@ -2,7 +2,7 @@ "claims": [ {"surface":"evaluator","tier":1} ], -"corpus_hash": "sha256:548f54cacdcb700df0c47d67f86a944b5dbe6b0c6f96ef0c4c6fbb95b0494892", +"corpus_hash": "sha256:bb60ec82faba2e82ef1e7abaac8d26257db2a77cd4ce0c083b683c5bc063fd43", "entries": [ {"case_id":"comparison/eq-string-true","surface":"compiler","tier":1}, {"case_id":"comparison/gt-int-boundary","surface":"compiler","tier":1}, @@ -62,6 +62,8 @@ {"case_id":"legacy/and-true-true","surface":"evaluator","tier":1}, {"case_id":"legacy/or-false-true","surface":"evaluator","tier":1}, {"case_id":"legacy/or-rejects-non-boolean","surface":"evaluator","tier":1}, +{"case_id":"library/holds-pending-negation","surface":"evaluator","tier":1}, +{"case_id":"library/max-renewals-boundary","surface":"evaluator","tier":1}, {"case_id":"logical/and-short-circuit-false","surface":"evaluator","tier":1}, {"case_id":"logical/not-true","surface":"evaluator","tier":1}, {"case_id":"logical/or-short-circuit-true","surface":"evaluator","tier":1}, diff --git a/conformance/manifest.json b/conformance/manifest.json index cac01c1..e16cba6 100644 --- a/conformance/manifest.json +++ b/conformance/manifest.json @@ -1 +1 @@ -{"corpus_hash":"sha256:548f54cacdcb700df0c47d67f86a944b5dbe6b0c6f96ef0c4c6fbb95b0494892","isa_version":6,"tiers":[{"case_count":63,"file":"corpus/tier-1.json","name":"core","opcodes":["compare","jump_if_falsy_or_pop","jump_if_true_or_pop","lit","load","not","unary_bang","unary_minus"],"tier":1},{"case_count":19,"file":"corpus/tier-2.json","name":"arithmetic","opcodes":["add","divide","modulo","multiply","subtract"],"tier":2},{"case_count":30,"file":"corpus/tier-3.json","name":"access","opcodes":["access","bracket_access","contains","in","make_list"],"tier":3},{"case_count":17,"file":"corpus/tier-4.json","name":"rich types","opcodes":["duration","object_new","object_set","relative_date"],"tier":4},{"case_count":29,"file":"corpus/tier-5.json","name":"functions","opcodes":["call"],"tier":5},{"case_count":9,"file":"corpus/tier-6.json","name":"statements","opcodes":["pop","store"],"tier":6},{"case_count":60,"file":"corpus/tier-7.json","name":"casts","opcodes":["cast"],"tier":7},{"case_count":17,"file":"corpus/tier-8.json","name":"control flow","opcodes":["jump","pop_jump_if_falsy"],"tier":8},{"case_count":6,"file":"corpus/tier-9.json","name":"loops","opcodes":["jump_backward"],"tier":9}]} +{"corpus_hash":"sha256:bb60ec82faba2e82ef1e7abaac8d26257db2a77cd4ce0c083b683c5bc063fd43","isa_version":6,"tiers":[{"case_count":65,"file":"corpus/tier-1.json","name":"core","opcodes":["compare","jump_if_falsy_or_pop","jump_if_true_or_pop","lit","load","not","unary_bang","unary_minus"],"tier":1},{"case_count":19,"file":"corpus/tier-2.json","name":"arithmetic","opcodes":["add","divide","modulo","multiply","subtract"],"tier":2},{"case_count":39,"file":"corpus/tier-3.json","name":"access","opcodes":["access","bracket_access","contains","in","make_list"],"tier":3},{"case_count":18,"file":"corpus/tier-4.json","name":"rich types","opcodes":["duration","object_new","object_set","relative_date"],"tier":4},{"case_count":29,"file":"corpus/tier-5.json","name":"functions","opcodes":["call"],"tier":5},{"case_count":9,"file":"corpus/tier-6.json","name":"statements","opcodes":["pop","store"],"tier":6},{"case_count":60,"file":"corpus/tier-7.json","name":"casts","opcodes":["cast"],"tier":7},{"case_count":17,"file":"corpus/tier-8.json","name":"control flow","opcodes":["jump","pop_jump_if_falsy"],"tier":8},{"case_count":6,"file":"corpus/tier-9.json","name":"loops","opcodes":["jump_backward"],"tier":9}]}