Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Texl is a template engine for text. A Texl template is any text, such as Markdown, HTML or a configuration file, with expressions between <% and %>. Rendering the template replaces each expression with its value.

<% user = {name: "Ada", tasks: ["review", "deploy"]} %>
Hi <% user.name %>, 
you have <% list.size user.tasks %> open tasks: <% list.enum user.tasks %>.
Hi Ada,
you have 2 open tasks: review, deploy.

The expressions are written in Swan, a small language designed for this job. Swan has values, operators, functions and namespaces, but no statements and no exceptions: every expression produces a value, and an invalid operation produces an Undefined value instead of stopping the rendering.

Templates can import other templates, by relative path or URL, to share headers, footers and libraries of functions. They can also wait for data that arrives later, such as a document fetched over the network.

Where Texl runs

Texl is written in Rust, and the same engine runs in several hosts:

HostStatus
Command lineavailable
JavaScript, in the browseravailable
Rustavailable
Pythonavailable
Node.jscoming soon

How this manual is organized

  • Getting started renders your first template.
  • Writing templates explains how to write templates: tags, data, conditions, repetition and imports. Start here if you write templates.
  • The Swan language describes the expression language in detail, with a reference of every built-in function.
  • Running texl explains how to render templates from the command line, JavaScript, Python and Rust, with the API reference of each.

Project status

Texl is young: the current version is 0.1. The language and the APIs may still change. The source code, the issue tracker and the change history are on GitHub. Texl is released under the MIT license.

Getting started

This page renders a first template with the command-line tool. If you want to use Texl from a program instead, see JavaScript, Python or Rust.

Install the command-line tool

You need Rust 1.85 or later. Then install the texl command from the repository:

cargo install --git https://github.com/onlabsorg/texl texl-cli

Check that it works:

texl --help

Write a template

Create a file named hello.texl:

<% name = "world", today = time.from_string "2026-10-16" %>Hello, <% name %>!
Today is <% text.head 10 (time.to_string today) %>.
2 + 2 = <% 2 + 2 %>

The first tag binds two names and writes nothing. The other tags write the values of their expressions.

Render it

texl hello.texl

The rendered text goes to the terminal:

Hello, world!
Today is 2026-10-16.
2 + 2 = 4

To write it to a file instead, add -o:

texl hello.texl -o hello.txt

Next steps

Templates

A Texl template is ordinary text with tags. A tag starts with <% and ends with %>, and contains an expression in the Swan language. Rendering the template replaces each tag with the value of its expression, and keeps the rest of the text as it is.

<% name = "Ada", items = 3 %>Dear <% name %>,
your order of <% items %> items has shipped.

renders as

Dear Ada,
your order of 3 items has shipped.

Templates can produce any kind of text: Markdown, HTML, configuration files, source code, e-mails. Texl does not care about the format; it only replaces the tags.

How rendering works

  1. The text outside the tags is copied to the output unchanged.
  2. The tags are evaluated in order, from the start of the template to the end.
  3. Each tag is replaced by its value, converted to text. A tag whose value is Nothing, like an assignment, produces no text at all.
  4. All the tags of a template share one set of names. A name bound in one tag is available in every later tag.
  5. If a tag's value is a promise, rendering waits until it resolves, then writes the resolved value.

Where templates come from

You write a template in a file, usually with the .texl extension, or in a string inside a program. A host renders it:

A Node.js library is coming soon.

The host can also give your template data to work with, such as the name of the reader or a list of products. See Names and data.

A template that a host loads by its file name or URL is called a document. Documents can import other documents, to reuse a header or a library of functions.

The expressions themselves are covered in the Swan manual. You can write useful templates knowing only a few parts of it: literals, names, operators, conditions and functions.

Tags and output

Writing tags

A tag starts at <% and ends at the first %> that follows. Spaces inside the tag are optional, and a tag can span several lines and contain comments:

<%
# Prices in euros
price = 12.5,
quantity = 4
%>Total: <%price * quantity%> euros
Total: 50 euros

A tag contains a single Swan expression. To do several things in one tag, separate the steps with commas (see Sequencing).

From values to text

Each tag is replaced by its value, converted to text:

ValueOutput
a textthe text itself, without quotes
a numberthe number: 42, 3.5, -0.25
a booleanTRUE or FALSE
Nothingnothing at all
a tupleits items, separated by ,
a list[[List of N]]
a namespace[[Namespace of N]]
a function[[Func]]
an undefined value[[Undefined]]
a promiseits value, once it resolves
<% "text" %> | <% 7 / 2 %> | <% 3 > 2 %> | [<% () %>] | <% 1, 2, 3 %>
<% [1, 2, 3] %> | <% {a: 1} %> | <% x -> x %> | <% "a" - 1 %>
text | 3.5 | TRUE | [] | 1, 2, 3
[[List of 3]] | [[Namespace of 1]] | [[Func]] | [[Undefined]]

The bracketed forms tell you that a value has no text form. To show the items of a list, turn it into a tuple with list.enum, or join them with text.join (see Conditions and repetition).

Numbers

Numbers are written with as many digits as needed to represent them exactly, which sometimes shows the limits of floating-point arithmetic:

<% 0.1 + 0.2 %>
<% math.round ((0.1 + 0.2) * 100) / 100 %>
0.30000000000000004
0.3

Round values that come from calculations before showing them.

Tags that write nothing

A tag whose value is Nothing disappears. Assignments return Nothing, so a tag that only binds names writes nothing:

<% greeting = "Hello" %><% greeting %>, world!
Hello, world!

Use : instead of = to bind a name and write its value:

The answer is <% answer : 6 * 7 %>. Again: <% answer %>.
The answer is 42. Again: 42.

Line breaks around tags

The text around a tag is kept exactly, including line breaks. A tag alone on a line leaves an empty line behind:

<% title = "Report" %>
# <% title %>

# Report

To avoid it, put the closing %> right before the next text, as the examples in this manual do:

<% title = "Report" %># <% title %>
# Report

or start the next line inside the tag:

<% title = "Report"
%># <% title %>
# Report

Writing the delimiters as text

There is no escape for <% in the text of a template: it always starts a tag. To write it, produce it from an expression:

Tags look like <% "<" + "%" %> ... <% "%" + ">" %>.
Tags look like <% ... %>.

Inside a tag, %> always ends the tag, even inside a text literal. Split it the same way: "%" + ">".

Swan text literals cannot contain a double quote. Write quotes in the text around the tags instead:

<% name = "Ada" %>She said "<% name %>".
She said "Ada".

Unfinished tags

If a <% has no matching %>, the rest of the template, from the <% on, is copied to the output as it is.

Names and data

The expressions in your tags use names: price, customer.name, text.upper. A name can come from four places.

The names you bind

A tag can bind names with = or :, and every later tag of the same template can use them. It is common to bind the values a template needs in a first tag:

<%
company = "Acme",
year = 2026,
copyright = "© " + text.from year + " " + company
%>Contact <% company %> for details.
<% copyright %>
Contact Acme for details.
© 2026 Acme

Functions work the same way. Define them once, and call them in later tags:

<% money = n -> "$" + text.from (math.round (n * 100) / 100) %>Subtotal: <% money 19.999 %>
Tax: <% money (19.999 * 0.2) %>
Subtotal: $20
Tax: $4

Built-in names

Every template can use the built-in library: text, list, math, time and the other namespaces, plus TRUE, FALSE, type and a few more. The engine adds the texl namespace, which imports other documents.

Data from the host

The program that renders your template can bind globals: names with the data the template needs. Which globals exist depends on the host, so a template usually documents what it expects, for example in a comment at the top:

<%
# Expects:
#   customer  {name, email}
#   orders    a list of {id, total}
%>

Values from the host become Swan values: strings become Text, numbers Numb, arrays List, objects namespaces, and functions Swan functions. See the host pages for how to pass globals: JavaScript and Rust. The command-line tool does not pass any.

Names from other documents

A document can import another document and use the names it binds. See Using a library.

Missing names

A name that is not bound anywhere is Nothing, which writes nothing. This is handy for optional data, but it also hides typos. Give optional names a default with ;:

Hello, <% reader ; "guest" %>!
Hello, guest!

Scopes of templates and documents

Each rendering of a template gets its own scope for the names it binds, below the globals and the built-ins. Binding a global or built-in name in a template hides it for the rest of that template only.

An imported document is rendered in a scope below the scope of the template that imports it, so it can read the importer's names (see Passing data to a partial).

Engine-wide names

bind binds a name in the global scope of the engine. The name is then visible in every template and document that the engine renders afterwards, not only in the current one. That is how a document shares a library with its importer (see Using a library), but it also means that two documents binding the same global name overwrite each other. Choose distinctive names for globals.

Conditions and repetition

Texl has no special tags for conditions or loops. A tag is an expression, so you use Swan's conditional operators and mapping to decide what to write.

Optional text

condition ? text writes text when the condition is true, and nothing otherwise. condition ? text ; other writes other when it is false:

<% stock = 0, vip = TRUE %>Status: <% stock > 0 ? "in stock" ; "sold out" %><% vip ? " (priority shipping)" %>
Status: sold out (priority shipping)

Chain conditions for more cases:

<% size = n -> n < 10 ? "small" ; n < 100 ? "medium" ; "large" %><% size 5 %>, <% size 50 %>, <% size 500 %>
small, medium, large

Optional paragraphs

A text literal can span several lines, so a condition can produce a whole paragraph:

<% overdue = TRUE %>Thank you for your order.
<% overdue ? "
Your payment is overdue. Please pay within
seven days to avoid a late fee.
" %>
Best regards
Thank you for your order.

Your payment is overdue. Please pay within
seven days to avoid a late fee.

Best regards

Remember that Swan text cannot contain " and that %> ends the tag. For longer optional parts, or parts with tags of their own, put them in a separate document and import it conditionally.

Repeating text for each item

To write something for each item of a list:

  1. turn the list into a tuple with list.enum;
  2. map a function that makes the text for one item over the tuple, with =>;
  3. join the results with text.join.
<%
fruits = ["apples", "pears", "plums"],
nl = "
"
%>Shopping list:
<% text.join nl (list.enum fruits => (f -> "- " + f)) %>
Shopping list:
- apples
- pears
- plums

Binding the line break to a name, like nl here, keeps the tags readable. Without text.join, the items would be written separated by commas, the way tuples are rendered.

The items are often namespaces, and the function picks their members. Numbers must be converted with text.from before they are joined to text:

<%
nl = "
",
orders = [{id: "A-1", total: 25}, {id: "B-7", total: 140}],
row = o -> "| " + o.id + " | " + text.from o.total + " |"
%>| Order | Total |
|-------|-------|
<% text.join nl (list.enum orders => row) %>
| Order | Total |
|-------|-------|
| A-1 | 25 |
| B-7 | 140 |

Empty lists

list.enum [] is Nothing, so an empty list writes nothing. To write a message instead, test the size:

<% items = [] %><% list.size items == 0 ? "No items." ; text.join ", " (list.enum items) %>
No items.

Filtering

A function that returns Nothing for some items drops them, because Nothing disappears from tuples:

<%
orders = [{id: "A-1", total: 25}, {id: "B-7", total: 140}],
big = o -> o.total > 100 ? o.id
%>Large orders: <% list.enum orders => big %>
Large orders: B-7

Totals

Swan has no loop and no built-in sum, but a short recursive function does the job:

<%
sum = xs -> list.size xs == 0 ? 0 ; xs 0 + sum (list.tail 1 xs),
totals = [25, 140, 60]
%>Total: <% sum totals %>
Total: 225

The recursion stops at the empty list, because the right side of ; is evaluated only when it is needed. See Evaluation order.

Reusable helpers

Collect the helpers you use often in a first tag, or in a library document that every template imports (see Using a library):

<%
nl = "
",
bullets = items -> text.join nl (list.enum items => (i -> "- " + text.from i)),
plural = (n, word) -> text.from n + " " + word + (n == 1 ? "" ; "s")
%><% plural (3, "file") %> changed:
<% bullets ["README.md", "main.rs", "lib.rs"] %>
3 files changed:
- README.md
- main.rs
- lib.rs

Documents and imports

A document is a template that the engine loads by its location: a file path for the command-line tool, a URL in a web page. A document can import other documents, to reuse pieces of text (partials) or collections of functions (libraries).

Importing a document

texl.import(uri) loads the document at uri, renders it, and returns a promise of a namespace with four members:

MemberContent
urithe full location of the document
sourceits template text
dataa namespace with the names its tags bound
renderingits rendered text

Because the result is a promise, you use it with ~>: texl.import(uri) ~> (doc -> ...) applies the function to the document once it is loaded. The tag waits for the result before rendering continues.

Inserting a partial

Suppose partials/footer.texl contains:

Questions? Write to help@example.com.

This template inserts it:

Thank you for your order.
<% texl.import("partials/footer.texl") ~> (doc -> doc.rendering) %>
Thank you for your order.
Questions? Write to help@example.com.

The extra empty line is the line break at the end of the footer file, followed by the line break after the tag. Use text.trim_tail to drop the first one:

Thank you for your order.
<% texl.import("partials/footer.texl") ~> (doc -> text.trim_tail doc.rendering) %>
Thank you for your order.
Questions? Write to help@example.com.

Passing data to a partial

An imported document can read the names of the template that imports it. With partials/header.texl containing:

== <% title %> ==

the importer decides the title:

<% title = "Monthly report" %><% texl.import("partials/header.texl") ~> (doc -> text.trim_tail doc.rendering) %>
== Monthly report ==

Each document is loaded and rendered only once per engine, and later imports reuse the first result. If two templates import the same partial with different data, both get the rendering made with the data of the first one. To produce a piece of text several times with different data, write a function instead, in a library.

Using a library

A library is a document that binds functions and renders nothing. Suppose lib/format.texl contains:

<%
# Formatting helpers. This document renders nothing.
plural = (n, word) -> text.from n + " " + word + (n == 1 ? "" ; "s"),
money = n -> "$" + text.from (math.round (n * 100) / 100)
%>

Its functions are in the data member of the document. Bind that namespace to a global name with bind, and use it in the following tags:

<% texl.import("lib/format.texl") ~> (lib -> bind "fmt" lib.data) %>You ordered <% fmt.plural (3, "book") %> for <% fmt.money 41.5 %>.
You ordered 3 books for $41.5.

This works because the import tag returns a promise, and rendering waits for it: by the time the next tag runs, the callback has bound fmt.

For a single use, call the function inside the callback instead:

<% texl.import("lib/format.texl") ~> (lib -> lib.data.plural (1, "book")) %>
1 book

Locations

The uri of texl.import is resolved against the location of the importing document, the way links work in a web page:

