<?xml version="1.0" encoding="utf-8"?>
<!--
  Compile package

  Copyright (C) 2014-2023 Ryan Specialty, 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/>.

  This will preprocess a package XML suitable for compilation into an object
  file.
-->
<stylesheet version="1.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:preproc="http://www.lovullo.com/rater/preproc"
            xmlns:lv="http://www.lovullo.com/rater"
            xmlns:t="http://www.lovullo.com/rater/apply-template"
            xmlns:c="http://www.lovullo.com/calc"
            xmlns:w="http://www.lovullo.com/rater/worksheet"
            xmlns:lvv="http://www.lovullo.com/rater/validate"
            xmlns:ext="http://www.lovullo.com/ext"
            xmlns:util="http://www.lovullo.com/util">


<include href="../dslc-base.xsl" />

<!-- phases -->
<include href="macros.xsl" />
<include href="expand.xsl" />
<include href="symtable.xsl" />

<include href="../../compiler/fragments.xsl" />


<!-- begin preprocessing from an arbitrary node -->
<template name="preproc:pkg-compile" as="element( lv:package )"
              match="*" mode="preproc:compile" priority="1">
  <param name="orig-root" as="element()"
             select="root(.)/lv:package" />

  <param name="stopshort" />

  <!-- should be provided externally -->
  <if test="not( $__rseed ) or ( $__rseed = '' )">
    <message terminate="yes">
      <text>[preproc] error: missing random seed `__rseed'</text>
    </message>
  </if>

  <message>
    <text>[preproc] *beginning macro expansion...</text>
  </message>

  <!-- these can contain repass nodes, thus the element()+ -->

  <!-- macro expansion -->
  <variable name="stage1" as="element()+">
    <apply-templates select="." mode="preproc:macropass" />

    <message>
      <text>[preproc] *macro pass complete; expanding...</text>
    </message>
  </variable>

  <!-- expand shorthands, etc -->
  <variable name="stage2" as="element()+">
    <apply-templates select="$stage1"
      mode="preproc:expand" />

    <message>
      <text>[preproc] *expansion complete; generating symbol table...</text>
    </message>
  </variable>

  <variable name="stage3" as="element()+">
    <apply-templates select="$stage2"
      mode="preproc:sym-discover">

      <with-param name="orig-root" select="$orig-root" />
    </apply-templates>

    <message>
      <text>[preproc] *symbol table generated; checking for </text>
      <text>unprocessed templates...</text>
    </message>
  </variable>


  <!-- TODO: resolve this mess -->
  <variable name="stage3-pkg" as="element( lv:package )"
                select="$stage3/preproc:symtable/parent::*" />

  <!-- TODO: move me somewhere more appropriate and create an error
       system that is _guaranteed_ to catch everything -->
  <for-each select="$stage3-pkg/preproc:symtable/preproc:error">
    <message terminate="yes">
      <text>!!! [preproc] error: </text>
      <value-of select="." />
    </message>
  </for-each>


  <!-- determine if we should finish or simply return for further processing -->
  <choose>
    <when test="not( $stopshort )">
      <!-- template expansions may have been deferred until their symbols were made
           available -->
      <variable name="final" as="element( lv:package )">
        <call-template name="preproc:tpl-sym-recurse">
          <with-param name="package" select="$stage3-pkg" />
          <with-param name="orig-root" select="$orig-root" />
        </call-template>
      </variable>

      <variable name="extern-chk" as="element( preproc:error )*"
                    select="preproc:final-extern-check(
                              $final/preproc:symtable )" />


      <if test="$extern-chk">
        <for-each select="$extern-chk">
          <message>
            <text>!!! [preproc] error: </text>
            <value-of select="." />
          </message>
        </for-each>

        <message>~~~~[begin document dump]~~~~</message>
        <message select="$final" />
        <message>~~~~[end document dump]~~~~</message>

        <message select="'Aborting due to unresolved externs'" />

        <message terminate="yes"
                     select="'Document dumped.'" />
      </if>

      <!-- ensure that all template parameters have been expanded -->
      <apply-templates select="$final" mode="preproc:tpl-check" />

      <!-- perform validation before dependency generation to ensure that all
           dependencies are available -->
      <apply-templates select="$final" mode="preproc:pkg-validate" />

      <!-- determine how many passes have been made -->
      <!-- TODO: reintroduce
      <variable name="repass-count"
        select="count( $final//preproc:repass-record )" />
        -->

      <!-- assign unique ids to each node -->
      <variable name="idized" as="element( lv:package )">
        <apply-templates select="$final" mode="preproc:idize" />
      </variable>

      <!-- generate deps -->
      <variable name="depd" as="element( lv:package )">
        <apply-templates select="$idized" mode="preproc:gen-deps" />
      </variable>

      <!-- post-process symbol table to resolve any unknowns that require a
           dependency graph -->
      <variable name="resolvd" as="element( lv:package )">
        <apply-templates select="$depd" mode="preproc:resolv-syms">
          <with-param name="orig-root" select="$orig-root" />
        </apply-templates>
      </variable>


      <!-- compile fragments -->
      <message>
        <text>[preproc] compiling fragments...</text>
      </message>


      <apply-templates select="$resolvd"
                           mode="preproc:compile-fragments" />


      <!-- output a repass count, which could be a strong indicator of performance
           issues -->
      <!-- TODO: reintroduce
      <message>
        <text>[Preprocessor repass count: </text>
          <value-of select="$repass-count" />
        <text>] [Node count: </text>
          <value-of select="count( $final//* )" />
        <text>]</text>
      </message>
      -->
    </when>

    <!-- return for further processing -->
    <otherwise>
      <sequence select="$stage3" />
    </otherwise>
  </choose>
</template>


<!--
  A very primitive guard against unexpanded template parameters.
-->
<template match="*[ starts-with( @*, '@' ) ]" mode="preproc:tpl-check" priority="5">
  <message>
    <text>[preproc] fatal: unexpanded template parameter: </text>
    <sequence select="@*[ starts-with( ., '@' ) ]" />
  </message>

  <message>
    <text>[preproc] fatal: reference node: </text>
    <sequence select="." />
  </message>

  <message>~~~~[begin document dump]~~~~</message>
  <message select="root(.)" />
  <message>~~~~[end document dump]~~~~</message>

  <message terminate="yes">[preproc] notice: Document dumped.</message>
</template>


<!-- this should never happen; but is has, so here's a failsafe -->
<template match="lv:apply-template" mode="preproc:tpl-check" priority="5">
  <message>
    <text>[preproc] fatal: unexpanded template: </text>
    <sequence select="." />
  </message>

  <message>
    <text>[preproc] fatal: reference node: </text>
    <sequence select="." />
  </message>

  <message select="'[preproc] internal error: there is a bug in the',
                       'preprocessor; this should never happen!'" />

  <message>~~~~[begin document dump]~~~~</message>
  <message select="root(.)" />
  <message>~~~~[end document dump]~~~~</message>

  <message terminate="yes">[preproc] notice: Document dumped.</message>
</template>


<!-- skip things that cannot contain template applications -->
<template match="lv:template|lv:const|lv:typedef|lv:param-copy"
              mode="preproc:tpl-check" priority="9">
</template>

<template match="*" mode="preproc:tpl-check" priority="1">
  <apply-templates select="*" mode="preproc:tpl-check" />
</template>



<!--
  TODO: This needs to go away in the form of a more performant system
  that marks a repass as needed *without* scanning the entire tree.
-->
<template name="preproc:tpl-sym-recurse" as="element( lv:package )">
  <param name="package" as="element( lv:package )" />
  <param name="orig-root" as="element()" />

  <!-- number of iterations where no template applications have
       happened -->
  <param name="tpl-stall-count"
             tunnel="yes"
             select="0" />

  <!-- get a list of needed template applications (ignoring applications that
       are within templates and nested applications) -->
  <variable name="apply" as="element( lv:apply-template )*"
                select="$package//lv:apply-template[
                          not(
                            @name=$package/lv:template/@name
                            or ancestor::lv:template
                            or ancestor::lv:apply-template
                          )
                        ]" />
  <variable name="napply" select="count( $apply )" />

  <choose>
    <when test="$apply">
      <!-- get a list of required templates -->
      <variable name="req">
        <preproc:tpl>
          <for-each select="$apply">
            <copy>
              <sequence select="@name" />
            </copy>
          </for-each>
        </preproc:tpl>
      </variable>

      <variable name="requniq" as="element( lv:apply-template )*" select="
          $req//lv:apply-template[
            not( @name=preceding-sibling::lv:apply-template/@name
                 or @name=$package//lv:template/@name ) ]
        " />

      <message>
        <text>[preproc] </text>
        <value-of select="count( $apply )" />
        <text> template(s) still need application: </text>

        <for-each select="$apply">
          <if test="position() gt 1">
            <text>, </text>
          </if>

          <value-of select="@name" />
        </for-each>
      </message>

      <!-- load each of the requested templates (but only once) -->
      <variable name="tpls">
        <!-- TODO: we no longer need to load the templates; the
             template replacement looks up the template on-demand from
             the symbol table; we're no longer injecting them -->
        <apply-templates mode="preproc:tpl-from-sym" select="$requniq">
          <with-param name="orig-root" select="$orig-root" />
          <with-param name="symtable" select="$package/preproc:symtable" />
        </apply-templates>
      </variable>

      <!-- if we have recursed and have not decreased the application count at all,
           then we have a problem -->
      <if test="$requniq
                    and ( $package//preproc:sym-available )
                    and not( $package//preproc:repass[ @tpl-applied ] )">
        <message terminate="yes">
          <text>!!! [preproc] fatal: unable to locate symbols for </text>
          <text>remaining templates: </text>

          <for-each select="$requniq">
            <if test="position() > 1">
              <text>; </text>
            </if>

            <value-of select="@name" />
          </for-each>
        </message>
      </if>

      <!-- if there was an error during this part of the process, halt -->
      <if test="$tpls//preproc:error">
        <message terminate="yes">
          <text>!!! [preproc] fatal: terminating due to errors</text>
        </message>
      </if>

      <!-- perform expansion on the new package with the needed templates -->
      <variable name="result" as="element( lv:package )">
        <apply-templates select="$package" mode="preproc:compile">
          <with-param name="orig-root" select="$orig-root" />
          <with-param name="stopshort" select="true()" />
        </apply-templates>

        <message>
          <text>[preproc] *expansion complete (recursive)</text>
        </message>
      </variable>

      <!-- failsafe to prevent infinite recursion (5 (0-indexed)
           should be plenty, since everything in the system results in
           a template application in fewer steps -->
      <if test="$requniq and $tpl-stall-count eq 4">
        <message select="'!!! [preproc] internal: expansion deadlock!'" />
        <message
            select="'!!! [preproc] internal: stalled for 5 iterations'" />

        <sequence select="preproc:dump-document( $result )" />

        <message terminate="yes">
          <text></text>
          <text>!!! [preproc] fatal: expansion of remaining </text>
          <text>templates aborted (have all been imported?): </text>

          <for-each select="$requniq">
            <if test="position() > 1">
              <text>; </text>
            </if>

            <value-of select="@name" />
          </for-each>
        </message>
      </if>

      <!-- recurse to continue expanding if need be -->
      <call-template name="preproc:tpl-sym-recurse">
        <with-param name="orig-root" select="$orig-root" />
        <with-param name="package" select="$result" />
        <with-param name="tpl-stall-count"
                        tunnel="yes"
                        select="if ( $package//preproc:tpl-step ) then
                                  0
                                else
                                  $tpl-stall-count + 1" />
      </call-template>
    </when>


    <!-- expansion sequences and template short-hand expansions that
         have not yet taken place, due to one reason or another (too
         few passes: bug as far as I'm concerned) -->
    <when test="$package//lv:expand-sequence[
                      not( ancestor::lv:template
                           or ancestor::lv:apply-template ) ]
                    |$package//t:*[
                       not( ancestor::lv:template
                            or ancestor::lv:apply-template ) ]">
      <message select="'[preproc] pending expansions still present'" />

      <variable name="result" as="element( lv:package )">
        <apply-templates select="$package" mode="preproc:compile">
          <with-param name="orig-root" select="$orig-root" />
          <with-param name="stopshort" select="true()" />
        </apply-templates>
      </variable>

      <call-template name="preproc:tpl-sym-recurse">
        <with-param name="orig-root" select="$orig-root" />
        <with-param name="package" select="$result" />
      </call-template>
    </when>


    <!-- no further applications are necessary -->
    <otherwise>
      <!-- apply one final pass for the eligibility class generation -->
      <call-template name="preproc:elig-class-pass">
        <with-param name="package" select="$package" />
        <with-param name="orig-root" select="$orig-root" />
      </call-template>
    </otherwise>
  </choose>
</template>


<template name="preproc:elig-class-pass" as="element( lv:package )">
  <param name="package" as="element( lv:package )" />
  <param name="orig-root" as="element()" />

  <variable name="final-pkg" as="element( lv:package )">
    <apply-templates select="$package" mode="preproc:expand-elig-class">
      <with-param name="orig-root" select="$package" />
    </apply-templates>
  </variable>

  <!-- yield the final pass against the elig-class-augmented package -->
  <apply-templates select="$final-pkg" mode="preproc:compile">
    <with-param name="orig-root" select="$orig-root" />
    <with-param name="stopshort" select="true()" />
  </apply-templates>
</template>


<!--
  This is but a shell of its former self.

  We are no longer injecting templates, so this can be removed or
  adapted to do just enough to verify that the template symbols exist.
-->
<template match="lv:apply-template" mode="preproc:tpl-from-sym">
  <param name="orig-root" as="element()" />

  <param name="symtable" as="element( preproc:symtable )"
             select="root(.)/preproc:symtable" />

  <variable name="tplname" select="@name" />

  <variable name="sym" as="element( preproc:sym )?" select="
      $symtable/preproc:sym[
        @name=$tplname
        and @type='tpl'
      ]
    " />

  <!-- if we have a symbol table, then attempt to locate it -->
  <choose>
    <!-- if we have located the template, then we know its source (@src must be
         set, otherwise the template is defined in this package and should have
         been available to begin with) -->
    <when test="$sym">
      <preproc:sym-available for="{$tplname}" />
    </when>

    <!-- nothing we can do yet -->
    <otherwise>
      <message>
        <text>[preproc] template symbol not yet available: </text>
        <value-of select="$tplname" />
      </message>
    </otherwise>
  </choose>
</template>


<!-- TODO: we should use an attr besides _id; one more definitive -->
<template match="*[ @_id ]" mode="preproc:macropass" priority="9">
  <!-- already preprocessed -->
  <sequence select="." />
</template>
<template match="*[ @_id ]" mode="preproc:idize" priority="9">
  <!-- already preprocessed -->
  <sequence select="." />
</template>



<template match="preproc:repass-record" mode="preproc:idize" priority="9">
  <!-- no longer needed; remove -->
</template>


<!-- do not idize preproc nodes (unneeded waste of cycles) -->
<template match="preproc:*" mode="preproc:idize" priority="5">
  <sequence select="." />
</template>

<!-- do not idize templates (will cause processing problems) -->
<template match="lv:template" mode="preproc:idize" priority="5">
  <sequence select="." />
</template>


<!--
  Generates a unique id for each element and stores it as @_id

  This allows the node set to be copied and maintain its identity.
-->
<template match="*" mode="preproc:idize" priority="1">
  <variable name="id">
    <value-of select="generate-id(.)" />
  </variable>

  <copy>
    <sequence select="@*" />

    <attribute name="_id">
      <value-of select="$id" />
    </attribute>

    <apply-templates mode="preproc:idize" />
  </copy>
</template>


<template match="lv:package" mode="preproc:resolv-syms" as="element( lv:package )"
              priority="9">
  <param name="orig-root" as="element()" />
  <param name="rpcount" select="0" />

  <variable name="symtable-map" as="map( xs:string, element( preproc:sym ) )"
            select="map:merge(
                      for $sym in preproc:symtable/preproc:sym
                        return map{ string( $sym/@name ) : $sym } )" />

  <variable name="symdep-map" as="map( xs:string, element( preproc:sym-dep ) )"
            select="map:merge(
                      for $sym-dep in preproc:sym-deps/preproc:sym-dep
                        return map{ string( $sym-dep/@name ) : $sym-dep } )" />

  <!-- arbitrary; intended to prevent infinite recursion -->
  <!-- TODO: same method as for templates; ensure changes, but do not create
       arbitrary limit -->
  <if test="$rpcount = 100">
    <sequence select="preproc:dump-document( root() )" />

    <message terminate="yes">
      <text>[preproc] !!! recursion limit reached in resolving `</text>
        <value-of select="@name" />
      <text>' symbols</text>
    </message>
  </if>

  <variable name="result" as="element( lv:package )">
    <copy>
      <sequence select="@*" />

      <apply-templates mode="preproc:resolv-syms">
        <with-param name="orig-root" select="$orig-root" />
        <with-param name="symtable-map" select="$symtable-map" tunnel="yes" />
        <with-param name="symdep-map" select="$symdep-map" tunnel="yes" />
      </apply-templates>
    </copy>
  </variable>

  <variable name="repass"
    select="$result//preproc:symtable/preproc:repass" />

  <choose>
    <!-- repass scheduled; go for it -->
    <when test="$repass">
      <apply-templates select="$result" mode="preproc:resolv-syms">
        <with-param name="orig-root" select="$orig-root" />
        <with-param name="rpcount" select="$rpcount + 1" />
      </apply-templates>
    </when>

    <!-- no repass needed; done -->
    <otherwise>
      <sequence select="$result" />
    </otherwise>
  </choose>
