Repository navigation
Architecture
Sashfold is one engine in one repository: every stage between a byte of HTML arriving on a socket and a pixel on the screen is written here, and every stage is testable on its own. This page walks the pipeline in the order a page travels it, then the pieces that sit beside it, then the engine that is still under construction — a JavaScript engine of our own.
src/core bitmaps, PNG, GIF, JPEG, BMP and ICO, inflate, brotli, JSON, Unicode tables, bidi
src/html encoding sniffing, the tokenizer, the tree builder
src/dom the document tree
src/css tokenizer, parser, selectors, stylesheets, the style resolver
src/layout block, inline, float, flex, grid, table and positioned layout
src/paint the painter
src/svg the SVG subset: paths, shapes, gradients, clips, text, and its own rasterizer
src/text TrueType and CFF, the rasterizer, the font manager, Sashfold Mono
src/net URLs and IDNA, HTTP/1.1, connections, the cache, cookies, the content-blocking lists, Content Security Policy
src/platform the seam: window, input, clipboard, sockets, TLS, font dirs
src/ui the browser core, frames, forms, reader mode, downloads, script replay
src/js the JavaScript engine (see below)
src/bindings the page's realm: the DOM as scripts see it, events, timers, the window; generated/ from idl/
idl the WebIDL the bindings generator reads (tools/gen-bindings.cpp)
Everything above src/platform is operating-system-free and identical on Windows, Linux and macOS. The seam is a set of small interfaces (Window, Input, Clipboard, Net, Tls, Fonts) with one implementation per operating system under platform/win32, platform/linux (the Wayland window, the keymap parser and the clipboard) and platform/posix (sockets, TLS, fonts, shared by Linux and macOS); nothing else in the engine includes an OS header. That is what lets the same reference tests produce byte-identical pixels on three operating systems in CI.
Bytes arrive through the fetch stack (Networking): a URL parser written to the WHATWG standard with IDNA tables generated from the Unicode data files, HTTP/1.1 with persistent connections, a freshness-honouring cache, a cookie jar that blocks third parties by default, and TLS through the operating system's own library behind the Tls seam. A single choke point owns every socket, and a CI check reads the binary to prove nothing else opens one.
The bytes are sniffed for their encoding (BOM, <meta> prescan, the windows-1252 fallback), decoded, and fed to the HTML tokenizer and tree builder, both written state-for-state from the WHATWG specification — foster parenting, the adoption agency, <template> contents and all. They pass every html5lib tokenizer and tree-construction test. The result is a document tree owned by the document as a node soup: reparenting during parsing is pointer surgery, never an ownership move.
Stylesheets — the user-agent sheet, <style> elements, external sheets, @media, @font-face — go through the CSS tokenizer and parser into rule lists. The style resolver matches selectors right to left against each element, orders the declarations by origin, specificity and source order, and produces one computed style per element: inheritance, inherit/initial/unset, custom properties, calc(), the flow-relative properties resolved against the element's writing mode and direction. Generated content and counters are resolved here too.
Layout takes the tree and the styles and produces fragments: boxes with a position and size, text runs with a baseline, floats, positioned boxes and stacking contexts. The algorithms are the specifications' own — block and inline formatting with margin collapsing, floats and clearance, flexbox, grid (tracks, fr, repeat(), areas, named lines, spans, alignment, baseline alignment), tables in both border models, positioning with the static-position rules of CSS 2.1, scroll containers with their scrollable overflow, sticky positioning, and writing modes.
Vertical text has a design worth naming. Layout reads a box's inline axis as x and its block axis as y, which is exactly what a horizontal writing mode means — so a vertical box is laid out through a style whose physical edges have been permuted into that frame, every algorithm runs unchanged, and the finished fragments are turned back into the page. A box whose mode differs from its container's starts an orthogonal flow in a frame of its own. Glyphs turn a quarter circle through a scratch bitmap. The Unicode bidirectional algorithm reorders each line, at 100% of the Unicode conformance files.
A fragment-tree checker runs on every page the reference suite renders: an element placed more than once where only an inline box may be, a coordinate that is not a number, a size below zero. Each fault it knows has a hand-built positive control in the unit tests, so it is known to fire.
The painter walks the fragments into a bitmap of our own: backgrounds (colours, images, gradients, tiling, clipping), borders with rounded corners and the collapsing table model, text, replaced content, stacking contexts in their painting order, group opacity, and the scrollbars of boxes that scroll. There is no compositor library and no GPU path; the bitmap is presented through the seam.
Text is drawn by a TrueType reader and rasterizer written here — glyf outlines, .ttc collections, the metrics that ex and ch are measured from, a font manager that scans the operating system's font directories itself and builds fallback chains — with Sashfold Mono, a face generated by a tool in this repository, as the last resort that guarantees every character has a glyph. @font-face TrueType files are loaded from pages, plain or in a WOFF wrapper (src/text/Woff, over the engine's own inflate); there is no WOFF2 decoder yet.
Pictures come through decoders written here: PNG on top of our own inflate, GIF, baseline and progressive JPEG, and BMP with ICO — the format a page's icon usually arrives in, which the shell fetches from <link rel=icon> or /favicon.ico and draws in the tab — with srcset and <picture> negotiated before the fetch.
The browser is an operating-system-free core (ui/Browser) that owns tabs, history, the address bar, forms, selection and the clipboard, find-in-page, reader mode, keyboard link hints and a devtools panel, and draws its own chrome with the same rasterizer from a JSON theme file. A --script mode replays a text file of clicks, keys and assertions into that core, which is how the shell is tested on every operating system in CI without a window: the chrome screenshots those scripts take are byte-identical too.
A page's heap runs under a ceiling (js::Heap::set_limit; what happens at it is under The shell), and its collector is scheduled by what the cells say they hold now, asked again on a cadence, since a cell grows after it is made.
Sashfold's JavaScript engine lives in src/js, under the same rule as everything else: no engine is embedded, and no code is vendored. It runs scripts today, it is scored on test262 (Measurements), and it is connected to the page through the bindings in src/bindings: one realm per document, one wrapper per node cached on the node, the connected tree one root for the collector, and an event loop the host pumps — see Scripting.
What is written:
- Values and strings. A sixteen-byte tagged value for the language's types. Strings are sequences of UTF-16 code units, which is what the language defines them to be, converted to and from the document's WTF-8 at the boundary; property names are interned atoms that compare by pointer.
- The heap. A precise, non-moving mark-and-sweep collector with explicit roots: a root stack in the interpreter, persistent handles for the outside world, and a stress mode that collects at every allocation. The unit tests run under stress, so a value held across an allocation without a root fails the first time, not in someone's tab.
-
The object model. Ordinary objects with creation-ordered properties and the essential internal methods of the specification as virtual functions, so the exotic objects override exactly the ones the specification says they do: arrays with a dense fast path and a virtual
length, string wrappers, bound functions, native functions, arguments objects (the sloppy kind aliasing their parameters), typed arrays (whose numeric keys — every canonical numeric string, not only the array indices — are elements read from and written to a buffer, with nothing looked up the chain for them), and environment records for scopes and closures. -
The tokenizer and parser. ECMAScript's lexical grammar, including the regular-expression and template ambiguities that only the parser can settle, and a recursive-descent parser with automatic semicolon insertion, strict-mode early errors and per-scope declaration lists, producing a syntax tree. It covers ES5 and the later additions real pages lean on —
let/const, arrow functions, template literals,**,??,?., destructuring patterns, default and rest parameters, classes with fields, private members, static blocks,superandnew.target, tagged templates — and a feature it does not cover is a syntax error that names itself, never a silent misparse. -
Regular expressions. A compiler to a small instruction set and an iterative backtracking matcher with an explicit stack and a step budget, so a pathological pattern on hostile input exhausts the budget rather than the process. Named groups, lookahead, the
s,uandyflags. -
The evaluator. The mechanisms every script runs on, whichever tier drives them: environment records resolved the way the specification lays them out (the global object's record with the script-level
lets in a declarative record in front of it; function environments with their arguments object; block environments made only when a block declares something; a fresh copy of afor (let …)head's bindings every iteration, so closures keep their own), references evaluated as references so that a method call gets itsthisand an optional chain short-circuits as one, completion records for the statements, direct and indirecteval,new Function,with, labelledbreakandcontinue,for-ofover the iterator protocol with the iterator closed when the body leaves early, spread in array literals, argument lists and object literals, destructuring patterns wherever a name may stand — declarations, parameters, catch clauses, for-in/of heads and assignments — with defaults and rest elements, default and rest parameters (a default that could make a closure puts the parameters in a scope of their own, as the specification asks), classes — the prototype and constructor pair, derived constructors whosethiswaits forsuper(), methods and accessors carrying a home object forsuper.x, instance fields defined before the body runs, private fields, methods and accessors keyed by a Private Name made fresh for each evaluation of the class and found on the object alone (never up the prototype chain, never listed, refused on a non-extensible object),#x in o, static fields and blocks run with the class asthis,new.target— the temporal dead zone, and sloppy and strict code each with their rules. A runaway script is stopped by an interrupt polled every so many steps and by a budget on the C++ stack it may use, and neither stop can be caught. -
The library. Object, Function, the errors, Symbol with its registry and the well-known symbols, Reflect, Boolean, Number, BigInt (arbitrary-precision integers of the engine's own — literals, every operator with the specification's mixing rules,
asIntN/asUintN, any radix out), Math, the global functions and the URI coders; Array, every method written generically over an array-like so thatargumentsobjects and plain objects with alengthare served too, and the iterator protocol —%IteratorPrototype%, the array and string iterators behindentries,keys,valuesand@,Array.fromandObject.fromEntriesover any iterable; Map, Set, WeakMap and WeakSet over one insertion-ordered table keyed by SameValueZero, with their iterators, the Set methods of ES2025 (union,intersection,difference,symmetricDifference,isSubsetOf,isSupersetOf,isDisjointFrom) over any set-like,Map.groupBy/Object.groupByand thegetOrInsertpair — the weak kinds hold their keys strongly for now, since the collector has no ephemerons and no script can tell; ArrayBuffer — fixed or resizable up to a maximum,sliceover species,transfer, detachable — with the eleven typed-array kinds fromInt8ArraytoFloat64ArrayplusFloat16Array,BigInt64ArrayandBigUint64Array(the four ways of constructing one,%TypedArray%.from/of, every prototype method fromattowith, views that follow a resizable buffer's end and go out of bounds rather than lie when it shrinks under them, the byte conversions of the specification down to ties-to-even, the two content types never mixing) andDataViewin either byte order, its BigInt methods included; String, with the well-known-symbol protocol that handsreplace,match,searchandsplitto a pattern; RegExp withexec,lastIndex, the flags, named groups and the symbol methods; JSON, the strict grammar of the specification and a stringifier with replacers, gaps and cycle detection; Date, with the specification's arithmetic, the ISO format and the informal forms pages write, and the local zone read from the C runtime; Promise — the constructor and its resolving functions,then/catch/finally,all/allSettled/any/race,resolve/reject/withResolvers/try, species-aware — with AggregateError, on a job queue of the engine's own: reactions andqueueMicrotaskcallbacks in one FIFO that the host drains at every microtask checkpoint, a rejection nobody handled by the end of a drain reported once; and aconsolethe shell listens to. -
The bytecode machine. Every body compiles to a stack bytecode and runs on a loop over an explicit frame on the engine's own stacks — and, once it runs often, as machine code on the same frame (Scripting): a function's body, its parameter list when it has defaults, patterns or a rest, a script's or an eval's statement list with its completion value tracked in a register, a module's body, and a class in steps — its scope, its heritage, each element with its computed key evaluated where the specification evaluates it, then the class finished — so a
yieldor anawaitmay stand anywhere an expression may. A generator that yields is a frame the collector traces, resumed bynext(), and an async function that awaits is the same frame, resumed by a reaction job. The instructions call the evaluator's reference, environment, class and pattern mechanisms; the tree-walking interpreter the machine grew out of ran every body beside it until a differential run proved the two agreed on the whole conformance suite, and was then deleted.for awaitruns on this tier too — the async iterator protocol with each step and each close awaited, and a sync iterable wrapped by an async-from-sync iterator whose results come through promises. An async generator is the same frame with a queue: eachnext,returnorthrowis answered with a promise and queued, the body serves the queue's head, ayieldawaits its value before it goes out and areturnawaits what it returns, a request that arrives while the body runs resumes it the moment the yield completes, andyield*speaks the async iterator protocol, every inner answer awaited. -
Modules. Source text module records over the Module goal's parse, with the specification's own algorithms: linking as a depth-first walk that initialises each module's environment once its dependencies have theirs, an import a live indirect binding into the exporting module (the dead zone crosses modules, and a cycle links as one), the namespace exotic object whose properties read those bindings, dynamic
import()on a promise made before anything can fail, andimport.metaone object per module. A body runs on the bytecode machine as a function of its own; one with a top-levelawaitruns as an async function, and its importers wait for it through the asynchronous half of the evaluation algorithm, in the order they were found to be waiting. The host names a module by a key and supplies two hooks, one resolving a specifier against the importing module's key and one fetching a key's source, and the module map lives with the realm.
What is not: an await or yield inside a class heritage or a computed key, and the regular-expression forms of Not written yet — each a syntax error that names itself.
Three rules hold across the engine. A thrown value is a return value — std::optional, empty when an exception is pending — never a C++ exception. Every walk over a script is depth-capped, and a runaway script is stopped by an interrupt the script cannot catch. And a realm — heap, global object, intrinsics — belongs to one document and dies with it.
The lifetime of a DOM node once a script can hold it is decided and recorded in the repository (docs/adr/0001-dom-lifetime.md): reference counts on nodes, with the script heap's collector doing the cycle work by treating a connected tree as one root, staged behind the document arena until a measurement says the arena is the problem.
The yardstick is test262, the ECMAScript conformance suite, pinned to a revision and fetched into a gitignored checkout exactly as the Web Platform Tests are, with a runner written to the suite's own instructions — harness first, then the test, once as sloppy code and once as strict. Two rules of our own are built into it: every test under a listed directory counts, modules and async included, and a test expecting a syntax error scores only on a real one, never on the engine declining a feature it does not have. It scores 32,329 of 41,250 tests today; the table by directory is at sashfold.com/test262.html.
- Determinism. Reference tests are byte-identical on three operating systems, so layout and paint use no transcendental function from the C library, floating-point contraction is off on every lane, and every number a test can see is one the engine computed the same way everywhere.
- Bounds. No recursion runs unbounded on a script, which is input: the JavaScript parser caps its nesting and the regular-expression matcher its steps, and a failure is a named error rather than an exhausted stack.
-
Two compilers, warnings as errors. Every commit builds under GCC and Clang with
-Wall -Wextra -Wpedantic -Werrorand the shadowing and cast warnings on, plus a sanitizer lane and a fuzzing lane in CI. -
Proof of the pledge.
tools/pledge-check.shreads each release binary's import table;tools/egress-check.shproves every network primitive sits behind the choke point. Both fail the build (The Pledge).
Getting started
What works today
Measured
Design decisions
Testing and tools