Skip to content

Example site ​

한국어.

The example site renders shared templates with mock data through TypeScript, Go, Rust, PHP and Python. Each implementation loads the committed AST artifacts, passes variables as assign, registers the layout and its parts with define, and renders layout. Page complexity belongs in templates and mock data. Controllers, authentication, DI and form generation are outside this example.

Shared inputs ​

Each directory under examples/site/scenarios/ contains these inputs. The language-neutral shapes and adapter rules are fixed in the cross-language render contract.

InputPurpose
*.tplShared layout, partials, conditions, loops and scope arguments
data.jsonMock values passed as assign
define.jsonDirect identifier-to-template-path registrations passed as define
env.json (optional)Fixed clock and timezone for reproducible output
compiled/<language>/Build-time AST artifacts and source/artifact hashes used by the runtime

For example, the react-boundary scenario registers two templates:

json
{
  "layout": "layout.tpl",
  "content": "content.tpl"
}

Contract gate ​

The adapter structures are part of the shared compiler interface manifest. That manifest is the source for the generated declarations and Mermaid files under examples/showcase/adapters/generated/, while the language adapters implement the mapped type and operation names. The generator and checker are permanent repository tools, so a declaration or diagram can never drift silently from the manifest.

Run the gate directly with:

sh
make contract-generate
make contract-check

contract-check compiles the TypeScript, Go and Rust declarations, checks PHP with ReflectionClass, parses the Python adapter and its declarations, checks the JavaScript assertion helpers and runs all six adapters on all five scenarios. Each run checks the request shape, UTF-8 output hash, repeated render, invalid-target failure and recovery. The manifest also declares core-runtime, source-compiler and artifact-runtime; implementations with the same support level must provide the same logical operations. make showcase generates the artifacts before this gate; make showcase-check loads only committed artifacts.

The same files go to each implementation's existing CLI. Only the executable differs; every scenario renders the layout target with --data data.json --define define.json. The showcase adapter uses the language-specific artifact loader, while the normal CLI remains the source-mode development entry point. The runtime render call is:

js
engine.render('layout', assign, { define });

Set SHOWCASE_EXECUTION_MODE=generated to run generated mode. The build step creates one host-language source module containing the canonical templates for all showcase scenarios. The contract checker runs every scenario in both modes and compares output bytes, repeated renders and recovery after an invalid target.

The same request in five languages ​

The API examples below, one for each of the five languages and one for the JavaScript build of the TypeScript package, read the same scenario directory. They do not construct different business objects in each language: data.json is the assign object and define.json is the same identifier-to-path registry for every adapter. A definition object is reserved for per-definition data or finished HTML. The scope-precedence scenario is used because it contains a layout, a nested template definition and definition data.

TypeScript ​

ts
import { readFileSync } from 'node:fs';
import { AstProgram, Engine } from '@polyspec/template';
import { FsLoader } from '@polyspec/template/node';

const root = 'examples/site/scenarios/scope-precedence';
const readJson = (name: string) => JSON.parse(readFileSync(`${root}/${name}`, 'utf8'));
const engine = new Engine(new AstProgram({ loader: new FsLoader(root) }));
const assign = readJson('data.json');
const define = readJson('define.json');
const html = engine.render('layout', assign, { define });
console.log(html);

JavaScript ​

js
import { readFileSync } from 'node:fs';
import { AstProgram, Engine } from '@polyspec/template';
import { FsLoader } from '@polyspec/template/node';

const root = 'examples/site/scenarios/scope-precedence';
const readJson = name => JSON.parse(readFileSync(`${root}/${name}`, 'utf8'));
const engine = new Engine(new AstProgram({ loader: new FsLoader(root) }));
const assign = readJson('data.json');
const define = readJson('define.json');
const html = engine.render('layout', assign, { define });
console.log(html);

Go ​

go
package main

import (
  "encoding/json"
  "fmt"
  "os"

  template "github.com/polyspec/template/packages/go"
)

func read(path string) []byte {
  data, err := os.ReadFile(path)
  if err != nil {
    panic(err)
  }
  return data
}

func main() {
  root := "examples/site/scenarios/scope-precedence"
  program, err := template.NewAstProgram(template.Options{Loader: template.NewFSLoader(os.DirFS(root))})
  if err != nil {
    panic(err)
  }
  engine := template.NewEngine(program)

  var assign map[string]any
  if err := json.Unmarshal(read(root+"/data.json"), &assign); err != nil {
    panic(err)
  }
  var define map[string]template.DefineInput
  if err := json.Unmarshal(read(root+"/define.json"), &define); err != nil {
    panic(err)
  }
  html, err := engine.Render("layout", assign, template.RenderOptions{
    Define: define,
  })
  if err != nil {
    panic(err)
  }
  fmt.Print(html)
}

Rust ​

rust
use polyspec_template::{defines_from_json, AstProgram, Engine, EngineOptions, FsLoader, RenderOptions, RenderTarget};

