Specs

How Common works inside, for the curious: the pipeline, the values, the safety model, the limits, and how fast it is. Everything here is true of version 0.4.0.

At a glance

Version0.4.0 (cl version)
ImplementationOne C11 file, src/cl.c, about 3,400 lines. Interpreter, dev server and CGI mode in one program.
BinaryAbout 140 KB on macOS (Apple Silicon) with SQLite and Postgres linked in.
Required dependenciesNone. A C compiler and make.
Optional librariesSQLite (public domain) and libpq (PostgreSQL License), found and linked by the Makefile when installed.
Vocabulary59 words, 17 of them commands that start a line. One accepted form each, counted live from dictionary as this page loads.
PlatformsmacOS and Linux, tested. Windows has code paths for MinGW and Visual Studio, not yet confirmed by a build (see below).
LicenseApache License 2.0.
TestsAbout 340 checks in make test, also run under AddressSanitizer, UBSan and the Linux leak checker.

How a page runs

  1. The .html fileread whole. Text outside <cl> tags is copied to the output untouched.
  2. Blockseach <cl> … </cl> block is cut into lines. Line numbers count from the top of the file, so errors point at the real line.
  3. Tokensthe first time a line runs, it is split into words, numbers, text and symbols, and the tokens are kept with the line. A loop does not re-read its own lines.
  4. Parse and run, togethera recursive-descent parser reads each line and evaluates it as it goes. There is no syntax tree and no bytecode: the tokens are the program.
  5. Outputwhat the block showed replaces the block. Visitor values were HTML-escaped as they were shown or placed in text, exactly once.

Blocks are found by their shape before anything runs: if, for each, repeat, to, send email, a record and a text block each look ahead for the end that closes them, so a missing end is reported at the line that opened the block.

Values

KindDetails
TextUTF-8. first of and last of take whole letters, not bytes. Holes like [name] are kept inside the text token and filled when the text is used.
NumberA 64-bit floating-point number (C double), shown with up to 15 significant digits, so 0.1 + 0.2 shows 0.3.
Yes / noThe result of a condition. Shown as yes or no.
ListAn ordered list of any values, counting from 1.
RecordNamed columns. Every database row is a record, and so is set NAME to record.

Values are copied, never shared: setting one name to another gives an independent copy. When one side of a comparison is a number, text written as a number is compared as a number, so query "age" is more than 12 works on what a visitor typed. Comparing a number with other text is an error, not a quiet no.

The safety model

Every value carries two marks. Untrusted means it came straight from a visitor: query, form, path, remembered, and text read from a database. Tainted means it was built from untrusted text, through brackets, trim of, replace, split, a text block or any other word. The marks travel with the value through every word.

Where a marked value goesWhat Common does
Shown on the page, or placed in textHTML-escaped once: & < > " '. A link that starts with javascript:, vbscript: or data: becomes #unsafe-link.
SQL textRefused. Values go after with and are bound as parameters.
include, open databaseRefused. File names are written by the page.
go toAllowed only to an address on this site that starts with /.
send email to, from:Refused. A visitor's address can only be reply to:, and only if it is one real address.
Database values, remember, email, go toSent as plain text, with the escaping taken off again, so stored text is not escaped twice when it is shown later.

The server

cl serveHTTP/1.0, one process forked per request (on Windows, one request at a time). Each request gets 30 seconds. Headers up to 64 KB, bodies up to 1 MB.
cl cgiFor Apache, nginx with fcgiwrap, and shared hosts. One process per request, 30 seconds each.
Addresses/about runs about.html; /blog/hello runs blog.html with the rest in path; a 404.html is shown for anything missing, with a real 404 status.
Static filesSent as they are, cached for an hour: .css .png .jpg .gif .svg .ico .webp .woff2 .pdf .zip .txt .md.
Never sentAny .db .sqlite .sqlite3 .sql .secret .eml .log file, the outbox/ folder, and any address with .., whatever the case or trailing dots.
Every responseX-Content-Type-Options: nosniff and X-Frame-Options: SAMEORIGIN.
Forms from other sitesA POST whose Origin names another host is refused with 403. X-Forwarded-Host counts, for proxies.

Sessions and passwords

Databases and email

Limits

WhatLimitWhy
A verb calling itself100 levelsstops runaway recursion with a clear error
Files including files10 levelscatches include loops
repeat2 billion turnsa ceiling, so a page always ends
A request30 secondsa slow page or client cannot hold the server
try practice code20,000 steps, 64 MB, 50,000 letters of outputpractice pages run code from strangers
Inside tryno include, database, cookies, go to or emailthe sandbox cannot touch the server

Memory

There is no garbage collector. Values are copied when they are stored and freed when their name is replaced or its page, verb or loop ends. A verb reads its caller's names in place instead of copying them all, and makes its own copy only when it changes one in place (add, set … of), so the caller's values never change.

Errors are a longjmp out of whatever was running. Anything a frame was holding at that moment (token lists, values, text buffers, verb environments) is listed in small registries, and the block that catches the error frees what is left, so an error never leaks.

Speed

Common is an interpreter, and for a website the time goes to the network and the database: the pages on this site render in about 10 ms. In tight loops, the numbers below were measured on an Apple Silicon laptop with make bench. Expect different numbers on other machines; the comparison is what matters.

BenchmarkCommon 0.3Common 0.4Python 3PHP
A loop of 1,000,000 turns with math2.96 s0.3 s0.21 s0.07 s
Text with holes, built 200,000 times0.25 s0.2 s0.11 s–
2,000 verb calls over a 2,000-item list0.45 s0.02 s––

What made 0.4 faster, each found with a profiler rather than guessed:

Not done yet: numbered slots for names instead of lookups by name, and keeping a server process alive with pages already parsed. Each would help, but neither is needed for pages this size.

Platforms

macOSLinuxWindows
Builds and passes make testyesyesnot yet confirmed: it cross-compiles with MinGW, but has not been run on Windows
cl serveone process per requestone process per requestone request at a time
30-second limit per requestyesyesno
send email through sendmailyesyesoutbox only

See also: how a site fits together, the glossary of every word, and the error messages.