Compiler runtime optimizations with classification system rewrite

See RELEASES.md for a list of changes.

This was a significant effort that began about six months ago, but was
paused at a number of points.  Rather than risking further pauses from
interruptions, the new classification system has been gated behind a
package-level feature flag, since it causes BC breaks in certain buggy
situations.

Since this flag was introduced late, there is the potential that it causes
bugs when new optimizations are mixed with the old system.
master
Mike Gerwitz 2021-06-23 12:47:12 -04:00
commit eef2a5d4bc
15 changed files with 2818 additions and 942 deletions

View File

@ -1,4 +1,4 @@
# This number is incremented for every compiler change to force rebuilding
# of xmlo files.
2
3

View File

@ -16,9 +16,45 @@ commits that introduce the changes. To make a new release, run
NEXT
====
This release focuses primarily on compiler optimizations that affect runtime
performance (both CPU and memory). The classification system has undergone
a rewrite, but the new system is gated behind a template-based feature flag
`_use-new-classification-system_` (see Core below). Many optimizations
listed below are _not_ affected by this toggle.
Compiler
--------
- Numerous compiler optimizations including (but not limited to):
- Classification system rewrite with significant correctness and
performance improvements, with significantly less generated code.
- There is more work to be done in TAMER.
- This change is gated behind a feature toggle (see
`_use-new-classification-system_` in Core below).
- Significant reduction in byte count of JavaScript target output.
- Classifications with single-`TRUE` predicate matches are aliases and now
compile more efficiently.
- Classifications that are a disjunction of conjunctions with a common
predicate will have the common predicate hoisted out, resulting in more
efficeint code generation.
- Classifications with equality matches entirely on a single param are
compiled into a `Set` lookup.
- Most self-executing functions in target JavaScript code have been
eliminated, resulting in a performance improvement.
- Floating point truncation now takes place without using `toFixed` in
JavaScript target, eliminating expensive number->string->number
conversions.
- Code paths are entirely skipped if a calculation predicate indicates
that it should not be executed, rather than simply multiplying by 0
after performing potentially expensive calculations.
- A bunch of wasteful casting has been eliminated, supplanted by proper
casting of param inputs.
- Unnecessary debug output removed, significantly improving performance in
certain cases.
- Single-predicate any/all blocks stripped rather than being extracted
into separate classifications.
- Extracted any/all classifications are inlined at the reference site when
the new classification system is enabled, reducing the number of
temporaries created at runtime in JavaScript.
- Summary Page now displays values of `lv:match/@on` instead of debug
values.
- This provides more useful information and is not subject to the
@ -30,8 +66,9 @@ Compiler
Core
----
- New feature flag template `_use-new-classification-system_`.
- This is not yet utilized, but will enable a classification system
rewrite once merged.
- This allows selectively enabling code generation for the new
classification system, which has BC breaks in certain buggy situations.
See `core/test/core/class` package for more information.
- Remove `core/aggregate`.
- This package is not currently utilized and is dangerous---it could
easily aggregate unintended values if used carelessly. Those who know

View File

