Imported from chemicallang/chemical (
.agents/skills/building/SKILL.md). Install upstream withnpx skills add chemicallang/chemical --skill building. Copyright stays with the author.
Building
Its likely user is using CLion, in this case in the root dir, you'll find cmake-build-debug where there are two
compilers present
TCCCompiler(the compiler that embeds Tiny CC)- This compiler cannot create llvm ir, It translates Chemical to C and then compiles that using Tiny CC
- This compiler is faster to build, faster at compilation but generates slower code.
Compiler(the compiler that embeds LLVM/Clang and Tiny CC)- This compiler can do anything TCCCompiler can do and more, It performs optimizations using LLVM. But you
- can also tell it to use Tiny CC with flag
--use-tccor ask it to translate to C and then compile that using - clang with flag
--use-c - This compiler takes time to build, slow at compilation but generates fast code
It's likely that you don't need to rebuild the compiler, because you are working on libraries (lang/libs/<library> or lang/compiled/<library>).
To properly understand building of the compilers, You should analyze the CMakeLists.txt in the root of the repo.
If you are working on any code that doesn't interact with LLVM/Clang then you should not compile Compiler target. you should focus on TCCCompiler which would build faster and would compile faster.
Configuration
# Configure with LLVM support (default)
./scripts/configure.sh
# Configure without LLVM (TCCCompiler only)
./scripts/configure.sh --no-llvm
# Configure with AddressSanitizer enabled
cmake -S . -B cmake-build-debug -DENABLE_ASAN=ON
AddressSanitizer (ASAN)
ASAN detects memory errors at runtime: heap-buffer-overflow, use-after-free, stack-use-after-scope, new-delete-type-mismatch, and more.
How it works
ASAN is a compile-time instrumentation of the compiler binary itself. When enabled, the compiler's C++ code is instrumented so that every memory access is checked. The compiler can then be used normally β ASAN reports errors while the compiler processes your Chemical project.
Setup
# One-time: configure with ASAN
cmake -S . -B cmake-build-debug -DENABLE_ASAN=ON
# Build the compiler
./scripts/build.sh --tcc
Usage
# Run the compiler on any project β ASAN is active automatically
cmake-build-debug/TCCCompiler lang/compiled/cdm/chemical.mod -o lang/compiled/cdm/bin/cdm --mode debug_quick --no-cache
# Run on tests
cmake-build-debug/TCCCompiler lang/tests/build.lab -arg-minimal -bm -v --assertions --mode debug_complete --no-cache
# Suppress specific error types via environment variable
ASAN_OPTIONS=new_delete_type_mismatch=0 cmake-build-debug/TCCCompiler ...
Error types ASAN catches
| Error | What it means |
|---|---|
heap-buffer-overflow |
Writing past the end of a heap allocation |
stack-use-after-scope |
Reading a local variable after its scope ended |
new-delete-type-mismatch |
delete called with wrong type size (missing virtual destructor) |
use-after-free |
Accessing memory after it was freed |
stack-buffer-overflow |
Writing past the end of a stack buffer |
Tips
- ASAN adds ~2x memory overhead and ~1.5x slowdown β acceptable for debugging
LeakSanitizer(part of ASAN) reports memory leaks at exit β these are often expected for arena allocators and long-lived compiler objects- Combine with
ASAN_OPTIONSenv var to suppress known/non-critical errors while focusing on the real bug - The ASAN binary is the same
cmake-build-debug/TCCCompilerβ no separate binary needed
Building Compiler
There are three CMake targets: TCCCompiler, Compiler, and ChemicalLsp.
Using build scripts (recommended)
./scripts/build.sh --tcc # Build TCCCompiler only
./scripts/build.sh --llvm # Build Compiler (LLVM/Clang backend)
./scripts/build.sh --lsp # Build ChemicalLsp
./scripts/build.sh --all # Build all targets
./scripts/build.sh --llvm -j 16 # Build with 16 parallel jobs
Using make directly
make -C cmake-build-debug Compiler -j$(nproc)
make -C cmake-build-debug TCCCompiler -j$(nproc)
make -C cmake-build-debug ChemicalLsp -j$(nproc)
Notes
- Prefer building one target at a time, not all.
- The Makefile is at
cmake-build-debug/Makefile(configured by CLion). cmakeis at/opt/clion/bin/cmake/linux/x64/bin/cmake(not in PATH).out/hostcontains LLVM/Clang Libraries β do not overwrite.- If user doesn't have LLVM, use
./scripts/configure.sh --no-llvm(sets-DBUILD_COMPILER=OFF).
Building Tests
β οΈ AI agents must NOT run
./scripts/test.sh --all. It runs every suite and is only for a human doing a fresh-clone/new-machine sanity check; it takes a very long time (tlsalone runs for minutes). Run the single suite relevant to your change instead and stop. If a human runs--all, note the slowtlssuite is skipped unless--include-tlsis passed.
Using test script (recommended)
# Build TCCCompiler, compile tests, run them
./scripts/test.sh --tcc
# Build Compiler, compile tests, run them
./scripts/test.sh --llvm
# Include library tests
./scripts/test.sh --tcc --plugins
# Process + environment library tests (no special system deps)
./scripts/test.sh --tcc --process
# Webview library tests (requires GTK3 + WebKit2GTK to link/run)
./scripts/test.sh --tcc --webview
# Universal component tests in a real WebView (#universal_test; needs a display)
./scripts/test.sh --tcc --universal
# Negative (compiler-failure verification) tests
./scripts/test.sh --tcc --negative
# Forward a target triple to the compiler (omitted unless specified)
./scripts/test.sh --tcc --process --target x86_64-linux-gnu
# Custom output path
./scripts/test.sh --tcc -o my_tests
# Build only (no run)
./scripts/test.sh --tcc --no-run
# Skip compiler rebuild, use existing binary
./scripts/test.sh --tcc --no-build
# Run tests under GDB batch mode to capture backtrace on crash:
./scripts/test.sh --tcc --bt # gdb -batch, print bt full
./scripts/test.sh --tcc --bt-full # gdb -batch with full bt, registers, disasm
π
-bt/-bt-full: These flags wrap the test executable ingdb -batchmode. On crash, they print a backtrace (and optionally registers, disassembly, locals). Implies-gautomatically. Works for both compiled and interpretation modes.
-btβgdb -batch -ex "run" -ex "bt full" --args <program>-bt-fullβgdb -batch -ex "run" -ex "thread apply all bt full" -ex "info registers" -ex "x/16i $pc" -ex "info locals" -ex "info args" --args <program>
β οΈ
--no-buildwarning: This flag skips rebuilding the C++ compiler binary. Any changes to.cpp/.hfiles will NOT be reflected β the previously built binary is used as-is. Only use--no-buildwhen iterating on.chtest files or Chemical library sources without any compiler C++ changes. To include C++ changes, omit--no-build(or run once without it to rebuild, then you can use--no-buildfor subsequent iterations).
Manual commands
# TCC backend
cmake-build-debug/TCCCompiler "lang/tests/build.lab" -o lang/tests/build/tests-tcc.exe --mode debug_quick --no-cache
# LLVM backend
cmake-build-debug/Compiler "lang/tests/build.lab" -o lang/tests/build/tests.exe --mode debug_complete --no-cache
Interpretation Tests
./scripts/test.sh --tcc --interpret # Build TCC + run interpretation tests
./scripts/test.sh --tcc --interpret --no-build # Skip rebuild
# Manual:
./chemical lang/tests/build.lab --arg-interpret --mode debug_complete --no-cache
The --arg-interpret flag causes build.lab to create a LabJobType::Interpretation job. The compiler calls do_interpretation_job() in compiler/lab/LabBuildCompiler.cpp, which:
- Parses, symres, and typechecks
interpret/+common/modules - Initializes module-level
VarInitStmtvariables on the global scope - Calls
main()via the AST interpreter directly β no object code generated
How Tests Work
Tests use a common framework at lang/tests/common/src/test.ch that works in both modes:
comptime if(intrinsics::is_interpretation()) {
intrinsics::expr_println(`${ANSI_COLOR_GREEN}Test ${total_tests + 1} [${name}] succeeded${ANSI_COLOR_RESET}`);
} else {
printf("%sTest %d [%s] succeeded %s\n", ANSI_COLOR_GREEN, total_tests + 1, name, ANSI_COLOR_RESET);
}
- Interpretation path:
intrinsics::expr_println(expr: %expressive_string)walks the expressive string's parts βStringValueliterals go directly tostd::cout,${}expressions are evaluated and printed viaRepresentationVisitorwithinterpret_representation = true(no quotes). - Compiled path: Standard
printfwith ANSI escape string constants.
Compiler flags explained
--mode debug_quickβ quickly compile the project with debug info--mode debug_completeβ full debug mode for LLVM backend--no-cacheβ do not rely on previously generated objects--emit-cβ write the Translated.c file to the build directory--arg-interpretβ run in interpretation mode (interpret AST directly, no codegen)--arg-test-pluginsβ build library tests executable-frecompile-pluginsβ recompile compiler plugins
Building Library Tests
We test most libraries in the tests above (lang/tests/build.lab) but some libraries like:
html_cbi,css_cbi,js_cbi,componentsuniversal_cbi
These are compiler plugins, tested via a separate executable:
# Using test script
./scripts/test.sh --tcc --plugins
# Manual
cmake-build-debug/TCCCompiler "lang/tests/build.lab" -o lang/tests/build/lib-tests-tcc.exe --mode debug_quick --no-cache --arg-test-plugins -frecompile-plugins
Helpful flags:
--plugin-mode debug_completeβ compile plugins in debug mode for full stack traces--arg-test-html,--arg-test-css, etc. β individual library tests
Building LSP
If you modify anything inside the server directory, note that it's part of the LSP target.
These modules are all LSP-related and require the LSP server running to verify:
html_ide,css_ide,js_ide,universal_ide,md_ide
# Build LSP
./scripts/build.sh --lsp
LSP target name is ChemicalLsp. Read CMakeLists.txt before building the LSP target.
Developing Comptime Tests
Test Structure
Interpret tests run via --arg-interpret and execute the AST interpreter directly. The entry point is:
// lang/tests/interpret/src/main.ch
public func main() {
run_common_tests(); // Tests 1-97 (core language features)
run_native_common_tests(); // Pointer arithmetic, casts, comptime pointers
print_test_stats();
}
| Module | Location | What it tests |
|---|---|---|
common_tests |
lang/tests/common/ |
Core features (arithmetic, loops, structs, variants, inc/dec) |
native_common_tests |
lang/tests/native_common/ |
Pointer operations, casts, comptime pointer arithmetic |
interpret_tests |
lang/tests/interpret/ |
Wrapper that imports both and runs main() |
Comptime Test Files (in lang/tests/src/comptime/)
| File | Tests |
|---|---|
basic.ch |
Basic comptime: sum, structs, strings, enums, constructors, get_child_fn, get_line_no |
features.ch |
Comptime features: bitwise ops, loops, casting, logical ops, for-in, struct mutation, destructors |
expressions.ch |
comptime { } block expressions: arithmetic in comptime blocks |
satisfies.ch |
intrinsics::satisfies<T, U>() type relationship tests |
is_value.ch |
intrinsics::is_same_type() and is operator type identity tests |
vector.ch |
intrinsics::vector<T>() vector operations |
Running Tests
# Quick iteration (interpret only, skip rebuild)
./scripts/test.sh --tcc --interpret --no-build
# Full compiled test run
./scripts/test.sh --tcc --no-build
# Both in sequence
./scripts/test.sh --tcc --interpret --no-build && ./scripts/test.sh --tcc --no-build
Adding a New Comptime Test
-
Create the test source in
lang/tests/src/comptime/orlang/tests/common/src/:// lang/tests/src/comptime/my_feature.ch comptime func my_feature(a : int, b : int) : int { return a * b + a; } func test_my_feature() { test("my feature works", () => { return my_feature(3, 4) == 15; }); } -
Register the test by calling the function from the appropriate runner:
- For tests shared with runs: Add
test_my_feature();torun_common_tests()inlang/tests/common/src/main.ch - For tests with pointer arithmetic: Add to
run_native_common_tests()inlang/tests/native_common/src/main.ch - For compiled-only tests: Add to
main()inlang/tests/src/tests.ch
- For tests shared with runs: Add
-
Build and test: Use the commands above.
Common Interpreter Pitfalls
- Pointer bounds: The interpreter tracks
ahead/behindon PointerValues. Dereferencing past bounds returns null (not crash). Tests with pointer arithmetic reaching one-past-end may fail. - Struct pointers (
&raw struct_val): Not supported in interpreter (returns error). Use&mut struct_val(ReferenceOfValue) instead, or parameter passing by reference. - Function references as parameters: Functions passed as
(params) => return_typeare called viaFunctionDeclaration::call(). If the function reference can't be resolved, a non-fatal "function not found" error appears. - Float/Double casts:
floatβintanddoubleβintare supported. Other float/double combinations work via standard arithmetic. - Destructors: Structs with
@deletedestructors have their destructor body interpreted when the scope exits. Empty destructors work fine.
Debugging Interpret Test Failures
- Check the test output for
[InterpretError]messages β these indicate exact failures - Look for "cannot dereference pointer" β pointer went past allocated bounds
- Look for "function call" β a function reference couldn't be resolved
- Look for "Operation between values" β an operation between incompatible types
- Add
std::cerr << "[DEBUG] ..." << std::endl;to interpreter source files to trace specific operations - The worst failures ("RUNTIME ERROR: invalid memory access") happen when
deref()itself crashes β we've made this non-fatal by returninggetNullValue()instead
Debugging with a Single Test File
Running the full test suite on every iteration is slow. Instead of working through
the entire test suite, isolate the failing test by copying its source code
(plus the types/functions it depends on) into a single file at lang/compiled/temp.ch.
The lang/compiled/ directory is in .gitignore, so nothing there will be committed.
Workflow
- Copy the test β put the failing test's code into
lang/compiled/temp.ch - Rebuild the compiler after any C++ changes:
./scripts/build.sh --tcc # For TCCCompiler ./scripts/build.sh --llvm # For Compiler (LLVM) - Compile the single file (choose one):
- LLVM IR inspection (Compiler target):
LLVM IR is emitted atcmake-build-debug/Compiler "lang/compiled/temp.ch" --out-ll-all --build-dir "lang/compiled" \ -o "lang/compiled/temp.exe" --mode debug_complete --debug-ir -v --assertions -fno-unwind-tableslang/compiled/modules/main/llvm_ir.ll - C translation (TCCCompiler target):
# Produce .c output: cmake-build-debug/TCCCompiler "lang/compiled/temp.ch" -o "lang/compiled/temp.c" -v -bm-modules # Or produce an executable: cmake-build-debug/TCCCompiler "lang/compiled/temp.ch" -o "lang/compiled/temp.exe" -v -bm-modules
- LLVM IR inspection (Compiler target):
- Run the executable:
./lang/compiled/temp.exe - Inspect the generated IR / C to diagnose codegen bugs
- Add debug logs to the compiler source, rebuild, and repeat
Flags Explained
--mode debug_completeβ maximum debug info in LLVM IR; omit for cleaner IR without metadata--mode debug_quickβ minimal debug info (good for TCCCompiler)--debug-irβ don't crash on potentially bad IR--assertionsβ verify the generated IR is valid-fno-unwind-tablesβ cleaner IR (removes unwind data on Windows)-vβ verbose output-bm-modulesβ emit build module information
β οΈ Always rebuild the compiler (
./scripts/build.sh --tccor--llvm) after changing.cpp/.hfiles. The previously built binary is used otherwise and your changes won't be reflected.
Related Skills
- Build System (
.agents/skills/build_system/SKILL.md) β Detailed internals of the Lab build system, job execution, plugin compilation, caching, dependency management - Performance (
.agents/skills/performance/SKILL.md) β Compiler optimization patterns, parallelization strategies, arena allocation - Testing Guide (
.agents/skills/testing/SKILL.md) β Test infrastructure, writing tests, how tests are wired inlang/tests/build.lab