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
| Version | 0.4.0 (cl version) |
| Implementation | One C11 file, src/cl.c, about 3,400 lines. Interpreter, dev server and CGI mode in one program. |
| Binary | About 140 KB on macOS (Apple Silicon) with SQLite and Postgres linked in. |
| Required dependencies | None. A C compiler and make. |
| Optional libraries | SQLite (public domain) and libpq (PostgreSQL License), found and linked by the Makefile when installed. |
| Vocabulary | 59 words, 17 of them commands that start a line. One accepted form each, counted live from dictionary as this page loads. |
| Platforms | macOS and Linux, tested. Windows has code paths for MinGW and Visual Studio, not yet confirmed by a build (see below). |
| License | Apache License 2.0. |
| Tests | About 340 checks in make test, also run under AddressSanitizer, UBSan and the Linux leak checker. |
How a page runs
- The .html fileread whole. Text outside
<cl>tags is copied to the output untouched. - Blockseach
<cl> … </cl>block is cut into lines. Line numbers count from the top of the file, so errors point at the real line. - 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.
- 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.
- 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
| Kind | Details |
|---|---|
| Text | UTF-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. |
| Number | A 64-bit floating-point number (C double), shown with up to 15 significant digits, so 0.1 + 0.2 shows 0.3. |
| Yes / no | The result of a condition. Shown as yes or no. |
| List | An ordered list of any values, counting from 1. |
| Record | Named 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 goes | What Common does |
|---|---|
| Shown on the page, or placed in text | HTML-escaped once: & < > " '. A link that starts with javascript:, vbscript: or data: becomes #unsafe-link. |
| SQL text | Refused. Values go after with and are bound as parameters. |
include, open database | Refused. File names are written by the page. |
go to | Allowed 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 to | Sent as plain text, with the escaping taken off again, so stored text is not escaped twice when it is shown later. |
The server
cl serve | HTTP/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 cgi | For 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 files | Sent as they are, cached for an hour: .css .png .jpg .gif .svg .ico .webp .woff2 .pdf .zip .txt .md. |
| Never sent | Any .db .sqlite .sqlite3 .sql .secret .eml .log file, the outbox/ folder, and any address with .., whatever the case or trailing dots. |
| Every response | X-Content-Type-Options: nosniff and X-Frame-Options: SAMEORIGIN. |
| Forms from other sites | A POST whose Origin names another host is refused with 403. X-Forwarded-Host counts, for proxies. |
Sessions and passwords
- Sessions.
rememberstores values in one cookie,cl_session: the values and an end date 30 days out, signed with HMAC-SHA256 under a 32-byte random secret kept incl.secret. The signature is checked in constant time, and a cookie that fails is ignored. The cookie isHttpOnlyandSameSite=Lax. - Passwords.
hash ofmakes PBKDF2-HMAC-SHA256 with 200,000 rounds and a 16-byte random salt, writtencl2$200000$salt$hash.matcheschecks it in constant time. Hashes from 0.3.0 and earlier (cl1$) still match. - Randomness. From
/dev/urandom, orrand_son Windows. With neither,clstops rather than guess. - SHA-256, HMAC and PBKDF2 are written out in
cl.c: no crypto library is needed.
Databases and email
- SQLite waits up to 3 seconds for a write lock instead of failing. Postgres gets the same
?placeholders, turned into$1,$2. Numbers come back as numbers, and booleans as yes or no. - Email goes to an
outbox/folder undercl serve, and tosendmailundercl cgi. Plain text, UTF-8, with a non-ASCII subject encoded. At most 3 a page and 20 an hour per site, a body of at most 50,000 letters, and line breaks in a subject become spaces.
Limits
| What | Limit | Why |
|---|---|---|
| A verb calling itself | 100 levels | stops runaway recursion with a clear error |
| Files including files | 10 levels | catches include loops |
repeat | 2 billion turns | a ceiling, so a page always ends |
| A request | 30 seconds | a slow page or client cannot hold the server |
try practice code | 20,000 steps, 64 MB, 50,000 letters of output | practice pages run code from strangers |
Inside try | no include, database, cookies, go to or email | the 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.
| Benchmark | Common 0.3 | Common 0.4 | Python 3 | PHP |
|---|---|---|---|---|
| A loop of 1,000,000 turns with math | 2.96 s | 0.3 s | 0.21 s | 0.07 s |
| Text with holes, built 200,000 times | 0.25 s | 0.2 s | 0.11 s | – |
| 2,000 verb calls over a 2,000-item list | 0.45 s | 0.02 s | – | – |
What made 0.4 faster, each found with a profiler rather than guessed:
- Lines are read once. Tokens are kept with their line, and a verb's body is split once rather than on every call.
- Holes are read once. The tokens of each
[hole]are kept by their text in a table of 1,024 entries (by content, never by address, since text can be temporary). - Names are checked once. "Is this a Common word?" is answered once per token, and dictionary lookups use a hash table.
- Verbs share instead of copy. Calling a verb used to copy every name on the page.
- Hints are lazy. The "you forgot the quotes" check runs only the first time a line runs, and error hints are built only when there is an error.
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
| macOS | Linux | Windows | |
|---|---|---|---|
Builds and passes make test | yes | yes | not yet confirmed: it cross-compiles with MinGW, but has not been run on Windows |
cl serve | one process per request | one process per request | one request at a time |
| 30-second limit per request | yes | yes | no |
send email through sendmail | yes | yes | outbox only |
See also: how a site fits together, the glossary of every word, and the error messages.