Skip to content

Architecture

src/ is flat and loaded :serial t, so the component order in cl-cc-javascript.asd is the dependency order. 94 files, about 15,900 lines.

Group Files Role
package / macros 2 the defpackage form and the shared define-*-table-style codegen macros every group below builds on
lexer-* 5 source text to token plists
parser-* 19 tokens to cl-cc AST nodes
runtime-* 67 the %js-* helpers generated code calls
vm-integration 1 the JS-to-VM callback direction

Why the runtime dominates

Two thirds of the file count is runtime. That is not accidental complexity: it is the size of the JavaScript standard library. Every Array.prototype method, every Math function, Map/Set/WeakMap/WeakSet, Promise, Date, Intl, RegExp, TypedArrays and Temporal each need a Lisp implementation, because the VM has no host JavaScript engine to delegate to.

The split within runtime-* is by builtin family, one or more files each (runtime-array-core, runtime-array-transforms, runtime-array-es2023, ...), which keeps individual files near the org's 300-line guideline. The alternative — one runtime-array.lisp of 1,500 lines — was rejected for the usual reason: nothing else in the file gives you context for the function you are reading.

Load order constraints

Three positions in the component list are load-bearing and are commented as such in the .asd.

runtime-bridge-provider must come after every %js-* definition, because it registers the whole set of helpers with cl-cc as a backend bridge provider. Registering a name that is not yet fbound would fail at load time.

vm-integration is last. It writes into the runtime specials (*js-apply-fn*, *js-callable-p*, *js-apply-with-this-fn*), so it has to load after runtime-call defines them.

package is first, and is the only file containing a defpackage. It is 489 lines, which makes it the largest file in the repository — almost entirely the :import-from and :export lists. As a manifest, it is exempt from the file-length guideline; a large defpackage form is one CL idiom to keep as a single file regardless of line count, unlike a large function or a data-plus-logic file, which genuinely benefit from a split (see runtime-regex-combinators.lisp/runtime-regex.lisp for an example of the latter).

Where the frontend boundary sits

cl-cc-javascript owns tokenizing, parsing, and the JavaScript standard library. It does not own AST optimisation, code generation, or the VM. The AST types it constructs (ast-let, ast-call, ast-var, ...) belong to cl-cc-ast.

The dependency edge that surprises people is cl-cc-vm. A frontend should not need the virtual machine — but vm-integration.lisp does, because Array.prototype.map must be able to invoke a callback that is a compiled VM closure rather than a CL function. That glue used to live in cl-cc's pipeline, which coupled the pipeline to this package's internals; moving it here inverted the dependency, at the cost of this one edge. The pipeline now runs every registered backend's installer without naming any backend.

Known issue: the duplicate definition in cl-cc

The cl-cc monorepo still contains packages/javascript/, a second system with the same name. The two trees have diverged further with every session: as of this checkout's pinned cl-cc commit, of the 86 files common to both, 70 differ; this checkout also has 8 files the monorepo copy lacks entirely (macros.lisp, runtime-bridge-provider.lisp, runtime-console.lisp, runtime-generator.lisp, runtime-promise.lisp, runtime-regex-combinators.lisp, runtime-temporal-datetime.lisp, vm-integration.lisp), and the monorepo copy has three this checkout lacks: runtime-async.lisp (an older combined Console/Promise/Generator file, itself "a simplified synchronous model" per its own header comment; this checkout split and materially upgraded all three, replacing it — Generator, for one, now runs on a real second thread rather than a synchronous approximation), and parser-pattern.lisp/ parser-pattern-lower.lisp (a whole parallel destructuring-pattern parser+lowering implementation, confirmed fully dead in this checkout and removed — real destructuring has always gone through the separate, actively-used implementation in parser-stmt-binding.lisp). The :depends-on lists also disagree — this repository declares :cl-cc-vm, the monorepo copy does not.

This is a known design problem and is not resolved here. For anything in this repository, treat cl-cc-javascript.asd at the root of this checkout as authoritative. Which copy ASDF actually loads depends on the source registry, so a build that resolves to the monorepo copy will silently lack the VM integration described above and fall back to runtime-async.lisp's weaker Generator/Promise model.

Testing

t/ holds 32 files and about 7,700 lines, split by concern: lexer tests, parser tests (declarations, statements), end-to-end execution tests (core, AST-shape, advanced, modern), and runtime tests per builtin family, each file kept under the org's 500-line cap (five files that had grown past it were split along their own existing section boundaries; the largest is now 477 lines). End-to-end tests compile to the VM and compare captured console.log output, so they exercise the real path rather than the parser alone. See Development.