| name | Build System (LabBuildCompiler) |
| description | Comprehensive deep-dive into the Chemical build pipeline — how chemical.mod is converted to build.lab, how build scripts are JIT-compiled via TinyCC, how jobs are created and executed, and how ASTProcessor orchestrates the parallel compilation passes. |
Build System (LabBuildCompiler)
The Lab build system is the entry point and orchestration layer of the Chemical compiler. It reads build.lab or chemical.mod files, creates compilation jobs, manages dependencies, coordinates compiler plugins, and drives the entire multi-pass compilation pipeline.
Full Pipeline Overview
User Input
│
├── chemical.mod → [ModToLabConverter] → build.lab (C code string)
│
└── build.lab → [2c backend] → C code → [TinyCC JIT] → executable build script
│
▼
LabBuildCompiler::execute_job()
│
▼
┌─────────────────────────────────┐
│ Job Execution │
│ │
│ 1. Parse all source files │
│ (lexer → parser → AST) │
│ Parallel per file │
│ │
│ 2. Symbol Resolution │
│ (6 parallel passes) │
│ See ASTProcessor section │
│ │
│ 3. Type Verification │
│ Parallel per file │
│ │
│ 4. C Translation (2c) │
│ Forward decl → Type aliases │
│ → Declare → Implement │
│ │
│ 5. Backend Codegen │
│ LLVM.cpp or TinyCC │
│ │
│ 6. Link │
│ Produce executable/library │
└─────────────────────────────────┘
Phase 1: Input Loading
chemical.mod → build.lab (ModToLabConverter)
The ModToLabConverter (in compiler/lab/mod_conv/ModToLabConverter.cpp) translates a concise .mod file into a full .lab build script.
Input example (chemical.mod):
application my_app
source "src"
import "../lib_mod"
import "github.com/owner/repo" version "1.0.0"
link "mylib"
link c "helper.c"
Output (generated build.lab):
import lab;
import std;
import "../lib_mod/build.lab" as __mod_0_stmt;
public func build(ctx : *mut BuildContext, __chx_job : *mut LabJob) : *mut Module {
var __curr_lab_path = lab::get_my_path();
const __chx_already_exists = ctx.get_cached(__chx_job, &__curr_lab_path);
if(__chx_already_exists != null) { return __chx_already_exists; }
const deps : []ModuleDependency = [
ModuleDependency { module: __mod_0_stmt.build(ctx, __chx_job), info: null },
];
const mod = ctx.new_package(ModuleType.Directory, PackageKind.Application,
"", "my_app",
std::span<ModuleDependency>(deps, 1));
ctx.set_cached(__chx_job, &__curr_lab_path, mod);
// Remote imports
ctx.fetch_mod_dependency(__chx_job, mod, ImportRepo {
from: "github.com/owner/repo",
version: "1.0.0",
});
// Source paths
mod.add_source("src");
// Link statements
mod.add_link_path("./my_libs");
mod.add_link("mylib");
mod.add_link_c("helper.c");
return mod;
}
Key conversion logic in convertToBuildLab():
- Imports for dependencies: Each
import statement in .mod becomes a build.lab import with a unique identifier like __mod_N_stmt
- Dependency list: Each import generates a
ModuleDependency entry calling the imported module's build() function
- Module creation:
ctx.new_package() with the correct ModuleType and PackageKind
- Remote imports:
ctx.fetch_mod_dependency() with ImportRepo struct
- Source paths:
mod.add_source(path) for each source directory
- Link statements:
mod.add_link_path(), mod.add_link(), mod.add_link_c() for each link directive
- Conditional imports:
if(windows) generates an if(__chx_job.getTarget().windows) block
- Symbol info: For
import Foo from "..." style, generates DependencySymbolInfo with alias and symbol list
build.lab → JIT-compiled executable
The build script itself is Chemical code that must be compiled and executed:
- Lex, Parse: The build.lab is lexed and parsed like any Chemical file
- C Translation (2c): The parsed AST is translated to C code via the 2c backend
- TinyCC JIT: The generated C is compiled in-memory using TinyCC
- Symbols exposed: LabBuildCompiler exposes CBI functions to the build script:
ctx.new_package() — create a module
ctx.set_cached() — cache the module
mod.add_source() — add source files
mod.add_link() — add link libraries
ctx.fetch_mod_dependency() — handle remote imports
- etc.
The compiled build script is executed by calling its build() function, which returns a Module* containing all the module's metadata.
Phase 2: Job Creation
After the build script executes, LabBuildCompiler has a tree of LabModule objects representing the dependency graph. Each LabModule has:
struct LabModule {
chem::string name;
chem::string scope_name;
LabModuleType type;
PackageKind package_kind;
std::vector<ModuleDependency> dependencies;
std::vector<chem::string> paths;
ModuleScope module_scope;
std::vector<ASTFileMetaData> direct_files;
};
LabBuildCompiler::execute_job() iterates jobs and processes each module's files through the full pipeline.
Phase 3: ASTProcessor — The Compilation Orchestrator
ASTProcessor (in compiler/ASTProcessor.h/.cpp) is the core orchestrator that drives the compilation of each module. It owns the resolver, binder, allocators, and manages all passes.
Key Components in ASTProcessor
| Member | Type | Purpose |
|---|
resolver | SymbolResolver* | Symbol resolution |
loc_man | LocationManager& | Source location management |
controller | AnnotationController& | Annotation handling |
binder | CompilerBinder& | CBI function binding |
path_handler | ImportPathHandler& | Import resolution |
container | InstantiationsContainer& | Generic instantiation tracking |
file_allocator | ASTAllocator | Per-file arena allocator |
mod_allocator | ASTAllocator | Per-module arena allocator |
job_allocator | ASTAllocator | Per-job arena allocator |
mod_storage | ModuleStorage& | Module storage and lookup |
cache | std::unordered_map<unsigned, ASTFileResult*> | File result cache |
import_mutex | std::mutex | Thread safety for imports |
print_mutex | std::mutex | Thread safety for printing |
The 6 Symbol Resolution Passes
ASTProcessor::sym_res_module() runs 6 passes in sequence:
Pass 1: Top-Level Declaration (Serial)
for(auto& file_ptr : module->direct_files) {
file.private_symbol_range = sym_res_tld_declare_file(
file.unit.scope.body, file.file_id, file.abs_path);
}
- What: Declares all top-level symbols (functions, structs, variants, namespaces, impls)
- Why serial: Declarations must happen in order to avoid race conditions on the shared symbol table
- Output:
SymbolRange indicating start/end indices of file-private symbols
- Allocator cleared:
file_allocator.clear() after pass
Pass 2: Link Signatures (Parallel per file)
for(auto& file_ptr : module->direct_files) {
futures.emplace_back(pool.push([this, &file](int id){
auto res = link_sig_file_task(resolver, &file);
return has_errors;
}));
}
- What: Resolves all type signatures (function parameter types, return types, struct member types, variant member types, type aliases) — does NOT enter function bodies
- Why parallel: Each file has its own per-file SymbolTable and diagnoser
- Key detail: Uses
sym_res_signature() which creates a per-file TopLevelLinkSignature with its own SymbolTable for file-private symbols (generic type params, using-imports, aliases)
- Output:
SymResSignatureResult with inline instantiations collected
- Allocator cleared:
file_allocator.clear() after pass
Pass 3: Generic Instantiation (Parallel per file)
for(auto& file_ptr : module->direct_files) {
futures.emplace_back(pool.push([this, &file](int id){
auto res = gen_inst_file_task(resolver, &file);
return res.has_errors;
}));
}
- What: Finalizes inline generic instantiations discovered during link signatures
- Key function:
sym_res_generic_instantiation() in GenericInstantiationPass.cpp
- Calls
GenericTypeDecl::finalize_signature() for each inline instantiation
- Calls
GenericInstantiator::FinalizeSignature() to resolve generic type parameters
- Visits the scope to trigger nested generic instantiations
- Why separate: Generic types discovered during link signatures need to be instantiated BEFORE body resolution so that bodies can reference concrete types
- Registration only: This pass requires only
InstantiationRequirement::Registration — not full body finalization
- Allocator cleared:
file_allocator.clear() after pass
Pass 4: After Link Signature (Serial)
for(auto& file_ptr : module->direct_files) {
sym_res_after_link_sig_file(file.unit.scope.body, file.file_id,
file.abs_path, file.private_symbol_range);
}
- What: Calls
sym_res_after_signature() — handles tasks that must happen after all signatures are linked but before bodies
- Why serial: May modify shared resolver state
- Allocator cleared:
file_allocator.clear() after pass
Pass 5: Link Body — Generic Decls (Parallel per file)
for(auto& file_ptr : module->direct_files) {
futures.emplace_back(pool.push([this, &file](int id){
auto res = link_body_generic_decls_task(resolver, &file);
return res.has_errors;
}));
}
- What: Resolves the master bodies of generic declarations
- Why separate: Generic master bodies need to be resolved before instantiation-specific bodies
- Function:
sym_res_link_body_generic_decls_pass()
Pass 6: Link Body — Full (Parallel per file)
for(auto& file_ptr : module->direct_files) {
futures.emplace_back(pool.push([this, &file](int id){
auto res = link_body_task(resolver, &file);
return res.has_errors;
}));
}
- What: The main body resolution pass. Resolves ALL remaining symbols in function bodies:
- Variable references and identifiers
- Function calls
- Expressions and operators
- Type references in expressions
- Access chains (
a.b.c())
- Move semantics tracking
- Operator overload resolution
- Pattern matching
- Lambdas and closures
- Comptime blocks
- Function:
sym_res_link_body_pass() which creates a per-file SymResLinkBody with its own SymbolTable, GenericInstantiatorAPI, and ASTDiagnoser
- Allocator cleared:
file_allocator.clear() after pass
The Sequential Path
ASTProcessor::sym_res_module_seq() is an alternative path that processes files sequentially (one file at a time, all passes together). This is used for the build.lab itself (small, not worth parallel overhead). It calls declare_and_link_file() which runs all passes in order for a single file.
Type Verification (Parallel per file)
After symbol resolution, ASTProcessor::type_verify_module_parallel() runs type verification:
for(auto& fileData : module->direct_files) {
futures.emplace_back(pool.push([this, &fileData](int id){
return type_verify_file_task(this, fileData.result);
}));
}
Each file gets its own ASTDiagnoser. Results are merged after all files complete.
C Translation
After type verification, ASTProcessor::declare_module() and ASTProcessor::implement_module() handle the 2c backend translation:
for(auto& file_ptr : module->direct_files) {
c_visitor.fwd_declare(unit.scope.body.nodes);
}
for(auto& file_ptr : module->direct_files) {
c_visitor.declare_type_aliases(unit.scope.body.nodes);
}
for(auto& file_ptr : module->direct_files) {
c_visitor.declare_before_translation(unit.scope.body.nodes);
}
for(auto& file_ptr : module->direct_files) {
c_visitor.translate_after_declaration(unit.scope.body.nodes);
c_visitor.file_level_reset();
}
Generic instantiations are translated between the module files:
c_visitor.fwd_declare(container.get_current_module_instantiations());
c_visitor.declare_before_translation(container.get_current_module_instantiations());
c_visitor.translate_after_declaration(container.get_current_module_instantiations());
container.clear_current_module_instantiations();
File Import and Caching
ASTFileResult Cache
The cache map in ASTProcessor stores parsed file results:
std::unordered_map<unsigned, ASTFileResult*> cache;
When a file is first imported, it's:
- Lexed and parsed into an
ASTFileResult
- Cached by file ID for reuse
- Import statements in the file trigger recursive imports
- Import resolution is handled by
figure_out_direct_imports()
Recursive Import Pattern
import_chemical_file_recursive() handles recursive imports:
- Lex and parse the current file
- Print diagnostics
- Call
figure_out_direct_imports() to find local file imports
- Recursively import those files via thread pool
- Uses
ConcurrentParsingState to track completion (atomic task counter)
Debug Feature: File Shuffling
In DEBUG builds, files within a directory are shuffled before compilation:
#ifdef DEBUG
shuffle_files(filePaths);
#endif
This randomly reorders files to expose compilation order bugs. A seed is printed so failures can be reproduced:
File order seed: 12345678 (set FILE_ORDER_SEED to reproduce)
Job Types in LabBuildCompiler
Compilation Job
Standard compilation — produces executable or library:
- Parse sources
- Symbol resolve (via ASTProcessor)
- Type verify
- C translation (2c)
- Backend codegen (LLVM or TinyCC)
- Link
Interpretation Job
When --arg-interpret is passed:
int LabBuildCompiler::do_interpretation_job(LabJob* job) {
}
CBI Plugin Job
Compiles a compiler plugin:
int LabBuildCompiler::link_cbi_job(LabJobCBI* cbiJob, std::vector<LabModule*>& dependencies) {
}
Thread Safety Architecture
What's Serial
- Top-level declaration (Pass 1)
- After-link-signature (Pass 4)
- Module scope start/end
- Main function no_mangle marking
What's Parallel
- File importing (lex + parse)
- Link signatures (Pass 2)
- Generic instantiation (Pass 3)
- Generic decl body linking (Pass 5)
- Full body linking (Pass 6)
- Type verification
Synchronization Points
print_mutex: Guards diagnostic output — each parallel task acquires it before printing
import_mutex: Guards the file cache during recursive imports
generic_inst_reg_mutex: Guards generic instantiation registration maps (recursive mutex)
implsIndex.index_mutex: A std::shared_mutex — shared_lock for reads, unique_lock for writes
Allocator Strategy
file_allocator.clear();
mod_allocator;
job_allocator;
ast_allocator;
Key Files Reference
| File | Lines | Role |
|---|
compiler/lab/LabBuildCompiler.cpp | ~5000+ | Full build system: job creation, execution, link, interpretation, plugin compilation |
compiler/lab/LabBuildCompiler.h | ~200 | Core build compiler class declaration |
compiler/ASTProcessor.cpp | ~1500 | The pass orchestration: sym_res_module, type_verify_module, declare/implement module |
compiler/ASTProcessor.h | ~200 | ASTProcessor class declaration |
compiler/lab/mod_conv/ModToLabConverter.cpp | ~300 | chemical.mod → build.lab conversion |
compiler/lab/mod_conv/ModToLabConverter.h | ~50 | ModToLabConverter header |
compiler/lab/LabJob.cpp | ~100 | Job implementation |
compiler/lab/LabBuildContext.cpp | ~300 | Module and resource management |
compiler/lab/LabBuildContext.h | ~200 | LabBuildContext header |
compiler/lab/LabModule.h | ~50 | Module structure |
compiler/lab/ModuleStorage.h | ~50 | Module storage and lookup |
The build.lab API (Lab Module)
The lab module (lang/libs/lab/src/lab.ch) exposes the build system API to build.lab scripts. Every build.lab file imports this module and calls its API to define modules, dependencies, and jobs.
Key Interfaces
BuildContext Interface
The primary interface for build scripts. Available as ctx in the build() function:
@compiler.interface
public interface BuildContext {
// Module creation
func new_package(&self, type : ModuleType, package_kind : PackageKind,
scope_name : &std::string_view, name : &std::string_view,
dependencies : std::span<ModuleDependency>) : *mut Module
// Caching
func get_cached(&self, job : *LabJob, path : &std::string_view) : *mut Module
func set_cached(&self, job : *LabJob, path : &std::string_view, module : *mut Module)
// Module management
func add_path(&self, module : *mut Module, path : &std::string_view)
func add_dependency(&self, job : *mut LabJob, module : *mut Module, info : *mut DependencySymbolInfo)
func add_module(&self, job : *mut LabJob, module : *mut Module)
func put_job_before(&self, newJob : *mut LabJob, existingJob : *mut LabJob)
// Linking
func link_system_lib(&self, job : *mut LabJob, name : &std::string_view, module : *mut Module = null)
func add_lib_search_path(&self, job : *mut LabJob, path : &std::string_view, module : *mut Module = null)
func add_object(&self, job : *LabJob, path : &std::string_view)
// Job creation (returns new LabJob*)
func build_exe(&self, name : &std::string_view) : *mut LabJob
func run_jit_exe(&self, name : &std::string_view) : *mut LabJob
func build_dynamic_lib(&self, name : &std::string_view) : *mut LabJob
func build_interpretation(&self, name : &std::string_view) : *mut LabJob
func build_cbi(&self, name : &std::string_view) : *mut LabJobCBI
func translate_to_c(&self, name : &std::string_view, output_path : &std::string_view) : *mut LabJob
func translate_to_chemical(&self, module : *mut Module, output_path : &std::string_view) : *mut LabJob
// Remote imports
func fetch_mod_dependency(&self, job : *mut LabJob, mod : *mut Module,
dep : &ImportRepo, strategy : ConflictResolutionStrategy = ...) : bool
func fetch_job_dependency(&self, job : *mut LabJob,
dep : &ImportRepo, strategy : ConflictResolutionStrategy = ...) : bool
// CLI argument handling
func has_arg(&self, name : &std::string_view) : bool
func get_arg(&self, name : &std::string_view) : std::string_view
func define(&self, job : *LabJob, name : &std::string_view) : bool
func undefine(&self, job : *LabJob, name : &std::string_view) : bool
// Testing
func set_environment_testing(&self, job : *mut LabJob, value : bool)
// Compiler interfaces (for CBI plugins)
func add_compiler_interface(&self, module : *mut Module, interface : &std::string_view) : bool
func contains_cbi(&self, key : &std::string_view) : bool
func index_cbi_fn(&self, job : *mut LabJobCBI, key : &std::string_view,
fn_name : &std::string_view, fn_type : CBIFunctionType) : bool
// Tool invocation
func build_path(&self) : std::string_view
func invoke_ar(&self, string_arr : std::span<std::string_view>) : int
func invoke_ranlib(&self, string_arr : std::span<std::string_view>) : int
}
LabJob Interface
Represents a compilation job:
public interface LabJob {
func getType(&self) : LabJobType
func getName(&self) : std::string_view
func getAbsPath(&self) : std::string_view
func getBuildDir(&self) : std::string_view
func getStatus(&self) : LabJobStatus
func getTargetTriple(&self) : std::string_view
func getMode(&self) : OutputMode
func getTarget(&self) : &TargetData
func setAbsPath(&self, path : std::string_view)
}
Module Interface
Represents a compiled module:
public interface Module {
func getType(&self) : ModuleType
func getScopeName(&self) : std::string_view
func getName(&self) : std::string_view
func getBitcodePath(&self) : std::string_view
func setBitcodePath(&self, path : &std::string_view)
func getObjectPath(&self) : std::string_view
func setObjectPath(&self, path : &std::string_view)
// ... etc
}
Key Enums
public enum ModuleType { File, CFile, CPPFile, ObjFile, Directory }
public enum PackageKind { Library, Application }
public enum LabJobType {
Executable, JITExecutable, Library, ToCTranslation,
ToChemicalTranslation, ProcessingOnly, CBI, Interpretation
}
public enum LabJobStatus { Pending, Launched, Success, Failure }
public enum OutputMode : int {
Debug, DebugQuick, DebugComplete,
ReleaseFast, ReleaseSmall, ReleaseSafe
}
Common Patterns in build.lab
Pattern 1: Standard Executable
import lab
func build(ctx : *mut AppBuildContext) : *mut LabJob {
const exe_job = ctx.build_exe("my_app")
const deps = [/* dependency modules */]
const mod = ctx.directory_app_module("", "main", "src", deps)
ctx.add_module(exe_job, mod)
return exe_job
}
Pattern 2: Remote Dependency
ctx.fetch_mod_dependency(job, mod, ImportRepo{
from : "github.com/owner/repo",
version : "v1.0",
location : intrinsics::get_raw_location()
})
Pattern 3: CBI Plugin
const cbi_name = std::string_view("my_plugin")
const cbi_job = ctx.build_cbi(&cbi_name)
ctx.add_module(cbi_job, my_plugin_mod)
Pattern 4: Conditional Build
if(ctx.has_arg("interpret")) {
const interp_job = ctx.build_interpretation("my-interp")
// ... add modules
return interp_job
}
Pattern 5: Build Path Helpers (in lab namespace)
// Path to the current build.lab's directory
lab::curr_dir()
// Absolute path relative to current build.lab
lab::rel_path_to("src")
// Build directory paths
ctx.build_job_dir_path("job_name")
ctx.build_mod_file_path("job", "scope", "mod", "file")
ctx.build_llvm_ir_path("job", "scope", "mod")
// Common helpers
const mod = ctx.directory_app_module("", "main", lab::rel_path_to("src"), deps)
How Tests Are Wired (lang/tests/build.lab Example)
See the Testing Guide for how lang/tests/build.lab wires:
- Interpretation tests (
--arg-interpret)
- Compiled tests (default)
- Library tests (
--arg-test-plugins)
- Individual library tests (
--arg-test-html, --arg-test-css, etc.)
The test wiring uses ctx.build_interpretation(), ctx.build_exe(), set_environment_testing(), has_arg(), and define() to configure the build environment.
CLI Arguments — How They Connect to the Build Pipeline
Entry Point: compiler_main()
File: core/main/CompilerMain.cpp
The CLI entry point is the single compiler_main(int argc, char* argv[]) function declared in core/main/CompilerMain.h. It parses all command-line options and routes them to the appropriate subsystem.
Option Parsing Infrastructure
File: utils/CmdUtils.h, utils/CmdUtils.cpp
struct CmdOption {
std::string_view name;
std::string_view short_name;
CmdOptionType type;
};
class CmdOptions {
std::vector<CmdOption> options;
std::vector<std::string_view> arguments;
void register_options(CmdOption* data, size_t count);
void parse_cmd_options(int argc, char* argv[], int start_index);
bool has_value(const std::string_view& name, const std::string_view& alt = "");
std::optional<std::string_view> option_new(const std::string_view& name, const std::string_view& alt = "");
CmdOptionValue& cmd_opt(const std::string_view& name);
};
Full Option Reference
Options are registered in CompilerMain.cpp as a CmdOption[] array:
| CLI Flag | Alias | Type | Effect |
|---|
--include | — | MultiValued | Add include path or file (.ch = source, else = header) |
--build-dir | — | SingleValue | Set build output directory |
--library | -l | MultiValued | Link a system library |
configure | — | SubCommand | Run OS configuration (sets CHEMICAL_HOME, PATH) |
--cc | — | SubCommand | Forward remaining args to Clang |
--run | — | SubCommand | Run a build.lab or chemical.mod file (chemical run file.mod) |
--linker | — | SubCommand | Forward to system linker |
--ar | — | SubCommand | Forward to LLVM ar |
--dlltool | — | SubCommand | Forward to LLVM dlltool |
--ranlib | — | SubCommand | Forward to LLVM ranlib |
--lib | — | SubCommand | Forward to LLVM lib |
--tcc-jit | — | SubCommand | Forward to TinyCC JIT runner |
--mode | -m | SingleValue | Output mode: debug, debug_quick, debug_complete, release, release_fast, release_small |
--plugin-mode | -pm | SingleValue | Plugin compilation mode (same values as --mode) |
--version | — | NoValue | Print version and exit |
--help | — | NoValue | Print help message |
--verbose | -v | NoValue | Verbose output |
--verbose-link | -vl | NoValue | Verbose linker output |
--output | -o | SingleValue | Output file path |
--out-ll <path> | — | SingleValue | Emit LLVM IR to path |
--out-bc <path> | — | SingleValue | Emit LLVM bitcode to path |
--out-obj <path> | — | SingleValue | Emit object file to path |
--out-asm <path> | — | SingleValue | Emit assembly to path |
--out-bin <path> | — | SingleValue | Emit binary executable to path |
--out-ll-all | — | NoValue | Emit LLVM IR for ALL modules |
--out-asm-all | — | NoValue | Emit assembly for ALL modules |
--emit-c | --emit-c | NoValue | Keep generated C files |
--minify-c | --minify-c | NoValue | Minify generated C output |
--incremental | --incremental | NoValue | Per-file C translation (not merged) |
--keepc | --keepc | NoValue | Keep C files after compilation |
--ignore-extension | — | NoValue | Output file extension is NOT used to determine output type |
--jobs | -j | SingleValue | Thread count for parallel compilation |
--job-type | -jt | SingleValue | Force job type: exe, jit-exe, lib, 2c, 2ch, inter, proc |
--target | -t | SingleValue | Target triple (default: host triple) |
--lto | — | NoValue | Enable Link Time Optimization |
--assertions | — | NoValue | Enable IR assertions for codegen validation |
--debug-ir | — | NoValue | Don't crash on potentially bad IR |
--benchmark | -bm | NoValue | Benchmark lexing/parsing/compilation |
--benchmark-files | -bm-files | NoValue | Per-file benchmark breakdown |
--benchmark-modules | -bm-modules | NoValue | Per-module benchmark breakdown |
--test | --test | NoValue | Set is_testing_env flag |
--no-cache | — | NoValue | Disable module caching (default in CLI mode) |
--no-cbi | — | NoValue | Ignore CBI annotations in translation |
--frecompile-plugins | --frecompile-plugins | NoValue | Force recompilation of all CBI plugins |
--sanitize | -fsanitize | SingleValue | Enable sanitizers: address, memory, thread, undefined, leak, hwaddress, dataflow (comma-separated) |
--tsan | — | NoValue | Enable ThreadSanitizer |
--no-pie | --no-pie | NoValue | Disable position-independent executable |
--jit | --jit | NoValue | JIT compilation mode (TinyCC) |
--use-tcc | --use-tcc | NoValue | Use TinyCC backend (C translation + libtcc) |
--use-c | --use-c | NoValue | Translate to C + compile with embedded Clang |
--use-lld | --use-lld | NoValue | Use LLD for linking |
--use-bc | --use-bitcode | NoValue | Use bitcode format for module objects |
--resources | --res | SingleValue | Override resources directory path |
--mod | — | MultiValued | Specify module files |
--cbi-m | --cbi-m | MultiValued | CBI module names |
--download | --download | NoValue | Download-only (don't build) |
--check | --check | NoValue | Check-only (don't build) |
--run-negative-tests | — | NoValue | Run negative lifetime tests (DEBUG-only) |
--ignore-errors | --ignore-errors | NoValue | Continue despite errors |
-g | — | NoValue | Emit debug info |
-c | — | NoValue | Compile to object files only (no link) |
--fno-unwind-tables | — | NoValue | Disable unwind tables (cleaner IR) |
--fno-asynchronous-unwind-tables | — | NoValue | Disable async unwind tables |
--arg-<name> | -arg-<name> | String | Pass arbitrary args to build.lab scripts (e.g., --arg-interpret) |
--cpp-like | — | NoValue | C translation output similar to C++ |
--res <dir> | -res <dir> | SingleValue | Resources directory |
How CLI Args Flow Through the Pipeline
CLI (argc, argv)
│
▼
CmdOptions::parse_cmd_options()
│
├── SubCommand Detected?
│ ├── "configure" → configure_exe()
│ ├── "cc" → chemical_clang_main2()
│ ├── "ar"/"ranlib"/"lib"/"dlltool" → llvm_ar_main2()
│ ├── "run" → LabBuildCompiler::run_invocation()
│ ├── "tcc-jit" → LabBuildCompiler::tcc_run_invocation()
│ └── "linker" → forward to system linker
│
├── No Input?
│ └── print_usage(), exit 1
│
├── Input is .lab or .mod file?
│ ├── .mod → build via LabBuildCompiler::build_mod_file()
│ ├── .lab → build via LabBuildCompiler::build_lab_file()
│ └── .mod → .lab → convert via ModToLabConverter
│
└── Input is .ch file(s)?
└── Single-file compilation path:
1. Create LabModule, LabJob, LabBuildCompilerOptions
2. prepare_options() fills options from parsed CLI
3. Handle output file extension → set module type:
.o → obj output, .s → asm, .ll → llvm ir, .bc → bitcode
4. compiler.do_job_allocating(&job)
The prepare_options() Lambda
In CompilerMain.cpp, the prepare_options lambda maps CLI flags to LabBuildCompilerOptions:
auto prepare_options = [&](LabBuildCompilerOptions* opts) -> void {
opts->benchmark = options.has_value("benchmark", "bm");
opts->verbose = verbose;
opts->verbose_link = options.has_value("verbose-link", "vl");
opts->minify_c = options.has_value("minify-c");
opts->emit_c = options.has_value("emit-c", "emit-c");
opts->debug_info = options.has_value("", "g");
opts->use_c = options.has_value("use-c", "use-c");
opts->use_tcc = options.has_value("use-tcc", "use-tcc");
opts->use_lld = options.has_value("use-lld", "use-lld");
opts->is_testing_env = options.has_value("test");
opts->ignore_errors = options.has_value("ignore-errors", "ignore-errors");
opts->is_caching_enabled = !options.has_value("no-cache");
opts->force_recompile_plugins = options.has_value("frecompile-plugins");
opts->debug_ir = options.has_value("debug-ir");
opts->def_lto_on = options.has_value("lto");
opts->def_assertions_on = options.has_value("assertions");
opts->fno_unwind_tables = options.has_value("", "fno-unwind-tables");
opts->no_pie = options.has_value("no-pie", "no-pie");
opts->def_plugin_mode = parsed_plugin_mode;
opts->sanitizers = parse_sanitizers(options.option_new("sanitize", "fsanitize"));
};
LabBuildCompilerOptions Structure
File: compiler/lab/LabBuildCompilerOptions.h
struct LabBuildCompilerOptions {
chem::string chemical_exe_path;
chem::string target_triple;
chem::string build_dir;
chem::string resources_path;
bool is64Bit = false;
bool verbose = false;
bool verbose_link = false;
bool benchmark = false;
bool benchmark_files = false;
bool benchmark_modules = false;
bool minify_c = false;
bool emit_c = false;
bool translate_to_single_file = true;
bool debug_info = true;
bool use_c = false;
bool use_tcc = false;
bool use_lld = false;
bool debug_ir = false;
bool fno_unwind_tables = false;
bool fno_asynchronous_unwind_tables = false;
bool no_pie = false;
bool out_ll_all = false;
bool out_asm_all = false;
bool use_mod_obj_format = true;
bool is_testing_env = false;
bool ignore_errors = false;
bool is_caching_enabled = true;
bool force_recompile_plugins = false;
bool def_lto_on = false;
bool def_assertions_on = false;
bool prefer_tcc = false;
bool make_executable = false;
OutputMode out_mode = OutputMode::Debug;
OutputMode def_out_mode = OutputMode::Debug;
OutputMode def_plugin_mode = OutputMode::Debug;
int sanitizers = 0;
int thread_count = 0;
};
Build Args Passthrough to build.lab
When running a .lab file, CLI --arg-* flags are forwarded to LabBuildContext.build_args:
for(auto& opt : options.options) {
if(opt.first.starts_with("arg-")) {
context.build_args[opt.first.data() + 4] = opt.second;
}
}
The build.lab script then checks these via ctx.has_arg() and ctx.get_arg().
Output Extension Routing
The compiler determines what type of compilation to do based on the output file extension:
| Extension | Behavior |
|---|
.exe (or --out-bin) | Link into an executable |
.o | Compile to object file only (set as module.object_path) |
.ll | Compile to LLVM IR only (set as module.llvm_ir_path) |
.bc | Compile to LLVM bitcode only (set as module.bitcode_path) |
.s | Compile to assembly only (set as module.asm_path) |
.c | Translate to C (set job.type = ToCTranslation) |
.ch | Translate to Chemical (set job.type = ToChemicalTranslation) |
-c flag | Processing only (compile to object, no link) |
| None / default | a.exe (Win) or a (Unix) — compile and link |
Entry Point Decision Flow (Simplified)
compiler_main(argc, argv)
│
├── if "-cc1" → chemical_clang_main()
├── if configure → configure_exe()
├── if version → print version
├── if help → print_help()
├── if run → LabBuildCompiler::run_invocation()
├── if cc → chemical_clang_main2()
├── if ar/dlltool/ranlib/lib → llvm_ar_main2()
├── if tcc-jit → LabBuildCompiler::tcc_run_invocation()
│
└── else (normal compilation)
├── Parse options → LabBuildCompilerOptions
├── Input = .mod file?
│ ├── → output.c? → translate to C (job type = ToCTranslation)
│ └── → default → build_mod_file()
├── Input = .lab file?
│ └── → build_lab_file()
└── Input = .ch/.c/.o files?
└── → Single-file compilation via do_job_allocating()
Related Skills
- Symbol Resolution (
.agents/skills/symres/SKILL.md) — Detailed symres pipeline
- Generic Instantiation (
.agents/skills/generics/SKILL.md) — Generic monomorphization
- C Codegen (
.agents/skills/c_codegen/SKILL.md) — 2c translation backend
- Performance (
.agents/skills/performance/SKILL.md) — Optimization and parallelization patterns
- Compiler Bindings (
.agents/skills/compiler_bindings/SKILL.md) — CBI and TinyCC integration
- Testing Guide (
.agents/skills/testing/SKILL.md) — How tests are wired and executed