Reintroduce legacy classification system, place new behind flag

This largely reintroduces the legacy classification system, but there are a
number of things that are not affected by the flag.  For example:

  1. Alias classifications are still optimized when the flag is off;
  2. Classifications without predicates emit slightly different code than
     before, though their functionality has not changed;
  3. There's been a lot of refactoring and minor optimizations that are
     unaffected by the flag;
  4. lv:match/@pattern will now emit a warning; and
  5. Cleaning and casting of input data is not gated.

This allows us to incrementally migrate to the new system where behavior may
be different, but this is admittedly a bit dangerous in that the new system
was aggressively tested and reasoned about, so reintroducing the legacy
system may combine in unexpected ways.
master
Mike Gerwitz 2021-06-09 15:21:34 -04:00
parent 5f6cb4cf51
commit 934824b2ee
5 changed files with 1112 additions and 158 deletions

View File

@ -31,179 +31,228 @@
may need further adjustment to thwart future optimizations (or a way to
explicitly inhibit them).
<t:describe name="classify">
<t:describe name="without predicates">
<t:it desc="yields TRUE for conjunction">
<classify as="conj-no-pred" yields="conjNoPred"
desc="No predicate, conjunction" />
<t:given name="conjNoPred" />
<template name="_class-tests_" desc="Classification system tests">
<param name="@system@" desc="SUT (lowercase)" />
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<param name="@systemuc@" desc="SUT (title case)">
<param-value name="@system@" ucfirst="true" />
</param>
<t:it desc="yields FALSE for disjunction">
<classify as="disj-no-pred" yields="disjNoPred"
any="true"
desc="No predicate, disjunction" />
<t:describe name="{@system@} classify">
<t:describe name="without predicates">
<t:it desc="yields TRUE for conjunction">
<classify as="conj-no-pred-{@system@}"
yields="conjNoPred{@systemuc@}"
desc="No predicate, conjunction" />
<t:given name="disjNoPred" />
<t:given name="conjNoPred{@systemuc@}" />
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
</t:describe>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:describe name="with scalar predicates">
<t:it desc="yields TRUE when scalar value is TRUE">
<t:given-classify>
<match on="alwaysTrue" />
</t:given-classify>
<t:it desc="yields FALSE for disjunction">
<classify as="disj-no-pred-{@system@}"
yields="disjNoPred{@systemuc@}"
any="true"
desc="No predicate, disjunction" />
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:given name="disjNoPred{@systemuc@}" />
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
</t:describe>
<t:it desc="yields FALSE when scalar value is FALSE">
<t:given-classify>
<match on="neverTrue" />
</t:given-classify>
<t:describe name="with scalar predicates">
<t:it desc="yields TRUE when scalar value is TRUE">
<t:given-classify>
<match on="alwaysTrue" />
</t:given-classify>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:it desc="yields TRUE for all-true scalar conjunction">
<t:given-classify>
<match on="alwaysTrue" />
<match on="neverTrue" value="FALSE" />
</t:given-classify>
<t:it desc="yields FALSE when scalar value is FALSE">
<t:given-classify>
<match on="neverTrue" />
</t:given-classify>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:it desc="yields TRUE for all-true scalar disjunction">
<t:given-classify>
<any>
<t:it desc="yields TRUE for all-true scalar conjunction">
<t:given-classify>
<match on="alwaysTrue" />
<match on="neverTrue" value="FALSE" />
</any>
</t:given-classify>
</t:given-classify>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:it desc="yields TRUE for single-true scalar disjunction">
<t:given-classify>
<any>
<match on="alwaysTrue" />
<match on="neverTrue" />
</any>
</t:given-classify>
<t:it desc="yields TRUE for all-true scalar disjunction">
<t:given-classify>
<any>
<match on="alwaysTrue" />
<match on="neverTrue" value="FALSE" />
</any>
</t:given-classify>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
</t:describe>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:describe name="with vector predicates">
<t:it desc="yields TRUE for all-true element-wise conjunction">
<t:given-classify-scalar>
<match on="NVEC3" value="ZERO" />
<match on="nClass3" value="TRUE" />
</t:given-classify-scalar>
<t:it desc="yields TRUE for single-true scalar disjunction">
<t:given-classify>
<any>
<match on="alwaysTrue" />
<match on="neverTrue" />
</any>
</t:given-classify>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
</t:describe>
<t:it desc="yields FALSE for some-true element-wise conjunction">
<t:given-classify-scalar>
<match on="NVEC3" value="ZERO" />
<match on="nClass3" value="FALSE" />
</t:given-classify-scalar>
<t:describe name="with vector predicates">
<t:it desc="yields TRUE for all-true element-wise conjunction">
<t:given-classify-scalar>
<match on="NVEC3" value="ZERO" />
<match on="nClass3" value="TRUE" />
</t:given-classify-scalar>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:it desc="yields TRUE for some-true element-wise disjunction">
<t:given-classify-scalar>
<any>
<t:it desc="yields FALSE for some-true element-wise conjunction">
<t:given-classify-scalar>
<match on="NVEC3" value="ZERO" />
<match on="nClass3" value="FALSE" />
</any>
</t:given-classify-scalar>
</t:given-classify-scalar>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:it desc="yields FALSE for all-false element-wise disjunction">
<t:given-classify-scalar>
<any>
<match on="NVEC3" value="TRUE" />
<match on="nClass3" value="FALSE" />
</any>
</t:given-classify-scalar>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
The old classification system would interpret missing values as $0$,
which could potentially trigger a match.
The new classification system will always yield \tparam{FALSE}
regardless of predicate when values are undefined.
<t:describe name="of different lengths">
<t:describe name="with legacy classification system">
<t:it desc="interprets undefined values as zero during match">
<classify as="vec-len-mismatch-conj-legacy"
yields="vecLenMismatchConjLegacy"
desc="Multi vector length mismatch (legacy)">
<!-- actually ZERO for all indexes -->
<t:it desc="yields TRUE for some-true element-wise disjunction">
<t:given-classify-scalar>
<any>
<match on="NVEC3" value="ZERO" />
<match on="nClass3" value="FALSE" />
</any>
</t:given-classify-scalar>
<!-- legacy system, implicitly zero for match -->
<match on="NVEC2" value="ZERO" />
</classify>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:given>
<c:value-of name="vecLenMismatchConjLegacy" index="#2" />
</t:given>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:it desc="yields FALSE for all-false element-wise disjunction">
<t:given-classify-scalar>
<any>
<match on="NVEC3" value="TRUE" />
<match on="nClass3" value="FALSE" />
</any>
</t:given-classify-scalar>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
The old classification system would interpret missing values as $0$,
which could potentially trigger a match.
The new classification system will always yield \tparam{FALSE}
regardless of predicate when values are undefined.
<t:describe name="of different lengths">
<if name="@system@" eq="legacy">
<t:describe name="with legacy classification system">
<t:it desc="interprets undefined values as zero during match">
<classify as="vec-len-mismatch-conj-{@system@}"
yields="vecLenMismatchConj{@systemuc@}"
desc="Multi vector length mismatch (legacy)">
<!-- actually ZERO for all indexes -->
<match on="NVEC3" value="ZERO" />
<!-- legacy system, implicitly zero for match -->
<match on="NVEC2" value="ZERO" />
</classify>
<t:given>
<c:value-of name="vecLenMismatchConj{@systemuc@}" index="#2" />
</t:given>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
</t:describe>
</if>
<if name="@system@" eq="new">
<t:describe name="with new classification system">
<t:it desc="yields false for conjunction rather than implicit zero">
<classify as="vec-len-mismatch-conj-{@system@}"
yields="vecLenMismatchConj{@systemuc@}"
desc="Multi vector length mismatch (new system)">
<!-- actually ZERO for all indexes -->
<match on="NVEC3" value="ZERO" />
<!-- must not be implicitly ZERO for third index -->
<match on="NVEC2" value="ZERO" />
</classify>
<t:given>
<c:value-of name="vecLenMismatchConj{@systemuc@}" index="#2" />
</t:given>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
</t:describe>
</if>
</t:describe>
</t:describe>
</t:describe>
</t:describe>
</template>
<section title="Legacy System Tests">
<t:class-tests system="legacy" />
</section>
<section title="New System Tests">
<t:use-new-classification-system />
<t:class-tests system="new" />
</section>
</package>