fn render() -> Result<String, Box<dyn std::error::Error>> {
    let root = "examples/site/scenarios/scope-precedence";
    let assign: serde_json::Value = serde_json::from_slice(&std::fs::read(format!("{root}/data.json"))?)?;
    let define_json: serde_json::Value = serde_json::from_slice(&std::fs::read(format!("{root}/define.json"))?)?;
    let engine = Engine::new(AstProgram::new(EngineOptions {
        loader: Some(Box::new(FsLoader::new(root))),
        ..Default::default()
    })?);

    let options = RenderOptions {
        define: defines_from_json(&define_json)?,
        ..Default::default()
    };
    engine.render(RenderTarget::Name("layout"), &assign, &options)
}

PHP ​

php
use Polyspec\Template\AstProgram;
use Polyspec\Template\Engine;
use Polyspec\Template\Loader\FilesystemLoader;

$root = 'examples/site/scenarios/scope-precedence';
$readJson = fn (string $name): array => json_decode(file_get_contents("{$root}/{$name}"), true, flags: JSON_THROW_ON_ERROR);
$engine = new Engine(new AstProgram(new FilesystemLoader($root)));
$html = $engine->render('layout', $readJson('data.json'), ['define' => $readJson('define.json')]);
echo $html;

Python ​

python
import json

from polyspec.template import AstProgram, Engine, EngineOptions, FsLoader, RenderOptions

root = "examples/site/scenarios/scope-precedence"


def read_json(name):
    with open(f"{root}/{name}", encoding="utf-8") as file:
        return json.load(file)


engine = Engine(AstProgram(EngineOptions(loader=FsLoader(root))))
html = engine.render(
    "layout", read_json("data.json"), RenderOptions(define=read_json("define.json"))
)
print(html, end="")

Build the artifact ​

sh
make showcase SHOWCASE_ITERS=3000 SHOWCASE_WARMUP=300

This writes one committed canonical AST graph for each scenario, expected.html, the site's shared inputs in examples/site/data/scenarios.json, the comparison results in examples/site/data/results.json, and measurements in examples/site/data/benchmark.json. Every language loads that same AST graph and renders twice; the TypeScript API also renders twice through one engine instance. All five implementations must produce the same raw UTF-8 bytes without output filters.

The benchmark drivers keep one engine instance for warmup and measurement, then render once more and compare its hash. Five independent samples are recorded per implementation and scenario. The site reports median throughput and the P95 of the sample mean render times. These are warm renders after parsing and cache population, not individual request latency measurements. Absolute times depend on the machine and toolchain; compare measurements within one run.

Verify the artifact ​

sh
make showcase-check

This compares the committed HTML, JSON and AST artifacts with fresh artifact-only renders, then validates the generated page with an HTML structure parser. The page exposes the mock assign data, define registry, templates, artifacts and HTML output. It contains no runtime parser, renderer or browser verification step.

Type-fixed source generation runs with make typed-generator. One compiler entry builds the shared typed IR and passes it to an independent TypeScript, Go, Rust, PHP or Python backend module. It reads the complete compiled source-graph manifest and an explicit type manifest. Before a language backend runs, the shared compiler IR validates every template, resolves include and block paths, and types root variables, template inputs, locals, loop items, record members and function calls. An include declares the values required by its target and the compiler binds each value from caller scope; generated template functions therefore do not pretend that include-local values belong to root assign data. Each definition also declares its generated template target and whether HTML is accepted. The compiler rejects block scope fields that its target template did not declare. The compiler emits one static function per graph template plus a closed template dispatch in PHP, Go, Rust, TypeScript and Python, with manifest-derived assign, definition, template-input and record declarations. A generated block consumes the typed definition registry and invokes its generated child function itself; callers never pre-render a string slot. Definition data records preserve field presence. Input precedence is root assign, then supplied definition data fields, then explicit block scope. Nullable fields become ?T, *T, Option<T> and optional properties respectively. Lists and insertion-ordered maps retain their element types recursively. make compiler-ir-check covers every canonical node and expression variant and verifies rejection of undeclared symbols. make typed-generator-check verifies reproducibility. make typed-generator-compile-check compiles and executes all five scenario graphs in each of the five generated languages and requires byte-identical output. A manifest is required because arbitrary JSON does not contain enough information to infer static types safely.

Generated showcase execution enters the same public Engine API as AST execution. Each generated artifact implements GeneratedProgram; each adapter constructs Engine with either AstProgram or GeneratedProgram, then calls Engine.render. The contract checker rejects removed callback types and mode selection inside the engine.

Generated mode supplies no loader because its host-language artifact is the executable template graph. It does not package an AST graph as unused input. AstProgram alone loads compiled AST artifacts.

The static source view calls analyze once while building the site. Template tag boundaries and grammar kinds come from the template parser, while variables, literals and operators come from tokens consumed by the expression lexer. The site build fails on invalid template source. Compiled artifacts and generated source use bounded, two-axis scroll containers so a scenario remains readable without truncating its evidence.

The compiler-coverage scenario prevents a narrow generated backend from passing on text-only templates. One page executes assignment, list and map spread, member and index access, function, unary, binary and ternary expressions, conditionals, loops and every loop metadata field, an include that shares local scope, an if-block, definition data and block scope. It also exposes all five HTML escape characters, boolean results from && and ||, and empty-list/map truthiness in the rendered HTML. The contract runs that page through AST and generated mode in every runtime and requires the same 351 UTF-8 bytes after repeated rendering and failure recovery.