@ -27,183 +27,707 @@
<import package="../../base" />
Note that many of these classifications may match on similar values to try
to thwart potential optimizations, present or future, but these approaches
may need further adjustment to thwart future optimizations (or a way to
explicitly inhibit them).
to thwart potential optimizations,
present or future,
but these approaches
may need further adjustment to thwart future optimizations (or a way to
explicitly inhibit them).
These tests are also written a bit lazily,
given the difficulties in matching comprehensively;
that ought to be fixed in the future.
<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" />
<const name="MAT3X3" desc="3x3 Matrix, Ones">
<set desc="Row 0">
<item value="1" desc="0,0" />
<item value="1" desc="0,1" />
<item value="1" desc="0,2" />
</set>
<set desc="Row 1">
<item value="1" desc="1,0" />
<item value="1" desc="1,1" />
<item value="1" desc="1,2" />
</set>
<set desc="Row 2">
<item value="1" desc="2,0" />
<item value="1" desc="2,1" />
<item value="1" desc="2,2" />
</set>
</const>
<t:given name="conjNoPred" />
<const name="MAT3X3Z" desc="3x3 Matrix, Zeroes">
<set desc="Row 0">
<item value="0" desc="0,0" />
<item value="0" desc="0,1" />
<item value="0" desc="0,2" />
</set>
<set desc="Row 1">
<item value="0" desc="1,0" />
<item value="0" desc="1,1" />
<item value="0" desc="1,2" />
</set>
<set desc="Row 2">
<item value="0" desc="2,0" />
<item value="0" desc="2,1" />
<item value="0" desc="2,2" />
</set>
</const>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<const name="MAT3X3OOZ" desc="3x3 Matrix, Columns 1, 1, 0">
<set desc="Row 0">
<item value="1" desc="0,0" />
<item value="1" desc="0,1" />
<item value="0" desc="0,2" />
</set>
<set desc="Row 1">
<item value="1" desc="1,0" />
<item value="1" desc="1,1" />
<item value="0" desc="1,2" />
</set>
<set desc="Row 2">
<item value="1" desc="2,0" />
<item value="1" desc="2,1" />
<item value="0" desc="2,2" />
</set>
</const>
<const name="MAT3X1" desc="3x2 Matrix, Ones">
<set desc="Row 0">
<item value="1" desc="0,0" />
</set>
<set desc="Row 1">
<item value="1" desc="1,0" />
</set>
<set desc="Row 2">
<item value="1" desc="2,0" />
</set>
</const>
<const name="MAT3X1Z" desc="3x2 Matrix, Zeroes">
<set desc="Row 0">
<item value="0" desc="0,0" />
</set>
<set desc="Row 1">
<item value="0" desc="1,0" />
</set>
<set desc="Row 2">
<item value="0" desc="2,0" />
</set>
</const>
<const name="MAT1X3Z" desc="1x3 Matrix, Zeroes">
<set desc="Row 0">
<item value="0" desc="0,0" />
<item value="0" desc="0,1" />
<item value="0" desc="0,2" />
</set>
</const>
<template name="_class-tests_" desc="Classification system tests">
<param name="@system@" desc="SUT (lowercase)" />
<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: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 name="with matrix predicates">
<t:it desc="yields TRUE for all-true element-wise conjunction">
<t:given-classify-scalar>
<match on="MAT3X3Z" value="FALSE" />
<match on="MAT3X3" value="TRUE" />
</t:given-classify-scalar>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:it desc="yields FALSE for some-true element-wise conjunction">
<t:given-classify-scalar>
<match on="MAT3X3Z" value="TRUE" />
<match on="MAT3X3" value="TRUE" />
</t:given-classify-scalar>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:it desc="yields TRUE for some-true element-wise disjunction">
<t:given-classify-scalar>
<any>
<match on="MAT3X3Z" value="ZERO" />
<match on="MAT3X3" value="ZERO" />
</any>
</t:given-classify-scalar>
<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="MAT3X3Z" value="TRUE" />
<match on="MAT3X3" value="FALSE" />
</any>
</t:given-classify-scalar>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:describe name="of different column lengths">
Certain behavior is the same between the old and the new system---%
in particular,
when the match of lower length is first.
<classify as="mat-len-mismatch-first-conj-{@system@}"
yields="matLenMismatchFirstConj{@systemuc@}"
desc="Multi matrix length mismatch when first match">
<!-- fallthrough for undefined (note that this is
intentionally matching on FALSE to test against an
implicit 0 in place of undefined) -->
<match on="MAT3X1Z" value="FALSE" />
<!-- first two columns ones, last column zero -->
<match on="MAT3X3OOZ" value="TRUE" />
</classify>
<t:it desc="always yields FALSE when first match (TRUE)">
<t:given>
<c:value-of name="vecLenMismatchConjLegacy" index="#2" />
<c:value-of name="matLenMismatchFirstConj{@systemuc@}">
<c:index>
<c:value-of name="#1" />
</c:index>
<c:index>
<c:value-of name="#1" />
</c:index>
</c:value-of>
</t:given>
<t:expect>
<t:match-result value="TRUE" />
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:it desc="always yields FALSE when first match (FALSE)">
<t:given>
<c:value-of name="matLenMismatchFirstConj{@systemuc@}">
<c:index>
<c:value-of name="#2" />
</c:index>
<c:index>
<c:value-of name="#2" />
</c:index>
</c:value-of>
</t:given>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<if name="@system@" eq="legacy">
<t:describe name="with legacy classification system">
The legacy system is frightenly problematic when the matrix of
lesser column length appears after the first match---%
the commutative properites of the system are lost,
and the value from the previous match falls through!
<classify as="mat-len-mismatch-conj-{@system@}"
yields="matLenMismatchConj{@systemuc@}"
desc="Multi matrix length mismatch (legacy)">
<!-- first two columns ones, last column zero -->
<match on="MAT3X3OOZ" value="TRUE" />
<!-- fallthrough for undefined (note that this is
intentionally matching on FALSE to test against an
implicit 0 in place of undefined) -->
<match on="MAT3X1Z" value="FALSE" />
</classify>
<!-- which means that it's not cummutatitve! -->
<t:it desc="causes values from previous match to fall through
into undefined (TRUE)">
<t:given>
<c:value-of name="matLenMismatchConj{@systemuc@}">
<c:index>
<c:value-of name="#1" />
</c:index>
<c:index>
<c:value-of name="#1" />
</c:index>
</c:value-of>
</t:given>
<t:expect>
<t:match-result value="TRUE" />
</t:expect>
</t:it>
<t:it desc="causes values from previous match to fall through
into undefined (FALSE)">
<t:given>
<c:value-of name="matLenMismatchConj{@systemuc@}">
<c:index>
<c:value-of name="#2" />
</c:index>
<c:index>
<c:value-of name="#2" />
</c:index>
</c:value-of>
</t:given>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
</t:describe>
</if>
<if name="@system@" eq="new">
<t:describe name="with new classification system">
<classify as="mat-len-mismatch-conj-{@system@}"
yields="matLenMismatchConj{@systemuc@}"
desc="Multi matrix length mismatch (new)">
<!-- first two columns ones, last column zero -->
<match on="MAT3X3OOZ" value="TRUE" />
<!-- must not fall through like legacy (must always be
FALSE; note that this is intentionally matching on
FALSE to test against an implicit 0 in place of
undefined) -->
<match on="MAT3X1Z" value="FALSE" />
</classify>
<!-- which means that it's not cummutatitve! -->
<t:it desc="is FALSE regardless of previous match or current
value (TRUE)">
<t:given>
<c:value-of name="matLenMismatchConj{@systemuc@}">
<c:index>
<c:value-of name="#1" />
</c:index>
<c:index>
<c:value-of name="#1" />
</c:index>
</c:value-of>
</t:given>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:it desc="is FALSE regardless of previous match or current
value (FALSE)">
<t:given>
<c:value-of name="matLenMismatchConj{@systemuc@}">
<c:index>
<c:value-of name="#2" />
</c:index>
<c:index>
<c:value-of name="#2" />
</c:index>
</c:value-of>
</t:given>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
</t:describe>
</if>
</t:describe>
<t:describe name="of different row lengths">
<if name="@system@" eq="legacy">
<t:describe name="with legacy classification system">
The legacy classification system does something terrible when
the second match is the shorter---%
it discards the indexes entirely!
<classify as="mat-len-mismatch-rows-conj-{@system@}"
yields="matLenMismatchRowsConj{@systemuc@}"
desc="Multi matrix row mismatch (legacy)">
<!-- we _should_ have a 3x3 result matrix -->
<match on="MAT3X3OOZ" value="TRUE" />
<!-- but instead we get [[1, 1, 1], [0], [0]] because of
this match being second! -->
<match on="MAT1X3Z" value="FALSE" />
</classify>
<classify as="mat-len-mismatch-rows-first-conj-{@system@}"
yields="matLenMismatchRowsFirstConj{@systemuc@}"
desc="Multi matrix row mismatch first match (legacy)">
<match on="MAT1X3Z" value="TRUE" />
<match on="MAT3X3OOZ" value="TRUE" />
</classify>
<!-- note that this is testing buggy behavior; the new system
corrects it -->
<t:it desc="replaces all inner vectors of other rows">
<t:given>
<c:length-of>
<c:value-of name="matLenMismatchRowsConj{@systemuc@}">
<c:index>
<c:value-of name="#1" />
</c:index>
</c:value-of>
</c:length-of>
</t:given>
<t:expect>
<!-- were it not for this bug, it should be 3 -->
<t:match-result value="#1" />
</t:expect>
</t:it>
<!-- note that this is testing buggy behavior; the new system
corrects it -->
<t:it desc="considers only defined rows' values when smaller
is first">
<t:given>
<c:value-of name="matLenMismatchRowsFirstConj{@systemuc@}">
<c:index>
<c:value-of name="#1" />
</c:index>
<c:index>
<c:value-of name="#0" />
</c:index>
</c:value-of>
</t:given>
<!-- we get [[0, 0, 0], [1, 1, 0], [1, 1, 0]] -->
<!-- ^ -->
<t:expect>
<t:match-result value="#1" />
</t:expect>
</t:it>
</t:describe>
</if>
<if name="@system@" eq="new">
<t:describe name="with new classification system">
<classify as="mat-len-mismatch-rows-conj-{@system@}"
yields="matLenMismatchRowsConj{@systemuc@}"
desc="Multi matrix row mismatch (new)">
<match on="MAT3X3OOZ" value="TRUE" />
<!-- must yield FALSE rather than matching on 0 -->
<match on="MAT1X3Z" value="FALSE" />
</classify>
<classify as="mat-len-mismatch-rows-first-conj-{@system@}"
yields="matLenMismatchRowsFirstConj{@systemuc@}"
desc="Multi matrix row mismatch first match (new)">
<match on="MAT1X3Z" value="TRUE" />
<match on="MAT3X3OOZ" value="TRUE" />
</classify>
<t:it desc="retains shape of larger matrix">
<t:given>
<c:length-of>
<c:value-of name="matLenMismatchRowsConj{@systemuc@}">
<c:index>
<c:value-of name="#2" />
</c:index>
</c:value-of>
</c:length-of>
</t:given>
<t:expect>
<!-- unlike legacy system -->
<t:match-result value="#3" />
</t:expect>
</t:it>
<t:it desc="always yields FALSE for each undefined element (TRUE)">
<t:given>
<c:length-of>
<c:value-of name="matLenMismatchRowsConj{@systemuc@}">
<c:index>
<c:value-of name="#2" />
</c:index>
<c:index>
<c:value-of name="#1" />
</c:index>
</c:value-of>
</c:length-of>
</t:given>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<t:it desc="always yields FALSE for each undefined element (FALSE)">
<t:given>
<c:length-of>
<c:value-of name="matLenMismatchRowsConj{@systemuc@}">
<c:index>
<c:value-of name="#1" />
</c:index>
<c:index>
<c:value-of name="#2" />
</c:index>
</c:value-of>
</c:length-of>
</t:given>
<t:expect>
<t:match-result value="FALSE" />
</t:expect>
</t:it>
<!-- unlike legacy -->
<t:it desc="is commutative with different row lengths">
<t:given>
<c:value-of name="matLenMismatchRowsFirstConj{@systemuc@}">
<c:index>
<c:value-of name="#1" />
</c:index>
<c:index>
<c:value-of name="#2" />
</c:index>
</c:value-of>
</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

