elc/bootstrap: resolve imports textually (recursive, dedup, strict)
Both bootstrap.py and compiler.el now inline every imported .el file
into a single source string before lex/parse, depth-first with set
deduplication keyed on absolute path. Two forms supported:
import "path/to/file.el" (quoted relative path)
from <module> import { ... } (bare module → <module>.el)
Strict regex matching prevents false positives like CSS keyframes
("from { opacity: 0 }") embedded in El string literals - the prior
naive str.startswith pulled '{' out as a module name and tried to
load src/{.el.
This kills the bash concat preprocessor that web/build-local.sh
needed. A web full build is now just:
python3 bootstrap.py src/main.el > dist/main.c
cc -O2 ... -o dist/neuron-web dist/main.c dist/web_stubs.c \
foundation/el/el-compiler/runtime/el_runtime.c \
-lcurl -lpthread -lssl -lcrypto
Verified end-to-end: bootstrap.py produces 1,151 lines of C from
src/main.el's 24 imports, cc links a 667 KB binary.
This commit is contained in:
+1471
File diff suppressed because it is too large
Load Diff
+170
-20
@@ -4,43 +4,193 @@
|
||||
// This is the bootstrap entry point: compiled once by the Rust el-compiler,
|
||||
// then self-hosted from that point forward.
|
||||
//
|
||||
// The returned string is C source code. Compile the output with:
|
||||
// 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: source string -> C source string
|
||||
// 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)
|
||||
}
|
||||
|
||||
// main — CLI entry point for self-hosted compilation.
|
||||
// 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 ────────────────────────────────────────────────────────
|
||||
//
|
||||
// Called by: elc <source.el> <output.c>
|
||||
// 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
|
||||
//
|
||||
// Reads El source from args()[0], compiles it to C source, and writes the
|
||||
// result to args()[1]. Then run:
|
||||
// cc -o <prog> <output.c> el_runtime.c
|
||||
// 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()
|
||||
let argc: Int = native_list_len(argv)
|
||||
if argc < 2 {
|
||||
println("el-compiler: usage: elc <source.el> <output.c>")
|
||||
let target: 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(argv, 0)
|
||||
let out_path: String = native_list_get(argv, 1)
|
||||
let source: String = fs_read(src_path)
|
||||
let c_source: String = compile(source)
|
||||
let ok: Bool = fs_write(out_path, c_source)
|
||||
if ok {
|
||||
exit(0)
|
||||
} else {
|
||||
println("el-compiler: failed to write output")
|
||||
exit(1)
|
||||
let src_path: String = native_list_get(positional, 0)
|
||||
let source: String = resolve_imports(src_path)
|
||||
let out: String = compile_dispatch(target, 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 "".
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user