Files
el/el-compiler/src/compiler.el
T
Will Anderson 62f4d56a62 runtime: HEAD method dispatches as GET, body suppressed in response
Per RFC 9110 §9.3.2, HEAD must mirror GET headers + Content-Length
without sending a body. Existing http_worker / http_worker_v2 dropped
HEAD straight to the El handler, which had no idea what to do and
returned the catch-all 404 envelope. Link checkers and SEO bots saw
the 404 and reported the site as broken.

Fix layer is in the runtime, not the El handler:

  * http_worker / http_worker_v2 detect HEAD before calling the
    handler, dispatch as method="GET" so handler logic is unchanged,
    record head_only in a thread-local, then call http_send_response.
  * http_send_response reads the thread-local and skips the
    final http_send_all of the body. Status line + headers +
    Content-Length still go out in full.

Verified locally on engram /health: HEAD returns
  HTTP/1.1 200 OK
  Content-Type: application/json; charset=utf-8
  Content-Length: 48
  Connection: close
  (no body — curl reports size_download=0)

compiler.el: rename `target` → `tgt` in main(); the lexer reserves
`target` as a keyword, and the let-binding position requires Ident.
The naming convention was already followed elsewhere in the file
(compile_dispatch's parameter is tgt for exactly this reason); main
was an outlier that the existing Rust-genesis-built elc happened to
parse but bootstrap.py refused, blocking self-host.
2026-05-02 01:15:11 -05:00

200 lines
7.2 KiB
EmacsLisp

// compiler.el el self-hosting compiler pipeline
//
// Wires lexer -> parser -> codegen into a single compile() function.
// This is the bootstrap entry point: compiled once by the Rust el-compiler,
// then self-hosted from that point forward.
//
// Two backends:
// - C (default) compile() -> emits C source linked against el_runtime.c
// - JS (--target=js) compile_js() -> emits JS source linked against el_runtime.js
//
// Compile the C output with:
// cc -o <prog> <prog>.c el_runtime.c
//
// Run the JS output with:
// node <prog>.js (after copying el_runtime.js next to it)
import "lexer.el"
import "parser.el"
import "codegen.el"
import "codegen-js.el"
// compile full pipeline (C target): source string -> C source string
fn compile(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory
// before codegen allocates its own working data on large source files.
el_release(tokens)
codegen(stmts, source)
}
// compile_js full pipeline (JS target): source string -> JS source string
fn compile_js(source: String) -> String {
let tokens: [Map<String, Any>] = lex(source)
let stmts: [Map<String, Any>] = parse(tokens)
// Token list is no longer needed after parsing release it to free memory.
el_release(tokens)
codegen_js(stmts, source)
}
// compile_dispatch pick a backend based on the requested target.
// tgt = "c" | "js"
// (The parameter is named `tgt` because `target` is a reserved keyword
// in El's lexer it would be tokenised as `Target`, breaking the
// parser's identifier resolution.)
fn compile_dispatch(tgt: String, source: String) -> String {
if str_eq(tgt, "js") { return compile_js(source) }
compile(source)
}
// Detect a `--target=<lang>` flag in argv and return the target.
// Returns "c" if none specified or unrecognized.
fn detect_target(argv: [String]) -> String {
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if str_starts_with(a, "--target=") {
let v: String = str_slice(a, 9, str_len(a))
return v
}
let i = i + 1
}
return "c"
}
// Strip flags from argv, leaving only positional arguments.
fn strip_flags(argv: [String]) -> [String] {
let out: [String] = native_list_empty()
let n: Int = native_list_len(argv)
let i = 0
while i < n {
let a: String = native_list_get(argv, i)
if !str_starts_with(a, "--") {
let out = native_list_append(out, a)
}
let i = i + 1
}
return out
}
// Import resolution
//
// elc supports two forms of import:
// import "path/to/file.el" quoted relative path
// from module import { Name } bare module name resolves to module.el
// in the entry source's directory
//
// Codegen treats Import statements as no-ops (declarations only), so to
// actually link bodies across files we textually concatenate every imported
// source ahead of the entry source before lex/parse. resolve_imports does a
// depth-first traversal with deduplication so any module that gets pulled in
// transitively is included exactly once.
fn dirname_of(path: String) -> String {
let n: Int = str_len(path)
let i: Int = n - 1
while i >= 0 {
let c: String = str_slice(path, i, i + 1)
if str_eq(c, "/") {
return str_slice(path, 0, i)
}
let i = i - 1
}
return "."
}
// Extract the resolved file path from a single trimmed source line. Returns
// "" if the line is not an import.
fn parse_import_line(trimmed: String, dir: String) -> String {
if str_starts_with(trimmed, "import \"") {
let after: String = str_slice(trimmed, 8, str_len(trimmed))
let q: Int = str_index_of(after, "\"")
if q > 0 {
let mod: String = str_slice(after, 0, q)
return dir + "/" + mod
}
}
if str_starts_with(trimmed, "from ") {
let after: String = str_slice(trimmed, 5, str_len(trimmed))
// module name is the first whitespace-delimited token
let sp: Int = str_index_of(after, " ")
if sp > 0 {
let mod_raw: String = str_slice(after, 0, sp)
let mod: String = str_trim(mod_raw)
if !str_eq(mod, "") {
return dir + "/" + mod + ".el"
}
}
}
return ""
}
// Recursively resolve imports starting from src_path. Returns the combined
// source text with every imported module's body inlined ahead of the entry
// source, deduplicated by absolute path. Uses state_set to track which paths
// have already been pulled in for this run.
fn resolve_imports(src_path: String) -> String {
let seen_key: String = "__elc_imp__:" + src_path
let already: String = state_get(seen_key)
if !str_eq(already, "") { return "" }
state_set(seen_key, "1")
let source: String = fs_read(src_path)
let dir: String = dirname_of(src_path)
let lines: [String] = str_split(source, "\n")
let n: Int = native_list_len(lines)
// First pass: pull in every import body ahead of this file's body.
let prefix: String = ""
let body: String = ""
let i: Int = 0
while i < n {
let line: String = native_list_get(lines, i)
let trimmed: String = str_trim(line)
let imp_path: String = parse_import_line(trimmed, dir)
if !str_eq(imp_path, "") {
let prefix = prefix + resolve_imports(imp_path)
} else {
let body = body + line + "\n"
}
let i = i + 1
}
return prefix + body
}
// main CLI entry point.
//
// elc <source.el> # emit C to stdout
// elc --target=js <source.el> # emit JS to stdout
// elc --target=c <source.el> <out.c> # write C to file
// elc --target=js <source.el> <out.js> # write JS to file
fn main() -> Void {
let argv: [String] = args()
// Use `tgt` not `target`: `target` is a reserved keyword in the lexer
// (Section 1.5 of the language spec). detect_target itself is fine
// because the function-name position has no token-class restriction.
let tgt: String = detect_target(argv)
let positional: [String] = strip_flags(argv)
let argc: Int = native_list_len(positional)
if argc < 1 {
println("el-compiler: usage: elc [--target=c|js] <source.el> [<output>]")
exit(1)
}
let src_path: String = native_list_get(positional, 0)
let source: String = resolve_imports(src_path)
let out: String = compile_dispatch(tgt, source)
if argc >= 2 {
let out_path: String = native_list_get(positional, 1)
let ok: Bool = fs_write(out_path, out)
if ok {
exit(0)
} else {
println("el-compiler: failed to write output")
exit(1)
}
}
// No output path: codegen streamed to stdout already; out is "".
}