Getting Started¶
cl-cc-javascript is consumed as a Nix flake input and loaded through ASDF.
Add the flake input¶
# flake.nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
cl-cc-javascript = {
url = "github:nerima-lisp/cl-cc-javascript";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
This input is not yet pinned to a tag
Consumers inside this org normally pin a release tag
(github:nerima-lisp/cl-cc-javascript/v0.1.0) rather than follow the default
branch, because an upstream push to main otherwise breaks downstream CI without
warning. cl-cc-javascript has no release tag yet, so there is nothing to pin to.
Pin one as soon as the first tag exists.
Add the system dependency¶
Loading the system is all that is required to register the JavaScript frontend: the last
two components (runtime-bridge-provider and vm-integration) self-register with
cl-cc/bootstrap, so cl-cc:compile-string accepts :language :javascript from then
on. There is no separate initialisation call.
Consuming the flake needs nothing else. Building or testing this repository from a
raw checkout does: cl-cc-javascript depends on systems that still live inside the
cl-cc checkout, plus several nerima-lisp siblings. nix develop wires all of them
up from the pinned flake inputs; outside it, see
Development.
Verify¶
This runs the compile gate, the test suite, the Nix formatting gate and the docs build. See Development.
Run a program¶
The rest of this page compiles and runs one JavaScript program, then looks at the two intermediate stages it passed through.
cl-cc:compile-string is the entry point. Loading cl-cc-javascript is what makes
:language :javascript available.
(asdf:load-system "cl-cc-javascript")
(let* ((result (cl-cc:compile-string "console.log(1 + 1);"
:target :vm :language :javascript))
(program (cl-cc/compile:compilation-result-program result)))
(cl-cc/vm:run-compiled program))
Seed the runtime globals¶
The program above only needed console. Anything that touches Math, JSON, Map,
Promise and the rest of the standard globals needs those seeded into the VM state
first, which is what cl-cc/pipeline:seed-js-runtime-globals does.
(let* ((result (cl-cc:compile-string "console.log(Math.max(2, 3, 1));"
:target :vm :language :javascript))
(program (cl-cc/compile:compilation-result-program result))
(out (make-string-output-stream))
(state (cl-cc/vm:make-vm-state :output-stream out)))
(cl-cc/pipeline:seed-js-runtime-globals state)
(cl-cc/vm:run-compiled program :output-stream out :state state)
(get-output-stream-string out))
;; => "3
;; "
This is exactly the shape of %js-run-capture, the helper the end-to-end test suite
uses, so it is the path to copy.
Look at the tokens¶
tokenize-js-source returns a flat list of token plists, ending with :T-EOF.
(cl-cc/javascript:tokenize-js-source "const x = 42;")
;; => ((:TYPE :T-CONST :VALUE "const")
;; (:TYPE :T-IDENT :VALUE "x")
;; (:TYPE :T-OP :VALUE "=")
;; (:TYPE :T-NUMBER :VALUE 42)
;; (:TYPE :T-SEMI :VALUE ";")
;; (:TYPE :T-EOF :VALUE NIL))
Keywords and punctuators carry their source text as :value; = is a generic :T-OP
rather than a token type of its own.
Numeric values are already decoded: 0xFF lexes to 255, 1_000_000 to 1000000,
3.14 to a double-float, and 42n to a :T-BIGINT token.
Look at the AST¶
parse-js-source returns a list of top-level cl-cc AST nodes — not a JavaScript-shaped
tree. const x = 42 becomes an ast-let:
(let ((node (first (cl-cc/javascript:parse-js-source "const x = 42;"))))
(list (cl-cc:ast-let-p node)
(length (cl-cc:ast-let-bindings node))
(cl-cc:ast-int-p (cdr (first (cl-cc:ast-let-bindings node))))))
;; => (T 1 T)
import/export syntax parses unconditionally regardless of which entry point you
call — parse-js-module is parse-js-source under a name that says what the source
is, not a different parser:
Parse with the prelude attached¶
js-program-forms is parse-js-source with the standard-globals prelude prepended. It
returns forms ready to hand to the compiler backend, and is what the pipeline calls.
(length (cl-cc/javascript:js-program-forms "console.log('hi');"))
;; => one form per prelude global, plus the parsed program
Complete example¶
(asdf:load-system "cl-cc-javascript")
(defun run-js (source)
"Compile and run JavaScript SOURCE, returning everything it printed."
(let* ((result (cl-cc:compile-string source :target :vm :language :javascript))
(program (cl-cc/compile:compilation-result-program result))
(out (make-string-output-stream))
(state (cl-cc/vm:make-vm-state :output-stream out)))
(cl-cc/pipeline:seed-js-runtime-globals state)
(cl-cc/vm:run-compiled program :output-stream out :state state)
(string-right-trim '(#\Newline) (get-output-stream-string out))))
(run-js "
class Greeter {
constructor(name) { this.name = name; }
greet() { return `hello, ${this.name}`; }
}
console.log(new Greeter('world').greet());
")
;; => "hello, world"
Next¶
- Core Concepts for the vocabulary used above.
- API Reference for every exported symbol.