uriRefers to
footer.texla file in the same folder as the importing document
partials/footer.texla file in a subfolder of that folder
../shared/footer.texla file in a sibling folder
/shared/footer.texla path from the root: the root of the web site on the web, the root of the file system on the command line
https://example.com/a.texla full URL (on the web)
file:///home/me/a.texla full file URL (on the command line)

A template that is not a document, such as a string rendered by a program, resolves relative locations against the root, unless the program gives it the location of a document to stand for.

Imports inside an imported document are resolved against its location, not the location of the first template. So a partial can import its own neighbors with simple relative paths.

Missing documents

If a document cannot be loaded, the promise resolves to Nothing. Give a fallback with ;:

<% texl.import("missing.texl") ~> (doc -> doc.rendering ; "(not found)") %>
(not found)

What a document can see

An imported document is rendered:

  • with the built-in names and the host's globals;
  • with the names of the template that imported it, as they are at the time of the import;
  • with its own texl namespace, which imports relative to its own location.

Its data contains the names its own tags bound, plus texl.

Waiting for values

Some values are not available right away: an imported document has to be loaded, and a host function may have to fetch data from a server. Such values are promises.

Tags that return a promise

When a tag's value is a promise, rendering stops at that tag until the promise resolves, writes the resolved value, and then continues with the rest of the template. You do not have to do anything special:

<% 20 ~> (n -> n + 1) %> is a promise of 21.
21 is a promise of 21.

Promises usually come from:

  • texl.import, which loads a document;
  • functions provided by the host. In a web page, a JavaScript async function passed to the template returns a promise.

Transforming the value

Use ~> to apply a function to the value before it is written. If a host function fetch_rate returned a promise of an exchange rate, you would write:

1 EUR = <% fetch_rate "USD" ~> (rate -> text.from (math.round (rate * 100) / 100)) %> USD

Using the value in later tags

The function after ~> can bind the value to a global name with bind. The tag waits for the promise, so the name is bound before the next tag runs:

<% 42 ~> (n -> bind "answer" n) %>The answer is <% answer %>.
The answer is 42.

This is the usual way to load a library.

Several promises

Rendering only waits for a tag whose value is a promise. A tuple that contains promises is written right away, and a promise that is still pending is written as [[Pending Promise]]:

<% promise.create (), 1 %>
[[Pending Promise]], 1

To wait for several promises, nest the callbacks, so that the tag returns a single promise:

<% texl.import("a.texl") ~> (a -> (texl.import("b.texl") ~> (b -> a.rendering + b.rendering))) %>

Or import them in separate tags, one after the other.

Promises never fail

A promise always resolves. When the work behind it fails, for example when a document cannot be loaded, the promise resolves to Nothing. A JavaScript promise that is rejected also becomes Nothing. Use ; to show a fallback.

Troubleshooting

Texl never stops with an error. When something goes wrong, the output shows it in one of the ways below.

The output contains [[Undefined]]

An operation in the tag was invalid. Find out which one by inspecting the value with undef.inspect. Show the members of the description, or a few of them:

<% e = undef.inspect ("Total" + 10) %><% e.type %>: <% e.op %> of <% e.lhv %> and <% e.rhv %>
BinaryOperation: sum of Total and 10

Here the sum of a text and a number is invalid. The fix is "Total" + text.from 10.

pos tells where the error is, counted in bytes from the start of the tag's content (just after <%). Errors raised by built-in functions report 0.

The most common causes are:

  • mixing types: "Total: " + 10, or a number given to a text function;
  • a missing argument, which makes it Nothing: add 3 instead of add (3, 4);
  • calling the wrong value: f g x instead of f (g x) (see pitfalls);
  • a syntax error, such as a missing bracket or a single-quoted text.

A value is missing

If a tag writes nothing where you expected a value:

  • Check the spelling. A misspelled name is Nothing, and Nothing writes nothing.
  • Check that the name is bound before the tag. Tags run in order, and a name bound in a later tag is not yet available.
  • Check the host's globals. Is the data you expect actually passed to the template?
  • Check ? conditions. cond ? value writes nothing when the condition is false.

Give optional names a default to make missing values visible: <% customer.name ; "MISSING NAME" %>.

The output contains [[List of N]], [[Namespace of N]] or [[Func]]

The tag's value is a list, a namespace or a function, which have no text form.

  • For a list, write its items: list.enum items, or text.join ", " (list.enum items).
  • For a namespace, pick a member: customer.name.
  • For a function, you probably forgot to call it: today () instead of today.

The output contains [[Pending Promise]]

A promise inside a tuple, or inside another value, was not ready when the tag was written. Only a promise returned by the tag itself is awaited. See Several promises.

Values are separated by commas

The tag's value is a tuple, which is written as its items separated by , . Often this is a sequence of steps whose intermediate values are not Nothing: use = rather than : for the intermediate steps. To join items with another separator, use text.join.

Unexpected empty lines

A tag alone on its line leaves the line break behind. See Line breaks around tags.

Rendering crashes with a stack overflow

A recursive function never reaches the case that stops it. Check that the condition that ends the recursion is reached for every input. See Evaluation order.

Part of the template is copied as it is

A <% without a matching %> makes the rest of the template plain text. A %> inside a text literal ends the tag early. See Writing the delimiters as text.

Known issues

  • Infinite recursion stops the program instead of producing Undefined.

A complete example

This example renders an invoice in Markdown. It uses most of what the previous chapters describe: data in a first tag, a library, a partial, repetition and a condition.

The files

invoice/
├── invoice.texl
├── lib/
│   └── money.texl
└── partials/
    └── letterhead.texl

invoice.texl

The main document. Its first tag binds the data; in a real application the host would pass it as globals. The next two tags import the library and the letterhead.

<%
# The data of the invoice. A real application would pass it as globals.
number = "2026-042",
date = time.from_string "2026-10-16",
customer = {name: "Ada Lovelace", city: "London"},
items = [
    {what: "Analytical Engine manual", qty: 1, price: 120},
    {what: "Punched cards (box)", qty: 3, price: 15.5},
    {what: "Bernoulli tables", qty: 2, price: 40}
],
nl = "
"
%><% texl.import("lib/money.texl") ~> (lib -> bind "money" lib.data)
%><% texl.import("partials/letterhead.texl") ~> (doc -> doc.rendering) %>
# Invoice <% number %>

Date: <% text.head 10 (time.to_string date) %>
Bill to: <% customer.name %>, <% customer.city %>

<%
amount = item -> item.qty * item.price,
row = item -> "| " + item.what + " | " + text.from item.qty + " | "
    + money.format item.price + " | " + money.format (amount item) + " |"
%>| Item | Qty | Price | Amount |
|------|----:|------:|-------:|
<% text.join nl (list.enum items => row) %>

**Total: <% money.format (money.sum [list.enum items => amount]) %>**

<% list.size items > 2 ? "Thank you for being a loyal customer!" ; "Thank you!" %>

lib/money.texl

A library: it binds two functions and renders nothing. sum is recursive.

<%
# Formats an amount with two decimals: 15.5 -> "$15.50"
format = n -> (
    cents = math.round (n * 100),
    rest = cents % 100,
    "$" + text.from ((cents - rest) / 100) + "." + (rest < 10 ? "0" ; "") + text.from rest
),

# Adds up the numbers in a list
sum = numbers -> list.size numbers == 0 ? 0 ; numbers 0 + sum (list.tail 1 numbers)
%>

partials/letterhead.texl

A partial: plain text to insert at the top.

**Babbage & Co.** · 12 Dorset Street, London

Rendering it

With the command-line tool:

texl invoice/invoice.texl -o invoice.md

invoice.md contains:

**Babbage & Co.** · 12 Dorset Street, London

# Invoice 2026-042

Date: 2026-10-16
Bill to: Ada Lovelace, London

| Item | Qty | Price | Amount |
|------|----:|------:|-------:|
| Analytical Engine manual | 1 | $120.00 | $120.00 |
| Punched cards (box) | 3 | $15.50 | $46.50 |
| Bernoulli tables | 2 | $40.00 | $80.00 |

**Total: $246.50**

Thank you for being a loyal customer!

How it works

  • The data tag binds number, date, customer, items and nl, a line break used to join the rows. Each %> is placed right before the next text, so that the tags leave no empty lines (see Line breaks around tags).
  • The library is imported with texl.import, and its data is bound to the global name money with bind. Rendering waits for the import, so the later tags can call money.format and money.sum.
  • The letterhead is inserted with its rendering. It ends with a line break, which separates it from the title.
  • The table rows are produced by mapping row over the items and joining the results with line breaks. See Repeating text for each item.
  • The total maps amount over the items, puts the results in a list, and adds them up with money.sum.
  • The closing line depends on a condition.

The imports use paths relative to invoice.texl, so the command works from any folder.

The Swan language

Swan is the language you write inside Texl tags. It is small, dynamically typed and expression-oriented: a Swan program is a single expression, and evaluating it always produces a value. There are no statements, no keywords and no exceptions. An invalid operation does not stop the program; it produces an Undefined value instead.

This part of the manual describes the language. To learn how Swan fits into a template, start with Templates.

A quick tour

name = "Swan"                        # bind a name
"Hello, " + name + "!"               # => "Hello, Swan!"

double = x -> x * 2                  # define a function
double 21                            # => 42

(1, 2, 3) => double                  # => (2, 4, 6)
[10, 20, 30] 1                       # => 20

point = {x: 3, y: 4}                 # a namespace
point.x                              # => 3
math.sqrt (point.x ^ 2 + point.y ^ 2)  # => 5

age = 20
age >= 18 ? "adult" ; "minor"        # => "adult"

"hello" - 1                          # => Undefined

Reading the examples

Each example block is a short session. The lines run in order, and names bound on one line are available on the next. When a line ends with # =>, the text after the arrow is the value of that line, written in Swan notation:

ValueWritten as
numbers42, 3.14, inf
text"hello"
booleansTRUE, FALSE
nothing()
tuples(1, 2, 3)
lists[1, 2, 3]
namespaces{x: 3, y: 4}
functionsFunc
promisesPromise(42)
undefined valuesUndefined

A template shows some of these values differently. For example, a list renders as [[List of 3]]. See Tags and output.

Trying Swan

The quickest way to evaluate Swan expressions is a template. Save this as try.texl:

<% double = x -> x * 2 %><% double 21 %>

and render it with the command-line tool:

texl try.texl

Contents

Syntax

A Swan program is one expression, made of operands (names, literals and bracketed groups) joined by operators. This chapter describes the operands; Operators describes the rest.

Whitespace and comments

Spaces, tabs and line breaks separate tokens and are otherwise ignored, so an expression can span several lines.

A comment starts with # and runs to the end of the line:

x = 10     # bind 10 to x
x + 1      # => 11

Names

A name starts with a letter or _ and continues with letters, digits and _. Names are case-sensitive.

total = 1
_tmp = 2
item2 = 3
Total = 4
total                # => 1

Swan has no keywords. TRUE, FALSE and the built-in namespaces are ordinary names bound in the global scope, so you can shadow them (but you rarely want to). A name that is not bound evaluates to Nothing, not to an error:

not_bound_anywhere   # => ()

Numbers

A number literal is a sequence of digits, with an optional fraction and an optional exponent. All numbers are 64-bit floating-point values.

42                   # => 42
3.14                 # => 3.14
1.5e3                # => 1500
2E-2                 # => 0.02

A number cannot start or end with a dot, and cannot contain _. A negative number is the - operator applied to a positive literal.

.5                   # => Undefined
1_000                # => Undefined
-0.5                 # => -0.5

Text

A text literal is enclosed in double quotes. It can span several lines.

"hello"              # => "hello"
greeting = "Dear reader,
  welcome."
text.size greeting   # => 23

Text literals have no escape sequences: a backslash is an ordinary character, and a literal cannot contain a double quote. Single quotes do not delimit text.

"a\nb"               # this is the four characters a, \, n, b
text.size "a\nb"     # => 4
'single'             # => Undefined

In a template, write quotes and other special characters in the text around the tags instead.

Brackets

BracketsMeaning
( ... )Grouping. () alone is Nothing, the empty value.
[ ... ]A list of the values inside.
{ ... }A namespace of the names bound inside.
(1 + 2) * 3          # => 9
()                   # => ()
[1, 2, 3]            # => [1, 2, 3]
{a: 1, b: 2}         # => {a: 1, b: 2}

Syntax errors

Swan never rejects a program. A syntax error, such as a missing closing bracket or a misplaced operator, produces an Undefined value in the place where the error is, and the rest of the expression is evaluated as usual.

