Date: 2026-09-07. Branch wt/rif-dtb.
Tracking: https://github.com/danbri/factoidal/issues/664
Owner, 2026-09-07, verbatim: "it looks like we should implement all of the latest version of RIF-DTB. Do so in a way that assures us of its integrity using Lean theorems (and F* equiv is possible) and aim for tight integrations with other pieces of Factoidal - from XSLT/XPath/XML to SPARQL."
Specifications:
The same F&O function is written more than once in the Lean tree today. Three front ends carry their own copy of the string and numeric semantics:
| function | RIF | SPARQL | XPath 1.0 |
|---|---|---|---|
percent-encoding (fn:encode-for-uri) |
RIF/Builtins.lean encodeForUri, pctEncodeWith, utf8Bytes, pctByte |
SPARQL/Expr.lean strEncodeUri, encodeUriChars, percentEncodeChar, percentEncodeByte, nibbleToHex |
absent |
| substring | substring2, substring3 |
substrSpec |
substrChars (double-valued, XPath 1.0 §4.2) |
| substring-before/after | evalFunc "substring-before" |
strBefore/strAfter |
substring-before in the function table |
| upper/lower case | evalFunc |
.ucase/.lcase |
absent (XPath 1.0 has neither) |
| string length | evalFunc |
.strlen |
string-length |
| decimal arithmetic | decParts, decRender, addDec, subDec, mulDec, divDec |
Scaled |
Num (XPath 1.0 doubles — a different specification, not a duplicate) |
daysFromCivil |
private copy | — | — |
| dateTime lexical to timeline | dateTimeSecsOfLex |
valueCompare compares LEXICAL strings |
— |
XSD/Datatypes.lean already carries the value spaces, the lexical mappings,
the canonical mappings and the orders (Dec, DTValue, DurValue,
parseDateTimeLex, parseDurationLex, timeOnTimeline, dtCompare,
durCompare, canonicalDateTime, canonicalDuration). What is missing is a
layer ABOVE it: the F&O functions themselves.
Design: L4Factoidal/Fn/ holds the F&O semantics once, over the
XSD.Datatypes value spaces. RIF, SPARQL and XPath become signature layers —
each keeps its own argument typing, its own coercions and its own error
discipline, and none of them reimplements the function.
The XPath 1.0 number grammar (XPath/Number.lean) is NOT consolidated. XPath
1.0 §3.5 defines its own lexical space and its own IEEE 754 double semantics;
that is a different specification, not a duplicate of XSD's, and the
2026-09-07 XSD audit §8 already records it as out of scope.
Counted from RIF-DTB 1.0 2nd Edition section 4. "Today" is the state of
L4Factoidal/RIF/Builtins.lean at commit 709f3c49c.
| DTB § | title | names | today | after |
|---|---|---|---|---|
| 4.1 | Comparison for literals | 1 | 1 | 1 |
| 4.2 | Guard predicates | 35 | 33 | 35 |
| 4.3 | Negative guard predicates | 35 | 33 | 35 |
| 4.4 | Casting (33 xs:, rdf:XMLLiteral, rdf:PlainLiteral, pred:iri-string) |
36 | 30 | 36 |
| 4.5 | Numeric | 12 | 12 | 12 |
| 4.6 | Boolean | 4 | 3 | 4 |
| 4.7 | Strings | 17 | 17 | 17 |
| 4.8 | Dates, times and durations | 72 | 2 | 72 |
| 4.9 | rdf:XMLLiteral |
2 | 0 | 2 |
| 4.10 | rdf:PlainLiteral |
6 | 5 | 6 |
| 4.11 | RIF lists | 16 | 16 | 16 |
| total | 236 | 152 | 236 |
The header comment in RIF/Builtins.lean says "RIF-DTB defines 197
built-ins". That figure was never sourced; 236 is the count of the names
listed in section 4 of the specification, and it is what this record uses.
func:numeric-mod. Every fixture in
third_party/testing/rif-core-suite writes func:numeric-integer-mod.
Both names are accepted, and the second is noted as the corpus spelling.Builtins_Time-premise.rifps writes
External( func:add-dayTimeDuration-to-dateTime(...) ) = "2000-11-02T12:27:00"^^xs:dayTime.
There is no xs:dayTime datatype. It is a typo for xs:dateTime in the
Approved fixture. The F* tree skips the whole test rather than decide it.
This work decides the test and records the accommodation at its single site
(RIF/Builtins.lean, xsdFamily), naming the fixture line.Column (a) is the F&O function RIF-DTB cites; (b) is where the same semantics already existed in the Lean tree before this work; (c) is the XSD value space it computes over.
| DTB name | (a) F&O | (b) already in tree | (c) value space |
|---|---|---|---|
pred:literal-not-identical |
— (RIF-specific: identity of symbol AND symbol space) | RIF/Builtins.lean |
every |
pred:is-literal-T (35) |
— (RIF-specific: value-space membership) | RIF/Builtins.lean inLexicalSpace, xsdFamily; XSD.lexicalMap is the same decision written properly |
the named datatype |
pred:is-literal-not-T (35) |
— | the negation of the above | the named datatype |
The two guards missing before this work are pred:is-literal-time /
-not-time reaching a time lexical space that CSVW.parseCanonicalDate
does not model, and dateTimeStamp (a dateTime with the timezone
REQUIRED). Both are decided by XSD.parseDateTimeLex.
| DTB name | (a) F&O | (b) already in tree | (c) value space |
|---|---|---|---|
xs:T(...) (33) |
F&O 17 casting | RIF/Builtins.lean evalFunc "cast-…" over inLexicalSpace; SPARQL/Expr.lean §17.1 casts; XSD.lexicalMap |
the named datatype |
rdf:XMLLiteral, rdf:PlainLiteral |
— (RDF Concepts) | RIF/Builtins.lean |
— |
pred:iri-string |
— | RIF/Builtins.lean + iriStringBind |
xs:string / IRI |
| DTB name | (a) F&O | (b) already in tree | (c) value space |
|---|---|---|---|
func:numeric-add |
op:numeric-add |
addDec; SPARQL Scaled add |
xs:decimal (exact) |
func:numeric-subtract |
op:numeric-subtract |
subDec |
xs:decimal |
func:numeric-multiply |
op:numeric-multiply |
mulDec |
xs:decimal |
func:numeric-divide |
op:numeric-divide |
divDec |
xs:decimal |
func:numeric-integer-divide |
op:numeric-integer-divide |
evalFunc |
xs:integer |
func:numeric-mod / -integer-mod |
op:numeric-mod |
evalFunc |
xs:integer |
pred:numeric-* (6) |
op:numeric-equal, -less-than, -greater-than |
cmpNum over CSVW.decimalCompare; SPARQL §17.4.1.7 |
xs:decimal order |
| DTB name | (a) F&O | (b) already in tree | (c) value space |
|---|---|---|---|
func:not |
fn:not |
XPath "not" in the function table; SPARQL .not |
xs:boolean |
pred:boolean-equal |
op:boolean-equal |
boolValue + evalPred |
xs:boolean |
pred:boolean-less-than |
op:boolean-less-than |
evalPred |
xs:boolean |
pred:boolean-greater-than |
op:boolean-greater-than |
evalPred |
xs:boolean |
| DTB name | (a) F&O | (b) already in tree | (c) value space |
|---|---|---|---|
func:compare |
fn:compare |
evalFunc "compare" |
xs:string codepoint order |
func:concat |
fn:concat |
evalFunc; SPARQL .concat; XPath "concat" |
xs:string |
func:string-join |
fn:string-join |
evalFunc |
xs:string |
func:substring |
fn:substring |
substring2, substring3; SPARQL substrSpec; XPath substrChars — all three differ (§4 below) |
xs:string |
func:string-length |
fn:string-length |
evalFunc; SPARQL .strlen; XPath "string-length" |
xs:string |
func:upper-case / lower-case |
fn:upper-case / fn:lower-case |
evalFunc; SPARQL .ucase / .lcase |
xs:string |
func:encode-for-uri |
fn:encode-for-uri |
encodeForUri; SPARQL strEncodeUri — two copies of one function |
xs:string |
func:iri-to-uri |
fn:iri-to-uri |
iriToUri |
xs:string |
func:escape-html-uri |
fn:escape-html-uri |
escapeHtmlUri |
xs:string |
func:substring-before / -after |
fn:substring-before / -after |
evalFunc; SPARQL strBefore/strAfter; XPath function table |
xs:string |
func:replace |
fn:replace |
evalFunc over Regex.XPath |
xs:string |
pred:contains, starts-with, ends-with |
fn:contains, fn:starts-with, fn:ends-with |
evalPred; SPARQL; XPath (no ends-with in XPath 1.0) |
xs:string |
pred:matches |
fn:matches |
evalPred over Regex.XPath |
xs:string |
Before this work: func:subtract-dateTimes and func:days-from-duration
only, written against a private dateTimeSecsOfLex that reads a four-digit
year and treats an absent timezone as UTC. The other 70 names were absent and
Builtins_Time was undecided in both trees.
All 72 are cited by RIF-DTB from F&O 10.5 (accessors), 10.6 (duration
arithmetic), 10.7 (date/time arithmetic), 10.8 (date/time subtraction) and
10.4 (comparison). They compute over XSD.DTValue (§3.3.7's seven-property
model) and XSD.DurValue (§3.3.6's two-property model), and their results are
written back through XSD.canonicalDateTime and XSD.canonicalDuration.
| DTB name | (a) F&O | (b) already in tree | (c) value space |
|---|---|---|---|
pred:XMLLiteral-equal / -not-equal |
— | absent | rdf:XMLLiteral |
func:PlainLiteral-from-string-lang |
— | evalFunc |
rdf:PlainLiteral |
func:string-from-PlainLiteral |
— | plainParts |
xs:string |
func:lang-from-PlainLiteral |
— | plainParts |
xs:string |
func:PlainLiteral-compare |
fn:compare on the pair |
evalFunc |
codepoint order |
func:PlainLiteral-length |
fn:string-length |
absent | xs:integer |
pred:matches-language-range |
— (RFC 4647 §3.3.2 extended filtering) | matchesLanguageRange; SPARQL fnLangMatches is RFC 4647 §3.3.1 BASIC filtering — a real difference, §4 below |
— |
All 16 were already present (listIndex, dedup and the evalFunc cases).
They are RIF-specific: F&O's sequence functions are the model, but a RIF list
is a term, not a sequence, so the correspondence is by analogy only. They move
to Fn/List.lean as polymorphic functions over List α with BEq α, which
is what makes them reusable by a SPARQL or XPath sequence layer later.
These are not defects to reconcile. Each is a stated difference between two W3C Recommendations, and each gets a named theorem with the input that witnesses it.
substring, three ways. F&O fn:substring is 1-based and keeps every
position p with start <= p < start + length, with start and length
rounded as xs:double. SPARQL SUBSTR (§17.4.3.3) cites fn:substring
but takes xs:integer arguments. XPath 1.0 §4.2 substring() rounds its
double arguments with round() and has the same window rule. The RIF Core
Approved Builtins_String fixture additionally asserts
substring("foobar" 3) = "bar", which is 0-BASED — the 2-argument and
3-argument forms in that fixture disagree with each other on the base, and
the fixture is the authority for the RIF front end.pred:matches-language-range is RFC 4647
§3.3.2 EXTENDED filtering (wildcards inside the range). SPARQL
langMatches is §3.3.1 BASIC filtering. Witness: tag de-Latn-DE, range
de-*-DE — extended matches, basic does not.false; XPath 1.0 has no error
value at all and returns the empty string or NaN; RIF-DTB leaves the
built-in with no value, which RIF/Builtins.lean reports as unknown so
the rule does not fire. The library returns Option; each front end maps
none to its own discipline.concat arity. fn:concat and SPARQL CONCAT are variadic over the
whole argument list; XPath 1.0 concat() requires at least two arguments.| module | holds |
|---|---|
L4Factoidal/Fn/Numeric.lean |
XSD.Dec multiplication, division, integer division, modulus, rounding; the decimal-numeral string layer the RIF front end works in |
L4Factoidal/Fn/String.lean |
F&O 5.2-5.4: compare, concat, string-join, substring, length, case folding, the three URI escapers, substring-before/after, contains/starts-with/ends-with, RFC 4647 matching |
L4Factoidal/Fn/Boolean.lean |
F&O 9.1-9.2 |
L4Factoidal/Fn/Duration.lean |
F&O 10.5.1-10.5.6 accessors and 10.6 arithmetic over XSD.DurValue |
L4Factoidal/Fn/DateTime.lean |
F&O 10.5.7-10.5.20 accessors, 10.4 comparison, 10.7/10.8 arithmetic over XSD.DTValue |
L4Factoidal/Fn/List.lean |
the RIF-DTB 4.11 list functions, polymorphic |
L4Factoidal/Fn/Casting.lean |
F&O 17: a lexical form to a value of a named datatype, over XSD.lexicalMap |
L4Factoidal/Fn/Theorems.lean |
the agreement theorems and the named difference theorems |
Two defects, both from the same wrong assumption: that Lean 4's Int /
truncates toward zero. It is EUCLIDEAN — (-7) / 2 is -4 and (-7) % 2
is 1.
XSD.daysFromCivil was one day short for every year in [-399, -1].
Hinnant's days_from_civil takes the era with a TRUNCATING division and
writes y' - 399 to emulate a floor. Written with Lean's /, the
adjustment fires a second time. Found by the round-trip #guard between
Fn.DateTime.civilFromDays and XSD.daysFromCivil on -44-03-15; fixed
with Int.tdiv. Every date/time comparison and every timeOnTimeline for
a BCE date read the wrong instant before this. No vendored fixture has a
negative year, which is why 18,951 of 18,956 datatype tests passed over it.func:numeric-divide floored where F&O §6.2.4 truncates. The comment
above divDec in RIF/Builtins.lean asserted the opposite of what the
code did. divDec "-1" "3" gave -0.333333333333333334. Fixed with
Int.tdiv, with a #guard on both signs.Both are the shape anti-pattern #28 names: a comment stating a property of a
primitive is evidence about the comment, not about the primitive.
Fn/Numeric.lean now pins all four division directions as examples, so the
next reader does not have to take anyone's word for it.
/Users/danbri/working/factoidal-wt-dtb)#Every number below was produced by running the probe in this worktree, before and after, not quoted from a dashboard.
| gate | before | after |
|---|---|---|
l4rif |
42 pass, 0 fail (out of 42 decided); 3 undecided, 1 local override | 43 pass, 0 fail (out of 43 decided); 2 undecided, 1 local override |
l4xpath |
100 pass, 0 fail (out of 100) | 100 pass, 0 fail (out of 100) — unchanged |
l4xslt |
84 pass, 3 fail (out of 87 decided), 1 refused | 84 pass, 3 fail (out of 87 decided), 1 refused — unchanged |
l4xsd-datatypes |
18951 pass, 5 fail (out of 18956 scored) | 18951 pass, 5 fail (out of 18956 scored) — unchanged |
SPARQL 1.1 l4w3c |
631 pass, 0 fail (out of 631) | 631 pass, 0 fail (out of 631) — unchanged |
SPARQL 1.2 l4w3c |
254 pass, 0 fail (out of 254) | 254 pass, 0 fail (out of 254) — unchanged |
The l4xsd-datatypes score not moving is the expected result of the
daysFromCivil repair, not evidence against it: no vendored instance file in
that suite carries a negative proleptic year, so the suite cannot see the
defect in either direction. The #guard in Fn/DateTime.lean is what sees
it.
Not a gate here, and reported without a claim: the superseded SPARQL 1.0 evaluation manifest gives 208 pass, 74 fail (out of 282). No baseline for it is recorded anywhere in the repository, so this run establishes one rather than measuring a change.
Hygiene: #print axioms over all 27 theorems of L4Factoidal.Fn gives
propext, Classical.choice, Quot.sound or fewer. No sorry, no user
axiom, no native_decide, no unsafe. partial def count across
L4Factoidal is 170, down from the 172 baseline.
Builtins_Time was undecided in BOTH trees for lack of DTB §4.8. It is now
decided and passes. The 2 remaining undecided cases are
Modeling_Brain_Anatomy and Non-Annotation_Entailment, both blocked on an
OWL entailment regime rather than on a built-in
(#665).
The task brief allowed vendoring w3c/qt3tests shallow if it is under
300 MB. It was not vendored, and the reason is the disk rather than the
licence: this machine ran to 162 MB free during the session with five
worktrees live, and a new submodule plus its build products is exactly the
thing that filled it. No QT3 number is reported, and none is estimated. The
executable evidence for the shared functions is the #guard set in each Fn
module (the F&O examples and the RIF corpus's own equations) plus the five
suites above.
CSVW.isXsdNumericLexical
at the RIF call site rather than through XSD.lexicalMap. That move
changes ACCEPTANCE and its gate is the SPARQL suites, so it is commit 7 of
docs/designissues/2026-09-07-xsd-datatypes-audit.md §8 and not this work.civilFromDays and daysFromCivil are shown mutually inverse by #guard
on eight dates, not by a theorem for every year.LWS/Operations.lean keeps a third civilFromDays for HTTP-date
formatting. It is a different specification's formatter and moving it is a
separate commit with its own gate.valueCompare still orders same-datatype xs:dateTime literals
by LEXICAL string rather than by XSD.dtCompare. Fn.DateTime.compare
is now available to it; the repair is commit 8 of the XSD audit plan.