View File

@ -0,0 +1,508 @@
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Compiles rater XML into JavaScript (legacy classification system)
Copyright (C) 2014-2021 Ryan Specialty Group, LLC.
This file is part of TAME.
TAME is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see
<http://www.gnu.org/licenses/>.
-->
<stylesheet version="2.0"
xmlns="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:lv="http://www.lovullo.com/rater"
xmlns:lvp="http://www.lovullo.com"
xmlns:c="http://www.lovullo.com/calc"
xmlns:w="http://www.lovullo.com/rater/worksheet"
xmlns:wc="http://www.lovullo.com/rater/worksheet/compiler"
xmlns:compiler="http://www.lovullo.com/rater/compiler"
xmlns:calc-compiler="http://www.lovullo.com/calc/compiler"
xmlns:preproc="http://www.lovullo.com/rater/preproc"
xmlns:util="http://www.lovullo.com/util"
xmlns:ext="http://www.lovullo.com/ext">
<function name="compiler:compile-classify-legacy" as="xs:string+">
<param name="symtable-map" as="map(*)" />
<param name="classify" as="element( lv:classify )" />
<apply-templates mode="compile-legacy" select="$classify">
<with-param name="symtable-map" select="$symtable-map"
tunnel="yes" />
</apply-templates>
</function>
<!--
Generate code to perform a classification
Based on the criteria provided by the classification, generate and store the
result of a boolean expression performing the classification using global
arguments.
@return generated classification expression
-->
<template match="lv:classify" mode="compile-legacy">
<param name="symtable-map" as="map(*)" tunnel="yes" />
<param name="noclass" />
<param name="ignores" />
<variable name="self" select="." />
<value-of select="$compiler:nl" />
<variable name="dest">
<text>A['</text>
<value-of select="@yields" />
<text>']</text>
</variable>
<if test="not( $noclass )">
<sequence select="concat( $dest, '=[];', $compiler:nl )" />
<if test="@preproc:generated='true'">
<text>g</text>
</if>
<text>c['</text>
<value-of select="@as" />
<text>'] = (function(){var result,tmp; </text>
</if>
<!-- locate classification predicates (since lv:any and lv:all are split
into their own classifications, matching on any depth ensures we get
into any preproc:* nodes as well) -->
<variable name="criteria" as="element( lv:match )*"
select="./lv:match[
not( $ignores )
or not( @on=$ignores/@ref ) ]" />
<variable name="criteria-syms" as="element( preproc:sym )*"
select="for $match in $criteria
return $symtable-map( $match/@on )" />
<!-- generate boolean value from match expressions -->
<choose>
<!-- if classification criteria were provided, then use them -->
<when test="$criteria">
<variable name="op" as="xs:string"
select="compiler:match-group-op( $self )" />
<text></text>
<!-- order matches from highest to lowest dimensions (required for
the cmatch algorithm)-->
<for-each select="reverse( xs:integer( min( $criteria-syms/@dim ) )
to xs:integer( max( $criteria-syms/@dim ) ) )">
<apply-templates mode="compile-legacy"
select="$criteria[
@on = $criteria-syms[
@dim = current() ]/@name ]">
<with-param name="ignores" select="$ignores" />
<with-param name="operator" select="$op" />
</apply-templates>
</for-each>
</when>
<!-- if no classification criteria, then always true/false -->
<otherwise>
<!-- this is only useful if $noclass is *not* set -->
<if test="not( $noclass )">
<choose>
<!-- universal -->
<when test="not( @any='true' )">
<text>tmp = true; </text>
</when>
<!-- existential -->
<otherwise>
<text>tmp = false; </text>
</otherwise>
</choose>
</if>
<!-- if @yields was provided, then store the value in a variable of their
choice as well (since cmatch will not be done) -->
<if test="@yields">
<value-of select="$dest" />
<choose>
<!-- universal -->
<when test="not( @any='true' )">
<text> = 1;</text>
</when>
<!-- existential -->
<otherwise>
<text> = 0;</text>
</otherwise>
</choose>
</if>
</otherwise>
</choose>
<text> return tmp;})();</text>
<!-- support termination on certain classifications (useful for eligibility
and error conditions) -->
<if test="@terminate = 'true'">
<text>if ( _canterm &amp;&amp; </text>
<if test="@preproc:generated='true'">
<text>g</text>
</if>
<text>c['</text>
<value-of select="@as" />
<text>'] ) throw Error( '</text>
<value-of select="replace( @desc, '''', '\\''' )" />
<text>' );</text>
<value-of select="$compiler:nl" />
</if>
<variable name="sym"
select="$symtable-map( $self/@yields )" />
<!-- if we are not any type of set, then yield the value of the first
index (note the $criteria check; see above); note that we do not do
not( @set ) here, since that may have ill effects as it implies that
the node is not preprocessed -->
<!-- TODO: this can be simplified, since @yields is always provided -->
<if test="$criteria and @yields and ( $sym/@dim='0' )">
<value-of select="$dest" />
<text> = </text>
<value-of select="$dest" />
<text>[0];</text>
<value-of select="$compiler:nl" />
</if>
</template>
<!--
Generate code asserting a match
Siblings are joined by default with ampersands to denote an AND relationship,
unless overridden.
@return generated match code
-->
<template match="lv:match" mode="compile-legacy" priority="1">
<param name="symtable-map" as="map(*)" tunnel="yes" />
<!-- default to all matches being required -->
<param name="operator" select="'&amp;&amp;'" />
<param name="yields" select="../@yields" />
<variable name="name" select="@on" />
<variable name="sym-on" as="element( preproc:sym )"
select="$symtable-map( $name )" />
<text> tmp = </text>
<variable name="input-raw">
<choose>
<!-- if we have assumptions, then we'll be recalculating (rather than
referencing) an existing classification -->
<when test="lv:assuming">
<text>_cassume</text>
</when>
<otherwise>
<choose>
<when test="$sym-on/@type = 'const'">
<text>consts</text>
</when>
<otherwise>
<text>A</text>
</otherwise>
</choose>
<text>['</text>
<value-of select="translate( @on, &quot;'&quot;, '' )" />
<text>']</text>
</otherwise>
</choose>
</variable>
<!-- yields (if not set, generate one so that cmatches still works properly)
-->
<variable name="yieldto">
<call-template name="compiler:gen-match-yieldto">
<with-param name="yields" select="$yields" />
</call-template>
</variable>
<!-- the input value -->
<variable name="input">
<choose>
<when test="@scalar = 'true'">
<text>stov( </text>
<value-of select="$input-raw" />
<text>, ( ( </text>
<value-of select="$yieldto" />
<!-- note that we default to 1 so that there is at least a single
element (which will be the case of the scalar is the first match)
in a given classification; the awkward inner [] is to protect
against potentially undefined values and will hopefully never
happen, and the length is checked on the inner grouping rather than
on the outside of the entire expression to ensure that it will
yield the intended result if yieldto.length === 0 -->
<text> || [] ).length || 1 ) )</text>
</when>
<otherwise>
<value-of select="$input-raw" />
</otherwise>
</choose>
</variable>
<if test="lv:assuming">
<text>(function(){</text>
<!-- initialize variable (ensuring that the closure we're about to generate
will properly assign the value rather than encapsulate it) -->
<text>var </text>
<value-of select="$input-raw" />
<text>; </text>
<!-- generate assumptions and recursively generate the referenced
classification -->
<apply-templates select="." mode="compile-match-assumptions">
<with-param name="result-var" select="$input-raw" />
</apply-templates>
<text>; return </text>
</if>
<!-- invoke the classification matcher on this input -->
<text>anyValue( </text>
<value-of select="$input" />
<text>, </text>
<!-- TODO: error if multiple; also, refactor -->
<choose>
<when test="@value">
<variable name="value" select="@value" />
<variable name="sym" as="element( preproc:sym )?"
select="$symtable-map( $value )" />
<choose>
<!-- value unavailable (TODO: vector/matrix support) -->
<when test="$sym and not( $sym/@value )">
<message>
<text>[jsc] !!! bad classification match: `</text>
<value-of select="$value" />
<text>' is not a scalar constant</text>
</message>
</when>
<!-- simple constant -->
<when test="$sym and @value">
<value-of select="$sym/@value" />
</when>
<otherwise>
<text>'</text>
<!-- TODO: Should we disallow entirely? -->
<message>
<text>[jsc] warning: static classification match '</text>
<value-of select="$value" />
<text>' in </text>
<value-of select="ancestor::lv:classify[1]/@as" />
<text>; use calculation predicate or constant instead</text>
</message>
<value-of select="$value" />
<text>'</text>
</otherwise>
</choose>
</when>
<when test="@pattern">
<text>function( val ) { </text>
<text>return /</text>
<value-of select="@pattern" />
<text>/.test( val );</text>
<text> }</text>
</when>
<when test="./c:*">
<text>function( val, __$$i ) { </text>
<text>return ( </text>
<for-each select="./c:*">
<if test="position() > 1">
<text disable-output-escaping="yes"> &amp;&amp; </text>
</if>
<text>( val </text>
<apply-templates select="." mode="compile-calc-when" />
<text> ) </text>
</for-each>
<text>);</text>
<text>}</text>
</when>
<otherwise>
<apply-templates select="." mode="compiler:match-anyof-legacy" />
</otherwise>
</choose>
<text>, </text>
<value-of select="$yieldto" />
<text>, </text>
<!-- if this match is part of a classification that should yield a matrix,
then force a matrix set -->
<choose>
<when test="ancestor::lv:classify/@set = 'matrix'">
<text>true</text>
</when>
<otherwise>
<text>false</text>
</otherwise>
</choose>
<text>, </text>
<choose>
<when test="parent::lv:classify/@any='true'">
<text>false</text>
</when>
<otherwise>
<text>true</text>
</otherwise>
</choose>
<!-- for debugging -->
<if test="$debug-id-on-stack">
<text>/*!+*/,"</text>
<value-of select="$input" />
<text>"/*!-*/</text>
</if>
<!-- end of anyValue() call -->
<text> ) </text>
<!-- end of assuming function call -->
<if test="lv:assuming">
<text>})()</text>
</if>
<text>;</text>
<text>/*!+*/( D['</text>
<value-of select="@_id" />
<text>'] || ( D['</text>
<value-of select="@_id" />
<text>'] = [] ) ).push( tmp );/*!-*/ </text>
<text>result = </text>
<choose>
<!-- join with operator if not first in set -->
<when test="position() > 1">
<text>result </text>
<value-of select="$operator" />
<text> tmp;</text>
</when>
<otherwise>
<text>tmp;</text>
</otherwise>
</choose>
</template>
<!--
Handles the special "float" domain
Rather than the impossible task of calculating all possible floats and
asserting that the given values is within that set, the obvious task is to
assert whether or not the value is logically capable of existing in such a
set based on a definition of such a set.
See also "integer"
-->
<template match="lv:match[ @anyOf='float' ]" mode="compiler:match-anyof-legacy" priority="5">
<!-- ceil(x) - floor(x) = [ x is not an integer ] -->
<text>function( val ) {</text>
<text>return ( typeof +val === 'number' ) </text>
<text disable-output-escaping="yes">&amp;&amp; </text>
<!-- note: greater than or equal to, since we want to permit integers as
well -->
<text disable-output-escaping="yes">( Math.ceil( val ) >= Math.floor( val ) )</text>
<text>;</text>
<text>}</text>
</template>
<!--
Handles the special "float" domain
Rather than the impossible task of calculating all possible integers and
asserting that the given values is within that set, the obvious task is to
assert whether or not the value is logically capable of existing in such a
set based on a definition of such a set.
See also "float"
-->
<template match="lv:match[ @anyOf='integer' ]" mode="compiler:match-anyof-legacy" priority="5">
<!-- ceil(x) - floor(x) = [ x is not an integer ] -->
<text>function( val ) {</text>
<text>return ( typeof +val === 'number' ) </text>
<text disable-output-escaping="yes">&amp;&amp; </text>
<text>( Math.floor( val ) === Math.ceil( val ) )</text>
<text>;</text>
<text>}</text>
</template>
<!--
Handles matching on an empty set
This is useful for asserting against fields that may have default values,
since in such a case an empty value would be permitted.
-->
<template match="lv:match[ @anyOf='empty' ]" mode="compiler:match-anyof-legacy" priority="5">
<!-- ceil(x) - floor(x) = [ x is not an integer ] -->
<text>function( val ) {</text>
<text>return ( val === '' ) </text>
<text>|| ( val === undefined ) || ( val === null )</text>
<text>;</text>
<text>}</text>
</template>
<!--
Uh oh. Hopefully this never happens; will throw an exception if a type is
defined as a base type (using typedef), but is not handled by the compiler.
-->
<template match="lv:match[ @anyOf=root(.)//lv:typedef[ ./lv:base-type ]/@name ]"
mode="compiler:match-anyof-legacy" priority="3">
<text>function( val ) {</text>
<text>throw Error( 'CRITICAL: Unhandled base type: </text>
<value-of select="@anyOf" />
<text>' );</text>
<text>}</text>
</template>
<!--
Used for user-defined domains
-->
<template match="lv:match[ @anyOf ]" mode="compiler:match-anyof-legacy" priority="1">
<text>types['</text>
<value-of select="@anyOf" />
<text>'].values</text>
</template>
</stylesheet>

View File

@ -39,6 +39,8 @@
xmlns:util="http://www.lovullo.com/util"
xmlns:ext="http://www.lovullo.com/ext">
<!-- legacy classification system -->
<include href="js-legacy.xsl" />
<!-- calculation compiler -->
<include href="js-calc.xsl" />
@ -629,15 +631,33 @@
</function>
<template mode="compile" priority="6"
match="lv:classify[ compiler:use-legacy-classify(.) ]">
<param name="symtable-map" as="map(*)" tunnel="yes" />
<sequence select="concat(
$compiler:nl,
'/*!lc*/',
string-join(
compiler:compile-classify-legacy( $symtable-map, . ),
'' ),
'/*lc!*/' )" />
</template>
<template match="lv:classify" mode="compile" priority="5">
<param name="symtable-map" as="map(*)" tunnel="yes" />
<message select="concat( 'internal: new: ', @as )" />
<sequence select="compiler:compile-classify-assign( $symtable-map, . )" />
</template>
<template mode="compile" priority="8"
match="lv:classify[ @preproc:inline='true' ]">
match="lv:classify[
@preproc:inline='true'
and not( compiler:use-legacy-classify(.) ) ]">
<!-- emit nothing; it'll be inlined at the match site -->
</template>
@ -680,6 +700,19 @@
</function>
<function name="compiler:use-legacy-classify" as="xs:boolean">
<param name="classify" as="element( lv:classify )" />
<variable name="flagname" as="xs:string"
select="'___feature-newclassify'" />
<sequence select="empty(
( $classify | $classify/ancestor::* )
/preceding-sibling::preproc:tpl-meta[
@name=$flagname and @value = '1' ] )" />
</function>
<function name="compiler:compile-classify-assign" as="xs:string">
<param name="symtable-map" as="map(*)" />
<param name="classify" as="element( lv:classify )" />
@ -1230,6 +1263,142 @@
</function>
<!--
Generate code asserting a match
Siblings are joined by default with ampersands to denote an AND relationship,
unless overridden.
@return generated match code
-->
<template match="lv:match" mode="compile" priority="1">
<param name="symtable-map" as="map(*)" tunnel="yes" />
<!-- default to all matches being required -->
<param name="operator" select="'&amp;&amp;'" />
<param name="yields" select="../@yields" />
<variable name="name" select="@on" />
<text> tmp=</text>
<variable name="input-raw" as="xs:string"
select="compiler:match-name-on( $symtable-map, . )" />
<!-- yields (if not set, generate one so that cmatches still works properly)
-->
<variable name="yieldto">
<call-template name="compiler:gen-match-yieldto">
<with-param name="yields" select="$yields" />
</call-template>
</variable>
<!-- the input value -->
<variable name="input">
<choose>
<when test="@scalar = 'true'">
<text>stov( </text>
<value-of select="$input-raw" />
<text>, ((</text>
<value-of select="$yieldto" />
<!-- note that we default to 1 so that there is at least a single
element (which will be the case of the scalar is the first match)
in a given classification; the awkward inner [] is to protect
against potentially undefined values and will hopefully never
happen, and the length is checked on the inner grouping rather than
on the outside of the entire expression to ensure that it will
yield the intended result if yieldto.length === 0 -->
<text>||[]).length||1))</text>
</when>
<otherwise>
<value-of select="$input-raw" />
</otherwise>
</choose>
</variable>
<!-- invoke the classification matcher on this input -->
<text>anyValue( </text>
<value-of select="$input" />
<text>, </text>
<!-- TODO: error if multiple; also, refactor -->
<choose>
<when test="@value">
<value-of select="compiler:match-value( $symtable-map, . )" />
</when>
<when test="@pattern">
<text>function(val) {</text>
<text>return /</text>
<value-of select="@pattern" />
<text>/.test(val);</text>
<text>}</text>
</when>
<when test="./c:*">
<text>function(val, __$$i) { </text>
<text>return (</text>
<for-each select="./c:*">
<if test="position() > 1">
<text disable-output-escaping="yes"> &amp;&amp; </text>
</if>
<text>(val </text>
<apply-templates select="." mode="compile-calc-when" />
<text>)</text>
</for-each>
<text>);</text>
<text>}</text>
</when>
<otherwise>
<apply-templates select="." mode="compiler:match-anyof" />
</otherwise>
</choose>
<text>, </text>
<value-of select="$yieldto" />
<text>, </text>
<!-- if this match is part of a classification that should yield a matrix,
then force a matrix set -->
<choose>
<when test="ancestor::lv:classify/@set = 'matrix'">
<text>true</text>
</when>
<otherwise>
<text>false</text>
</otherwise>
</choose>
<text>, </text>
<choose>
<when test="parent::lv:classify/@any='true'">
<text>false</text>
</when>
<otherwise>
<text>true</text>
</otherwise>
</choose>
<!-- for debugging -->
<if test="$debug-id-on-stack">
<text>/*!+*/,"</text>
<value-of select="$input" />
<text>"/*!-*/</text>
</if>
<!-- end of anyValue() call -->
<text>);</text>
<text>/*!+*/(D['</text>
<value-of select="@_id" />
<text>']||(D['</text>
<value-of select="@_id" />
<text>']=[])).push(tmp);/*!-*/ </text>
</template>
<template name="compiler:gen-match-yieldto">
<param name="yields" />
@ -1822,29 +1991,190 @@
function cgtei(y) { return function (x, i) { return +(x >= (y[i]||0)); }; }
function cltei(y) { return function (x, i) { return +(x <= (y[i]||0)); }; }
/**
* Return the length of the longest set
* Checks for matches against values for any param value
*
* Provide each set as its own argument.
* A single successful match will result in a successful classification.
*
* @return number length of longest set
* For an explanation and formal definition of this algorithm, please see
* the section entitled "Classification Match (cmatch) Algorithm" in the
* manual.
*
* @param {Array|string} param value or set of values to check
* @param {Array|string} values or set of values to match against
* @param {Object} yield_to object to yield into
* @param {boolean} clear when true, AND results; otherwise, OR
*
* @return {boolean} true if any match is found, otherwise false
*/
function longerOf()
function anyValue( param, values, yield_to, ismatrix, clear, _id )
{
var i = arguments.length,
len = 0;
// convert everything to an array if needed (we'll assume all objects to
// be arrays; Array.isArray() is ES5-only) to make them easier to work
// with
if ( !Array.isArray( param ) )
{
param = [ param ];
}
var values_orig = values;
if ( typeof values !== 'object' )
{
// the || 0 here ensures that non-values are treated as 0, as
// mentioned in the specification
values = [ values || 0 ];
}
else
{
var tmp = [];
for ( var v in values )
{
tmp.push( v );
}
values = tmp;
}
// if no yield var name was provided, we'll just be storing in a
// temporary array which will be discarded when it goes out of scope
// (this is the result vector in the specification)
var store = yield_to || [];
var i = param.length,
found = false,
scalar = ( i === 0 ),
u = ( store.length === 0 ) ? clear : false;
while ( i-- )
{
var thislen = arguments[ i ].length;
// these var names are as they appear in the algorithm---temporary,
// and value
var t,
v = returnOrReduceOr( store[ i ], u );
if ( thislen > len )
// recurse on vectors
if ( Array.isArray( param[ i ] ) || Array.isArray( store[ i ] ) )
{
len = thislen;
var r = deepClone( store[ i ] || [] );
if ( !Array.isArray( r ) )
{
r = [ r ];
}
var rfound = !!anyValue( param[ i ], values_orig, r, false, clear, _id );
found = ( found || rfound );
if ( Array.isArray( store[ i ] )
|| ( store[ i ] === undefined )
)
{
// we do not want to reduce; this is the match that we are
// interested in
store[ i ] = r;
continue;
}
else
{
t = returnOrReduceOr( r, clear );
}
}
else
{
// we have a scalar, folks!
scalar = true;
t = anyPredicate( values, ( param[ i ] || 0 ), i );
}
store[ i ] = +( ( clear )
? ( v && t )
: ( v || t )
);
// equivalent of "Boolean Classification Match" section of manual
found = ( found || !!store[ i ] );
}
if ( store.length > param.length )
{
var sval = ( scalar ) ? anyPredicate( values, param[0] ) : null;
if ( typeof sval === 'function' )
{
// pass the scalar value to the function
sval = values[0]( param[0] );
}
// XXX: review the algorithm; this is a mess
for ( var k = param.length, l = store.length; k < l; k++ )
{
// note that this has the same effect as initializing (in the
// case of a scalar) the scalar to the length of the store
var v = +(
( returnOrReduceOr( store[ k ], clear )
|| ( !clear && ( scalar && sval ) )
)
&& ( !clear || ( scalar && sval ) )
);
store[ k ] = ( scalar )
? v
: [ v ];
found = ( found || !!v );
}
}
return len;
return found;
}
function anyPredicate( preds, value, index )
{
return preds.some( function( p ) {
return (typeof p === 'function')
? p(value, index)
: p == value;
} );
}
function returnOrReduceOr( arr, c )
{
if ( arr === undefined )
{
return !!c;
}
else if ( !( arr.length ) )
{
return arr;
}
return arr.reduce( function( a, b ) {
return a || returnOrReduceOr( b, c );
} );
}
function returnOrReduceAnd( arr, c )
{
if ( arr === undefined )
{
return !!c;
}
else if ( !( arr.length ) )
{
return arr;
}
return arr.reduce( function( a, b ) {
return a && returnOrReduceAnd( b, c );
} );
}
function deepClone( arr )
{
if ( !Array.isArray( arr ) ) return arr;
return arr.map( deepClone );
}
@ -1943,6 +2273,31 @@
}
/**
* Return the length of the longest set
*
* Provide each set as its own argument.
*
* @return number length of longest set
*/
function longerOf()
{
var i = arguments.length,
len = 0;
while ( i-- )
{
var thislen = arguments[ i ].length;
if ( thislen > len )
{
len = thislen;
}
}
return len;
}
/* scalar to vector */
function stov( s, n )
{
@ -1964,6 +2319,21 @@
}
function argreplace( orig, value )
{
if ( !( typeof orig === 'object' ) )
{
return value;
}
// we have an object; recurse
for ( var i in orig )
{
return argreplace( orig[ i ], value );
}
}
function init_defaults( args, params )
{
for ( var param in params )

View File

@ -38,6 +38,7 @@
xmlns:lv="http://www.lovullo.com/rater"
xmlns:ext="http://www.lovullo.com/ext"
xmlns:c="http://www.lovullo.com/calc"
xmlns:compiler="http://www.lovullo.com/rater/compiler"
xmlns:lvv="http://www.lovullo.com/rater/validate"
xmlns:sym="http://www.lovullo.com/rater/symbol-map"
xmlns:preproc="http://www.lovullo.com/rater/preproc">
@ -268,17 +269,30 @@
</template>
<!-- @pattern support removed -->
<template match="lv:match[@pattern]" mode="lvv:validate-match" priority="9">
<call-template name="lvv:error">
<with-param name="desc" select="'lv:match[@pattern] support removed'" />
<with-param name="refnode" select="." />
<with-param name="content">
<text>use lookup tables in place of @pattern in `</text>
<value-of select="parent::lv:classify/@as" />
<text>'</text>
</with-param>
</call-template>
<choose>
<!-- warn of upcoming removal -->
<when test="compiler:use-legacy-classify( ancestor::lv:classify )">
<message select="concat( 'warning: ',
ancestor::lv:classify/@as,
': lv:match[@pattern] support is deprecated ',
'and is removed with the new classification ',
'system; use lookup tables instead' )" />
</when>
<!-- @pattern support removed in the new classification system -->
<otherwise>
<call-template name="lvv:error">
<with-param name="desc" select="'lv:match[@pattern] support removed'" />
<with-param name="refnode" select="." />
<with-param name="content">
<text>use lookup tables in place of @pattern in `</text>
<value-of select="parent::lv:classify/@as" />
<text>'</text>
</with-param>
</call-template>
</otherwise>
</choose>
</template>

View File

@ -27,6 +27,7 @@
xmlns:lv="http://www.lovullo.com/rater"
xmlns:t="http://www.lovullo.com/rater/apply-template"
xmlns:c="http://www.lovullo.com/calc"
xmlns:compiler="http://www.lovullo.com/rater/compiler"
xmlns:eseq="http://www.lovullo.com/tame/preproc/expand/eseq"
xmlns:ext="http://www.lovullo.com/ext">
@ -287,7 +288,11 @@
( lv:any | lv:all )
and not( eseq:is-expandable(.) ) ]">
<variable name="result">
<apply-templates select="." mode="preproc:class-groupgen" />
<apply-templates select="." mode="preproc:class-groupgen">
<with-param name="legacy-classify"
select="compiler:use-legacy-classify( . )"
tunnel="yes" />
</apply-templates>
</variable>
<apply-templates select="$result/lv:classify" mode="preproc:class-extract" />
@ -408,6 +413,8 @@
<template match="lv:any|lv:all" mode="preproc:class-groupgen" priority="5">
<param name="legacy-classify" as="xs:boolean" tunnel="yes" />
<!-- this needs to be unique enough that there is unlikely to be a conflict
between generated ids in various packages; generate-id is not enough for
cross-package guarantees (indeed, I did witness conflicts), so there is
@ -422,19 +429,25 @@
<lv:classify as="{$id}" yields="{$yields}"
preproc:generated="true"
preproc:generated-from="{$parent-name}"
preproc:inline="true"
desc="(generated from predicate group of {$parent-name}">
<if test="local-name() = 'any'">
<attribute name="any" select="'true'" />
</if>
<if test="not( $legacy-classify )">
<attribute name="preproc:inline" select="'true'" />
</if>
<apply-templates mode="preproc:class-groupgen" />
</lv:classify>
<!-- this will remain in its place -->
<lv:match on="{$yields}" value="TRUE"
preproc:generated="true"
preproc:inline="true" />
preproc:generated="true">
<if test="not( $legacy-classify )">
<attribute name="preproc:inline" select="'true'" />
</if>
</lv:match>
</template>