Skip to content

jit: daslang -lib emits a C-ABI library - #3927

Open
aleksisch wants to merge 5 commits into
masterfrom
aleksisch/standalone-jit
Open

jit: daslang -lib emits a C-ABI library#3927
aleksisch wants to merge 5 commits into
masterfrom
aleksisch/standalone-jit

Conversation

@aleksisch

@aleksisch aleksisch commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

A host that only needs to call one daslang script had two options, and both asked
too much. The C++ API embeds the compiler. Standalone AOT emits C++ source the host
has to compile itself, and the result is a C++ class. Neither works when the host
is C, or when it wants a plain ABI boundary.

daslang -lib script.das -output build/script now writes a native library and the
C header a host calls it through. --jit-lib-static gives an archive instead. Which
functions cross is your choice: [export_c] marks them one at a time, and
-lib-export-all offers every public function of the entry module whose signature C
can spell, naming the ones it skips. Selection reads one bit, Function.flags.exports,
so the interpreter, AOT and the header emitter cannot drift apart. Scalars, string,
pointers and enums cross by value; structures and the float2..uint4 / range
families cross as const T * and return through a trailing out pointer. Every
declared structure carries a size assert and one offset assert per field, so a host
built for another target fails to compile rather than misreading memory.

A library is called by code that cannot catch anything, which shapes the entry
points. Every thunk runs its body under the context's catch boundary: a das panic
returns zero, leaves the out param untouched, and reports through
<P>_last_error(ctx). <P>_create works on any thread, because daslang's
environment is thread-local while the registration behind it happens once. A second
daslang runtime in one process declines with a null instance instead of aborting.
And <P>_destroy emits the program's [finalize] calls itself, because a jitted
SimFunction carries no shutdown flag for the runtime's own drain to find.

Where to look: modules/dasLLVM/daslib/llvm_lib.das is the entry and thunk emitter,
daslib/c_api_header.das decides what can cross and writes the header, and the four
emit_standalone_* helpers in llvm_exe.das are the exe startup both artifacts now
share. The riskiest spot is the thunk ABI: a structure result is built in an alloca
of its own alignment, never in a frame field, because an in-struct [N x i8] slot
aligns to 1 while the body stores through it at the structure's real alignment.

@aleksisch
aleksisch force-pushed the aleksisch/standalone-jit branch 12 times, most recently from c2233ed to 82adf41 Compare September 9, 2026 16:38
@aleksisch
aleksisch marked this pull request as ready for review September 10, 2026 22:40
@aleksisch
aleksisch force-pushed the aleksisch/standalone-jit branch 9 times, most recently from 064c115 to 21620d3 Compare September 11, 2026 12:26
@aleksisch
aleksisch requested review from borisbat and removed request for borisbat September 11, 2026 12:27
@aleksisch
aleksisch force-pushed the aleksisch/standalone-jit branch 2 times, most recently from 642f7ef to 42685b7 Compare September 11, 2026 12:34
src/builtin/jit_runtime.cpp is not a module - it is the set of extern "C"
symbols jitted and LLVM-AOT-emitted code links against, plus the address
takers the jit module binds. module_jit.cpp keeps the das module: bindings,
linker invocation and emit orchestration.
@aleksisch
aleksisch force-pushed the aleksisch/standalone-jit branch from 42685b7 to 11de83c Compare September 11, 2026 12:42
@borisbat
borisbat requested a balanced review from Copilot September 11, 2026 13:27
… field's module

A member-pointer expression spelled its structure with the module's own name rather
than aotModuleName, so a namespaced module - every standalone context - emitted
`&Node::next` against an undeclared Node. A raw `?` in an emitted literal read as a
trigraph. dumpAnnotationInfo clears its name table before writing rather than after.

A used structure keeps its field types' modules alive: a handled field is the only
thing reaching the module that declares it, and dropping that module dropped its
aot_require include. A standalone context emits its own module's structures whether
or not code reaches them, so an unreferenced one is walked too.
`daslang -aot script.das out.cpp -aot-macros` core dumped whenever the module graph
carried a dasbind [extern], which covers every dasLLVM binding:

    Should be unreachable. We handled it in transformCall. Failed on: c_strlen.

-aot-macros sets export_all, which force-marks every function of every module and skips
removeUnusedSymbols, so an [extern] declaration - which has no body - reached simulate.
transformCall rewrites every CALL to a bound extern, which is what matters; simulating
the function itself is harmless because nothing calls it. The base annotation already
returns nullptr, so the override goes.
The generated .cpp held a C++ class whose methods call the AOT functions directly. It
now also emits one `extern "C"` entry point per export, wrapping the method beside it,
plus an opaque handle and <prefix>_create / _destroy / _last_error - so one pair serves
a C++ host and a C host at once. daslib/c_api_header owns which signatures can cross,
how each type is spelled and the layout asserts; aot_standalone owns the C++ spellings.
Function.flags.exports is the only selection truth, which is what licenses skipping an
unspellable signature with a warning while a function that ASKED for C and cannot cross
is an error. The .cpp carries the header's text rather than including it: the header is
the host's copy to move or edit, and the implementation must not break with it.

