One wayto say it.

Topaz is a programming language designed for typesetting. Use it for programs that produce print-ready documents, from books to reports and invoices, and for command-line tools and application logic as well. Run the same source on your computer or build a package for the browser.

Run this program today and get the output shown here.

TOPAZ
function greet(name: string) -> string {
    return "Hello, {name}!"
}

print(greet("Topaz"))
output
Hello, Topaz!

ATLAS

A typesetting tool built with Topaz

ATLAS turns a manuscript into print-ready book pages. Here is a page it set, with part of its line-breaking rules in Topaz. ATLAS names these functions with Korean typesetting terms, shown here in English.

Page 18 of a Korean book set with ATLAS
Page 18 of a book set with ATLAS
TOPAZ
function nextIsNotLatinSpace(next: Option<string>) -> bool {
  match next {
    case Some(text) => !isLatinSpace(text)
    case None => true
  }
}
function nextHasNoLineStartBan(next: Option<string>) -> bool {
  match next {
    case Some(text) => !hasLineStartBan(text)
    case None => true
  }
}

Part of the ATLAS line-breaking rules

Why Topaz handles typesetting well

Typesetting words as names

Name functions and variables with the words of typesetting, such as badness, in Korean, Russian, or English. Topaz also keeps the characters of a manuscript exactly as entered.

Strings & Templates →

One rule for preview and output

Write a typesetting rule once and build the same source as an executable for your computer and a package for the browser. Both follow the same language rules, so the rule you check on screen is the rule your output uses.

Interpreter & Backends →

No silent arithmetic errors

An int is a signed 64-bit integer, and overflow stops the program at that point instead of continuing with a wrong value. When you measure line widths and layout costs in integers, an overflow cannot hide in the result.

Runtime Behavior →

Missing cases stop the build

Put a fixed set of cases, such as line-break classes, in an enum, and the compiler checks that each match handles every one. When you add a case later, the build reports each match that does not handle it yet.

Patterns & Control Flow →

Lispex

Topaz computes, Lispex decides

Topaz does the computing that breaks lines and builds pages. Decisions that change with each publisher and book, such as which page a chapter starts on or how many lines may stand alone, go into separate Lispex rules. The evaluator built into Topaz runs those rules within set limits. Change a rule file and lock the package again, and the same manuscript takes another style without any change to the application code.

  • Rules never touch files, the system clock, or the network.
  • A rule stops where it runs past its limit.
  • The same rule, input, and settings give the same decision every time.
LISPEX
(let ((min-lines 2))
  (if (< input min-lines) "next-page" "keep"))

All of rules/widow-lines.lspx

TOPAZ
import src.values { singleDigit, textOf }
import std.lispex { defaultLimits, evaluate }
import std.lispex.rules as rules

export function main(args: Array<string>, stdin: string) -> Result<int, string> {
    let rule = rules.widow_lines()
    for linesLeft in [1, 2] {
        match evaluate(rule, singleDigit(linesLeft)?, defaultLimits(rule)) {
            case Ok(Complete(result)) => print("lines left {linesLeft}: {textOf(result)}")
            case _ => return Err("the rule did not finish")
        }
    }
    Ok(0)
}
output
lines left 1: next-page
lines left 2: keep

All of src/main.tpz. singleDigit and textOf are small helpers that convert Lispex values.

Lispex rules run in the interpreter and in native builds. For the full setup, including the manifest and limit files, see Run Lispex Rules.

Business rules and data processing, too

TOPAZ
function discount(customer: { tier: string, years: int }) -> float {
    return match customer {
        case { tier: "vip", years } if years > 5 => 0.3
        case { tier: "vip" } => 0.2
        case { years } if years > 1 => 0.1
        case _ => 0.0
    }
}

print("{discount({ tier: "vip", years: 7 })}")
print("{discount({ tier: "new", years: 0 })}")
output
0.3
0.0
TOPAZ
function clean(words: Array<string>) -> Array<string> {
    return filter(words, (w: string) => w != "")
}

function shout(words: Array<string>) -> Array<string> {
    return map(words, (w: string) => w + "!")
}

let line = ["one", "", "way", "", "to", "say", "it"]
    |> clean
    |> shout
print("{line}")
output
[one!, way!, to!, say!, it!]

Concurrency and cleanup examples are in Concurrency and Defer & Resources.