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:
| Host | Status |
|---|---|
| Command line | available |
| JavaScript, in the browser | available |
| Rust | available |
| Python | available |
| Node.js | coming 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
- Learn the template basics.
- Take the quick tour of the Swan language.
- Read the full command-line reference.
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
- The text outside the tags is copied to the output unchanged.
- The tags are evaluated in order, from the start of the template to the end.
- Each tag is replaced by its value, converted to text. A tag whose value is
Nothing, like an assignment, produces no text at all. - All the tags of a template share one set of names. A name bound in one tag is available in every later tag.
- 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:
- the
texlcommand renders a file from the command line; - the JavaScript library renders templates in a web page;
- the Python library renders templates in a Python program;
- the Rust library renders templates in a Rust program.
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.
What to read next
- Tags and output: how tags are written and how values become text.
- Names and data: where the names in your expressions come from.
- Conditions and repetition: optional text, lists and tables.
- Documents and imports: splitting your work into several files.
- Waiting for values: data that arrives later.
- Troubleshooting: when the output is not what you expect.
- A complete example: all of it together.
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:
| Value | Output |
|---|---|
| a text | the text itself, without quotes |
| a number | the number: 42, 3.5, -0.25 |
| a boolean | TRUE or FALSE |
Nothing | nothing at all |
| a tuple | its items, separated by , |
| a list | [[List of N]] |
| a namespace | [[Namespace of N]] |
| a function | [[Func]] |
| an undefined value | [[Undefined]] |
| a promise | its 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:
- turn the list into a tuple with
list.enum; - map a function that makes the text for one item over the tuple, with
=>; - 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:
| Member | Content |
|---|---|
uri | the full location of the document |
source | its template text |
data | a namespace with the names its tags bound |
rendering | its 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:
uri | Refers to |
|---|---|
footer.texl | a file in the same folder as the importing document |
partials/footer.texl | a file in a subfolder of that folder |
../shared/footer.texl | a file in a sibling folder |
/shared/footer.texl | a 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.texl | a full URL (on the web) |
file:///home/me/a.texl | a 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
texlnamespace, 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
asyncfunction 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 3instead ofadd (3, 4); - calling the wrong value:
f g xinstead off (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, andNothingwrites 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 ? valuewrites 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, ortext.join ", " (list.enum items). - For a namespace, pick a member:
customer.name. - For a function, you probably forgot to call it:
today ()instead oftoday.
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,itemsandnl, 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 itsdatais bound to the global namemoneywithbind. Rendering waits for the import, so the later tags can callmoney.formatandmoney.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
rowover the items and joining the results with line breaks. See Repeating text for each item. - The total maps
amountover the items, puts the results in a list, and adds them up withmoney.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:
| Value | Written as |
|---|---|
| numbers | 42, 3.14, inf |
| text | "hello" |
| booleans | TRUE, FALSE |
| nothing | () |
| tuples | (1, 2, 3) |
| lists | [1, 2, 3] |
| namespaces | {x: 3, y: 4} |
| functions | Func |
| promises | Promise(42) |
| undefined values | Undefined |
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: comments, names and literals.
- Values and types: the ten types, truth values and comparison.
- Operators: every operator, its precedence and its meaning.
- Tuples: comma-separated values, and how operators work on them.
- Lists, text and namespaces: the collection types.
- Names and scopes: binding names, destructuring, scopes.
- Conditions and logic:
?,;,&,|and!. - Functions: definition, application, closures, composition.
- Promises: values that arrive later.
- Undefined values: how Swan reports errors.
- Common pitfalls: surprises worth knowing in advance.
- Built-in library: every built-in name.
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
| Brackets | Meaning |
|---|---|
( ... ) | 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.
| Type | Example | Description |
|---|---|---|
Nothing | () | The absence of a value. It is also the empty tuple. |
Bool | TRUE, FALSE | A truth value. |
Numb | 42, 3.14 | A 64-bit floating-point number. |
Text | "hello" | A sequence of Unicode characters. |
Tuple | 1, 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. |
Func | x -> x * 2 | A function, defined in Swan or built in. |
Promise | promise.create() | A value that may arrive later. |
Undefined | "a" - 1 | The 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:
FALSE0""(empty text)[](empty list){}(empty namespace)Nothing- every
Undefinedvalue - 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
| To | Use |
|---|---|
Text | text.from |
Numb | numb.from |
Bool | bool.from |
List | [value] (a tuple becomes the items of the list) |
Tuple | list.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.
| Precedence | Operators | Meaning | Associativity |
|---|---|---|---|
| highest | +x -x !x $x | prefix operators | |
| 30 | x.y | subcontexting | left |
| 28 | f x | function application | left |
| 27 | x ^ y | power | right |
| 26 | x * y x / y x % y | product, division, modulo | left |
| 25 | x + y x - y | sum, subtraction | left |
| 24 | == != < <= > >= | comparison | left |
| 23 | x & y x | y | logical and, or | left |
| 22 | x ? y | conditional | left |
| 21 | x ; y | alternative | left |
| 15 | x -> y | function definition | right |
| 14 | f >> g g << f | composition | left |
| 13 | x => f | mapping | right |
| 13 | p ~> f | then | left |
| 12 | x = y x : y | assignment, labelling | left |
| lowest | x , y | pairing | left |
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
| Operator | Numbers | Text | Lists | Booleans |
|---|---|---|---|---|
x + y | sum | concatenation | concatenation | logical or |
x - y | subtraction | |||
x * y | product | logical and | ||
x / y | division | |||
x % y | remainder | |||
x ^ y | power |
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
| Operator | Result |
|---|---|
!x | FALSE if x is true, TRUE if it is false. |
x & y | x if x is false, otherwise y. |
x | y | x if x is true, otherwise y. |
x ? y | y if x is true, otherwise Nothing. |
x ; y | x 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
| Operator | Result |
|---|---|
x -> y | A function with parameters x and body y. |
f x | f applied to x. Lists, text and namespaces can be applied too. |
f >> g | A function that applies f, then g to the result. |
g << f | The same function: g after f. |
x => f | f applied to each item of the tuple x. |
See Functions.
Names and namespaces
| Operator | Result |
|---|---|
x = y | Binds the name(s) x to y, and returns Nothing. |
x : y | Binds the name(s) x to y, and returns y. |
ns.x | Evaluates the expression x inside the namespace ns. |
$x | The value of the name x in the outermost scope that binds it. |
See Names and scopes and Namespaces.
Tuples and promises
| Operator | Result |
|---|---|
x , y | A tuple of the items of x followed by the items of y. |
p ~> f | A promise of f applied to the value of the promise p. |
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.nameevaluatesnameinside 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:
| Value | Argument | Result |
|---|---|---|
| a list | a number n | the item at position n |
| a text | a number n | the character at position n |
| a namespace | a text k | the 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
Undefinedvalue, it holdsNothinginstead.
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 notNothing.- 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
type | Cause | Other members |
|---|---|---|
UnaryOperation | a prefix operator or a built-in function | op, val, pos |
BinaryOperation | a binary operator or an application | op, lhv, rhv, pos |
Termination | a missing closing bracket | expected, pos |
Token | a character or token in the wrong place | token, pos |
Name | a name was expected | pos |
Value | created with undef.from | val |
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:
| Name | Contents |
|---|---|
| Root names | TRUE, FALSE, type, parse, bind, label |
bool | Converting and combining truth values |
numb | Converting values to numbers |
text | Measuring, searching, slicing, splitting text |
list | Measuring, searching and slicing lists |
nspace | Inspecting namespaces |
math | Mathematical constants and functions |
time | Timestamps and calendar dates |
promise | Creating and resolving promises |
undef | Creating 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;
TRUEis1andFALSEis0;- 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:
| Value | Text |
|---|---|
| a text | the text itself |
| a number | the number, such as 42 or 1.5 |
| a boolean | TRUE 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:
type | Members | Meaning |
|---|---|---|
UnaryOperation | op, val, pos | operation op failed on the operand val |
BinaryOperation | op, lhv, rhv, pos | operation op failed on lhv and rhv |
Termination | expected, pos | the closing bracket expected is missing |
Token | token, pos | a token is in the wrong place |
Name | pos | a name was expected |
Value | val | created 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:
| Member | Type | Content |
|---|---|---|
uri | Text | The full URI of the document |
source | Text | The template source |
data | Namespace | The names the document's tags bound |
rendering | Text | The 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.
| Host | Use it to | Reference |
|---|---|---|
| Command line | render template files from a terminal or a script | texl command |
| JavaScript | render templates in a web page or a web worker | JavaScript API |
| Python | render templates in a Python program, with asyncio | Python API |
| Rust | render templates in a Rust program, or build a new host | texl_core, swan_core |
| Node.js | coming 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:
| Crate | Contents |
|---|---|
swan-core | The Swan language: lexer, parser, evaluator and built-in library. |
texl-core | The template renderer and the engine: documents, loaders, imports. |
texl-cli | The texl command. |
texl-wasm | The JavaScript library, compiled to WebAssembly (@onlabsorg/texl on npm). |
pytexl | The 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
| Argument | Meaning |
|---|---|
FILE | The template file to render. Required. A relative path is relative to the current folder. |
-o OUTPUT, --output OUTPUT | Writes the rendering to the file OUTPUT instead of standard output. The file is created, or replaced if it exists. Its folder must exist. |
-h, --help | Prints 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(...) of | loads |
|---|---|
"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
| Status | Meaning |
|---|---|
| 0 | The template was rendered and written. |
| 1 | FILE could not be read, or OUTPUT could not be written. |
| 2 | The 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(...) of | fetches |
|---|---|
"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:
| JavaScript | Swan |
|---|---|
null, undefined | Nothing |
| boolean | Bool |
| number | Numb |
| string | Text |
| array | List |
Promise, or any object with a then method | Promise; a rejection resolves to Nothing |
| function | Func (see below) |
| other object | Namespace 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:
| Swan | JavaScript |
|---|---|
Nothing | null |
Bool, Numb, Text | boolean, number, string |
List, Tuple | array |
Namespace | plain object with its members |
Promise | Promise |
Func, Undefined | undefined |
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.
| Argument | Description |
|---|---|
globals | An object whose properties become globals of every template and document. Optional. |
baseUrl | The 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.
| Argument | Description |
|---|---|
name | The name of the global. |
value | Its 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.
| Argument | Description |
|---|---|
uri | The 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.
| Argument | Description |
|---|---|
template | The 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.
| Argument | Description |
|---|---|
vars | An object whose properties are bound in the context. Optional. |
uri | The 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.
| Argument | Description |
|---|---|
name | The 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.
| Argument | Description |
|---|---|
name | The name to bind. |
value | Its 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.
| Argument | Description |
|---|---|
template | The 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.
| Property | Description |
|---|---|
uri | The URL of the document. |
source | The template source. |
data | The names the document's tags bound, converted to JavaScript values. Functions are undefined. |
rendering | The 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:
| URL | Loader | Reads |
|---|---|---|
file:///home/ada/report.texl | loaders.load_file | a local file, as UTF-8 |
/home/ada/report.texl (no scheme) | loaders.load_file, the default loader | a local file, by absolute path |
http://..., https://... | loaders.load_http | a 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(...) of | reads |
|---|---|
"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:
| Python | Swan |
|---|---|
None | Nothing |
bool | Bool |
int, float | Numb |
str | Text |
list | List |
tuple | Tuple |
dict, or any other mapping | Namespace of its string keys |
| awaitable: coroutine, future or task | Promise; an exception resolves to Nothing |
| function, or any other callable | Func (see below) |
Promise, Function, Undefined | the Swan value they wrap |
| anything else | Nothing |
Values that come back to Python, from context.get, from document data and
as function arguments, are converted the other way:
| Swan | Python |
|---|---|
Nothing | None |
Bool, Text | bool, str |
Numb | int if it is a whole number (up to 253), float otherwise |
List | list |
Tuple | tuple |
Namespace | dict of its members, sorted by name |
Promise | Promise, which you can await |
Func | Function, which you can call |
Undefined | Undefined, 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.
| Argument | Description |
|---|---|
globals | A mapping whose items become globals of every template and document. Optional. |
base_url | The 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.
| Argument | Description |
|---|---|
name | The name of the global. |
value | Its 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.
| Argument | Description |
|---|---|
scheme | The URL scheme, without the colon, such as "mem". It is case-insensitive. |
loader | A 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.
| Argument | Description |
|---|---|
loader | A 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.
| Argument | Description |
|---|---|
uri | The 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.
| Argument | Description |
|---|---|
template | The 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.
| Argument | Description |
|---|---|
vars | A mapping whose items are bound in the context. Optional. |
uri | The 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.
| Argument | Description |
|---|---|
name | The 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.
| Argument | Description |
|---|---|
name | The name to bind. |
value | Its 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.
| Argument | Description |
|---|---|
template | The 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.
| Attribute | Description |
|---|---|
uri | The URL of the document. |
source | The template source. |
data | The names the document's tags bound, converted to Python values. It also holds texl. |
rendering | The 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.
type | Attributes | Meaning |
|---|---|---|
"UnaryOperation" | op, val, pos | operation op failed on the operand val |
"BinaryOperation" | op, lhv, rhv, pos | operation op failed on lhv and rhv |
"Termination" | expected, pos | the closing bracket expected is missing |
"Token" | token, pos | a token is in the wrong place |
"Name" | pos | a name was expected |
"Value" | val | created 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.
| Argument | Description |
|---|---|
uri | An http:// or https:// URL. |
timeout | The 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_corerenders templates and loads documents. Most programs only need itsEngine.swan_coreimplements the Swan language. You use itsValuetype 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 asfileorhttps.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.