The context namespace takes a ctx_ prefix - a name from the file stem lands inside
namespace das, where cast.das redeclared das::cast - while the C prefix drops keyword
escaping, since every emitted name suffixes it. Generated files are named for the input
file, not the module it declares. The fixed API is defined even when nothing is
exported, and what das owns on the heap crosses as an opaque handle a host hands back.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It is large ABI-level compiler/codegen work (thunk emission, C-header layout asserts, refactored exe/lib startup) whose correctness hinges on subtle alignment and error-boundary behavior that warrants human review despite its extensive tests.

Pull request overview

This PR adds a new daslang -lib emission mode that compiles a daslang script into a native, C-ABI library plus a generated C header, so a host that only needs to call one script can do so across a plain ABI boundary (including from C) without embedding the compiler or hand-compiling standalone-AOT C++. Function selection flows through a single bit, Function.flags.exports, set either per-function by the new [export_c] macro or wholesale by -lib-export-all (policies.export_public_functions), so the interpreter, AOT, and header emitter cannot drift. Each thunk runs under the context's catch boundary (panics return zero and surface via <P>_last_error), and <P>_destroy runs [finalize] itself. The change also refactors the shared exe/lib startup helpers and adds two opt-in standalone "sweeps" (AOT -ctx and JIT -lib) over the test_aot corpus.

Changes:

  • New -lib/-lib-export-all CLI mode in utils/daslang/main.cpp (with a -output requirement guard), plus the LLVM thunk/entry-point + C-header emitters and an [export_c] selection macro.
  • Refactor of the JIT tool's CoP setup into jit_setup_cop, batch --lib-output-dir/--lib-export-all clargs, and unified write_artifact/artifact_path replacing the per-kind exe/dll paths.
  • New opt-in tests/standalone-sweep harness (C and JIT drivers, CMake generators) and jit_lib test fixtures.
File summaries
File Description
utils/daslang/main.cpp Adds JitMode::Library, -lib/-lib-export-all parsing, jit_lib option injection, help text, and the -output requirement guard.
utils/internal/jit/main.das Extracts jit_setup_cop; adds batch --lib-output-dir/--lib-export-all clargs and platform-aware shared-lib suffix for the log line.
utils/aot/main.das Threads -bindings/-bindings_library into standalone_aot; enforces one -ctx input when -bindings is set.
utils/CMakeLists.txt Adds daslib/c_api_header.das to the watchdog standalone-AOT source list.
utils/watchdog/main.cpp Updates to the renamed ctx_main::Standalone namespace.
tutorials/integration/cpp/20_standalone_context.cpp Updates to the renamed ctx_standalone_context::Standalone namespace.
tests/standalone-sweep/CMakeLists.txt New opt-in sweep targets over the test_aot corpus with a filter knob.
tests/standalone-sweep/sweep_jit.cmake JIT-tier sweep: batch-emit libraries, then load/drive each via a generated host.
tests/standalone-sweep/sweep_contexts.cmake Generates the X-macro context list/header for the C++ driver.
tests/standalone-sweep/sweep_driver.c C driver exercising every emitted context through its C entry points.
src/ast/ast_module.cpp, src/ast/ast_parse.cpp Remove the describe_pending_dynamic_modules() forward decl (now in dyn_modules.h); leaves a stale comment.
Review details
  • Files reviewed: 97/98 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/ast/ast_module.cpp Outdated
Comment thread src/ast/ast_parse.cpp Outdated
The JIT could produce an exe and an AOT object; a host that only calls one script had
neither. -lib writes a shared library (or an archive under --jit-lib-static) plus a C
header, and links no daslang API into the host. The C surface is daslib/c_api_header's,
so a JIT library and a standalone context declare the same API and skip the same
signatures. Which functions cross is Function.flags.exports, read three ways:
[export_c] alone by default, the bit however it was set under --jit-lib-export-marked,
and every public function under -lib-export-all, which is a policy because MarkSymbolUse
consumes it before infer.

Codegen stays in LlvmJitMode.EXE, so inject_main's startup is shared. Each export gets
an extern "C" thunk that packs its arguments into a frame and hands it to a trampoline
through jit_lib_invoke_guarded, so a das panic reaches the caller as a zero return with
its text in <P>_last_error, never as an unwind through C. Nothing in the emit path
panics: the artifact writers return whether they wrote and run_jit collects it. A
library initializes the modules it registers - the host's own runtime is already
initialized, but an uninitialized module carries no functions, which an extern lookup
reported as a missing function in a module that plainly existed.

Two opt-in sweeps drive the corpus test_aot covers, one per backend: the C++ tier
compiles and launches every context in one process, the JIT tier emits each library and
loads it back. ARCHITECTURE_LIB.md#lib-runtime-scope records what a library's runtime
guarantees.
@aleksisch
aleksisch force-pushed the aleksisch/standalone-jit branch from 11de83c to 71df22a Compare September 11, 2026 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants