Initial support for slot data (no accessors or mutators)

* src/record.xsl: added R:init-slots and friends

* test/record.xspec: add slot data tests
master
Mike Gerwitz 2016-03-28 00:04:13 -04:00
parent a921741f09
commit d38c6b3a8f
No known key found for this signature in database
GPG Key ID: F22BB8158EE30EAB
2 changed files with 299 additions and 0 deletions

View File

@ -81,6 +81,7 @@
* Design Considerations: Record Design Considerations.
* Header: Record Header.
* Slot Naming: Record Slot Naming.
* Slot Data: Record Slot Data.
* Polymorphism: Record Polymorphism.
@end menu
@ -444,6 +445,182 @@
</function>
<!--
@node Record Slot Data
@section Record Slot Data
Slot data directly follows the @ref{Record Header} in a@tie{}sequence.
By default,
all records are initialized to a@tie{}special value @ref{R:empty-slot}
which indicates that the@tie{}slot contains no@tie{}value.@c
@footnote{It is otherwise not possible to indicate the@tie{}absence of
a@tie{}value,
since the@tie{}empty sequence within a@tie{}sequence is
eliminated@mdash{}that is, @t{(1, (), 2) eq (1, 2)}.}
-->
<!--
Special element indicating an empty slot.
This value will be automatically converted into the@tie{}empty sequence by
accessor functions.
-->
<variable name="R:empty-slot" as="element( R:empty-slot )">
<R:empty-slot />
</variable>
<!--
Slots cannot correspond 1-to-1 with sequence items,
because they might themselves contain records.
Let the @dfn{slot span}@tie{}@math{ϱ} of some slot@tie{}@math{s} be the
number of sequence items that constitute its value,
and let @math{#S} be the number of items in some set@tie{}@math{S};
then,
@itemize
@item @math{ϱ(s) = #T + ∑ᵢϱ(tᵢ) + 2, t∈T}, where @math{T} is the@tie{}set
of slots in@tie{}@math{s}, if @math{s}@tie{}is a record; and
@item @math{ϱ(s) = 1} otherwise.
@end itemize
@anchor{record span}
The first equation is the@tie{}@dfn{record span}.
It accounts for (@pxref{Record Slot Offsets}):
@enumerate
@item A @ref{Record Header} (one item);
@item The record span (one item);
@item @math{#T} items to hold the offset for each slot
(@pxref{Record Slot Offsets}); and
@item @math{∑ᵢϱ(tᵢ)} items to hold all slot contents.
@end enumerate
So, if a record in@tie{}@math{s} contains no nested records,
its span will always be @math{2#T + 2}.
All of this allows for dynamically-typed, dynamically-sized records.
@menu
* Slot Offsets: Record Slot Offsets.
@end menu
-->
<!--
@node Record Slot Offsets
@subsection Record Slot Offsets
Recursively calculating the slot span of a record would be an incredibly
costly operation for deeply nested records,
so we reduce this lookup to @math{O(1)} constant time by recomputing it
each time a record slot is set.
To eliminate an @math{O(n)} linear scan of the slots for record span
recomputation,
each slot has its offset cached; this also allows us to look up a slot
in constant time.
These offsets are stored directly after the @ref{Record Header},
before the slot values;
this allows quick lookup and modification@c
@mdash{}modifying the record header involves reconstructing an element,
which involves many more operations than swapping out items in a
sequence.
For records,
it is also important that we know the entire @ref{record span} so that
the end of the entire record in a sequence can be immediately known
without consulting the final slot span,
which involves looking up the last slot offset and then determining its
span.
This offset precedes the individual slot offsets.
@anchor{record sequence}
The resulting construction is called a @dfn{record sequence}:
@float Figure, fig:record-construction
@verbatim
(<header> as R:Record,
<record span> as xs:integer,
<slot span>... as xs:integer*,
<slot>... as item()*)
@end verbatim
@caption{Items of a record sequence}
@end float
-->
<!--
Produce a record sequence of @var{Record} initialized with
@math{N}@tie{}empty slots,
where @math{N}@tie{}is the@tie{}slot count of@tie{}@var{Record}.
Slots are initialized to @ref{R:empty-slot}.
The resulting sequence contains @math{2N + 2}@tie{}items,
with the first item being @var{Record}.
-->
<function name="R:init-slots" as="item()+">
<param name="Record" as="element( R:Record )" />
<variable name="n" as="xs:integer"
select="R:slot-count( $Record )" />
<variable name="offsets" as="xs:integer*"
select="for $i in 1 to $n
return $i" />
<variable name="slots" as="element( R:empty-slot )*"
select="for $i in 1 to $n
return $R:empty-slot" />
<sequence select="$Record, (2*$n + 2), $offsets, $slots" />
</function>
<!--
Determine whether the given sequence @var{seq} is a
valid@tie{}@ref{record sequence}.
This ensures that the a proper @ref{Record Header} is present and that
there are enough items in the sequence to satisfy all data requirements
for that record (@pxref{record span}).
It does not, however, guarantee that the items in the sequence do actually
belong to that record.
-->
<function name="R:is-record-seq" as="xs:boolean">
<param name="seq" as="item()*" />
<variable name="header" as="item()?"
select="$seq[ 1 ]" />
<variable name="slen" as="xs:integer"
select="max( ( 2, R:record-span( $seq ) ) )" />
<sequence select="$header
and R:is-record( $header )
and count( $seq ) ge $slen" />
</function>
<!--
Determine the @ref{record span} of the record sequence @var{Rseq}.
This function checks to ensure that the first item in the sequence is a
@ref{Record Header},
but it @emph{does not} ensure that the provided sequence is a valid
@ref{record sequence};
to do so, use @ref{R:is-record-seq#1}.
-->
<function name="R:record-span" as="xs:integer?">
<param name="Rseq" as="item()+" />
<variable name="header" as="item()?"
select="$Rseq[ 1 ]" />
<variable name="span" as="item()?"
select="$Rseq[ 2 ]" />
<sequence select="if ( R:is-record( $header )
and $span instance of xs:integer ) then
$span
else ()" />
</function>
<!--
@node Record Polymorphism
@section Record Polymorphism

View File

@ -281,6 +281,128 @@
</scenario>
<scenario label="Record data function">
<!-- yes, this tests implementation details rather than relying on
abstractions provided by the API; this is intentional to ensure
that records are operating according to their specification -->
<scenario label="R:init-slots">
<variable name="anon-record"
select="R:make-record-header( 3 )" />
<call function="R:init-slots">
<param name="Record" select="$anon-record" />
</call>
<expect label="produces a sequence of length 2N+2"
test="count( $x:result ) eq 8" />
<expect label="first item is record header"
test="$x:result[ 1 ] = $anon-record" />
<expect label="second item is record span"
test="$x:result[ 2 ] = 8" />
<expect label="R:record-span agrees with span item"
test="R:record-span( $x:result ) = $x:result[ 2 ]" />
<expect label="slot offsets are initialized to slot positions"
test="$x:result[ 3 ] eq 1
and $x:result[ 4 ] eq 2
and $x:result[ 5 ] eq 3" />
<expect label="slots are initialized with empty values"
test="every $slot in subsequence( $x:result, 6 ) satisfies
$slot eq $R:empty-slot" />
</scenario>
<scenario label="R:is-record-seq">
<scenario label="given an empty sequence">
<call function="R:is-record-seq">
<param name="seq"
select="()" />
</call>
<expect label="yields false()"
test="$x:result eq false()" />
</scenario>
<scenario label="given a non-record header">
<call function="R:is-record-seq">
<param name="seq"
select="n:element( $test-qname )" />
</call>
<expect label="yields false()"
test="$x:result eq false()" />
</scenario>
<scenario label="given a record header only">
<call function="R:is-record-seq">
<param name="seq"
select="R:make-record-header( 0 )" />
</call>
<expect label="yields false()"
test="$x:result eq false()" />
</scenario>
<scenario label="given a record with sequence lt span">
<call function="R:is-record-seq">
<param name="seq"
select="subsequence(
R:init-slots(
R:make-record-header( 2 ) ),
1, 4 )" />
</call>
<expect label="yields false()"
test="$x:result eq false()" />
</scenario>
<scenario label="given a full record sequence">
<call function="R:is-record-seq">
<param name="seq"
select="R:init-slots(
R:make-record-header( 2 ) )" />
</call>
<expect label="yields true()"
test="$x:result eq true()" />
</scenario>
</scenario>
<!-- see above scenario for record test -->
<scenario label="R:record-span">
<scenario label="given a non-record">
<call function="R:record-span">
<param name="Rseq"
select="n:element( $test-qname )" />
</call>
<expect label="returns the empty sequence"
test="empty( $x:result )" />
</scenario>
<scenario label="given a record with an invalid span">
<call function="R:record-span">
<param name="Rseq"
select="R:make-record-header( 0 ), 'invalid'" />
</call>
<expect label="returns the empty sequence"
test="empty( $x:result )" />
</scenario>
</scenario>
</scenario>
<scenario label="R:is-record">
<scenario label="given a record">
<call function="R:is-record">