</template>


<template match="preproc:symtable" mode="preproc:resolv-syms" priority="5">
  <param name="orig-root" as="element()" />

  <copy>
    <apply-templates mode="preproc:resolv-syms">
      <with-param name="orig-root" select="$orig-root" />
    </apply-templates>
  </copy>
</template>


<!--
  Calculate symbol dimensions by taking the highest dimension of its
  dependencies

  If all dependencies are not yet resolved, then schedule a repass.
-->
<template match="preproc:sym[ not( @src ) and @dim='?' ]" mode="preproc:resolv-syms" priority="5">
  <param name="orig-root" as="element()" />
  <param name="symtable-map" as="map(*)" tunnel="yes" />
  <param name="symdep-map" as="map(*)" tunnel="yes" />

  <variable name="name" select="@name" />
  <variable name="pkg"  as="element( lv:package )"
                select="root(.)" />

  <variable name="deps" as="element( preproc:sym-dep )?"
            select="$symdep-map( $name )" />

  <!-- TODO: make this fatal -->
  <if test="empty( $deps ) and not( @no-deps = 'true' )">
    <message select="concat( 'internal: failed to located dependencies for `',
                             $name, '''' )" />
  </if>

  <variable name="depsyms" as="element( preproc:sym )*"
            select="for $ref in $deps/preproc:sym-ref
                      return $symtable-map( $ref/@name )" />

  <choose>
    <!-- unresolved dependency dimensions; defer until next pass -->
    <when test="
        $depsyms/@dim = '?'
      ">
      <!-- schedule repass :x -->
      <sequence select="." />
      <preproc:repass src="preproc:sym resolv-syms"
                      ref="{$name}" />
    </when>

    <!-- all dependencies are resolved; calculate dimensions -->
    <otherwise>
      <variable name="max" as="xs:double"
                select="if ( empty( $depsyms ) ) then
                            0
                          else
                            max( $depsyms/@dim )" />

      <copy>
        <sequence select="@*" />
        <attribute name="dim" select="$max" />
        <sequence select="*" />
      </copy>
    </otherwise>
  </choose>
</template>


<template match="preproc:repass" mode="preproc:resolv-syms" priority="9">
  <!-- strip -->
</template>


<template match="*" mode="preproc:resolv-syms" priority="1">
  <sequence select="." />
</template>


<template match="lv:package" mode="preproc:pkg-validate">
  <variable name="symbol-map">
    <call-template name="get-symbol-map" />
  </variable>

  <variable name="err">
    <apply-templates select="." mode="lvv:validate">
      <with-param name="symbol-map" select="$symbol-map" />
    </apply-templates>
  </variable>

  <apply-templates select="$err//lvv:error" mode="preproc:handle-lvv-errors">
    <with-param name="document" select="." />
  </apply-templates>
</template>


<template match="*|text()" mode="preproc:pkg-validate">
</template>


<!-- errors should cause a failure -->
<template match="lvv:error" mode="preproc:handle-lvv-errors" priority="5">
  <param name="document" />

  <!-- output error -->
  <message>
    <text>!!! </text>
    <value-of select="@desc" />

    <if test="@path != ''">
      <text> (</text>
      <value-of select="@path" />
      <text>)</text>
    </if>

    <text>: </text>
    <value-of select="." />
  </message>

  <!-- terminate after we've output each error -->
  <if test="not( following-sibling::lvv:error )">
    <!-- dump document for debugging -->
    <sequence select="preproc:dump-document( $document )" />
    <message terminate="yes">Compilation failed due to validation errors.</message>
  </if>
</template>


<function name="preproc:dump-document">
  <param name="document" />

  <message>~~~~[begin document dump]~~~~</message>
  <message select="$document" />
  <message>~~~~[end document dump]~~~~</message>
  <message>internal: document dumped.</message>
</function>


<template match="node()|text()" mode="preproc:handle-lvv-errors" priority="1">
</template>

</stylesheet>