(1 + 2               # => Undefined
1 + * 2              # => Undefined

See Undefined values to find out what went wrong.

Values and types

Every Swan value has one of ten types. The type function returns the name of a value's type as text.

TypeExampleDescription
Nothing()The absence of a value. It is also the empty tuple.
BoolTRUE, FALSEA truth value.
Numb42, 3.14A 64-bit floating-point number.
Text"hello"A sequence of Unicode characters.
Tuple1, 2, "three"Two or more values separated by commas.
List[1, 2, 3]An ordered sequence of values, indexed from 0.
Namespace{x: 1, y: 2}A set of named values.
Funcx -> x * 2A function, defined in Swan or built in.
Promisepromise.create()A value that may arrive later.
Undefined"a" - 1The result of an invalid operation.
type 42              # => "Numb"
type "hello"         # => "Text"
type [1, 2]          # => "List"
type (x -> x)        # => "Func"
type math.sqrt       # => "Func"
type ("a" - 1)       # => "Undefined"

type works item by item on tuples, like most functions. So type (1, "a") is ("Numb", "Text"), and type () is (), not "Nothing".

type (1, "a")        # => ("Numb", "Text")
type ()              # => ()

All values are immutable, except namespaces and promises: a namespace can gain new names (see Namespaces), and a promise receives its value once (see Promises).

Truth values

Conditions (?, &, |, !) convert their operand to a truth value. These values are false:

  • FALSE
  • 0
  • "" (empty text)
  • [] (empty list)
  • {} (empty namespace)
  • Nothing
  • every Undefined value
  • a pending promise

Every other value is true, including every function. A tuple is true if at least one of its items is true.

bool.from 0          # => FALSE
bool.from "0"        # => TRUE
bool.from []         # => FALSE
bool.from (x -> x)   # => TRUE
bool.any (0, "", 3)  # => TRUE

A promise is false while it is pending and true once it is resolved, whatever its value:

p = promise.create ()
bool.from p          # => FALSE
promise.resolve p 0
bool.from p          # => TRUE

Equality

== and != compare values by content. Values of different types are never equal.

1 == 1               # => TRUE
"a" == "a"           # => TRUE
[1, [2]] == [1, [2]] # => TRUE
{x: 1} == {x: 1}     # => TRUE
1 == "1"             # => FALSE
  • Two namespaces are equal if they have the same names with equal values, and were created in the same scope.
  • A function is never equal to anything, not even to itself.
  • Two promises are equal only if they are the same promise.

Equality between tuples works item by item and gives a tuple of truth values. To compare two sequences as a whole, compare them as lists:

(1, 2) == (1, 3)     # => (TRUE, FALSE)
[1, 2] == [1, 3]     # => FALSE

Order

<, <=, > and >= compare two values of the same type:

  • numbers by value;
  • text alphabetically, by Unicode code point (so "Z" < "a");
  • booleans with FALSE < TRUE;
  • lists item by item; when one list is a prefix of the other, the shorter one comes first.

Nothing is less than every other value. Comparing values of different types, or of an unordered type (functions, namespaces, promises), gives Undefined.

2 < 10               # => TRUE
"apple" < "banana"   # => TRUE
"Z" < "a"            # => TRUE
[1, 2] < [1, 2, 0]   # => TRUE
() < -1000           # => TRUE
1 < "2"              # => Undefined

Numbers

Numbers follow the IEEE 754 rules. Dividing by zero does not fail: it gives an infinity, or "not a number" (NaN) for 0 / 0.

7 / 2                # => 3.5
1 / 0                # => inf
0.1 + 0.2            # => 0.30000000000000004

Use math.round, math.floor and the other math functions to get whole numbers.

Converting values

ToUse
Texttext.from
Numbnumb.from
Boolbool.from
List[value] (a tuple becomes the items of the list)
Tuplelist.enum, text.enum, nspace.enum
text.from 42 + "!"   # => "42!"
numb.from "41" + 1   # => 42
[(1, 2, 3)]          # => [1, 2, 3]
list.enum [1, 2, 3]  # => (1, 2, 3)

Operators

Precedence

Operators with a higher precedence bind tighter. Use parentheses whenever the order is not obvious.

PrecedenceOperatorsMeaningAssociativity
highest+x -x !x $xprefix operators
30x.ysubcontextingleft
28f xfunction applicationleft
27x ^ ypowerright
26x * y x / y x % yproduct, division, moduloleft
25x + y x - ysum, subtractionleft
24== != < <= > >=comparisonleft
23x & y x | ylogical and, orleft
22x ? yconditionalleft
21x ; yalternativeleft
15x -> yfunction definitionright
14f >> g g << fcompositionleft
13x => fmappingright
13p ~> fthenleft
12x = y x : yassignment, labellingleft
lowestx , ypairingleft

Some consequences:

double = x -> x * 2
double 2 + 3         # => 7, because application binds tighter than +
double (2 + 3)       # => 10
2 ^ 3 ^ 2            # => 512, because ^ is right-associative
-2 ^ 2               # => 4, because prefix - binds tighter than ^
-(2 ^ 2)             # => -4
x = 1, 2             # => 2, because , binds looser than =
x                    # => 1

Prefix operators bind tighter than everything, even . and application. Wrap the operand in parentheses when it is more than a name or a literal:

-math.PI             # => Undefined, because it means (-math).PI
-(math.PI)           # => -3.141592653589793

Arithmetic

OperatorNumbersTextListsBooleans
x + ysumconcatenationconcatenationlogical or
x - ysubtraction
x * yproductlogical and
x / ydivision
x % yremainder
x ^ ypower

Both operands must have the same type. Any other combination gives Undefined. Swan never converts types implicitly.

12 + 2 * 3           # => 18
7 % 3                # => 1
-7 % 3               # => -1, the sign of the left operand
"Te" + "xl"          # => "Texl"
[1, 2] + [3]         # => [1, 2, 3]
"Total: " + 10       # => Undefined
"Total: " + text.from 10   # => "Total: 10"

Prefix -x negates a number. Prefix +x returns x unchanged, whatever its type.

Comparison

==, !=, <, <=, > and >= compare two values and return TRUE or FALSE. See Equality and Order for the rules. Chained comparisons do not work: a < b < c compares a boolean with c. Write a < b & b < c instead.

Logic and conditions

OperatorResult
!xFALSE if x is true, TRUE if it is false.
x & yx if x is false, otherwise y.
x | yx if x is true, otherwise y.
x ? yy if x is true, otherwise Nothing.
x ; yx unless x is Nothing; then y.

In all four, y is evaluated only if it is needed. &, | and ? test x as a whole, even when it is a tuple: they do not work item by item.

Together, ? and ; make an if-else: cond ? then ; else. See Conditions and logic.

Functions

OperatorResult
x -> yA function with parameters x and body y.
f xf applied to x. Lists, text and namespaces can be applied too.
f >> gA function that applies f, then g to the result.
g << fThe same function: g after f.
x => ff applied to each item of the tuple x.

See Functions.

Names and namespaces

OperatorResult
x = yBinds the name(s) x to y, and returns Nothing.
x : yBinds the name(s) x to y, and returns y.
ns.xEvaluates the expression x inside the namespace ns.
$xThe value of the name x in the outermost scope that binds it.

See Names and scopes and Namespaces.

Tuples and promises

OperatorResult
x , yA tuple of the items of x followed by the items of y.
p ~> fA promise of f applied to the value of the promise p.

See Tuples and Promises.

Operators on tuples

All binary operators except ,, ;, =, :, ->, => and ~> work item by item when an operand is a tuple. The prefix operators - and ! do too.

(1, 2) + (10, 20)    # => (11, 22)
-(1, 2)              # => (-1, -2)

A single value counts as a tuple of one item. It is not repeated to match the other operand, so use mapping to apply the same operation to every item:

(1, 2, 3) * 10                 # => (10, Undefined, Undefined)
(1, 2, 3) => (x -> x * 10)     # => (10, 20, 30)

See Operations on tuples for the details.

Tuples

A tuple is a sequence of values separated by commas.

1, 2, "three"        # => (1, 2, "three")
(1, "b")             # => (1, "b")

Tuples are how Swan passes several arguments to a function, returns several results, and applies an operation to many values at once.

Tuples are flat

A tuple never contains another tuple or Nothing. When you build one, nested tuples are flattened and Nothing items are dropped:

(1, (2, 3))          # => (1, 2, 3)
(1, (), 2)           # => (1, 2)

There are no tuples of one item and no empty tuples: a single value is a tuple of one item, and Nothing is the empty tuple.

(5)                  # => 5
((), ())             # => ()

A trailing comma is a syntax error:

(1, 2,)              # => Undefined

To nest sequences, use lists, which keep their structure: [[1, 2], [3]] is a list of two lists.

Sequencing

Because Nothing items disappear, a tuple whose items are assignments acts like a sequence of steps. The assignments return Nothing, so only the last value remains:

x = 10, y = 5, x + y # => 15

This is the usual way to write a multi-step computation in one expression.

Operations on tuples

Most operators work item by item on tuples:

(10, 20) + (1, 2)    # => (11, 22)
(1, 2) == (1, 3)     # => (TRUE, FALSE)
!(0, 1)              # => (TRUE, FALSE)

When the two tuples have different lengths, the shorter one is padded with Nothing. Most operations between a value and Nothing are undefined:

(1, 2) + (10, 20, 30)          # => (11, 22, Undefined)

A single value is a tuple of one item, so it is combined with the first item only:

(1, 2, 3) * 10                 # => (10, Undefined, Undefined)

The conditions ?, &, | and ; are the exception: they look at their left side as a whole. See Conditions on tuples.

To apply the same operation to every item, map a function over the tuple with =>:

(1, 2, 3) => (x -> x * 10)     # => (10, 20, 30)

Tuples and functions

A tuple passed to a function becomes its arguments:

add = (a, b) -> a + b
add (3, 4)           # => 7

Built-in functions that take one value, such as math.sqrt or text.upper, are applied to each item of a tuple:

math.sqrt (4, 9, 16) # => (2, 3, 4)
text.upper ("a", "b")          # => ("A", "B")

Applying a tuple of functions applies each of them:

(math.floor, math.ceil) 2.5    # => (2, 3)

Working with items

Tuples have no indexing of their own. Put the tuple in a list to count or pick items, and use list.enum to turn a list back into a tuple:

t = (10, 20, 30)
[t] 1                # => 20
list.size [t]        # => 3
list.enum (list.reverse [t])   # => (30, 20, 10)

How tuples render

In a template, a tuple renders as its items joined by ", ":

<% 1, 2, "three" %>
1, 2, three

To join items with another separator, use text.join.

Lists, text and namespaces

Lists

A list is an ordered sequence of values, written in square brackets.

nums = [10, 20, 30]
nums                 # => [10, 20, 30]
[]                   # => []

The brackets enclose an expression, usually a tuple, and the list contains its items. Since tuples are flat, a tuple inside the brackets adds its items directly, and Nothing adds nothing. A list inside a list stays a single item:

[1, (2, 3)]          # => [1, 2, 3]
[1, (), 2]           # => [1, 2]
[[1, 2], [3]]        # => [[1, 2], [3]]

Getting items

Apply a list to a number to get the item at that position. Positions start at 0. A position that is negative, fractional or past the end gives Nothing.

nums = [10, 20, 30]
nums 0               # => 10
nums 2               # => 30
nums 10              # => ()
nums (0, 2)          # => (10, 30)

Combining and transforming

+ concatenates lists. The list namespace has functions to measure, search and slice them.

nums = [10, 20, 30]
nums + [40]          # => [10, 20, 30, 40]
list.size nums       # => 3
list.find 20 nums    # => 1
list.head 2 nums     # => [10, 20]
list.reverse nums    # => [30, 20, 10]

Lists are values: no operation changes a list. To process each item, turn the list into a tuple with list.enum, map a function over it, and put the result back in brackets:

prices = [5, 12, 20]
[list.enum prices => (p -> p * 2)]   # => [10, 24, 40]

Text

Text values support + (concatenation) and comparison. Applying a text to a number gives the character at that position, starting at 0. Positions count Unicode characters, not bytes.

word = "swan"
word 0               # => "s"
word (1, 2)          # => ("w", "a")
word 10              # => ()
"sw" + "an" == word  # => TRUE

The text namespace has functions to measure, search, slice, split and join text.

text.size "héllo"    # => 5
text.upper "texl"    # => "TEXL"
text.split "," "a,b,c"         # => ("a", "b", "c")

Namespaces

A namespace is a set of named values, written in curly braces.

point = {x: 3, y: 4}
point                # => {x: 3, y: 4}

The braces enclose an expression, which Swan evaluates in a new scope. Every name bound during that evaluation becomes a member of the namespace. The members can use the members defined before them, and the names around the namespace:

scale = 10
box = {width: 2, height: width * 3, area: width * height * scale}
box.area             # => 120

You can bind members with : or =. Inside the braces, the separators are just the comma operator, so any expression works, including definitions of functions:

counter = {start = 1, next = n -> n + 1}
counter.next counter.start     # => 2

Reading members

There are two ways to read a member:

  • ns.name evaluates name inside the namespace. The right side can be any expression, and it can use all the members.
  • ns "name" looks up the member called "name". The key is a text value, so it can be computed.
point = {x: 3, y: 4}
point.x              # => 3
point.(x * y)        # => 12
point "y"            # => 4
key = "x"
point key            # => 3
point "z"            # => ()

The two differ for names that are not members. ns "name" only sees the members of the namespace, while ns.name also sees the names around the place where the namespace was created, like any expression evaluated inside it:

point = {x: 3, y: 4}
point "math"         # => ()
type point.math      # => "Namespace"

Reading a member of Nothing gives Nothing, so a chain of . stops safely at the first missing link:

config = {}
config.server.port   # => ()

Adding members

A namespace can gain members after it is created. Bind them inside the namespace with .:

point = {x: 3}
point.(y = 4)
point                # => {x: 3, y: 4}

Inspecting namespaces

The nspace namespace lists and counts the members:

point = {x: 3, y: 4}
nspace.enum point    # => ("x", "y")
nspace.size point    # => 2

Namespaces as modules

Namespaces group related functions, like the built-in math and text namespaces:

geometry = {
    square = side -> side * side,
    circle = radius -> math.PI * radius ^ 2
}
geometry.square 3    # => 9

Names and scopes

Binding names

= binds a name to a value and returns Nothing. : does the same but returns the value.

x = 10               # => ()
x                    # => 10
y : 20               # => 20
y                    # => 20

= is handy in a sequence of steps, where the Nothing disappears (see Sequencing). : is handy when you want to keep the value, for example to show it in a template and remember it at the same time.

Binding a name again replaces its value in the current scope. The left side of = and : must be a name, a tuple of names, or (). Anything else gives Undefined and binds nothing.

1 = 2                # => Undefined

Assignments do not chain: a = b = 1 and a : b : 1 give Undefined. Write a = 1, b = 1 instead.

Destructuring

A tuple of names binds each name to the matching item:

(a, b) = (1, 2)
a                    # => 1
b                    # => 2
(a, b) = (b, a)
a                    # => 2

When there are more names than items, the extra names are bound to Nothing. When there are more items than names, the last name receives all the remaining items:

(first, rest) = (1, 2, 3)
first                # => 1
rest                 # => (2, 3)
(p, q, r) = (1, 2)
r                    # => ()

The same rules apply to function parameters.

Unbound names

A name that is not bound anywhere evaluates to Nothing. This is not an error, so a misspelled name silently gives Nothing. The alternative operator turns that into a default value:

title ; "Untitled"   # => "Untitled"
title = "Report"
title ; "Untitled"   # => "Report"

Scopes

A scope is a table of names. Each scope (except the outermost) has a parent, and looking up a name searches the scope itself, then its parent, and so on.

New scopes are created by:

  • function calls: the body of a function runs in a new scope, whose parent is the scope where the function was defined;
  • namespaces: the expression inside { } runs in a new scope, whose parent is the current scope.

ns.expr evaluates expr in the scope of the namespace ns.

Binding a name always affects the current scope. Inside a function, = creates a local name, even when an outer scope has the same name:

count = 0
increment = () -> (count = count + 1, count)
increment()          # => 1
count                # => 0

The outermost scopes hold the built-in names. Your code runs in a scope below them, so you can reuse a built-in name without changing the built-in.

Global names: bind and label

bind and label bind names in the global scope, the scope where top-level code runs, wherever they are called from. They take the name as text, which makes them useful inside functions and in promise callbacks:

remember = v -> bind "last" v
remember 42
last                 # => 42
label "answer" 42    # => 42
answer               # => 42

bind name value returns Nothing, like =. label name value returns the value, like :. Both accept a tuple of names. See Root names.

Lifting: $name

$name reads name from the outermost scope that binds it, skipping the nearer bindings that hide it:

x = 1
f = () -> (x = 2, (x, $x))
f()                  # => (2, 1)

The operand of $ must be a name or a tuple of names.

Conditions and logic

Swan has no if statement. Conditions are written with two operators: ? picks a value when a condition holds, and ; supplies an alternative when there is no value.

Conditional: ?

cond ? value returns value if cond is true, and Nothing otherwise. value is evaluated only if cond is true. See Truth values for what counts as true.

TRUE ? "yes"         # => "yes"
FALSE ? "yes"        # => ()
"" ? "yes"           # => ()

Alternative: ;

value ; other returns value, unless value is Nothing; then it evaluates and returns other.

() ; "fallback"      # => "fallback"
"value" ; "fallback" # => "value"

; only reacts to Nothing. Other false values, and Undefined values, are returned as they are:

FALSE ; 5            # => FALSE
0 ; 5                # => 0
"" ; "empty"         # => ""

If-else

cond ? a ; b combines the two: if cond is true, ? gives a and ; keeps it; otherwise ? gives Nothing and ; replaces it with b.

age = 20
age >= 18 ? "adult" ; "minor"  # => "adult"

Chain them for several cases. The conditions are tested from left to right:

grade = score -> score >= 90 ? "A" ; score >= 80 ? "B" ; "C"
grade 95             # => "A"
grade 85             # => "B"
grade 70             # => "C"

If the chosen value is itself Nothing, ; treats it as missing and moves on to the alternative:

TRUE ? () ; "fallback"         # => "fallback"

Evaluation order

The right side of ?, ;, & and | is evaluated only when it is needed. The other side is never evaluated, so an assignment there does not happen:

x = 1
FALSE ? (x = 2)      # => ()
x                    # => 1

This is also what stops a recursion. The recursive call can be on either side of ;:

fact = n -> n <= 1 ? 1 ; n * fact (n - 1)
fact 5               # => 120
fact2 = n -> n > 1 ? n * fact2 (n - 1) ; 1
fact2 5              # => 120

And, or

a & b returns a if a is false, and b otherwise. a | b returns a if a is true, and b otherwise. They return one of their operands, not necessarily a boolean. b is evaluated only when it is the result.

TRUE & "hello"       # => "hello"
0 & "hello"          # => 0
0 | "world"          # => "world"
"name" | "world"     # => "name"

| is a convenient way to replace any false value, including 0, "" and Undefined, with a default:

count = 0
count | "none"       # => "none"
("a" - 1) | "error"  # => "error"

& and | have the same precedence and associate to the left. When you mix them, add parentheses.

Not

!x returns FALSE if x is true, and TRUE if it is false.

!TRUE                # => FALSE
!0                   # => TRUE
!"hello"             # => FALSE

Like other operators, ! works item by item on tuples, so !() is (), not TRUE.

Testing for Nothing

x == () does not test for Nothing, because == works item by item and Nothing has no items. Put the value in a list instead: the list is empty exactly when the value is Nothing.

x = ()
[x] == []            # => TRUE
[0] == []            # => FALSE

To replace Nothing with a default, use ;.

Conditions on tuples

Unlike most operators, ?, & and | do not work item by item on tuples. They test their left side as a whole, and a tuple is true if at least one of its items is true:

(FALSE, 0, 1) ? "yes"                    # => "yes"
(FALSE, 0) ? "yes"                       # => ()
(TRUE, FALSE, TRUE) ? (1, 2, 3)          # => (1, 2, 3)

To filter a tuple, map it with a function that returns Nothing for the items to drop:

nums = (5, 12, 8, 30)
nums => (n -> n > 10 ? n)                # => (12, 30)

Functions

Defining functions

params -> body creates a function. Functions are values: you bind them to names, pass them to other functions and return them from functions.

double = x -> x * 2
double 21            # => 42

The parameters can be:

  • a name: x -> x * 2;
  • a tuple of names: (a, b) -> a + b;
  • (), for a function that ignores its argument: () -> "hello".

The body is any expression. It is evaluated each time the function is called, not when it is defined.

-> binds looser than most operators, but tighter than ,, = and :. Wrap the body in parentheses when it contains one of them:

f = x -> (y = x * 2, y + 1)
f 3                  # => 7

Calling functions

Write the function, then its argument. Parentheses around the argument are only needed when it is more than a name or a literal:

double = x -> x * 2
double 5             # => 10
double(5)            # => 10
double (2 + 3)       # => 10
double 2 + 3         # => 7

Pass several arguments as a tuple, and no argument as ():

add = (a, b) -> a + b
add (3, 4)           # => 7
hello = () -> "hello"
hello()              # => "hello"

Arguments bind to parameters like a destructuring assignment: missing arguments are Nothing, and the last parameter collects any extra arguments.

first = (head, tail) -> head
first (1, 2, 3)      # => 1
rest = (head, tail) -> tail
rest (1, 2, 3)       # => (2, 3)

Application is left-associative: f g x means (f g) x, which applies f to g. To apply f to the result of g x, write f (g x):

text.upper text.trim " hi "    # => Undefined
text.upper (text.trim " hi ")  # => "HI"

The same applies to a negative argument: f -1 is a subtraction. Write f (-1).

Closures

A function remembers the scope where it was defined, and its body can use the names of that scope. It sees their current values when it runs:

rate = 2
convert = amount -> amount * rate
convert 10           # => 20
rate = 3
convert 10           # => 30

A function that returns a function makes a closure over its own parameters:

multiplier = factor -> (x -> x * factor)
triple = multiplier 3
triple 5             # => 15

The names a function binds while running are local to that call.

Recursion

A function can call itself through the name it is bound to:

fact = n -> n <= 1 ? 1 ; n * fact (n - 1)
fact 10              # => 3628800

The recursion stops because the right side of ? and ; is evaluated only when it is needed. See Evaluation order.

Currying

A function with several parameters can also be written as a function that takes the first parameter and returns a function that takes the next one. This lets you supply the arguments one at a time:

add = a -> b -> a + b
add 3 4              # => 7
increment = add 1
increment 10         # => 11

Most built-in functions with two parameters are curried this way, for example text.find needle haystack and math.pow exponent base.

Mapping

items => f applies f to each item of the tuple items:

double = x -> x * 2
(1, 2, 3) => double            # => (2, 4, 6)
(4, 9, 16) => math.sqrt        # => (2, 3, 4)

With a tuple of functions, each function is applied to each item:

(1, 2) => (x -> x + 1, x -> x * 10)    # => (2, 10, 3, 20)

Results that are Nothing are dropped, so => also filters:

(1, 5, 12, 3) => (n -> n > 4 ? n)      # => (5, 12)

=> is right-associative: x => f => g means x => (f => g). To map twice, add parentheses or compose the functions:

double = x -> x * 2
inc = x -> x + 1
((1, 2) => double) => inc      # => (3, 5)
(1, 2) => (double >> inc)      # => (3, 5)

To map over a list, turn it into a tuple first, and put the result back in a list if you need one:

[list.enum [1, 2, 3] => (x -> x * x)]  # => [1, 4, 9]

Composition

f >> g is a function that applies f, then g. g << f is the same function, written in the other direction ("g after f"):

inc = x -> x + 1
double = x -> x * 2
(inc >> double) 3    # => 8
(double << inc) 3    # => 8
(inc << double) 3    # => 7

Other callable values

Lists, text and namespaces can also be applied to an argument. This is how you read their items:

ValueArgumentResult
a lista number nthe item at position n
a texta number nthe character at position n
a namespacea text kthe member named k
[10, 20, 30] 1       # => 20
"swan" 0             # => "s"
{a: 1} "a"           # => 1

Applying a tuple of functions applies each of them to the argument:

(math.floor, math.round, math.ceil) 2.5          # => (2, 3, 3)

Applying any other value, such as a number, gives Undefined.

Evaluating code at run time

parse compiles a text into a function, which evaluates the code in the namespace you apply it to:

area = parse "width * height"
area {width: 3, height: 4}     # => 12

Promises

A promise stands for a value that may not be available yet, such as a document that is still loading. It starts pending and later resolves to a value.

Promises usually come from the host or from the engine: texl.import returns one, and so does a JavaScript async function passed to a template. You can also create them yourself with the promise namespace.

Swan promises are simple:

  • A promise resolves at most once. Later attempts are ignored.
  • A promise never fails. If it resolves to an Undefined value, it holds Nothing instead.

Using a value that arrives later

Swan code cannot stop and wait. Instead, you give the promise a function to apply to its value. p ~> f returns a new promise, which resolves to f applied to the value of p:

p = promise.create()
q = p ~> (x -> x * 2)
promise.resolve p 21
q                    # => Promise(42)

If p is already resolved, f runs immediately. Otherwise it runs when p resolves. Several functions can wait on the same promise.

To chain several steps, write them one after the other. ~> is left-associative, so p ~> f ~> g means (p ~> f) ~> g: g is applied to the result of f:

p = promise.create()
q = p ~> (x -> x + 1) ~> (x -> x * 10)
promise.resolve p 1
q                    # => Promise(20)

A value that is not a promise is treated as an already resolved promise:

3 ~> (x -> x + 1)    # => Promise(4)

A tuple of promises gives a tuple of new promises, and a tuple of functions gives one new promise per function.

Keeping the result

The function you pass to ~> runs later, possibly after the rest of the expression. To make its result available to later code, bind it globally with bind:

p = promise.create()
p ~> (v -> bind "result" v)
promise.resolve p "done"
result               # => "done"

Promises in templates

When a tag of a template returns a promise, rendering waits until the promise resolves and writes its value. This is how a template uses an imported document:

<% texl.import("header.texl") ~> (doc -> doc.rendering) %>

The next tags run only after the promise resolves, so they can use what the callback bound. See Waiting for values.

Truth value

As a condition, a promise is false while it is pending and true once it is resolved, whatever its value. So p ? x tests whether p has resolved, not its value. To test the value, use it inside ~>:

p = promise.create ()
p ? "resolved" ; "pending"        # => "pending"
q = p ~> (v -> v ? "yes" ; "no")
promise.resolve p FALSE
p ? "resolved" ; "pending"        # => "resolved"
q                                 # => Promise("no")

Undefined values

When an operation makes no sense, such as subtracting a number from a text, Swan does not stop. The operation returns an Undefined value, which records what went wrong, and the evaluation continues.

"hello" - 1          # => Undefined
type ("hello" - 1)   # => "Undefined"

In a template, an undefined value renders as [[Undefined]].

Where they come from

  • Operators applied to the wrong types: "a" + 1, -"a", TRUE < 1.
  • Applying a value that cannot be applied: 5 3.
  • Built-in functions given the wrong arguments: text.size 42.
  • Math results that are not numbers: math.sqrt (-1).
  • Syntax errors: (1 + 2, 1 + * 2.
  • Binding something that is not a name: 1 = 2.
  • undef.from, which creates one on purpose.

An unbound name is not undefined: it is Nothing.

How they spread

Most operations with an undefined operand give an undefined result, so an error early in an expression usually shows in its final value:

("hello" - 1) + 5    # => Undefined
text.upper ("hello" - 1)       # => Undefined

A few operations handle them like other values:

  • An undefined value is false, so ?, &, | and ! treat it as such.
  • ; does not replace it, because it is not Nothing.
  • A tuple keeps it as one of its items.
!("hello" - 1)       # => TRUE
("hello" - 1) | 0    # => 0
("hello" - 1) ; 0    # => Undefined
(1, "a" - 1, 3)      # => (1, Undefined, 3)

So | is the way to replace an undefined value with a default.

Finding out what went wrong

undef.inspect turns an undefined value into a namespace that describes it:

err = undef.inspect ("hello" - 1)
err.type             # => "BinaryOperation"
err.op               # => "subtraction"
err.lhv              # => "hello"
err.rhv              # => 1
typeCauseOther members
UnaryOperationa prefix operator or a built-in functionop, val, pos
BinaryOperationa binary operator or an applicationop, lhv, rhv, pos
Terminationa missing closing bracketexpected, pos
Tokena character or token in the wrong placetoken, pos
Namea name was expectedpos
Valuecreated with undef.fromval

op names the operation, such as "sum", "application" or "text.size". pos is the position in the expression where the error happened, counted in bytes from the start of the expression (in a template, from the start of the tag's content). Built-in functions report position 0. When an operand is itself undefined, undef.inspect describes it too, so you can follow the chain back to the original error.

Creating undefined values

A function can return an undefined value to signal that its input was invalid. undef.from wraps any value, such as a message, into one:

safe_div = (a, b) -> b == 0 ? undef.from "division by zero" ; a / b
safe_div (1, 0)      # => Undefined
(undef.inspect (safe_div (1, 0))).val    # => "division by zero"

Common pitfalls

Each of these follows from the rules in the previous chapters, but they surprise most people at least once.

The comma binds loosest

x = 1, 2 binds 1 to x, then makes a tuple of Nothing and 2. Put a tuple in parentheses to bind it:

x = 1, 2
x                    # => 1
x = (1, 2)
x                    # => (1, 2)

The same goes for function bodies: f = x -> a, b is (f = x -> a), b. Write f = x -> (a, b).

Function bodies with = or :

A body that binds names must be in parentheses, because = binds looser than ->:

f = x -> y = x       # => Undefined, and nothing is bound
f = x -> (y = x, y * 2)
f 4                  # => 8

Application is left-associative

f g x applies f to g, then applies the result to x. Wrap the inner call in parentheses:

type promise.create ()         # => Undefined
type (promise.create ())       # => "Promise"

Negative arguments

f -1 subtracts 1 from f. Write f (-1):

text.head -2 "swanky"          # => Undefined
text.head (-2) "swanky"        # => "swan"

Prefix operators bind tightest

-x.y means (-x).y, and !f x means (!f) x. Add parentheses:

-(math.PI)           # => -3.141592653589793
!(bool.from 0)       # => TRUE

A single value is not repeated over a tuple

(1, 2, 3) * 2 multiplies only the first item. Map a function instead:

(1, 2, 3) => (x -> x * 2)      # => (2, 4, 6)

Comparisons on tuples give tuples

(1, 2) == (1, 2) is (TRUE, TRUE), not TRUE. Compare lists, or reduce with bool.all:

[1, 2] == [1, 2]     # => TRUE
bool.all ((1, 2) == (1, 2))    # => TRUE

; only replaces Nothing

FALSE ; 5 is FALSE, and an undefined value is not replaced either. Use | to replace every false value:

FALSE ; 5            # => FALSE
FALSE | 5            # => 5

Conditions do not work item by item

?, & and | test a tuple as a whole, so ? does not filter a tuple:

(TRUE, FALSE) ? (1, 2)         # => (1, 2)

Map the tuple instead. See Conditions on tuples.

Assignments do not chain

a = b = 1 and a : b : 1 give Undefined. Write a = 1, b = 1.

Text has no escapes

"\n" is a backslash and an n, and a text literal cannot contain ". Write line breaks directly inside the quotes.

Unbound names are Nothing

A misspelled name gives Nothing, which renders as empty text. If a value is unexpectedly missing, check the spelling first.

Mapping associates to the right

x => f => g means x => (f => g). Add parentheses around each step, or compose: x => (f >> g).

Joining requires text

text.join takes a tuple of text values. Convert numbers first, and turn lists into tuples:

text.join ", " (1, 2)                    # => Undefined
text.join ", " ((1, 2) => text.from)     # => "1, 2"
text.join ", " (list.enum ["a", "b"])    # => "a, b"

Built-in library

Every Swan program starts with these names already bound:

NameContents
Root namesTRUE, FALSE, type, parse, bind, label
boolConverting and combining truth values
numbConverting values to numbers
textMeasuring, searching, slicing, splitting text
listMeasuring, searching and slicing lists
nspaceInspecting namespaces
mathMathematical constants and functions
timeTimestamps and calendar dates
promiseCreating and resolving promises
undefCreating and inspecting undefined values

In a template, the engine also binds texl, which imports other documents, and the host can bind its own names (see Names and data).

Reading the reference

Each entry starts with a signature that shows how to call the function and what it returns:

text.find needle haystack -> Numb

This reads: apply text.find to needle, then apply the result to haystack, and you get a number. The description says what types the arguments must have.

Curried functions

Functions with two parameters are curried: they take the first argument and return a function that takes the second. You can call them in one go, or keep the intermediate function:

text.find "l" "hello"          # => 2
find_l = text.find "l"
find_l "world"                 # => 3

The first argument is the one you are most likely to fix in advance, such as the separator of text.split or the exponent of math.pow. Note that text.find ("l", "hello") passes a single tuple, not two arguments, and does not work.

Tuples

Unless an entry says otherwise, functions are applied to each item of a tuple argument and return a tuple of results. A Nothing argument gives Nothing.

text.upper ("a", "b")          # => ("A", "B")
text.find ("l", "o") "hello"   # => (2, 4)
text.upper ()                  # => ()

Functions that reduce a tuple to one value, such as bool.all and math.max, say so in their entry.

Errors

A function given an argument of the wrong type returns Undefined. The undefined value names the function in its op member.

text.size 42                   # => Undefined
(undef.inspect (text.size 42)).op        # => "text.size"

Root names

These names are bound directly in the global scope.

TRUE, FALSE

TRUE -> Bool
FALSE -> Bool

The two boolean values. They are also available as bool.TRUE and bool.FALSE.

TRUE & FALSE         # => FALSE

type

type value -> Text

Returns the name of the type of value: "Bool", "Numb", "Text", "List", "Namespace", "Func", "Promise" or "Undefined".

Like other functions, type is applied to each item of a tuple, so it never returns "Tuple" or "Nothing": type (1, "a") is ("Numb", "Text"), and type () is ().

type 42              # => "Numb"
type [1, 2]          # => "List"
type text.size       # => "Func"
type (1, "a")        # => ("Numb", "Text")

parse

parse source -> Func

Compiles the Swan expression in the text source and returns a function. The function takes a namespace and evaluates the expression inside it, so the expression can use the members of the namespace, and the names visible where the namespace was created.

The source is parsed once, when parse is called. A syntax error does not make parse fail: the function returns Undefined when it runs. A source that is not text gives Undefined, and so does applying the function to something other than a namespace.

area = parse "width * height"
area {width: 3, height: 4}     # => 12
area 42                        # => Undefined

bind

bind name value -> Nothing

Binds name to value in the global scope, wherever it is called from, and returns Nothing. name is a text, or a tuple of texts to bind several names at once, following the rules of destructuring.

bind is how a function, or a callback waiting on a promise, makes a value available to the rest of the program. In a template, the global scope is the engine's global scope, shared by every document the engine renders.

f = () -> bind "seen" TRUE
f()
seen                           # => TRUE
bind ("a", "b") (1, 2)
a + b                          # => 3

label

label name value -> value

Like bind, but returns value instead of Nothing, the way : differs from =.

label "answer" 42              # => 42
answer                         # => 42

bool

Converting and combining truth values. See Truth values for the rules.

bool.TRUE, bool.FALSE

bool.TRUE -> Bool
bool.FALSE -> Bool

The two boolean values, the same as the root names TRUE and FALSE.

bool.TRUE == TRUE    # => TRUE

bool.from

bool.from value -> Bool

Returns the truth value of value: FALSE for FALSE, 0, "", [], {}, undefined values and pending promises, TRUE for everything else.

bool.from 0          # => FALSE
bool.from "no"       # => TRUE
bool.from (0, 1)     # => (FALSE, TRUE)

bool.any

bool.any items -> Bool

Returns TRUE if at least one item of the tuple items is true. It reduces the whole tuple to one value, so bool.any () is FALSE.

bool.any (0, "", 3)  # => TRUE
bool.any (0, "")     # => FALSE

bool.all

bool.all items -> Bool

Returns TRUE if every item of the tuple items is true. It reduces the whole tuple to one value, so bool.all () is TRUE.

bool.all (1, "a")    # => TRUE
bool.all (1, 0)      # => FALSE
bool.all ([1, 2] == [1, 2], 3 > 2)       # => TRUE

numb

Converting values to numbers.

numb.from

numb.from value -> Numb

Converts value to a number:

  • a number is returned unchanged;
  • TRUE is 1 and FALSE is 0;
  • a text is read as a number literal, which may have a sign, a fraction and an exponent. The text must contain nothing else, not even spaces. "inf" and "NaN" are accepted too.

Any other value, and a text that is not a number, gives Undefined.

numb.from "42"       # => 42
numb.from "-1.5e3"   # => -1500
numb.from TRUE       # => 1
numb.from " 42"      # => Undefined
numb.from (text.trim " 42")    # => 42
numb.from ("1", "2") # => (1, 2)

text

Measuring, searching, slicing, splitting and joining text. Positions and lengths count Unicode characters, not bytes, and start at 0.

text.from

text.from value -> Text

Converts value to text, the way a template renders it:

ValueText
a textthe text itself
a numberthe number, such as 42 or 1.5
a booleanTRUE or FALSE
a list[[List of N]]
a namespace[[Namespace of N]]
a function[[Func]]
undefined[[Undefined]]

A promise gives Undefined. Use text.from to build text out of numbers, since + does not mix types.

text.from 42         # => "42"
text.from 42 + "!"   # => "42!"
text.from (1, TRUE)  # => ("1", "TRUE")

text.size

text.size string -> Numb

Returns the number of characters in string.

text.size "hello"    # => 5
text.size "héllo"    # => 5
text.size ""         # => 0

text.enum

text.enum string -> Tuple

Returns the characters of string as a tuple of one-character texts.

text.enum "abc"      # => ("a", "b", "c")

text.lower

text.lower string -> Text

Returns string with every letter in lower case.

text.lower "TeXL"    # => "texl"

text.upper

text.upper string -> Text

Returns string with every letter in upper case.

text.upper "TeXL"    # => "TEXL"

text.trim

text.trim string -> Text

Returns string without the white space (spaces, tabs, line breaks) at its start and end.

text.trim "  padded  "         # => "padded"

text.trim_head

text.trim_head string -> Text

Returns string without the white space at its start.

text.trim_head "  padded  "    # => "padded  "

text.trim_tail

text.trim_tail string -> Text

Returns string without the white space at its end. It is handy to remove the final line break of an imported document.

text.trim_tail "  padded  "    # => "  padded"

text.find

text.find needle haystack -> Numb

Returns the position of the first occurrence of the text needle in the text haystack, or -1 if there is none.

text.find "l" "hello"          # => 2
text.find "lo" "hello"         # => 3
text.find "z" "hello"          # => -1

text.rfind

text.rfind needle haystack -> Numb

Returns the position of the last occurrence of the text needle in the text haystack, or -1 if there is none.

text.rfind "l" "hello"         # => 3

text.head

text.head n string -> Text

Returns the first n characters of string. A negative n returns all the characters except the last -n. A fractional n is truncated toward zero.

text.head 3 "swanky"           # => "swa"
text.head (-2) "swanky"        # => "swan"
text.head 10 "swan"            # => "swan"

text.tail

text.tail n string -> Text

Returns the characters of string from position n on, that is, all but the first n. A negative n returns the last -n characters. A fractional n is truncated toward zero.

text.tail 4 "swanky"           # => "ky"
text.tail (-3) "swanky"        # => "nky"

Combined with text.rfind, it extracts the end of a path:

path = "docs/guide/intro.texl"
text.tail (text.rfind "/" path + 1) path # => "intro.texl"

text.split

text.split separator string -> Tuple

Splits string at every occurrence of the text separator and returns the parts as a tuple. If separator does not occur, the result is string itself.

text.split "," "a,b,c"         # => ("a", "b", "c")
text.split ", " "red, green"   # => ("red", "green")
text.split "," "abc"           # => "abc"

text.join

text.join separator items -> Text

Joins the texts of the tuple items, putting separator between them. Every item must be a text; otherwise the result is Undefined. items is taken as a whole, so text.join returns a single text.

text.join "-" ("a", "b", "c")  # => "a-b-c"
text.join "" ("a", "b")        # => "ab"
text.join ", " ((1, 2) => text.from)     # => "1, 2"
text.join ", " (list.enum ["x", "y"])    # => "x, y"

To join with line breaks, put a real line break between the quotes, since Swan text has no escapes:

<% text.join "
" ("first", "second") %>
first
second

list

Measuring, searching and slicing lists. All the functions return new values; none changes a list.

list.size

list.size items -> Numb

Returns the number of items in the list items.

list.size [10, 20, 30]         # => 3
list.size []                   # => 0

list.enum

list.enum items -> Tuple

Returns the items of the list items as a tuple. An empty list gives Nothing, and a list of one item gives that item. Use it to map a function over the items of a list.

list.enum [10, 20, 30]         # => (10, 20, 30)
list.enum [5]                  # => 5
list.enum []                   # => ()
[list.enum [1, 2, 3] => (x -> x * 2)]    # => [2, 4, 6]

list.reverse

list.reverse items -> List

Returns a list with the items of the list items in reverse order.

list.reverse [1, 2, 3]         # => [3, 2, 1]

list.find

list.find item items -> Numb

Returns the position of the first item of the list items that is equal to item, or -1 if there is none. Positions start at 0.

list.find 20 [10, 20, 30, 20]  # => 1
list.find 99 [10, 20, 30]      # => -1
list.find [1] [[1], [2]]       # => 0

list.rfind

list.rfind item items -> Numb

Returns the position of the last item of the list items that is equal to item, or -1 if there is none.

list.rfind 20 [10, 20, 30, 20] # => 3

list.head

list.head n items -> List

Returns the first n items of the list items. A negative n returns all the items except the last -n. A fractional n is truncated toward zero.

list.head 2 [1, 2, 3, 4]       # => [1, 2]
list.head (-1) [1, 2, 3, 4]    # => [1, 2, 3]
list.head 10 [1, 2]            # => [1, 2]

list.tail

list.tail n items -> List

Returns the items of the list items from position n on, that is, all but the first n. A negative n returns the last -n items. A fractional n is truncated toward zero.

list.tail 1 [1, 2, 3, 4]       # => [2, 3, 4]
list.tail (-2) [1, 2, 3, 4]    # => [3, 4]

nspace

Inspecting namespaces.

nspace.enum

nspace.enum ns -> Tuple

Returns the names of the members of the namespace ns, as a tuple of texts sorted by Unicode code point (so capital letters come first). Names inherited from enclosing scopes are not included. An empty namespace gives Nothing.

nspace.enum {b: 2, a: 1, C: 3} # => ("C", "a", "b")
nspace.enum {}                 # => ()

Combine it with namespace application to list the values:

ns = {x: 1, y: 2}
ns (nspace.enum ns)            # => (1, 2)

nspace.size

nspace.size ns -> Numb

Returns the number of members of the namespace ns. Names inherited from enclosing scopes are not counted.

nspace.size {x: 1, y: 2}       # => 2
nspace.size {}                 # => 0

nspace.parent

nspace.parent ns -> Namespace

Returns the scope in which the namespace ns was created, as a namespace, or Nothing for an outermost scope. This is the scope that ns.name falls back to for names that are not members of ns.

outer = {inner = {}}
nspace.parent outer.inner == outer       # => TRUE

math

Mathematical constants and functions. They take and return numbers. A result that is not a number, such as the square root of a negative number, is Undefined.

Constants

math.PI -> Numb
math.E -> Numb
math.INF -> Numb
math.INFINITY -> Numb

math.PI is π, math.E is Euler's number e, and math.INF and math.INFINITY are positive infinity. For negative infinity, write -(math.INF).

math.PI              # => 3.141592653589793
math.E               # => 2.718281828459045
-(math.INF)          # => -inf

Rounding

math.round x -> Numb
math.floor x -> Numb
math.ceil x -> Numb
math.trunc x -> Numb
math.abs x -> Numb

math.round rounds to the nearest whole number, and halfway cases away from zero. math.floor rounds down, math.ceil rounds up, and math.trunc drops the fraction. math.abs returns the absolute value.

math.round 2.5       # => 3
math.round (-2.5)    # => -3
math.floor (-1.5)    # => -2
math.ceil 1.2        # => 2
math.trunc (-1.7)    # => -1
math.abs (-3)        # => 3

To round to a number of decimals, scale first:

math.round (math.PI * 100) / 100         # => 3.14

Powers and logarithms

math.sqrt x -> Numb
math.exp x -> Numb
math.ln x -> Numb
math.log2 x -> Numb
math.log10 x -> Numb
math.pow exponent base -> Numb
math.log base x -> Numb

math.sqrt is the square root, math.exp x is e to the power x, and math.ln, math.log2 and math.log10 are the logarithms in base e, 2 and 10.

math.pow and math.log are curried, and take the fixed part first: math.pow exponent base is base ^ exponent, and math.log base x is the logarithm of x in base base.

math.sqrt 16         # => 4
math.sqrt (-1)       # => Undefined
math.log10 1000      # => 3
math.pow 2 5         # => 25
square = math.pow 2
(1, 2, 3) => square  # => (1, 4, 9)
math.log 2 8         # => 3

Trigonometry

math.sin x -> Numb
math.cos x -> Numb
math.tan x -> Numb
math.asin x -> Numb
math.acos x -> Numb
math.atan x -> Numb
math.atan2 x y -> Numb
math.hypot x y -> Numb

The trigonometric functions and their inverses work in radians. math.atan2 x y is the angle of the point (x, y) from the positive x axis, between -π and π. math.hypot x y is the distance of the point (x, y) from the origin.

math.sin 0           # => 0
math.cos math.PI     # => -1
math.atan2 1 1       # => 0.7853981633974483
math.hypot 3 4       # => 5

Hyperbolic functions

math.sinh x -> Numb
math.cosh x -> Numb
math.tanh x -> Numb
math.asinh x -> Numb
math.acosh x -> Numb
math.atanh x -> Numb

The hyperbolic functions and their inverses.

math.cosh 0          # => 1

math.max, math.min

math.max numbers -> Numb
math.min numbers -> Numb

Return the largest and the smallest item of the tuple numbers. They reduce the whole tuple to one value. Every item must be a number, and the tuple must not be empty; otherwise the result is Undefined.

math.max (1, 5, 2)   # => 5
math.min (1, 5, 2)   # => 1
math.max (list.enum [3, 9])    # => 9
math.max ()          # => Undefined

time

Timestamps and calendar dates. A timestamp is a number: the seconds elapsed since 1 January 1970 at 00:00 UTC, possibly with a fraction. Every date is in UTC.

time.now

time.now () -> Numb

Returns the current time as a timestamp. The argument is ignored.

time.now () > 1700000000       # => TRUE

time.to_date

time.to_date timestamp -> Namespace

Returns the calendar date of timestamp as a namespace with the members year, month (1 to 12), day (1 to 31), hour, minute and second. second keeps the fraction of the timestamp.

time.to_date 1780000000        # => {day: 28, hour: 20, minute: 26, month: 5, second: 40, year: 2026}
(time.to_date 0).year          # => 1970

time.from_date

time.from_date date -> Numb

Returns the timestamp of the date described by the namespace date, which can have the members year, month, day, hour, minute and second. Missing members default to year 0, month 1, day 1 and 00:00:00, so always give at least the year.

time.from_date {year: 2026, month: 1, day: 1}      # => 1767225600
time.from_date {year: 1970}                         # => 0

time.to_string

time.to_string timestamp -> Text

Returns timestamp as an ISO 8601 text with milliseconds, such as 2026-05-28T20:26:40.000Z.

time.to_string 0                # => "1970-01-01T00:00:00.000Z"
time.to_string 1780000000.5     # => "2026-05-28T20:26:40.500Z"

time.from_string

time.from_string string -> Numb

Reads an ISO 8601 date and returns its timestamp. string is a date YYYY-MM-DD, optionally followed by T (or a space) and a time HH:MM or HH:MM:SS, with an optional fraction of a second and an optional final Z. A missing time is midnight. Time zone offsets are not supported: the time is always UTC. A text in another format gives Undefined.

time.from_string "2026-05-28"            # => 1779926400
time.from_string "2026-05-28T20:26:40Z"  # => 1780000000
time.from_string "28/05/2026"            # => Undefined

time.week_day

time.week_day timestamp -> Numb

Returns the day of the week of timestamp, from 0 (Sunday) to 6 (Saturday).

time.week_day 0                # => 4
time.week_day 1780000000       # => 4

time.week_number

time.week_number timestamp -> Numb

Returns the ISO 8601 week number of timestamp, from 1 to 53. ISO weeks start on Monday, and week 1 is the week that contains the first Thursday of the year.

time.week_number 1780000000    # => 22
time.week_number (time.from_string "2027-01-01")   # => 53

promise

Creating and resolving promises. To use the value of a promise, use the ~> operator.

promise.create

promise.create () -> Promise

Returns a new pending promise. The argument is ignored.

p = promise.create ()
p                    # => Promise(pending)

promise.resolve

promise.resolve p value -> Nothing

Resolves the promise p with value, runs the functions waiting on p, and returns Nothing. If p is already resolved, nothing happens. An undefined value resolves p to Nothing. A tuple value is stored as a whole.

If p is not a promise, the result is Undefined.

p = promise.create ()
q = p ~> (x -> x * 2)
promise.resolve p 21
p                    # => Promise(21)
q                    # => Promise(42)
promise.resolve p 99
p                    # => Promise(21)

undef

Creating and inspecting undefined values.

undef.from

undef.from value -> Undefined

Returns an undefined value that carries value, for example a message that explains the error. The whole argument is wrapped, even a tuple.

err = undef.from "not found"
type err             # => "Undefined"
(undef.inspect err).val        # => "not found"

undef.inspect

undef.inspect u -> Namespace

Returns a namespace that describes the undefined value u. Its type member names the kind of error, and the other members depend on it:

typeMembersMeaning
UnaryOperationop, val, posoperation op failed on the operand val
BinaryOperationop, lhv, rhv, posoperation op failed on lhv and rhv
Terminationexpected, posthe closing bracket expected is missing
Tokentoken, posa token is in the wrong place
Nameposa name was expected
Valuevalcreated by undef.from

pos is the position of the error in the source, in bytes from the start of the expression; built-in functions report 0. Operands that are themselves undefined are described in the same way. If u is not undefined, the result is Undefined.

e = undef.inspect ([1] - 1)
e.type               # => "BinaryOperation"
e.op                 # => "subtraction"
(e.lhv, e.rhv)       # => ([1], 1)
(undef.inspect (text.size 1)).(op, val)  # => ("text.size", 1)

texl

The texl namespace is bound by the Texl engine in every template. It is not available in plain Swan.

texl.import

texl.import uri -> Promise

Loads the document at uri, renders it, and returns a promise of a namespace that describes it:

MemberTypeContent
uriTextThe full URI of the document
sourceTextThe template source
dataNamespaceThe names the document's tags bound
renderingTextThe rendered text

If the document cannot be loaded, the promise resolves to Nothing.

A relative uri is resolved against the URI of the importing document. Each document is loaded and rendered at most once per engine: importing it again, even through a different relative path, returns the same result.

<% texl.import("partials/header.texl") ~> (doc -> doc.rendering) %>

See Documents and imports for the details.

Hosts and APIs

A host is the program that runs the Texl engine: it gives the engine a template, the data the template needs, and a way to load the documents it imports. The template language is the same in every host.

HostUse it toReference
Command linerender template files from a terminal or a scripttexl command
JavaScriptrender templates in a web page or a web workerJavaScript API
Pythonrender templates in a Python program, with asyncioPython API
Rustrender templates in a Rust program, or build a new hosttexl_core, swan_core
Node.jscoming soon

What differs between hosts

The hosts differ in three ways:

  • Which locations they can load. The command-line tool reads local files; the JavaScript library fetches URLs over HTTP; the Python library does both. Python and Rust programs can add their own loaders.
  • Which globals they pass. The command-line tool passes none. JavaScript, Python and Rust programs pass any values and functions they want.
  • How they run asynchronous work. Each host connects the engine to its own event loop: the browser's, asyncio in Python. This only matters to Rust programs, which choose the runtime.

The crates

Texl is a Cargo workspace with five crates:

CrateContents
swan-coreThe Swan language: lexer, parser, evaluator and built-in library.
texl-coreThe template renderer and the engine: documents, loaders, imports.
texl-cliThe texl command.
texl-wasmThe JavaScript library, compiled to WebAssembly (@onlabsorg/texl on npm).
pytexlThe Python library: a native module and the pytexl package.

The API documentation of swan-core and texl-core is generated from the source code by rustdoc.

Command line

The texl command renders a template file and writes the result to the terminal or to a file.

texl report.texl                 # print the rendering
texl report.texl -o report.md    # write it to report.md

Installation

You need Rust 1.85 or later. Install the command from the repository:

cargo install --git https://github.com/onlabsorg/texl texl-cli

or, from a clone of the repository:

cargo install --path texl-cli

Both install a binary named texl in Cargo's bin folder (usually ~/.cargo/bin).

Reference

Synopsis

texl [-o OUTPUT] FILE
texl -h

Description

texl loads the template FILE, renders it, and writes the rendering to standard output, or to OUTPUT if given. The output is exactly the rendered text, encoded as UTF-8; texl adds nothing to it, not even a final line break.

Arguments and options

ArgumentMeaning
FILEThe template file to render. Required. A relative path is relative to the current folder.
-o OUTPUT, --output OUTPUTWrites the rendering to the file OUTPUT instead of standard output. The file is created, or replaced if it exists. Its folder must exist.
-h, --helpPrints a summary of the usage and exits.

Imports

FILE is loaded as a document, by its absolute file:// URL. The imports inside it are resolved against its location, whatever the current folder is:

In /home/me/site/index.texl, texl.import(...) ofloads
"header.texl"/home/me/site/header.texl
"../shared/footer.texl"/home/me/shared/footer.texl
"/etc/texl/lib.texl"/etc/texl/lib.texl
"file:///etc/texl/lib.texl"/etc/texl/lib.texl
"https://example.com/a.texl"nothing: other URL schemes are not supported

An import that cannot be read resolves to Nothing, and the rendering goes on.

Globals

The command passes no globals: a template can use the built-in names and the names it binds or imports.

Exit status

StatusMeaning
0The template was rendered and written.
1FILE could not be read, or OUTPUT could not be written.
2The command line is invalid, for example FILE is missing.

When the status is not 0, a message on standard error explains why:

texl: report.texl: No such file or directory (os error 2)

Errors inside the template do not change the status: they appear in the output as [[Undefined]] (see Troubleshooting).

Examples

Render a template and page through the result:

texl notes.texl | less

Render every template in a folder into Markdown files:

for f in pages/*.texl; do
    texl "$f" -o "build/$(basename "$f" .texl).md"
done

JavaScript

The @onlabsorg/texl package compiles the Texl engine to WebAssembly, with a JavaScript API for web pages and web workers. It renders templates from strings, loads documents over HTTP, and lets templates call your JavaScript functions.

A package for Node.js is coming soon.

Installing the package

Install the package from npm, and import it in code that a bundler, such as Vite or webpack, builds:

npm install @onlabsorg/texl
import init, { TexlEngine } from "@onlabsorg/texl";

The package contains the module texl_wasm.js, the WebAssembly binary texl_wasm_bg.wasm, and the TypeScript declarations texl_wasm.d.ts.

Without a bundler, import the module from a CDN that serves npm packages, with the version you want:

import init, { TexlEngine } from "https://cdn.jsdelivr.net/npm/@onlabsorg/texl@0.3.0/texl_wasm.js";

You can also build the package from a clone of the repository with wasm-pack, and copy the texl-wasm/pkg folder that it creates to your web site:

wasm-pack build texl-wasm --target web

Quick start

<script type="module">
import init, { TexlEngine } from "https://cdn.jsdelivr.net/npm/@onlabsorg/texl@0.3.0/texl_wasm.js";

await init();                                  // load the WebAssembly module

const engine = new TexlEngine({ user: { name: "Ada" } });
const text = await engine.render("Hello <% user.name %>!");
console.log(text);                             // Hello Ada!
</script>

The page must be served over HTTP: browsers do not load WebAssembly modules, or let them fetch documents, from file:// pages.

Loading documents

engine.load fetches a document by URL, renders it, and returns its rendering, source and data:

const doc = await engine.load("templates/report.texl");
if (doc) {
    document.querySelector("#report").textContent = doc.rendering;
}

A relative URL is resolved against the engine's base URL, which is the URL of the page by default. Inside a document, imports are resolved against the URL of the document:

In https://example.com/docs/report.texl, texl.import(...) offetches
"header.texl"https://example.com/docs/header.texl
"../lib/format.texl"https://example.com/lib/format.texl
"/lib/format.texl"https://example.com/lib/format.texl
"https://cdn.example.org/a.texl"https://cdn.example.org/a.texl

Templates rendered with engine.render, or with a context created without a URL, have no URL of their own: their imports are resolved against the root of the base URL, so "lib/format.texl" and "/lib/format.texl" both fetch https://example.com/lib/format.texl. A context created with a URL resolves them against that URL, like a document (see Contexts).

Documents are fetched with fetch, so the usual browser rules apply: a document on another origin must allow it with CORS headers. A response with an error status counts as a failed load: load gives null, and texl.import gives Nothing.

Each engine loads and renders each document once, and reuses the result for later loads and imports. Create a new engine to see changes to the documents.

Globals and context values

The objects you pass to the engine and to contexts become Swan values:

JavaScriptSwan
null, undefinedNothing
booleanBool
numberNumb
stringText
arrayList
Promise, or any object with a then methodPromise; a rejection resolves to Nothing
functionFunc (see below)
other objectNamespace of its own enumerable properties

Values that come back to JavaScript, from context.get, from document data and as function arguments, are converted the other way:

SwanJavaScript
Nothingnull
Bool, Numb, Textboolean, number, string
List, Tuplearray
Namespaceplain object with its members
PromisePromise
Func, Undefinedundefined

Functions

A JavaScript function becomes a Swan function of one argument. A Swan call with several arguments passes them as one array, so destructure it:

const engine = new TexlEngine({
    shout: (s) => s.toUpperCase() + "!",
    add: ([a, b]) => a + b,
});
await engine.render('<% shout "hi" %> <% add (2, 3) %>');   // "HI! 5"

The return value is converted to a Swan value. If the function throws, the result is Nothing. An async function, or a function that returns a promise, gives a Swan promise, and the tag waits for it:

const engine = new TexlEngine({
    weather: async (city) => (await fetch(`/api/weather/${city}`)).text(),
});
await engine.render('Today in Rome: <% weather "rome" %>');

Contexts

engine.render uses a fresh scope each time, so the names a template binds are lost when it ends. A context keeps them: it is a scope that you render several templates in, and that you can read and write from JavaScript.

const context = engine.createContext({ price: 12.5 });
await context.render("<% total = price * 4 %>");
context.get("total");                          // 50
await context.render("Total: <% total %>");    // "Total: 50"

A context can also stand for a document: pass its URL as the second argument, and the imports of the templates rendered in the context resolve against it. Use it to render a document whose source you already have, such as the text of an editor, without fetching it:

const context = engine.createContext(null, "docs/report.texl");
const rendering = await context.render(source);  // imports resolve in docs/

API reference

init

default function init(
    module_or_path?: { module_or_path: InitInput | Promise<InitInput> }
): Promise<InitOutput>

Loads and instantiates the WebAssembly module. Call it once, and wait for it, before using any other export.

By default, it fetches texl_wasm_bg.wasm from the folder of texl_wasm.js. Pass { module_or_path: url } to load it from somewhere else. The module also exports initSync({ module }), which instantiates an already compiled module synchronously.

import init from "@onlabsorg/texl";
await init();

TexlEngine

An engine renders templates and loads documents. Its globals, documents and engine-wide names (bound with bind) are shared by everything it renders.

new TexlEngine(globals, baseUrl)

constructor(globals?: object | null, baseUrl?: string | null)

Creates an engine.

ArgumentDescription
globalsAn object whose properties become globals of every template and document. Optional.
baseUrlThe URL that relative URLs resolve against. Optional; defaults to location.href, the URL of the page or worker.

Throws a string with an error message if baseUrl is not an absolute URL.

const engine = new TexlEngine(
    { site: { name: "My site" } },
    "https://example.com/templates/",
);

engine.baseUrl

readonly baseUrl: string | undefined

Returns the base URL of the engine, normalized. It is undefined only when no base URL was given and the global scope has no location.

new TexlEngine().baseUrl;                      // the URL of the page

engine.set(name, value)

set(name: string, value: any): void

Binds a global, visible to every template and document of the engine, including contexts created earlier. A global with the same name is replaced.

ArgumentDescription
nameThe name of the global.
valueIts value, converted to a Swan value.
engine.set("today", new Date().toISOString().slice(0, 10));

engine.load(uri)

load(uri: string): Promise<TexlDocument | null>

Loads and renders the document at uri.

ArgumentDescription
uriThe URL of the document, resolved against baseUrl.

Returns a promise of the document, or of null if it cannot be loaded. The promise rejects, with a string message, only if uri cannot be resolved to a URL. Each document is loaded once per engine; later calls with the same URL return the same result.

const doc = await engine.load("templates/report.texl");
console.log(doc?.rendering ?? "not found");

engine.render(template)

render(template: string): Promise<string>

Renders a template in a fresh scope, and returns a promise of the rendered text. The names the template binds with = or : are discarded afterwards.

ArgumentDescription
templateThe template source.
await engine.render("2 + 2 = <% 2 + 2 %>");    // "2 + 2 = 4"

engine.createContext(vars, uri)

createContext(vars?: object | null, uri?: string | null): TexlContext

Creates a context: a scope that keeps its names across renders.

ArgumentDescription
varsAn object whose properties are bound in the context. Optional.
uriThe URL of the document that the context stands for, resolved against baseUrl. Optional.

With a uri, imports in the context resolve against it, as in a document loaded with load; nothing is fetched from it. Without one, they resolve against the root of baseUrl, as in render. Throws, with a string message, if uri cannot be resolved to a URL.

const context = engine.createContext({ user: "Ada" });
const report = engine.createContext(null, "templates/report.texl");

engine.free()

free(): void

Releases the memory of the engine. The engine cannot be used afterwards. You only need it for engines that you create and discard many times; the engine also supports using declarations through Symbol.dispose.

TexlContext

A scope of an engine, created with engine.createContext. Templates rendered in a context share its names, and so do get and set. The context also sees the globals of its engine.

context.get(name)

get(name: string): any

Returns the value of a name, converted to a JavaScript value. The name is looked up in the context, then in the engine globals and the built-ins. A name that is not bound gives null.

ArgumentDescription
nameThe name to look up.
await context.render("<% x = (1, 2) %>");
context.get("x");                              // [1, 2]

context.set(name, value)

set(name: string, value: any): void

Binds a name in the context, replacing any previous value.

ArgumentDescription
nameThe name to bind.
valueIts value, converted to a Swan value.
context.set("items", ["a", "b"]);

context.names()

names(): string[]

Returns the names bound in the context, sorted. Engine globals and built-ins are not included. The list always includes texl, the namespace that holds texl.import.

engine.createContext({ b: 1, a: 2 }).names();  // ["a", "b", "texl"]

context.render(template)

render(template: string): Promise<string>

Renders a template in the context, and returns a promise of the rendered text. The names the template binds stay in the context.

ArgumentDescription
templateThe template source.
await context.render("<% count = 1 %>");
await context.render("Count: <% count %>");    // "Count: 1"

context.free()

free(): void

Releases the memory of the context, like engine.free.

TexlDocument

interface TexlDocument {
    uri: string;
    source: string;
    data: Record<string, any>;
    rendering: string;
}

A loaded document, as returned by engine.load.

PropertyDescription
uriThe URL of the document.
sourceThe template source.
dataThe names the document's tags bound, converted to JavaScript values. Functions are undefined.
renderingThe rendered text.
const doc = await engine.load("settings.texl");
console.log(doc.data.title, doc.rendering);

Python

The pytexl package runs the Texl engine in Python programs. It renders templates from strings, loads documents from local files and over HTTP, and lets templates call your Python functions, async ones included.

Installing the package

The package is not published on PyPI yet. Install it from a clone of the repository. You need Rust 1.85 or later and Python 3.10 or later:

pip install ./pytexl

pip builds the native module with maturin. To build a wheel instead, run maturin build --release -m pytexl/Cargo.toml; the wheel is written to target/wheels.

Quick start

import asyncio
import pytexl

engine = pytexl.Engine({"user": {"name": "Ada"}})
text = asyncio.run(engine.render("Hello <% user.name %>!"))
print(text)                                    # Hello Ada!

The engine is asynchronous: render and load are coroutines, because a tag may wait for a document being loaded or for an async function. In a program that is not asynchronous, run them with asyncio.run. In asyncio code, await them:

async def main():
    engine = pytexl.Engine()
    print(await engine.render("2 + 2 = <% 2 + 2 %>"))   # 2 + 2 = 4

asyncio.run(main())

Loading documents

engine.load reads a document, renders it, and returns its rendering, source and data:

from pathlib import Path

doc = await engine.load(Path("templates/report.texl"))
if doc:
    Path("report.md").write_text(doc.rendering)

A path is resolved against the current working directory. A string is a URL, resolved against the engine's base URL, which is the current working directory by default: engine.load("templates/report.texl") reads the same file. Full URLs work too:

doc = await engine.load("https://example.com/templates/report.texl")

The engine reads documents with loaders, chosen by the scheme of the URL:

URLLoaderReads
file:///home/ada/report.texlloaders.load_filea local file, as UTF-8
/home/ada/report.texl (no scheme)loaders.load_file, the default loadera local file, by absolute path
http://..., https://...loaders.load_httpa document over HTTP(S)

Inside a document, imports are resolved against the URL of the document:

In file:///home/ada/docs/report.texl, texl.import(...) ofreads
"header.texl"/home/ada/docs/header.texl
"../lib/format.texl"/home/ada/lib/format.texl
"/srv/lib/format.texl"/srv/lib/format.texl
"https://example.com/a.texl"https://example.com/a.texl, over HTTP

In a document fetched over HTTP, relative paths and absolute paths resolve against its URL in the same way, so "/lib/format.texl" in https://example.com/docs/report.texl fetches https://example.com/lib/format.texl.

Templates rendered with engine.render, or with a context created without a URL, have no URL of their own: their imports resolve against the root path /, so "lib/format.texl" and "/lib/format.texl" both read the local file /lib/format.texl. Use absolute paths there, load the template as a document, or give the context the URL of a document (see Contexts).

A document that cannot be read counts as a failed load: load gives None, and texl.import gives Nothing. A response with an HTTP error status is a failed load too.

Each engine loads and renders each document once, and reuses the result for later imports. Create a new engine to see changes to the documents.

Your own loaders

A loader is a function that takes the URL of a document and returns its source, or an awaitable of it. Register it for a scheme with add_loader:

templates = {"mem://greeting": "Hello <% reader %>!"}

engine = pytexl.Engine({"reader": "Ada"})
engine.add_loader("mem", lambda uri: templates[uri])

doc = await engine.load("mem://greeting")
print(doc.rendering)                           # Hello Ada!

If the loader raises an exception, or returns something other than a string, the document cannot be loaded. async loaders work the same way:

async def load_from_db(uri):
    row = await db.fetch_one("SELECT source FROM templates WHERE uri = ?", uri)
    return row["source"]

engine.add_loader("db", load_from_db)

set_default_loader replaces the loader of the URLs without a scheme. The engine normalizes those paths before calling it: they are absolute and percent-encoded, such as /dir/my%20doc.texl. This loader serves the files of one folder only:

root = Path("site").resolve()
engine.set_default_loader(lambda path: (root / path.lstrip("/")).read_text())

Globals and context values

The values you pass to the engine and to contexts become Swan values:

PythonSwan
NoneNothing
boolBool
int, floatNumb
strText
listList
tupleTuple
dict, or any other mappingNamespace of its string keys
awaitable: coroutine, future or taskPromise; an exception resolves to Nothing
function, or any other callableFunc (see below)
Promise, Function, Undefinedthe Swan value they wrap
anything elseNothing

Values that come back to Python, from context.get, from document data and as function arguments, are converted the other way:

SwanPython
NothingNone
Bool, Textbool, str
Numbint if it is a whole number (up to 253), float otherwise
Listlist
Tupletuple
Namespacedict of its members, sorted by name
PromisePromise, which you can await
FuncFunction, which you can call
UndefinedUndefined, with the details of the error

A tuple with one item is that item, and an empty tuple is Nothing, as in Swan.

Functions

A Python function becomes a Swan function. A Swan call with several arguments passes them as separate arguments, and a call with () passes none:

engine = pytexl.Engine({
    "shout": lambda s: s.upper() + "!",
    "add": lambda a, b: a + b,
    "answer": lambda: 42,
})
await engine.render('<% shout "hi" %> <% add(2, 3) %> <% answer() %>')   # 'HI! 5 42'

The return value is converted to a Swan value. If the function raises an exception, the result is Nothing. An async function, or a function that returns an awaitable, gives a Swan promise, and the tag waits for it:

async def weather(city):
    await asyncio.sleep(0.1)                   # a network request, in a real program
    return "sunny"

engine = pytexl.Engine({"weather": weather})
await engine.render('Today in Rome: <% weather "rome" %>')   # 'Today in Rome: sunny'

A coroutine object passed as a value is awaited once, and every template sees its result.

Swan functions and promises

The functions that a template defines come back to Python as Function objects, which you call like Python functions. A document can be a library:

doc = await engine.load("lib/format.texl")     # defines plural = (count, word) -> ...
doc.data["plural"](3, "apple")                 # '3 apples'

Swan promises come back as Promise objects, which you await:

context = engine.create_context({"weather": weather})
await context.render('<% forecast = weather "rome" %>')
await context.get("forecast")                  # 'sunny'

Undefined values

An invalid operation in Swan does not raise an exception: it produces an undefined value, which records what went wrong. It comes back to Python as an Undefined object, with the same details as undef.inspect:

context = engine.create_context({"price": "12.5"})    # a string, by mistake
await context.render("<% total = price * 4 %>")
total = context.get("total")
total                  # <pytexl.Undefined BinaryOperation op='product' lhv='12.5' rhv=4 pos=15>
total.op, total.lhv    # ('product', '12.5')
bool(total)            # False

None always means Nothing: a name that is not bound, or an empty tuple. To tell the two apart, test isinstance(value, pytexl.Undefined).

Contexts

engine.render uses a fresh scope each time, so the names a template binds are lost when it ends. A context keeps them: it is a scope that you render several templates in, and that you can read and write from Python.

context = engine.create_context({"price": 12.5})
await context.render("<% total = price * 4 %>")
context.get("total")                           # 50
await context.render("Total: <% total %>")     # 'Total: 50'

A context can also stand for a document: pass its URL or path as uri, and the imports of the templates rendered in the context resolve against it. Use it to render a document whose source you already have, without reading it again:

context = engine.create_context(uri=Path("templates/report.texl"))
rendering = await context.render(source)   # imports resolve in templates/

Event loops and threads

An engine runs its work on the asyncio event loop of the code that awaits it. It can outlive the loop: you can call asyncio.run several times with the same engine. Await Promise objects in a running loop.

The engine is single-threaded. Use an engine, its contexts, and the promises and functions that come from them on the thread that created the engine: the methods of an engine or a context raise RuntimeError on another thread. Each thread can have engines of its own. The loaders read files and fetch URLs in worker threads, so they do not block the event loop.

API reference

The docstrings of the package document the same API, in Google style. Run help(pytexl) to read them.

Engine

An engine renders templates and loads documents. Its globals, documents and engine-wide names (bound with bind) are shared by everything it renders.

Engine(globals, base_url)

Engine(globals: Mapping[str, Any] | None = None,
       base_url: str | os.PathLike[str] | None = None)

Creates an engine with the file and HTTP loaders.

ArgumentDescription
globalsA mapping whose items become globals of every template and document. Optional.
base_urlThe URL that load resolves relative URLs against. A path becomes a file:// URL, with a final slash if it is a folder. Optional; defaults to the current working directory.

Raises ValueError if base_url is not an absolute URL.

engine = pytexl.Engine({"site": {"name": "My site"}}, base_url=Path("templates"))

engine.base_url

base_url: str

Returns the base URL of the engine, normalized. It is read-only.

pytexl.Engine(base_url="https://Example.com/a/../b/").base_url   # 'https://example.com/b/'

engine.set(name, value)

def set(name: str, value: Any) -> None

Binds a global, visible to every template and document of the engine, including contexts created earlier. A global with the same name is replaced.

ArgumentDescription
nameThe name of the global.
valueIts value, converted to a Swan value.
engine.set("today", datetime.date.today().isoformat())

engine.add_loader(scheme, loader)

def add_loader(scheme: str, loader: Loader) -> None

Registers the loader of the URLs with a scheme, replacing any previous loader of that scheme, the built-in ones included.

ArgumentDescription
schemeThe URL scheme, without the colon, such as "mem". It is case-insensitive.
loaderA function that takes the full URL and returns the source, or an awaitable of it.
engine.add_loader("mem", lambda uri: templates[uri])

engine.set_default_loader(loader)

def set_default_loader(loader: Loader) -> None

Registers the loader of the URLs without a scheme, replacing loaders.load_file. The loader receives absolute, percent-encoded paths, such as /dir/my%20doc.texl.

ArgumentDescription
loaderA function that takes the path and returns the source, or an awaitable of it.
engine.set_default_loader(lambda path: (root / path.lstrip("/")).read_text())

engine.load(uri)

async def load(uri: str | os.PathLike[str]) -> Document | None

Loads and renders the document at uri.

ArgumentDescription
uriThe URL of the document, resolved against base_url. A path is resolved against the current working directory instead.

Returns the document, or None if it cannot be loaded. Raises ValueError if uri cannot be resolved to a URL. The result is cached: later imports of the same URL reuse it.

doc = await engine.load("templates/report.texl")
print(doc.rendering if doc else "not found")

engine.render(template)

async def render(template: str) -> str

Renders a template in a fresh scope, and returns the rendered text. The names the template binds with = or : are discarded afterwards.

ArgumentDescription
templateThe template source.
await engine.render("2 + 2 = <% 2 + 2 %>")    # '2 + 2 = 4'

engine.create_context(vars, uri)

def create_context(
    vars: Mapping[str, Any] | None = None,
    uri: str | os.PathLike[str] | None = None,
) -> Context

Creates a context: a scope that keeps its names across renders.

ArgumentDescription
varsA mapping whose items are bound in the context. Optional.
uriThe URL of the document that the context stands for, resolved like the uri of load. Optional.

With a uri, imports in the context resolve against it, as in a document loaded with load; nothing is read from it. Without one, they resolve against the root path /, as in render. Raises ValueError if uri cannot be resolved to a URL.

context = engine.create_context({"user": "Ada"})
report = engine.create_context(uri=Path("templates/report.texl"))

Context

A scope of an engine, created with engine.create_context. Templates rendered in a context share its names, and so do get and set. The context also sees the globals of its engine.

context.get(name)

def get(name: str) -> Any

Returns the value of a name, converted to a Python value. The name is looked up in the context, then in the engine globals and the built-ins. A name that is not bound gives None.

ArgumentDescription
nameThe name to look up.
await context.render("<% x = (1, 2) %>")
context.get("x")                               # (1, 2)

context.set(name, value)

def set(name: str, value: Any) -> None

Binds a name in the context, replacing any previous value.

ArgumentDescription
nameThe name to bind.
valueIts value, converted to a Swan value.
context.set("items", ["a", "b"])

context.names()

def names() -> list[str]

Returns the names bound in the context, sorted. Engine globals and built-ins are not included. The list always includes texl, the namespace that holds texl.import.

engine.create_context({"b": 1, "a": 2}).names()   # ['a', 'b', 'texl']

context.render(template)

async def render(template: str) -> str

Renders a template in the context, and returns the rendered text. The names the template binds stay in the context.

ArgumentDescription
templateThe template source.
await context.render("<% count = 1 %>")
await context.render("Count: <% count %>")     # 'Count: 1'

Document

@dataclass(frozen=True)
class Document:
    uri: str
    source: str
    data: dict[str, Any]
    rendering: str

A loaded document, as returned by engine.load.

AttributeDescription
uriThe URL of the document.
sourceThe template source.
dataThe names the document's tags bound, converted to Python values. It also holds texl.
renderingThe rendered text.
doc = await engine.load("settings.texl")
print(doc.data["title"], doc.rendering)

Promise

class Promise:
    done: bool
    def __await__(self) -> Generator[Any, None, Any]

A Swan promise. Awaiting it returns its value, converted to a Python value. Promises never fail: a promise whose value would be undefined resolves to None instead, as in Swan. The done attribute is True once the promise has a value. Await a promise in a running event loop.

await context.render("<% p = promise.create() %>")
p = context.get("p")                           # <pytexl.Promise pending>
await context.render("<% promise.resolve p 42 %>")
await p                                        # 42

Function

class Function:
    def __call__(self, *args: Any) -> Any

A Swan function. Calling it with no argument passes Nothing, with one argument passes that argument, and with several arguments passes them as a tuple. The result is converted to a Python value. A call never raises: an invalid call returns an Undefined.

await context.render("<% double = x -> x * 2 %>")
double = context.get("double")
double(21)             # 42
double("a")            # <pytexl.Undefined BinaryOperation op='product' lhv='a' rhv=2 pos=17>

Undefined

class Undefined:
    type: str
    op: str | None
    val: Any
    lhv: Any
    rhv: Any
    pos: int | None
    expected: str | None
    token: str | None
    def inspect(self) -> dict[str, Any]

A Swan undefined value, the result of an invalid operation. Its attributes are the members of undef.inspect: type names the kind of error, and the other attributes depend on it. The attributes that do not apply are None.

typeAttributesMeaning
"UnaryOperation"op, val, posoperation op failed on the operand val
"BinaryOperation"op, lhv, rhv, posoperation op failed on lhv and rhv
"Termination"expected, posthe closing bracket expected is missing
"Token"token, posa token is in the wrong place
"Name"posa name was expected
"Value"valcreated by undef.from

The operands are converted to Python values, so an operand that is itself undefined is an Undefined too. pos is the position of the error in the source of the tag, in bytes; built-in functions report 0. inspect() returns type and the attributes that apply, as a dict.

An Undefined is falsy. Two Undefined objects are equal when they have the same type and attributes, whatever their position. Passed back to Swan, an Undefined is the original undefined value.

await context.render('<% err = text.size 1 %>')
err = context.get("err")
err.type               # 'UnaryOperation'
err.inspect()          # {'type': 'UnaryOperation', 'op': 'text.size', 'val': 1, 'pos': 0}

Loader

Loader = Callable[[str], str | Awaitable[str]]

The type of a document loader: a function that takes a URL and returns the source of the document, or an awaitable of it. It raises an exception if the document cannot be read.

loaders

The module pytexl.loaders holds the loaders that every engine starts with. You can register them again, for example after replacing one.

loaders.load_file(uri)

async def load_file(uri: str) -> str

Reads a document from the local file system, as UTF-8, in a worker thread. It serves file:// URLs and paths without a scheme, which are absolute paths. Raises ValueError if uri is not a local file URL or path, and OSError if the file cannot be read.

await loaders.load_file("file:///home/ada/report.texl")

loaders.load_http(uri, timeout)

async def load_http(uri: str, *, timeout: float = 30.0) -> str

Fetches a document over HTTP or HTTPS, in a worker thread, with urllib.request. Redirects are followed, and the body is decoded with the charset of the response, UTF-8 by default. Raises urllib.error.URLError if the request fails or the response has an error status, and ValueError if uri is not an HTTP or HTTPS URL.

ArgumentDescription
uriAn http:// or https:// URL.
timeoutThe timeout of the request, in seconds.
engine.add_loader("https", functools.partial(loaders.load_http, timeout=5))

loaders.file_path(uri)

def file_path(uri: str) -> pathlib.Path

Returns the local path of a file:// URL or of an absolute path, with percent-encoded characters decoded. The query and the fragment are ignored. Raises ValueError if uri has another scheme or names a file on another host.

loaders.file_path("file:///my%20dir/doc.texl?v=1")   # PosixPath('/my dir/doc.texl')

Rust

Two crates make up the Rust API:

  • texl_core renders templates and loads documents. Most programs only need its Engine.
  • swan_core implements the Swan language. You use its Value type to pass data to templates, and you can use it on its own to evaluate expressions.

This page is a guide. The API reference generated by rustdoc documents every type and function.

Adding the dependencies

The crates are not published on crates.io yet. Add them from the repository:

[dependencies]
texl-core = { git = "https://github.com/onlabsorg/texl" }
swan-core = { git = "https://github.com/onlabsorg/texl" }
tokio = { version = "1", features = ["rt", "macros", "fs", "time"] }
url = "2"

The examples on this page use tokio and url, but the engine works with any async runtime, or with none.

Rendering a template

use swan_core::value::Value;
use texl_core::{Engine, helpers::spawner_fn};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    // The engine is single-threaded: run it on a LocalSet.
    tokio::task::LocalSet::new().run_until(async {
        let engine = Engine::new(Some(spawner_fn(|task| {
            tokio::task::spawn_local(task);
        })));
        engine.add_builtin("name", Value::Text("Ada".into()));

        let text = engine.render("Hello <% name %>!").await;
        assert_eq!(text, "Hello Ada!");
    }).await;
}

The engine and Swan values use Rc, so they are not Send, and rendering futures must run on one thread. With tokio, that means a LocalSet and spawn_local. The spawner passed to Engine::new is how the engine starts background tasks, such as imports.

Loading documents

The engine does not know how to read documents by itself. You register loaders: async functions that receive a URI and return the source text, or an error message.

use swan_core::value::Value;
use texl_core::{Engine, helpers::spawner_fn, loader_fn};
use url::Url;

async fn read_file(uri: String) -> Result<String, String> {
    let path = Url::parse(&uri)
        .ok()
        .and_then(|url| url.to_file_path().ok())
        .ok_or_else(|| format!("{uri}: not a file URL"))?;
    tokio::fs::read_to_string(&path).await.map_err(|e| e.to_string())
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    tokio::task::LocalSet::new().run_until(async {
        let engine = Engine::new(Some(spawner_fn(|task| {
            tokio::task::spawn_local(task);
        })));
        engine.add_loader("file", loader_fn(read_file));

        let path = std::fs::canonicalize("report.texl").unwrap();
        let url = Url::from_file_path(&path).unwrap();
        match engine.load(url.as_str()).await {
            Value::Namespace(doc) => match doc.get("rendering") {
                Value::Text(text) => print!("{text}"),
                _ => unreachable!("a document's rendering is always text"),
            },
            _ => eprintln!("report.texl could not be loaded"),
        }
    }).await;
}
  • add_loader(scheme, loader) handles the URIs with that scheme, such as file or https.
  • add_default_loader(loader) handles URIs without a scheme. The engine normalizes them into absolute paths, such as /docs/report.texl.

Engine::load returns the document as a namespace with the members uri, source, data and rendering, or Nothing if no loader matches or the loader fails. Load a document by an absolute URI, so that the imports inside it resolve against its location. The same loaders serve texl.import in templates, and the engine caches every document it loads.

Value has no Display implementation: match on the variant to get the text, as above.

When the host already has the source of a document, it can render it without a loader: create a scope with the document's URI, with engine.create_scope(Some(uri)), and pass it to template::render. The imports inside it resolve against the URI, as if the document had been loaded.

Globals and host functions

add_builtin binds a global, visible to every template and document of the engine. Any Value works, including namespaces and functions:

use swan_core::value::{BuiltinFn, Scope, Value};
use texl_core::{Engine, helpers::spawner_fn};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    tokio::task::LocalSet::new().run_until(async {
        let engine = Engine::new(Some(spawner_fn(|task| {
            tokio::task::spawn_local(task);
        })));

        // A namespace: {name: "Ada", tags: ["admin", "dev"]}
        let user = Scope::new();
        user.set("name", Value::Text("Ada".into()));
        user.set("tags", Value::list(vec![Value::Text("admin".into()), Value::Text("dev".into())]));
        engine.add_builtin("user", Value::Namespace(user));

        // A function. Several arguments arrive as one Value::Tuple.
        engine.add_builtin("repeat", Value::Builtin(BuiltinFn::new("repeat", |arg| {
            let items: Vec<Value> = arg.items().collect();
            match items.as_slice() {
                [Value::Text(t), Value::Numb(n)] => Value::Text(t.repeat(*n as usize).into()),
                _ => Value::undef_unary("repeat", arg, 0),
            }
        })));

        let text = engine.render("<% user.name %> <% repeat (\"*\", 3) %>").await;
        assert_eq!(text, "Ada ***");
    }).await;
}

A host function receives its whole argument as one Value. It must not panic: return an Undefined value, for example with Value::undef_unary, when the argument is invalid. Value::items iterates over the items of a tuple, and treats any other value as a tuple of one item.

Asynchronous host functions

A host function that needs to wait returns a promise. helpers::promise_async turns a future into one, and the template waits for it:

use std::time::Duration;
use swan_core::value::{BuiltinFn, Value};
use texl_core::{Engine, helpers::{promise_async, spawner_fn, SpawnFn}};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    tokio::task::LocalSet::new().run_until(async {
        let spawner: SpawnFn = spawner_fn(|task| {
            tokio::task::spawn_local(task);
        });
        let engine = Engine::new(Some(spawner.clone()));

        engine.add_builtin("slow_double", Value::Builtin(BuiltinFn::new("slow_double", move |arg| {
            promise_async(Some(&spawner), async move {
                tokio::time::sleep(Duration::from_millis(10)).await;
                match arg {
                    Value::Numb(n) => Value::Numb(n * 2.0),
                    other => Value::undef_unary("slow_double", other, 0),
                }
            })
        })));

        assert_eq!(engine.render("<% slow_double 21 %>").await, "42");
    }).await;
}

Keeping names between renders

Engine::render uses a fresh scope each time. To render several templates in the same scope, create one with Engine::create_scope and call template::render. Its argument is the URI of the document the scope stands for, which texl.import resolves relative URIs against; None resolves them against the root path /, as in Engine::render:

use swan_core::value::Value;
use texl_core::{Engine, helpers::spawner_fn, template};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    tokio::task::LocalSet::new().run_until(async {
        let engine = Engine::new(Some(spawner_fn(|task| {
            tokio::task::spawn_local(task);
        })));
        let scope = engine.create_scope(None);

        template::render("<% total = 6 * 7 %>", &scope).await;
        assert_eq!(scope.get("total"), Value::Numb(42.0));
        assert_eq!(template::render("Total: <% total %>", &scope).await, "Total: 42");
    }).await;
}

Without an async runtime

Pass None as the spawner, and the engine puts its background tasks on a thread-local queue. Drive the rendering future yourself, and call helpers::run_pending to run the queued tasks:

use std::{future::Future, pin::pin, task::{Context, Poll, Waker}};
use texl_core::{Engine, helpers::run_pending};

fn block_on<F: Future>(future: F) -> F::Output {
    let mut cx = Context::from_waker(Waker::noop());
    let mut future = pin!(future);
    loop {
        run_pending();
        if let Poll::Ready(value) = future.as_mut().poll(&mut cx) {
            return value;
        }
    }
}

fn main() {
    let engine = Engine::new(None);
    assert_eq!(block_on(engine.render("<% 6 * 7 %>")), "42");
}

This busy loop is fine for tests and tools whose loaders do not really wait. For real I/O, use a runtime.

Evaluating Swan expressions

swan_core evaluates Swan expressions without templates:

use swan_core::builtins::root_scope;
use swan_core::evaluator::eval;
use swan_core::lexer::Lexer;
use swan_core::parser::Parser;
use swan_core::value::Value;

fn main() {
    let scope = root_scope();
    let ast = Parser::new(Lexer::new("(1, 2, 3) => (x -> x * 10)")).parse();
    let value = eval(&ast, &scope);

    let items: Vec<Value> = value.items().collect();
    assert_eq!(items, vec![Value::Numb(10.0), Value::Numb(20.0), Value::Numb(30.0)]);
}

Evaluation never fails and never panics: errors are Value::Undefined values. See the swan_core documentation for the pipeline and the value model.

API reference

  • texl_core: Engine, template::render, loaders and the async helpers.
  • swan_core: Value, Scope, Promise, BuiltinFn, the lexer, the parser and the evaluator.