Printing the Tree with XSLT
The tree says what the program is. The printer says how it is written: which keywords and punctuation each kind of element becomes, where the lines break and how far they are indented. This is the kind of job XSLT's template rules were made for, as in Transforming with XSLT: the output follows the input, one rule for each kind of element, and the processor finds the rule for each one.
The printer
print.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<!-- Prints the tree of a TypeScript program as TypeScript text:
one template rule for each kind of element in the tree. -->
<xsl:stylesheet version="4.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:print="https://example.org/print"
exclude-result-prefixes="#all">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:preserve-space elements="string comment doc regex"/>
<!-- ===== Layout: the only rules that know about lines and indentation. -->
<!-- One level of indentation. -->
<xsl:param name="indent" as="xs:string" select="' '"/>
<!-- The indentation of a node: one level for each block it is in. -->
<xsl:function name="print:indent" as="xs:string">
<xsl:param name="node" as="element()"/>
<xsl:sequence select="string-join(
$node/ancestor::*[self::body or self::then or self::else or self::interface or self::object]
! $indent)"/>
</xsl:function>
<!-- Whether a block is written with braces: all but the then of an if
that has one statement and no else, which stays on the line of the if. -->
<xsl:function name="print:braces" as="xs:boolean">
<xsl:param name="block" as="element()"/>
<xsl:sequence select="not($block/self::then and count($block/*) = 1 and empty($block/../else))"/>
</xsl:function>
<!-- Declarations, statements, members and entries are lines of their own. -->
<xsl:template match="module/* | body/* | then[print:braces(.)]/* | else/*
| interface/(property | method) | object/entry" priority="1">
<xsl:value-of select="print:indent(.)"/>
<xsl:next-match/>
<xsl:text> </xsl:text>
</xsl:template>
<!-- An empty line before each declaration that follows another. -->
<xsl:template match="module/*[not(self::import)][preceding-sibling::*]" priority="2">
<xsl:text> </xsl:text>
<xsl:next-match/>
</xsl:template>
<!-- A block: its lines between braces, or its one statement. -->
<xsl:template match="body | then | else | object">
<xsl:call-template name="print:block"/>
</xsl:template>
<xsl:template name="print:block">
<xsl:choose>
<xsl:when test="print:braces(.)">
<xsl:text>{ </xsl:text>
<xsl:apply-templates select="* except doc"/>
<xsl:value-of select="print:indent(.)"/>
<xsl:text>}</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- A documentation comment, on the line before its declaration. -->
<xsl:template match="doc">
<xsl:text>/** </xsl:text>
<xsl:value-of select="."/>
<xsl:text> */ </xsl:text>
<xsl:value-of select="print:indent(..)"/>
</xsl:template>
<!-- ===== Files -->
<!-- A program is a set of modules; a module on its own is printed as
the principal result. -->
<xsl:template match="program | module">
<xsl:apply-templates/>
</xsl:template>
<!-- Each module of a program is a file. -->
<xsl:template match="program/module">
<xsl:result-document href="{@file}" method="text">
<xsl:apply-templates/>
</xsl:result-document>
</xsl:template>
<xsl:template match="comment">// <xsl:value-of select="."/></xsl:template>
<xsl:template match="import">
<xsl:text>import </xsl:text>
<xsl:if test="@type-only">type </xsl:if>
<xsl:text>{ </xsl:text>
<xsl:value-of select="name" separator=", "/>
<xsl:text> } from </xsl:text>
<xsl:value-of select="print:string(@from)"/>
<xsl:text>;</xsl:text>
</xsl:template>
<!-- ===== Declarations -->
<xsl:template match="@export">export </xsl:template>
<xsl:template match="type-alias">
<xsl:apply-templates select="@export"/>
<xsl:text>type </xsl:text>
<xsl:value-of select="@name"/>
<xsl:text> = </xsl:text>
<xsl:apply-templates/>
<xsl:text>;</xsl:text>
</xsl:template>
<xsl:template match="interface">
<xsl:apply-templates select="doc, @export"/>
<xsl:text>interface </xsl:text>
<xsl:value-of select="@name"/>
<xsl:text> </xsl:text>
<xsl:call-template name="print:block"/>
</xsl:template>
<xsl:template match="property">
<xsl:value-of select="@name"/>
<xsl:if test="@optional">?</xsl:if>
<xsl:text>: </xsl:text>
<xsl:apply-templates/>
<xsl:text>;</xsl:text>
</xsl:template>
<xsl:template match="method">
<xsl:apply-templates select="doc"/>
<xsl:value-of select="@name"/>
<xsl:apply-templates select="." mode="signature"/>
<xsl:text>;</xsl:text>
</xsl:template>
<xsl:template match="function">
<xsl:apply-templates select="doc, @export"/>
<xsl:if test="@async">async </xsl:if>
<xsl:text>function </xsl:text>
<xsl:value-of select="@name"/>
<xsl:apply-templates select="." mode="signature"/>
<xsl:text> </xsl:text>
<xsl:apply-templates select="body"/>
</xsl:template>
<!-- The parameters and the return type of a function or method. -->
<xsl:template match="*" mode="signature">
<xsl:text>(</xsl:text>
<xsl:for-each select="parameter" separator=", ">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:text>)</xsl:text>
<xsl:for-each select="returns">: <xsl:apply-templates/></xsl:for-each>
</xsl:template>
<xsl:template match="parameter">
<xsl:value-of select="@name"/>
<xsl:for-each select="*">: <xsl:apply-templates select="."/></xsl:for-each>
</xsl:template>
<!-- ===== Types -->
<xsl:template match="type">
<xsl:value-of select="@name"/>
<xsl:if test="*">
<xsl:text><</xsl:text>
<xsl:for-each select="*" separator=", ">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:text>></xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="array-type"><xsl:apply-templates/>[]</xsl:template>
<xsl:template match="union">
<xsl:for-each select="*" separator=" | ">
<xsl:apply-templates select="."/>
</xsl:for-each>
</xsl:template>
<xsl:template match="predicate"><xsl:value-of select="@parameter"/> is <xsl:apply-templates/></xsl:template>
<!-- ===== Statements -->
<xsl:template match="statement"><xsl:apply-templates/>;</xsl:template>
<xsl:template match="return">return <xsl:apply-templates/>;</xsl:template>
<xsl:template match="const">const <xsl:value-of select="@name"/> = <xsl:apply-templates/>;</xsl:template>
<xsl:template match="if">
<xsl:text>if (</xsl:text>
<xsl:apply-templates select="*[1]"/>
<xsl:text>) </xsl:text>
<xsl:apply-templates select="then"/>
<xsl:if test="else"> else </xsl:if>
<xsl:apply-templates select="else"/>
</xsl:template>
<xsl:template match="for-of">
<xsl:text>for (const </xsl:text>
<xsl:value-of select="@name"/>
<xsl:text> of </xsl:text>
<xsl:apply-templates select="*[1]"/>
<xsl:text>) </xsl:text>
<xsl:apply-templates select="body"/>
</xsl:template>
<!-- ===== Expressions -->
<xsl:template match="name | number | boolean">
<xsl:value-of select="."/>
</xsl:template>
<!-- A string literal: JSON's strings are JavaScript's. -->
<xsl:function name="print:string" as="xs:string">
<xsl:param name="text" as="xs:string"/>
<xsl:sequence select="serialize($text, { 'method': 'json', 'escape-solidus': false() })"/>
</xsl:function>
<xsl:template match="string">
<xsl:value-of select="print:string(.)"/>
</xsl:template>
<!-- A regular expression literal: a slash in it is escaped. -->
<xsl:template match="regex">
<xsl:text>/</xsl:text>
<xsl:value-of select="replace(., '/', '\\/')"/>
<xsl:text>/</xsl:text>
<xsl:value-of select="@flags"/>
</xsl:template>
<xsl:template match="member"><xsl:apply-templates/>.<xsl:value-of select="@name"/></xsl:template>
<xsl:template match="not">!<xsl:apply-templates/></xsl:template>
<xsl:template match="await">await <xsl:apply-templates/></xsl:template>
<!-- The symbol of each operator. The generator never puts one binary
expression in another, so no parentheses are needed. -->
<xsl:variable name="operators" select="{
'plus': '+', 'less': '<', 'greater': '>', 'at-most': '<=', 'at-least': '>=',
'equal': '===', 'not-equal': '!=='
}"/>
<xsl:template match="binary">
<xsl:for-each select="*" separator=" {$operators(current()/@operator)} ">
<xsl:apply-templates select="."/>
</xsl:for-each>
</xsl:template>
<!-- A call: the function's name, or a method of the first child. -->
<xsl:template match="call">
<xsl:choose>
<xsl:when test="@method">
<xsl:apply-templates select="*[1]"/>
<xsl:text>.</xsl:text>
<xsl:value-of select="@method"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="@function"/>
</xsl:otherwise>
</xsl:choose>
<xsl:if test="type">
<xsl:text><</xsl:text>
<xsl:for-each select="type" separator=", ">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:text>></xsl:text>
</xsl:if>
<xsl:text>(</xsl:text>
<xsl:for-each select="(if (@method) then tail(*) else *) except type" separator=", ">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:text>)</xsl:text>
</xsl:template>
<xsl:template match="array">
<xsl:text>[</xsl:text>
<xsl:for-each select="*" separator=", ">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:text>]</xsl:text>
</xsl:template>
<xsl:template match="arrow">
<xsl:text>(</xsl:text>
<xsl:for-each select="parameter" separator=", ">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:text>) => </xsl:text>
<xsl:apply-templates select="* except parameter"/>
</xsl:template>
<xsl:template match="entry"><xsl:value-of select="@name"/>: <xsl:apply-templates/>,</xsl:template>
<!-- A kind of element the printer doesn't know is an error, not a guess. -->
<xsl:template match="*">
<xsl:sequence select="error(#print:unknown, 'The printer has no rule for ' || name())"/>
</xsl:template>
</xsl:stylesheet>
The stylesheet writes text, so xsl:output says method="text", and
the text in a template body is the TypeScript itself: the rule for
return reads return <xsl:apply-templates/>;. xsl:strip-space
drops the spaces and line breaks that indent the tree, except inside
the elements whose text is part of the program, such as string.
Let's read the rules in groups.
- Layout. Only the first group knows about lines.
print:indentcounts the blocks around a node (a function'sbody, athen, an interface, an object) and indents by$indentfor each.print:bracessays which blocks get braces: all but thethenof anifwith one statement and noelse, which stays on the line of theif. The rule with priority 1 matches everything that is a line of its own: a declaration in a module, a statement in a block, a member of an interface, an entry of an object. It writes the indentation, callsxsl:next-matchto let the rule for the element write the element, and ends the line. The rule with priority 2 puts an empty line before each declaration, and then lets the line rule do the rest. So every other rule writes one construct without a thought for where it is. - Files. Each
moduleof aprogrambecomes a file throughxsl:result-document, named by itsfileattribute. Amoduleon its own is written to standard output, which is what the pieces of the last section used. - Declarations, types, statements. One rule each, in the words of the
vocabulary. Lists are joined with the
separatorattribute ofxsl:for-each, new in XSLT 4.0: parameters, arguments and type arguments with", ", a union with" | ". - Expressions. A
stringbecomes a JSON string, which is a valid JavaScript string, with every escape it needs; the same function writes thefromof an import. Aregexescapes its slashes. The operators are a map from their names to their symbols, used as the separator of abinaryexpression's operands. The generator never puts one binary expression into another, so the printer needs no rules for parentheses; a printer for any tree would add them where the precedence of the operators needs them. - Anything else is an error: the last rule matches every element that no other rule does, with the lowest priority, and stops the transformation, so an element the printer doesn't know can't be dropped without a word:
$ echo '<module><while/></module>' | sxml transform print.xsl -
print.xsl:302:20: print:unknown: The printer has no rule for while
Writing the files
generate.sh, in the last section, keeps the tree of the program in
generated/program.xml, next to the files printed from it. Print it
into a new directory: the --result-directory option lets the
transformation write its result documents there, as in Many Outputs,
Functions and Packages:
$ mkdir out
$ sxml transform --result-directory=out print.xsl generated/program.xml
$ ls out
browser.ts
console.ts
node.ts
server.ts
types.ts
A client module has one async function for each remote function that its client offers. The frontend SDK's has two. Each function checks the input before it sends it, so a wrong value fails at once, in the browser, and checks the output, so a service that answers with something else can't go unnoticed:
$ cat out/browser.ts
// Generated from the API description by program.xq and print.xsl. Do not edit.
import { call, expect } from "../runtime.ts";
import type { EvaluationContext, Evaluations, StatusRequest, FlagStatus } from "./types.ts";
import { checkEvaluationContext, checkEvaluations, checkStatusRequest, checkFlagStatus } from "./types.ts";
/** Whether each flag is on for one user, for an SDK in a browser page. */
export async function evaluateFlags(input: EvaluationContext): Promise<Evaluations> {
expect<EvaluationContext>(input, checkEvaluationContext);
const output = await call("evaluateFlags", "query", input);
return expect<Evaluations>(output, checkEvaluations);
}
/** The state of one flag and its failed checks since the last rollout step. */
export async function getStatus(input: StatusRequest): Promise<FlagStatus> {
expect<StatusRequest>(input, checkStatusRequest);
const output = await call("getStatus", "query", input);
return expect<FlagStatus>(output, checkFlagStatus);
}
The frontend SDK imports evaluateFlags and calls it like any other
function; that it runs in the service is a detail of how it is made.
node.ts and console.ts look the same, with the functions of the
backend SDK and of the console, and getStatus is in both SDKs'
modules. The service's side is an interface, which the service's own
code implements, and a handler that routes each request to it, with
the checks around it. It has every function, whichever client calls
it: a request is checked by the service whether or not it came through
a generated client.
$ cat out/server.ts
// Generated from the API description by program.xq and print.xsl. Do not edit.
import { serve } from "../runtime.ts";
import type { FlagsRequest, FlagSet, EvaluationContext, Evaluations, StatusRequest, FlagStatus, RolloutRequest, KillRequest, KillResult } from "./types.ts";
import { checkFlagsRequest, checkFlagSet, checkEvaluationContext, checkEvaluations, checkStatusRequest, checkFlagStatus, checkRolloutRequest, checkKillRequest, checkKillResult } from "./types.ts";
/** What the service does: one method for each remote function. */
export interface Implementation {
/** Every flag with its rules, for an SDK that evaluates flags itself. */
getFlags(input: FlagsRequest): Promise<FlagSet>;
/** Whether each flag is on for one user, for an SDK in a browser page. */
evaluateFlags(input: EvaluationContext): Promise<Evaluations>;
/** The state of one flag and its failed checks since the last rollout step. */
getStatus(input: StatusRequest): Promise<FlagStatus>;
/** Rolls a flag out to a new percentage once its guardrail checks pass. */
rollOutFlag(input: RolloutRequest): Promise<FlagStatus>;
/** Turns a flag off for every user at once. */
killFlag(input: KillRequest): Promise<KillResult>;
}
/** A Node.js request listener that checks every input and output. */
export function createHandler(implementation: Implementation) {
return serve({
getFlags: {
kind: "query",
checkInput: checkFlagsRequest,
checkOutput: checkFlagSet,
run: (input) => implementation.getFlags(input),
},
evaluateFlags: {
kind: "query",
checkInput: checkEvaluationContext,
checkOutput: checkEvaluations,
run: (input) => implementation.evaluateFlags(input),
},
getStatus: {
kind: "query",
checkInput: checkStatusRequest,
checkOutput: checkFlagStatus,
run: (input) => implementation.getStatus(input),
},
rollOutFlag: {
kind: "command",
checkInput: checkRolloutRequest,
checkOutput: checkFlagStatus,
run: (input) => implementation.rollOutFlag(input),
},
killFlag: {
kind: "command",
checkInput: checkKillRequest,
checkOutput: checkKillResult,
run: (input) => implementation.killFlag(input),
},
});
}
The objects inside objects of createHandler are indented by the same
rule as the statements: an entry is a line, inside one block more for
each object around it.
types.ts has every type of the schema, as in the pieces of the last
section. The check for a rollout request calls the checks of its three
properties:
$ sed -n '/^export function checkRolloutRequest/,/^}/p' out/types.ts
export function checkRolloutRequest(value: unknown, path: string, problems: string[]): value is RolloutRequest {
if (!checkObject(value, path, problems)) return false;
const count = problems.length;
checkFlagKey(value.flag, path + ".flag", problems);
checkPercentage(value.percentage, path + ".percentage", problems);
checkOwner(value.owner, path + ".owner", problems);
for (const key of Object.keys(value)) {
if (!["flag", "percentage", "owner"].includes(key)) problems.push(path + "." + key + ": is not allowed");
}
return problems.length === count;
}
The printer reads nothing but the tree: no date, no file name, no order that could change from one run to the next. So the same tree always gives the same text, and the same sources the same tree.
The same tree in another style
A team that wants four spaces, and braces around every block, doesn't
need another generator, or even another printer. A stylesheet imports
print.xsl and replaces the two things that decide the layout: the
parameter $indent and the function print:braces. A declaration in
the importing stylesheet wins over the one it imports, as a template
rule does:
braces.xsl:
<?xml version="1.0" encoding="UTF-8"?>
<!-- The printer in another style: four spaces, and braces around every block. -->
<xsl:stylesheet version="4.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:print="https://example.org/print"
exclude-result-prefixes="#all">
<xsl:import href="print.xsl"/>
<xsl:param name="indent" as="xs:string" select="' '"/>
<xsl:function name="print:braces" as="xs:boolean">
<xsl:param name="block" as="element()"/>
<xsl:sequence select="true()"/>
</xsl:function>
</xsl:stylesheet>
The check of a flag set, printed both ways:
$ sxml xquery --method=xml --expression='<module>{ doc("generated/program.xml")//function[@name = "checkFlagSet"] }</module>' > flag-set.xml
$ sxml transform print.xsl flag-set.xml
export function checkFlagSet(value: unknown, path: string, problems: string[]): value is FlagSet {
if (!checkObject(value, path, problems)) return false;
const count = problems.length;
if (Array.isArray(value.flags)) {
value.flags.forEach((item, index) => checkFlagConfig(item, path + ".flags[" + index + "]", problems));
} else {
problems.push(path + ".flags: expected an array");
}
for (const key of Object.keys(value)) {
if (!["flags"].includes(key)) problems.push(path + "." + key + ": is not allowed");
}
return problems.length === count;
}
$ sxml transform braces.xsl flag-set.xml
export function checkFlagSet(value: unknown, path: string, problems: string[]): value is FlagSet {
if (!checkObject(value, path, problems)) {
return false;
}
const count = problems.length;
if (Array.isArray(value.flags)) {
value.flags.forEach((item, index) => checkFlagConfig(item, path + ".flags[" + index + "]", problems));
} else {
problems.push(path + ".flags: expected an array");
}
for (const key of Object.keys(value)) {
if (!["flags"].includes(key)) {
problems.push(path + "." + key + ": is not allowed");
}
}
return problems.length === count;
}
Only the layout changed: the tree is the same, so what the code does is the same. A style is a few declarations, not a second copy of the generator.