@ -69,21 +69,17 @@
<template mode="compile" priority="1"
match="c:*">
<if test="$calcc-debug = 'yes'">
<text>( function() { var result = </text>
<text>/*!+*/(result=/*!-*/</text>
</if>
<apply-templates select="." mode="compile-pre" />
<if test="$calcc-debug = 'yes'">
<text>; </text>
<text>/*!+*/( debug['</text>
<text>/*!+*/,(D['</text>
<value-of select="@_id" />
<text>'] || ( debug['</text>
<text>']||(D['</text>
<value-of select="@_id" />
<text>'] = [] ) ).push( result );/*!-*/ </text>
<text>return result; </text>
<text>} )() </text>
<text>']=[])).push(result),result)/*!-*/</text>
</if>
</template>
@ -112,30 +108,80 @@
<template match="c:*" mode="compile-pre" priority="1">
<!-- ensure everything is grouped (for precedence) and converted to a
number -->
<text>( </text>
<apply-templates select="." mode="compile-calc" />
<text> )</text>
</template>
<template match="c:const[ ./c:when ]|c:value-of[ ./c:when ]" mode="compile-pre" priority="5">
<text>( </text>
<text>(</text>
<!-- first, do what we normally would do (compile the value) -->
<text>( </text>
<apply-templates select="." mode="compile-calc" />
<text> )</text>
<apply-templates select="." mode="compile-calc" />
<!-- then multiply by the c:when result -->
<text> * ( </text>
<for-each select="./c:when">
<if test="position() > 1">
<text> * </text>
</if>
<text> * </text>
<apply-templates select="." mode="compile" />
</for-each>
<text> )</text>
<text> )</text>
<for-each select="./c:when">
<if test="position() > 1">
<text> * </text>
</if>
<apply-templates select="." mode="compile" />
</for-each>
<text>)</text>
</template>
<template mode="js-name-ref" priority="5"
match="c:sum[@of]|c:product[@of]">
<variable name="of" select="@of" />
<variable name="func" select="ancestor::lv:function" />
<!-- XXX: this needs to use compile-calc-value, but can't right now
beacuse it's not a c:value-of! -->
<choose>
<!-- is @of a function param? -->
<when test="
$func
and root(.)/preproc:symtable/preproc:sym[
@type='lparam'
and @name=concat( ':', $func/@name, ':', $of )
]
">
<value-of select="@of" />
</when>
<!-- let expression -->
<when test="$of = ancestor::c:let/c:values/c:value/@name">
<value-of select="$of" />
</when>
<!-- maybe a constant? -->
<when test="
root(.)/preproc:symtable/preproc:sym[
@type='const'
and @name=$of
]
">
<text>C['</text>
<value-of select="@of" />
<text>']</text>
</when>
<otherwise>
<text>A['</text>
<value-of select="@of" />
<text>']</text>
</otherwise>
</choose>
</template>
<template mode="js-name-ref" priority="1"
match="*">
<message select="'internal error: invalid js-name-ref src'"
terminate="yes" />
</template>
@ -162,11 +208,11 @@
<variable name="precision">
<choose>
<when test="@precision">
<value-of select="@precision" />
<value-of select="concat( '1e', @precision )" />
</when>
<otherwise>
<text>8</text>
<text>1e8</text>
</otherwise>
</choose>
</variable>
@ -194,75 +240,36 @@
<!-- introduce scope both to encapsulate values and so we can insert this as
part of a larger expression (will return a value) -->
<text>( function() {</text>
<text>(function() {</text>
<!-- will store result of the summation/product -->
<text>var sum = 0;</text>
<variable name="of" select="@of" />
<variable name="func" select="ancestor::lv:function" />
<text>var sum=0;</text>
<!-- XXX: this needs to use compile-calc-value, but can't right now
beacuse it's not a c:value-of! -->
<variable name="value">
<choose>
<!-- is @of a function param? -->
<when test="
$func
and root(.)/preproc:symtable/preproc:sym[
@type='lparam'
and @name=concat( ':', $func/@name, ':', $of )
]
">
<value-of select="@of" />
</when>
<!-- let expression -->
<when test="$of = ancestor::c:let/c:values/c:value/@name">
<value-of select="$of" />
</when>
<!-- maybe a constant? -->
<when test="
root(.)/preproc:symtable/preproc:sym[
@type='const'
and @name=$of
]
">
<text>consts['</text>
<value-of select="@of" />
<text>']</text>
</when>
<otherwise>
<text>args['</text>
<value-of select="@of" />
<text>']</text>
</otherwise>
</choose>
<apply-templates mode="js-name-ref"
select="." />
</variable>
<!-- if we're looking to generate a set, initialize it -->
<if test="@generates">
<text>var G = []; </text>
<text>var G=[];</text>
</if>
<!-- loop through each value -->
<text>for ( var </text>
<text>for (var </text>
<value-of select="$index" />
<text> in </text>
<value-of select="$value" />
<text> ) {</text>
<text>) {</text>
<text>var result = </text>
<text>var result=</text>
<!-- if caller wants to yield a vector, don't cast -->
<sequence select="if ( not( $dim gt 0 ) ) then
'+(+( '
concat( 'p(', $precision, ', +(')
else
'(( '" />
'('" />
<choose>
<!-- if there are child nodes, use that as the summand/expression -->
<when test="./c:*">
@ -277,12 +284,10 @@
<text>]</text>
</otherwise>
</choose>
<text> ))</text>
<text>)</text>
<!-- if caller wants to yield a vector, don't truncate -->
<if test="not( $dim gt 0 )">
<text>.toFixed(</text>
<value-of select="$precision" />
<text>)</text>
</if>
@ -290,26 +295,26 @@
<!-- if generating a set, store this result -->
<if test="@generates">
<text>G.push( result ); </text>
<text>G.push(result); </text>
</if>
<!-- generate summand -->
<text>sum </text>
<value-of select="$operator" />
<text>= +result;</text>
<text>=+result;</text>
<!-- end of loop -->
<text>}</text>
<!-- if a set has been generated, store it -->
<if test="@generates">
<text>args['</text>
<text>A['</text>
<value-of select="@generates" />
<text>'] = G; </text>
<text>']=G;</text>
</if>
<text>return sum;</text>
<text>} )()</text>
<text>})()</text>
</template>
@ -322,12 +327,12 @@
validator, then the result is undefined.
-->
<template match="c:product[@dot]" mode="compile-calc" priority="5">
<text>( function() { </text>
<text>(function() { </text>
<!-- we need to determine which vector is the longest to ensure that we
properly compute every value (remember, undefined will be equivalent to
0, so the vectors needn't be of equal length *gasp* blasphemy!) -->
<text>var _$dlen$ = longerOf( </text>
<text>var _$dlen$=longerOf(</text>
<for-each select=".//c:value-of">
<if test="position() > 1">
<text>, </text>
@ -337,30 +342,30 @@
compile)-->
<apply-templates select="." mode="compile-calc" />
</for-each>
<text> ); </text>
<text>); </text>
<!-- will store the total sum -->
<text>var _$dsum$ = 0;</text>
<text>var _$dsum$=0;</text>
<!-- sum the product of each -->
<text disable-output-escaping="yes">for ( var _$d$ = 0; _$d$ &lt; _$dlen$; _$d$++ ) {</text>
<text>_$dsum$ += </text>
<text disable-output-escaping="yes">for(var _$d$=0; _$d$ &lt; _$dlen$; _$d$++) {</text>
<text>_$dsum$ +=</text>
<!-- product of each -->
<for-each select=".//c:value-of">
<if test="position() > 1">
<text> * </text>
</if>
<text>( ( </text>
<text>((</text>
<apply-templates select="." mode="compile" />
<text> || [] )[ _$d$ ] || 0 )</text>
<text>||[])[_$d$]||0)</text>
</for-each>
<text>; </text>
<text>}</text>
<text>return _$dsum$;</text>
<text> } )()</text>
<text>})()</text>
</template>
@ -381,6 +386,7 @@
<apply-templates select="." mode="compile-getop" />
</variable>
<text>(</text>
<for-each select="./c:*">
<!-- add operator between each expression -->
<if test="position() > 1">
@ -391,6 +397,7 @@
<apply-templates select="." mode="compile" />
</for-each>
<text>)</text>
</template>
@ -421,25 +428,25 @@
<choose>
<!-- if a precision was explicitly provided, then use that -->
<when test="@precision">
<value-of select="@precision" />
<value-of select="concat( '1e', @precision )" />
</when>
<!-- ECMAScript uses a default precision of 24; by reducing the
precision to 8 decimal places, we can drastically reduce the affect
of precision errors on the calculations -->
<otherwise>
<text>8</text>
<text>1e8</text>
</otherwise>
</choose>
</variable>
<text>Math.</text>
<value-of select="local-name()" />
<text>( +(</text>
<apply-templates select="./c:*" mode="compile" />
<text> ).toFixed( </text>
<text>(p(</text>
<value-of select="$precision" />
<text> ) )</text>
<text>, +(</text>
<apply-templates select="./c:*" mode="compile" />
<text>)))</text>
</template>
@ -470,13 +477,16 @@
<!-- TODO: this should really be decoupled -->
<!-- TODO: does not properly support matrices -->
<template match="c:value-of[ ancestor::lv:match ]" mode="compile-calc" priority="5">
<param name="noindex" as="xs:boolean" tunnel="yes"
select="false()" />
<variable name="name" select="@name" />
<choose>
<!-- scalar -->
<when test="
root(.)/preproc:symtable/preproc:sym[ @name=$name ]
/@dim = '0'
$noindex
or root(.)/preproc:symtable/preproc:sym[ @name=$name ]/@dim = '0'
">
<apply-templates select="." mode="compile-calc-value" />
</when>
@ -508,8 +518,6 @@
<apply-templates select="." mode="compile-calc-index">
<with-param name="value" select="@name" />
</apply-templates>
<text> || 0</text>
</template>
@ -523,8 +531,6 @@
<apply-templates select="." mode="compile-calc-index">
<with-param name="value" select="@name" />
</apply-templates>
<text> || 0</text>
</template>
@ -559,7 +565,7 @@
<choose>
<!-- "magic" constants should not have their values inlined -->
<when test="$sym/@magic='true'">
<text>consts['</text>
<text>C['</text>
<value-of select="@name" />
<text>']</text>
</when>
@ -593,7 +599,7 @@
">
<variable name="value">
<text>consts['</text>
<text>C['</text>
<value-of select="@name" />
<text>']</text>
</variable>
@ -601,9 +607,6 @@
<apply-templates select="." mode="compile-calc-index">
<with-param name="value" select="$value" />
</apply-templates>
<!-- undefined values in sets are considered to be 0 -->
<text> || 0</text>
</template>
@ -643,7 +646,7 @@
string concatenation bugs instead of integer/floating point arithmetic)
as long as we're either not a set, or provide an index for the set -->
<variable name="value">
<text>args['</text>
<text>A['</text>
<value-of select="@name" />
<text>']</text>
</variable>
@ -686,11 +689,6 @@
<apply-templates select="." mode="compile-calc-index">
<with-param name="value" select="$value" />
</apply-templates>
<!-- default to 0 if nothing is set (see notes on bottom of summary page; we
assume all undefined values in a set to be implicitly 0, which greatly
simplifies things) -->
<text> || 0</text>
</template>
@ -719,11 +717,8 @@
<choose>
<when test="@index">
<!-- output the value, falling back on an empty array to prevent errors
when attempting to access undefined values -->
<text>(</text>
<value-of select="$value" />
<text>||[])</text>
<value-of select="$value" />
<text>[</text>
@ -752,13 +747,14 @@
<!-- otherwise, it's a variable -->
<otherwise>
<text>args['</text>
<text>A['</text>
<value-of select="@index" />
<text>']</text>
</otherwise>
</choose>
<text>]</text>
<!-- be sure to default to 0 if the index is missing -->
<text>]||0)</text>
</when>
<!-- if index node(s) were provided, then recursively generate -->
@ -784,7 +780,7 @@
<variable name="value">
<value-of select="$wrap" />
<!-- generate index -->
<!-- generate index, with a default in case the lookup fails -->
<text>[</text>
<apply-templates select="./c:*[1]" mode="compile" />
<text>]</text>
@ -795,24 +791,20 @@
<!-- recurse on any sibling indexes, wrapping with default value -->
<apply-templates select="$next" mode="compile-calc-index">
<with-param name="wrap">
<!-- wrap the value in parenthesis so that we can provide a default value if
the index lookup fails -->
<text>(</text>
<value-of disable-output-escaping="yes" select="$value" />
<!-- using 0 as the default is fine, even if we have $next; accessing an index
of 0 is perfectly fine, since it will be cast to an object; we just must
make sure that it is not undefined -->
<text> || 0 )</text>
<!-- note that this default differs from ||0, since we're going to
be treating it as an array with this next index -->
<text>||[])</text>
</with-param>
</apply-templates>
</when>
<!-- if there is no sibling, just output our value with the index, unwrapped
(since the caller will handle its default value for us -->
<otherwise>
<text>(</text>
<value-of disable-output-escaping="yes" select="$value" />
<text>||0)</text>
</otherwise>
</choose>
</template>
@ -828,9 +820,11 @@
-->
<template match="c:quotient" mode="compile-calc">
<!-- we only accept a numerator and a denominator -->
<text>div(</text>
<apply-templates select="./c:*[1]" mode="compile" />
<text> / </text>
<text>,</text>
<apply-templates select="./c:*[2]" mode="compile" />
<text>)</text>
</template>
@ -862,7 +856,7 @@
<with-param name="name" select="@name" />
</call-template>
<text>( args</text>
<text>(args</text>
<variable name="arg-prefix" select="concat( ':', $name, ':' )" />
@ -893,7 +887,7 @@
</choose>
</for-each>
<text> )</text>
<text>)</text>
<!-- if c:when was provided, compile it in such a way that we retain the
function call (we want the result for providing debug information) -->
@ -1009,18 +1003,13 @@
<!-- note that if we have multiple c:whens, they'll be multiplied together by
whatever calls this, so we're probably fine -->
<text>( function() {</text>
<!-- return a 1 or a 0 depending on the result of the expression -->
<text>return ( </text>
<text>( </text>
<!-- get the value associated with this node -->
<apply-templates select="." mode="compile-calc-value" />
<text> ) </text>
<text>+(</text>
<apply-templates select="." mode="compile-calc-value" />
<!-- generate remainder of expression -->
<apply-templates select="./c:*[1]" mode="compile-calc-when" />
<text>) ? 1 : 0; </text>
<text>} )()</text>
<text>)</text>
</template>
@ -1037,6 +1026,7 @@
<calc-compiler:c id="lte">&lt;=</calc-compiler:c>
</variable>
<text> </text>
<value-of disable-output-escaping="yes" select="$map/*[ @id=$name ]" />
<text> </text>
@ -1050,7 +1040,7 @@
<template match="c:cases" mode="compile-calc">
<text>( function() {</text>
<text>((function() {</text>
<for-each select="./c:case">
<!-- turn "if" into an "else if" if needed -->
@ -1067,10 +1057,10 @@
<apply-templates select="." mode="compile" />
</for-each>
<text> ) { return </text>
<text>){return </text>
<!-- process on its own so that we can debug its final value -->
<apply-templates select="." mode="compile" />
<text>; } </text>
<text>;}</text>
</for-each>
<!-- check for the existence of an "otherwise" clause, which should be
@ -1085,7 +1075,7 @@
</when>
<otherwise>
<text>if ( true )</text>
<text>if (true)</text>
</otherwise>
</choose>
@ -1094,7 +1084,7 @@
<text>; } </text>
</if>
<text> } )() || 0</text>
<text> })()||0)</text>
</template>
<template match="c:case" mode="compile-calc">
@ -1142,12 +1132,12 @@
<text>(function(){</text>
<!-- duplicate the array just in case...if we notice a performance impact,
then we can determine if such a duplication is necessary -->
<text>var cdr = Array.prototype.slice.call(</text>
<text>var cdr=Array.prototype.slice.call(</text>
<apply-templates select="$cdr" mode="compile" />
<text>, 0);</text>
<text>cdr.unshift( </text>
<apply-templates select="$car" mode="compile" />
<text> ); </text>
<text>); </text>
<!-- no longer the cdr -->
<text>return cdr; </text>
<text>})()</text>
@ -1170,9 +1160,9 @@
Returns the length of any type of set (not just a vector)
-->
<template match="c:length-of" mode="compile-calc">
<text>( </text>
<text>(</text>
<apply-templates select="./c:*[1]" mode="compile" />
<text>.length || 0 )</text>
<text>.length||0)</text>
</template>
@ -1190,9 +1180,9 @@
</if>
</variable>
<text>function </text>
<text>(function </text>
<value-of select="$fname" />
<text>( </text>
<text>(</text>
<!-- generate arguments -->
<for-each select="$values">
<if test="position() > 1">
@ -1201,28 +1191,25 @@
<value-of select="@name" />
</for-each>
<text> ) { </text>
<text>){</text>
<!-- the second node is the body -->
<text>return </text>
<apply-templates select="./c:*[2]" mode="compile" />
<text>;</text>
<text>}</text>
<text>})</text>
<!-- assign the arguments according to the calculations -->
<text>( </text>
<text>(</text>
<for-each select="$values">
<if test="position() > 1">
<text>,</text>
</if>
<!-- compile the argument value (the parenthesis are just to make it
easier to read the compiled code) -->
<text>(</text>
<apply-templates select="./c:*[1]" mode="compile" />
<text>)</text>
<!-- compile the argument value -->
<apply-templates select="./c:*[1]" mode="compile" />
</for-each>
<text> ) </text>
<text>)</text>
</template>
@ -1242,20 +1229,20 @@
<template match="c:debug-to-console" mode="compile-calc">
<text>(function(){</text>
<text>var result = </text>
<text>var result=</text>
<apply-templates select="./c:*[1]" mode="compile" />
<text>;</text>
<!-- log the result and return it so that we do not inhibit the calculation
(allowing it to be inlined anywhere) -->
<text>console.log( </text>
<text>console.log(</text>
<if test="@label">
<text>'</text>
<value-of select="@label" />
<text>', </text>
</if>
<text>result ); </text>
<text>result); </text>
<text>return result; </text>
<text>})()</text>
</template>
@ -1272,11 +1259,8 @@
@return self-executing anonymous error function
-->
<template match="c:*" mode="compile-calc">
<text>( function () {</text>
<text>throw Error( "Unknown calculation: </text>
<value-of select="name()" />
<text>" ); </text>
<text>} )() </text>
<message terminate="yes"
select="'unknown calculation type:', name()" />
</template>
</stylesheet>

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>

File diff suppressed because it is too large Load Diff

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">
@ -267,6 +268,34 @@
<apply-templates select="." mode="lvv:validate-match" />
</template>
<template match="lv:match[@pattern]" mode="lvv:validate-match" priority="9">
<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>
<!--
Validate that non-numeric value matches actually exist and are constants
-->
@ -300,38 +329,68 @@
<apply-templates mode="lvv:validate-match" />
</template>
<!-- simplify optimizations -->
<template match="lv:match[ count(c:*) gt 1 ]" mode="lvv:validate-match" priority="4">
<call-template name="lvv:error">
<with-param name="desc" select="'Multi-c:* match expression'" />
<with-param name="refnode" select="." />
<with-param name="content">
<text>`</text>
<value-of select="@on" />
<text>' must include separate matches for each c:* in `</text>
<value-of select="parent::lv:classify/@as" />
<text>'</text>
</with-param>
</call-template>
<apply-templates mode="lvv:validate-match" />
</template>
<template match="lv:match" mode="lvv:validate-match" priority="2">
<apply-templates mode="lvv:validate-match" />
</template>
<!--
Classification match assumptions must operate only on other classifiers and
must assume values that the referenced classifier actually matches on
-->
<template match="lv:match/lv:assuming" mode="lvv:validate-match" priority="5">
<variable name="on" select="../@on" />
<variable name="ref" select="root(.)//lv:classify[ @yields=$on ]" />
<!-- assumptions must only operate on variables mentioned in the referenced
classification -->
<for-each select="
.//lv:that[
not( @name=$ref//lv:match/@on )
]
">
<!-- simplify optimizations -->
<template mode="lvv:validate-match" priority="5"
match="c:*[ not( c:value-of or c:const ) ]">
<call-template name="lvv:error">
<with-param name="desc" select="'invalid lv:match/c:*/c:*'" />
<with-param name="refnode" select="." />
<with-param name="content">
<text>`</text>
<value-of select="@on" />
<text>' must contain only c:value of or c:const in `</text>
<value-of select="parent::lv:classify/@as" />
<text>'</text>
</with-param>
</call-template>
<call-template name="lvv:error">
<with-param name="desc" select="'Invalid classification assumption'" />
<with-param name="refnode" select="." />
<with-param name="content">
<value-of select="@name" />
<text> is not used to classify </text>
<value-of select="$on" />
</with-param>
</call-template>
</for-each>
<apply-templates mode="lvv:validate-match" />
</template>
<!-- simplify optimizations -->
<template mode="lvv:validate-match" priority="4"
match="c:*[ c:const[ c:* ] ]">
<call-template name="lvv:error">
<with-param name="desc" select="'non-scalar lv:match/c:*/c:*'" />
<with-param name="refnode" select="." />
<with-param name="content">
<text>`</text>
<value-of select="@on" />
<text>' must contain only scalar c:const in `</text>
<value-of select="parent::lv:classify/@as" />
<text>'</text>
</with-param>
</call-template>
<apply-templates mode="lvv:validate-match" />
</template>
<template match="c:*" mode="lvv:validate-match" priority="2">
<apply-templates select="." mode="lvv:validate" />
</template>

View File

@ -312,6 +312,10 @@
<next-match />
</template>
<template mode="preproc:depgen" priority="9"
match="lv:classify[ preproc:inline='true' ]">
<!-- ignore; dependencies will be inlined -->
</template>
<template mode="preproc:depgen" priority="7"
match="lv:classify">
@ -460,6 +464,29 @@
</template>
<!--
Inlined matches will not be counted as dependencies themselves, but their
dependencies are our own
-->
<template match="lv:match[ @preproc:inline='true' ]"
mode="preproc:depgen" priority="7">
<variable name="self" as="element( lv:match )" select="." />
<variable name="classify" as="element( lv:classify )?"
select="( parent::lv:classify
/preceding-sibling::lv:classify[ @yields=$self/@on ] )[1]" />
<if test="empty( $classify )">
<message terminate="yes"
select="concat( 'internal error: inline depgen: ',
'cannot locate class `', @on, '''' )" />
</if>
<apply-templates mode="preproc:depgen"
select="$classify/element()" />
</template>
<template match="lv:match[ @value ]" mode="preproc:depgen" priority="5">
<!-- process the @value -->
<call-template name="preproc:depgen-c-normal">

View File

@ -596,46 +596,18 @@
</template>
<template mode="preproc:expand"
match="lv:join[ @all='true' ]"
priority="8">
<call-template name="preproc:mk-class-join-contents" />
</template>
<!-- expand lv:match/@value != 'TRUE' into a c:* expression to simpliy
optimizations -->
<template match="lv:match[ @value and @value != 'TRUE' ]"
mode="preproc:expand" priority="7">
<copy>
<copy-of select="@*[ not( local-name() = 'value' ) ]" />
<template mode="preproc:expand"
match="lv:join"
priority="7">
<lv:any>
<call-template name="preproc:mk-class-join-contents" />
</lv:any>
</template>
<template name="preproc:mk-class-join-contents">
<variable name="prefix" select="@prefix" />
<!-- TODO: remove lv:template nodes in a pass before this so that this
check is not necessary -->
<for-each select="root(.)/lv:classify[
starts-with( @as, $prefix )
and not( ancestor::lv:template )
]">
<lv:match value="TRUE">
<attribute name="on">
<choose>
<when test="@yields">
<value-of select="@yields" />
</when>
<otherwise>
<text>__is</text>
<value-of select="@as" />
</otherwise>
</choose>
</attribute>
</lv:match>
</for-each>
<c:eq>
<c:value-of name="{@value}" />
</c:eq>
</copy>
</template>

View File

@ -27,6 +27,8 @@
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">
@ -68,7 +70,9 @@
<preproc:repass-record />
-->
<!-- output is slow and this happens a lot
<message>[preproc] *REPASS*</message>
-->
<!-- perform the repass -->
<apply-templates select="$nodeset" mode="preproc:macropass">
@ -243,9 +247,52 @@
</template>
<template match="lv:classify[ .//lv:any|.//lv:all ]" mode="preproc:macros" priority="6">
<!--
Strip template barriers after expansion is complete.
These barriers really frustrate static analysis, and serve no use after
templates have been expanded, aside from indicating _what_ templates were
expanded. The benefits there do not outweigh the optimization
opportunities.
-->
<template mode="preproc:macros" priority="8"
match="lv:classify[
.//preproc:tpl-barrier
and not( eseq:is-expandable(.) ) ]">
<copy>
<sequence select="@*" />
<apply-templates mode="preproc:strip-tpl-barrier" />
<preproc:repass src="lv:classify tpl barrier strip" />
</copy>
</template>
<!-- strip preproc:* nodes -->
<template mode="preproc:strip-tpl-barrier" priority="5"
match="preproc:*">
<apply-templates mode="preproc:strip-tpl-barrier" />
</template>
<template mode="preproc:strip-tpl-barrier" priority="1"
match="element()">
<copy>
<sequence select="@*" />
<apply-templates mode="preproc:strip-tpl-barrier" />
</copy>
</template>
<template mode="preproc:macros" priority="6"
match="lv:classify[
( 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" />
@ -262,14 +309,112 @@
</template>
<template mode="preproc:class-groupgen" priority="9"
match="lv:any[ not( element() ) ]
|lv:all[ not( element() ) ]">
<!-- useless; remove -->
<!--
Not only should we not generate a group for single-predicate any/all, but
we should remove it entirely.
-->
<template mode="preproc:class-groupgen" priority="7"
match="(lv:any|lv:all)[ count( lv:* ) lt 2 ]">
<apply-templates mode="preproc:class-groupgen" />
</template>
<!--
A very specific optimization targeting a common scenario from template
expansions.
If there is an <any> expression of this form:
<any>
<all>
<match on="foo" />
<match on="bar" value="BAZ" />
</all>
<all>
<match on="foo" />
<match on="bar" value="QUUX" />
</all>
</any>
Then the common "foo" will be hoisted out and the expression will become:
<match on="foo" />
<any>
<match on="bar" value="BAZ" />
<match on="bar" value="QUUX" />
</any>
The goal of this optimization is primarily to significantly reduce the
number of wasteful intermediate classifications that are generated.
-->
<template mode="preproc:class-groupgen" priority="6"
match="(lv:classify[ @any='true' ] | lv:any)
[ count( lv:all[ count(lv:*) = 2 ] ) = count(lv:*) ]">
<!-- TODO: missing @value may not have been expanded yet...! -->
<variable name="ons" as="element( lv:match )*"
select="lv:all/lv:match[
@value = 'TRUE'
or (
not( @value )
and not( @anyOf )
and not( c:* ) ) ]" />
<variable name="distinct-ons" as="xs:string*"
select="distinct-values( $ons/@on )" />
<variable name="nall" as="xs:integer"
select="count( lv:all )" />
<choose>
<when test="count( $distinct-ons ) = 1
and count( $ons ) = $nall
and count( $ons/parent::lv:all ) = $nall">
<!-- then replace the <alls> with their remaining predicate (which is
either before or after) -->
<choose>
<when test="@any = 'true'">
<copy>
<sequence select="@*[ not( local-name() = 'any' ) ]" />
<!-- they're all the same, so hoist the first one out of the <any>,
which is now in a universal context because we omitted @any
above -->
<sequence select="$ons[1]" />
<lv:any>
<apply-templates mode="preproc:class-groupgen"
select="$ons/preceding-sibling::lv:match
| $ons/following-sibling::lv:match"/>
</lv:any>
</copy>
</when>
<otherwise>
<!-- they're all the same, so hoist the first one out of the <any>,
which must be in a universal context -->
<sequence select="$ons[1]" />
<copy>
<apply-templates mode="preproc:class-groupgen"
select="$ons/preceding-sibling::lv:match
| $ons/following-sibling::lv:match"/>
</copy>
</otherwise>
</choose>
</when>
<!-- none, or more than one -->
<otherwise>
<next-match />
</otherwise>
</choose>
</template>
<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
@ -289,11 +434,20 @@
<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" />
<lv:match on="{$yields}" value="TRUE"
preproc:generated="true">
<if test="not( $legacy-classify )">
<attribute name="preproc:inline" select="'true'" />
</if>
</lv:match>
</template>

View File

@ -594,10 +594,6 @@
<copy>
<sequence select="@*" />
<message>
<text>[preproc] *resolving symbol attributes...</text>
</message>
<apply-templates mode="preproc:resolv-syms">
<with-param name="orig-root" select="$orig-root" />
<with-param name="symtable-map" select="$symtable-map" tunnel="yes" />
@ -612,20 +608,6 @@
<choose>
<!-- repass scheduled; go for it -->
<when test="$repass">
<message>[preproc] *SYM REPASS*</message>
<message>
<text>[preproc] The following </text>
<value-of select="count( $repass )" />
<text> symbol(s) are still unresolved:</text>
</message>
<for-each select="$repass">
<message>
<text>[preproc] - </text>
<value-of select="@ref" />
</message>
</for-each>
<apply-templates select="$result" mode="preproc:resolv-syms">
<with-param name="orig-root" select="$orig-root" />
<with-param name="rpcount" select="$rpcount + 1" />

View File

@ -119,10 +119,6 @@
<sequence select="@*" />
<variable name="new">
<message>
<text>[preproc/symtable] discovering symbols...</text>
</message>
<preproc:syms>
<apply-templates mode="preproc:symtable">
<!-- we only need this param for the root children, so this is the only
@ -309,8 +305,6 @@
</apply-templates>
</preproc:symtable>
<message select="'[preproc/symtable] done.'" />
<!-- copy all of the original elements after the symbol table; we're not
outputting them as we go, so we need to make sure that we don't get
rid of them; discard any existing symbol table -->
@ -456,10 +450,6 @@
<param name="new" as="element( preproc:syms )" />
<param name="this-pkg" as="element( lv:package )" />
<message>
<text>[preproc/symtable] processing symbol table...</text>
</message>
<variable name="cursym" as="element( preproc:sym )*"
select="preproc:symtable/preproc:sym[
not( @held = 'true' ) ]" />
@ -549,8 +539,6 @@
</choose>
</for-each>
</preproc:syms>
<message select="'[preproc/symtable] done processing symbol table.'" />
</template>
@ -606,12 +594,6 @@
assertions, resolve relative paths -->
<variable name="import-default-path" select="$import-path" />
<message>
<text>[preproc/symtable] importing symbol table of </text>
<value-of select="$import-path" />
<text>...</text>
</message>
<!-- attempt to import symbols from the processed package -->
<if test="not( $syms )">
<message terminate="yes">

View File

@ -414,11 +414,6 @@
<variable name="name" select="concat( '___i', generate-id(.), '___' )" />
<variable name="inline" select="." />
<message>
<text>[preproc] preparing inline template </text>
<value-of select="$name" />
</message>
<!-- generate template -->
<lv:template name="{$name}" desc="Inline template"
preproc:from-inline="{$name}"

View File

@ -1193,44 +1193,10 @@
<xsl:text> must </xsl:text>
<xsl:apply-templates select="." mode="match-desc" />
<xsl:if test="lv:assuming">
<xsl:text>, assuming that:</xsl:text>
<ul>
<xsl:for-each select="lv:assuming/lv:that">
<li>
<!-- link to the ref -->
<a>
<xsl:attribute name="href">
<xsl:text>#</xsl:text>
<xsl:value-of select="@name" />
</xsl:attribute>
<xsl:value-of select="@name" />
</a>
<xsl:text> </xsl:text>
<xsl:apply-templates select="." />
</li>
</xsl:for-each>
</ul>
</xsl:if>
</p>
</xsl:template>
<xsl:template match="lv:assuming/lv:that[ @ignored ]" priority="5">
<xsl:text>is ignored during classification</xsl:text>
</xsl:template>
<!-- we only do consts right now -->
<xsl:template match="lv:assuming/lv:that" priority="1">
<xsl:text>has the value </xsl:text>
<xsl:value-of select="@const" />
</xsl:template>
<!--
Outputs a type match in plain english

73
tools/pkg-graph 100755
View File

@ -0,0 +1,73 @@
#!/bin/bash
# Output DOT file representing package dependencies
#
# Copyright (C) 2014-2021 Ryan Specialty Group, LLC.
#
# This program 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/>.
##
relpath()
{
while read p; do
if [[ ! "$p" =~ \.\. ]]; then
echo "$p"
else
sed 's|[^/]\+/\.\./|/|' | relpath
fi
done
}
undepify()
{
sed 's|^\(\./\)\?\(.*\)\.dep:|/\2 |'
}
dotify()
{
echo 'digraph {'
local from to
while read -r from to; do
printf '"%s" -> "%s";\n' "$from" "$to"
done
echo '}'
}
main()
{
local root="${1?Missing project root}"
cd "$root"
find . -name '*.dep' | while read -r depfile; do
# Absolute paths can be output verbatim
grep -H '^/' "$depfile"
# Relative paths need processing
local dir=$( dirname "$depfile" | sed 's|^\./|/|' )
grep -Hv '^/' "$depfile" \
| sed "s|:|:$dir/|"
done \
| grep -v '\$$' \
| relpath \
| undepify \
| dotify
}
main "$@"