Skip to content
cccskills
FreePublished on GitHub

Compiler Intrinsics and Reflection

Comprehensive guide to Chemical's compiler intrinsic functions and reflection APIs — how GlobalFunctions.cpp provides compile-time implementations, reflection, and metadata access.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31 stars(GitHub 31 + cccskills 0)

Run score Not tested

See how to install

Files included (1)

  • SKILL.md41.0 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

Compiler Intrinsics and Reflection

Chemical provides a rich set of compiler intrinsic functions (in ast/utils/GlobalFunctions.cpp) that are implemented directly in C++ and accessible as regular Chemical functions. These intrinsics provide reflection, compile-time evaluation, interpreter support for standard library types, and lower-level compiler interaction.

Everything lives under the intrinsics:: namespace (with intrinsics::llvm:: and intrinsics::mem:: sub-namespaces), plus a handful of top-level built-ins (defined(...) and the def target-data object). The single source of truth is ast/utils/GlobalFunctions.cpp (3369 lines, 60+ helper classes).

Architecture

The intrinsic functions are C++ classes that extend FunctionDeclaration and override call(). They are registered in the compiler's GlobalInterpretScope during initialization and become available as intrinsics::function_name().

// Pattern for creating an intrinsic:
class InterpretMyIntrinsic : public FunctionDeclaration {
public:
    explicit InterpretMyIntrinsic(TypeBuilder& cache, ASTNode* parent_node)
        : FunctionDeclaration(
            "my_intrinsic",                    // Name visible in Chemical code
            {cache.getVoidType(), ZERO_LOC},   // Return type
            false,                              // variadic?
            parent_node,                        // Parent node
            ZERO_LOC,                           // Source location
            AccessSpecifier::Public,            // Access specifier
            true                                // Is compiler declaration?
    ) {
        set_compiler_decl(true);  // Mark as compiler-provided
    }

    Value *call(InterpretScope *call_scope, ASTAllocator& allocator,
                FunctionCall *call, Value *parent_val,
                bool evaluate_refs) override {
        // Implementation here
        // Access arguments via call->values[i]->evaluated_value(*call_scope)
        // Use call_scope->error() for error reporting
        // Return values via allocator.allocate<T>()
    }
};

Key File

FileLinesPurpose
ast/utils/GlobalFunctions.cpp—All intrinsic function implementations + registration (IntrinsicsNamespace, create_container, rebind_container)
ast/utils/GlobalFunctions.cpp (#includes at :1-57)—The intrinsic classes are local to this translation unit — there is no GlobalFunctions.h
compiler/lab/BackendContext.h100Pure-virtual backend interface (name, emit, forget, mem_copy, supports, atomics) that codegen intrinsics call
compiler/lab/backend/atomics.h—BackendAtomicMemoryOrder, BackendAtomicSyncScope, BackendAtomicOp enums used by intrinsics::llvm::atomic_*
compiler/frontend/AnnotationController.h / .cpp—Annotation/marker/test collection read by get_tests and get_single_marked_decl_ptr

Registration Model (how intrinsics::foo resolves)

The intrinsics namespace is not declared in a .ch file — it is built in C++ and injected into every symbol resolver:

  1. struct IntrinsicsNamespace : public Namespace is named "intrinsics" and owns one member per intrinsic plus the LLVMNamespace llvmNamespace and MemNamespace memNamespace.
  2. Its constructor (:2891-2921) builds each FunctionDeclaration child and puts them in nodes = { &interpretSupports, &printFn, ... }. A class that is not listed in nodes is not reachable from Chemical.
  3. struct GlobalContainer (:2971) owns the IntrinsicsNamespace intrinsics_namespace, the top-level InterpretDefined defined, and DefThing defThing (which materializes the def target-data object).
  4. GlobalInterpretScope::create_container (:3342) and rebind_container (:3311) run TopLevelDeclSymDeclare::VisitNamespaceDecl on the namespace. That visitor (compiler/symres/DeclareTopLevel.cpp:263) calls linker.declare_tld_node("intrinsics", ns, ...) and registers every child in the namespace's extended map with MapSymbolDeclarer. Lookups like intrinsics::get_tests then walk Namespace::extended.
  5. Because the comptime allocator is destroyed at the end of every job, rebind_container first clears intrinsics_namespace.extended / memNamespace.extended (:3316-3317) and re-declares everything. Any per-job state an intrinsic holds (e.g. InterpretExprStrBlockValue::set_impls) must be re-wired here.
  6. defined and def are declared directly in the resolver root scope (:3330-3332) — as top-level names, not under intrinsics::.

The def target-data object

declare_def_values (:3259-3301) populates def with booleans for every target/build axis: def.c, def.tcc, def.clang, def.cbi, def.lsp, def.test, def.debug, def.debug_quick, def.debug_complete, def.release, def.release_safe, def.release_small, def.release_fast, def.little_endian, def.big_endian, def.is64Bit, def.windows, def.posix, def.win32, def.win64, def.linux, def.musl, def.macos, def.freebsd, def.unix, def.gnu, def.android, def.cygwin, def.mingw32, def.mingw64, def.x86_64, def.i386, def.arm, def.aarch64, def.powerpc, def.powerpc64, def.riscv32, def.riscv64, def.wasm32, def.wasm64. It is recomputed per job/target; defined("name") instead consults the job's definitions set (:1046).

Intrinsic Function Categories

1. Print/Debug Functions

IntrinsicClassSignaturePurpose
intrinsics::printInterpretPrint (:291)print(value : any...)Print values to stdout (space-joined via the visitor loop)
intrinsics::printlnInterpretPrintLn (:333)println(value : any...)Print + newline
intrinsics::to_stringInterpretToString (:412)to_string(value : any...)Format one or more values into a string
intrinsics::expr_printInterpretExprPrint (:345)expr_print(expr : %expressive_string)Print expressive string (backtick templates)
intrinsics::expr_printlnInterpretExprPrintLn (:399)expr_println(expr : %expressive_string)Print expressive string + newline

Key implementation details:

  • InterpretPrint and InterpretPrintLn use RepresentationVisitor with interpret_representation = true (set at :309) for type-aware printing; each value is written with visitor.visit(paramValue) and a null prints as null.
  • InterpretExprPrint/InterpretExprPrintLn handle expressive strings by iterating ExpressiveString::values (:380-393) — ValueKind::String segments are written to std::cout directly, ${} expression segments are evaluated and printed via RepresentationVisitor.
  • InterpretToString (:418-444) is the string-returning variant; it allocates the result on the arena with allocator.allocate_str.
  • The old print/println are variadic (any..., InterpretPrint passes true for the variadic flag at :301); the new expr_print/expr_println take a single %expressive_string (cache.getExprStrType(), :361).

2. Compile-Time Mode Detection

IntrinsicClassPurpose
intrinsics::is_interpretation()InterpretIsInterpretation (:666)True if running in interpretation mode (global->interpretation_mode)
intrinsics::is_comptime()InterpretIsComptime (:726)True if inside a comptime evaluation
intrinsics::is_runtime()InterpretIsRuntime (:689)True if running in codegen (not comptime)
intrinsics::is_tcc_based()InterpretIsTcc (:621)True if compiled with TCC_BUILD
intrinsics::is_clang()InterpretIsClang (:644)True if compiled with COMPILER_BUILD (LLVM/Clang)

Key implementation details:

  • is_interpretation() returns global->interpretation_mode (:682).
  • is_runtime()/is_comptime() behave differently depending on whether they are reached from codegen or from symres:
    • In interpretation mode: is_runtime()==false, is_comptime()==true (:708, :745).
    • In compiled mode outside codegen eval (global->is_runtime_call == false): is_runtime()==true, is_comptime()==false (:712-713, :749-750).
    • In compiled mode inside codegen eval: runtime iff global->call_stack.empty() (:720), comptime iff it is non-empty (:755).
  • is_interpretation() and is_comptime() set ContractFlag::IsInterpretation = true; is_runtime() sets it to false (:679, :702, :739).
  • The contract mechanism is what makes comptime if free. TypeVerifier::VisitIfStmt (compiler/typeverify/TypeVerify.cpp:1339-1431) recognizes a comptime if whose condition calls a contract-bearing function and sets its internal is_interpretation_mode for each branch. verifyArguments short-circuits when is_interpretation_mode is true (TypeVerify.cpp:847-848), so argument type-checking rules that only hold at runtime are skipped in the interpretation branch. Codegen later folds the branch.

3. Type Reflection

IntrinsicClassSignaturePurpose
intrinsics::satisfiesInterpretSatisfies (:1079)<T, U>()True if the type U can be used where T is expected (T->satisfies(U))
intrinsics::isInterpretIs (:1107)<T, U>()True if types T and U are identical (T->is_same(U->canonical()))
intrinsics::value_satisfiesInterpretValueSatisfies (:1135)(val1, val2)True if val1's declared type satisfies val2's value (val1.getType()->satisfies(val2, false))
intrinsics::is_same_typeInterpretIsSameType (:1176)(val1, val2)True if both values have the same type
intrinsics::type_to_stringInterpretTypeToString (:1508)<T>()Returns string representation of type T
intrinsics::sizeInterpretSize (:470)(value) : u64Returns the length of a string or the element count of an ArrayValue
intrinsics::supportsInterpretSupports (:565)(feature) : boolTrue if the backend supports a feature (string or enum-int)

Key implementation details:

  • satisfies / is operate on generic type arguments (call->generic_list), not values — they resolve at compile time. Source order is satisfies<T, U> → T->satisfies(U), which is assignability semantics: the derived-to-base tests in lang/tests/common/src/satisfies.ch:222-223 show satisfies<Base, Derived>() is true (Derived extends Base), and satisfies<Derived, Base>() is false. I.e. satisfies<To, From> asks "can a From be used where a To is required" — not "does To implement interface From".
  • is canonicalizes the second operand before comparing (:1131).
  • value_satisfies and is_same_type operate on runtime values; both declare their parameters with cache.getMaybeRuntimeAnyType() and return false when the first value has no type (:1168-1172, :1208-1213). value_satisfies(T, U) returned false in tests means the value's type does not satisfy the other value.
  • type_to_string uses RepresentationVisitor (interpret_representation = true, :1525) on generic_list.front()->pure_type(allocator).
  • size resolves references through resolve_ref() (:462-468) so size(&x) sees the underlying value; only String and ArrayValue are accepted, otherwise it errors (:501-503).
  • supports does backend lookup. With a string it hashes (djb2, std::hash<chem::string_view>) and matches only "float128", "atomic", "inlineasm" / "InlineAsm" (:595-608); any other string returns false. With a number it casts to CompilerFeatureKind (:611-614). The CompilerFeatureKind enum (compiler/lab/BackendContext.h:8-14) is Float128=0, AtomicBuiltins=1, InlineAsm=2, Volatile=3, but "volatile" is not in the string dispatch.
  • Backend answers differ: LLVMBackendContext::supports always returns true (compiler/backend/LLVMBackendContext.h:31-33); ToCBackendContext::supports (preprocess/2c/2cASTVisitor.cpp:8118-8145) returns Float128 only when !target.tcc, and true for AtomicBuiltins, InlineAsm, Volatile.

4. Source Location Reflection

IntrinsicClassPurpose
intrinsics::get_raw_location()InterpretGetRawLocation (:761)Returns the encoded SourceLocation of the call site (u64)
intrinsics::get_raw_loc_of(arg)InterpretGetRawLocOf (:781)Returns the encoded SourceLocation of the argument
intrinsics::get_line_no()InterpretGetLineNo (:807)1-based line number of the call site (u64)
intrinsics::get_char_no()InterpretGetCharacterNo (:830)1-based character offset of the call site (u64)
intrinsics::get_caller_line_no()InterpretGetCallerLineNo (:860)1-based line of the outermost comptime caller (0 if none)
intrinsics::get_caller_char_no()InterpretGetCallerCharacterNo (:887)1-based char of the outermost comptime caller (0 if none)
intrinsics::get_call_loc(back)InterpretGetCallLoc (:914)Encoded location of the Nth caller in global->call_stack
intrinsics::decode_location<T>(loc)InterpretDecodeLocation (:955)Decodes an encoded location into a T with filename, line, character fields
intrinsics::get_loc_file_path(loc)InterpretGetLocFilePath (:1271)File path string for an encoded location
intrinsics::get_current_file_path()InterpretGetCurrentFilePath (:1352)File path of the current source file

Key implementation details:

  • SourceLocation is a compact 64-bit encoded value (10-bit file id, 18-bit line start, 12-bit char start, 11-bit line-end offset, 12-bit char end; see core/source/LocationManager.h and the Compiler API skill).
  • get_caller_line_no / get_caller_char_no use get_runtime_call() (:851-858), which returns global->call_stack.back() — the outermost comptime call.
  • get_call_loc(back) indexes the call stack from the top; if back exceeds the stack size it falls back to call_stack.front() (:949); if the stack is empty it returns the call site's own location.
  • decode_location<T> requires exactly one generic argument naming a StructDecl; it fills filename (string), line (uint) and character (uint) (:1002-1016).
  • get_raw_location declares its return as cache.getUIntType() but constructs the value with getU64Type() — treat it as a 64-bit raw location.

5. Module/Scope Reflection

IntrinsicClassPurpose
intrinsics::get_module_scope()InterpretGetModuleScope (:1374)Returns the scope name of the enclosing module
intrinsics::get_module_name()InterpretGetModuleName (:1401)Returns the module name
intrinsics::get_module_dir()InterpretGetModuleDir (:1428)Returns the directory path of the enclosing module (empty for non-directory modules)
intrinsics::get_build_dir()InterpretGetBuildDir (:1305)Returns the build output directory
intrinsics::get_compiler_path()InterpretGetCompilerPath (:1329)Returns the compiler binary path
intrinsics::get_target()InterpretGetTarget (:1249)Returns the target triple string (e.g. "x86_64-linux-gnu")
intrinsics::get_libs_dir()InterpretGetLibsDir (:2130)Returns PROJECT_SOURCE_DIR/lang/libs (source build) or <exe>/../libs
intrinsics::version()InterpretCompilerVersion (:546)Returns the compiler version string (VERSION_STRING)
intrinsics::get_backend_name()InterpretGetBackendName (:1995)Returns "C" (TCC backend) or "LLVM"

Key implementation details:

  • Module information is retrieved via call_scope->global->current_func_type->get_parent() → get_mod_scope() (:1389-1397, :1416-1424). All return "" if there is no current function/module (:1390, :1417, :1444).
  • get_module_dir() accesses scope->container->paths[0] and returns "" unless container->type == LabModuleType::Directory (:1451-1455).
  • get_build_dir() reads build_compiler->options->build_dir; get_compiler_path() reads options->exe_path (:1324, :1347). Both fall back to "" when there is no build compiler (e.g. LSP).
  • Target triple is from build_compiler->current_job->target_triple (:1265).
  • get_backend_name() is global->backend_context->name() — used heavily by std::atomic to choose inline asm vs. LLVM atomics (see lang/libs/atomic/src/atomic.ch:692 etc.).

6. Function / Annotation / Test Reflection

IntrinsicClassPurpose
intrinsics::get_child_fn<T>(name)InterpretGetChildFunction (:1460)Returns a pointer to a direct child function of type T by name
intrinsics::get_single_marked_decl_ptr(name)InterpretGetSingleMarkedDeclPointer (:1575)Returns a pointer to the single declaration marked with an annotation name (functions only)
intrinsics::get_tests<T>()InterpretGetTests (:1623)Returns the generated table of @test functions for test struct T
defined(name : string) : boolInterpretDefined (:1022)Top-level (not intrinsics::) — true if a build definition is set

Key implementation details:

  • get_child_fn<T> requires exactly one value argument (the name) and exactly one generic argument (:1480-1487). It resolves type->get_members_container()->direct_child_function(name) (:1494-1496) and returns an AccessChain over a VariableIdentifier, or null on failure. Only direct children are supported (comment at :1459; there is a TODO at :2885 to remove it in favour of explicit destructor lookup). Example: intrinsics::get_child_fn<CTStructGetChild>("fake_sum") (lang/tests/common/src/comptime/basic.ch:65).
  • get_single_marked_decl_ptr uses build_compiler->controller.get_single_marked(name) and returns the function as a VariableIdentifier, or null if absent / not a FunctionDecl (:1598-1617). Used by the test runner for @test.before_each / @test.after_each (lang/libs/test/src/runner.ch:85-86).
  • get_tests<T> requires one generic argument naming a StructDecl (:1659-1670), fetches collection 0 from the AnnotationController (controller.get_definition("test") → get_collection(annot_def->collection_id), :1673-1675), and builds an ArrayValue of structs. Each element carries: id (from @test.id or INT_MAX/2 + i), name (@test.name or function name), group (@test.group), ptr, file, timeout (default 10000 ms), retry (clamped 0..999999), returns_bool, pass_on_crash, benchmark, lineNum, charNum (:1685-1789). Entry point: lang/libs/test/src/main.ch:1.
  • defined is a top-level function (registered at GlobalFunctions.cpp:3330) and is called as defined("CHECK_DEF") — not intrinsics::defined(...) (lang/tests/common/src/comptime/basic.ch:315). It reads build_compiler->current_job->definitions (:1046).
  • The annotation machinery (get_definition, get_collection, get_args, is_marked) is documented in the Annotations skill.

Reflection deep-dive: what can and cannot be queried

CapabilityAPIStatus
Look up a direct child functionget_child_fn<T>(name)Implemented
Fetch @test functionsget_tests<T>()Implemented
Fetch a single marker declget_single_marked_decl_ptr(name)Implemented (functions only)
Check annotation presence (compile time, C++ side)AnnotationController::is_marked(node, name)Implemented (not directly exposed as an intrinsic)
Enumerate struct fields / members—Not implemented (roadmap)
Enumerate implemented interfaces—Not implemented (roadmap)
Query member offsets / alignment—Not implemented (roadmap)
General get_marked_decls<T>() returning all marked decls—Does not exist despite mentions in AGENTS.md; only get_single_marked_decl_ptr and get_tests are real

7. Compile-Time Computation, Errors & Memory

IntrinsicClassSignaturePurpose
intrinsics::error(message)InterpretError (:1053)(value : string)Emits a compile-time error with the given message
intrinsics::forget(thing)InterpretForget (:1545)(thing) : boolTells the backend to skip destructor/drop for a value (manual memory management)
intrinsics::mem::copy(dest, src)InterpretMemCopy (:1217)(dest, src)Backend memory copy (backend_context->mem_copy)
intrinsics::return_struct(...)InterpretRetStructPtr (:520)(...) : *voidProduces a RetStructParamValue — sret handling (used by LLVM codegen; see compiler/backend/LLVM.cpp:1255)
intrinsics::raw(value)InterpretRawLiteral (:2059)(value : string)Wraps a string as a RawLiteral (raw backend text)
intrinsics::emit(value)InterpretEmitRaw (:2023)(value : string) : boolEmits raw text through backend_context->emit (C backend writes it; LLVM is a no-op)
intrinsics::multiple(...)InterpretMultiple (:2094)(value : any...)Produces a MultipleValue from the evaluated arguments (variadic)

%runtime_value(expr) is a value-level construct parsed by the lexer (parser/utils/LexValue.cpp, producing ValueKind::RuntimeValue), not an intrinsic function. It wraps an expression for runtime evaluation. %runtime, %maybe_runtime and %expressive_string are type wrappers, likewise not intrinsics.

Key implementation details:

  • error requires a string argument and reports through call_scope->error(call) << ... (:1074). It is a compile-time hard error.
  • forget looks up the argument's linked_node() and returns backend_context->forget(node) (:1569-1571). Used by std::vector/std::deque/json to suppress destruction of moved-out values.
  • mem::copy calls backend.mem_copy(call->values[0], call->values[1]); on the wrong arity it errors with "std::mem::copy called with arguments of length not equal to two" (:1240-1242).
  • emit/raw/multiple are mostly used by macro plugins and the standard streams (lang/libs/std/src/stream.ch).

8. Expressive-String Stream Block

intrinsics::expr_str_block_value(stream, expr, additional...) → InterpretExprStrBlockValue (:2162) is an internal intrinsic (returns i32, variadic) that turns an expressive string into a BlockValue of stream.writeX(...) method calls. It is what powers ${} interpolation for user-defined stream types (std::string at lang/libs/std/src/string.ch:379, CommandLineStream at lang/libs/std/src/stream.ch:250). It needs the ImplementationsIndex and CoreNodes, which are wired in per job via set_impls (:2186) from create_container/rebind_container (:3320, :3349). Supported ${} element types: string, char/uchar, signed/unsigned ints, float, double, and any struct with a stream child method (:2249-2317).

9. Exported Lambda Capture Intrinsics

These expose closure internals and are consumed by codegen instead of the interpreter. All produce an ExtractionValue whose ExtractionKind selects the operation (create_extraction_value_type):

IntrinsicClassExtractionKindPurpose
intrinsics::get_lambda_fn_ptr(thing)InterpretGetLambdaFnPtr (:1798)LambdaFnPtrRaw function pointer of a lambda
intrinsics::get_lambda_cap_ptr(thing)InterpretGetLambdaCapPtr (:1832)LambdaCapturedPtrPointer to the captured environment
intrinsics::get_lambda_cap_destructor(thing)InterpretGetLambdaCapDestructor (:1867)LambdaCapturedDestructorDestructor for the captured environment
intrinsics::sizeof_lambda_captured(thing)InterpretSizeOfLambdaCaptured (:1901)SizeOfLambdaCapturedSize (u64) of captured data
intrinsics::alignof_lambda_captured(thing)InterpretAlignOfLambdaCaptured (:1936)AlignOfLambdaCapturedAlignment (u64) of captured data

10. No Interpreter-Only Standard Library

intrinsics::vector<T> (and the InterpretVector* classes that implemented it) was deleted with the AST interpreter. Comptime now runs on the MIR interpreter, so comptime code uses the same std::string / std::vector<T> / std::unordered_map implementations as compiled code. The AST layer only folds literals (IntNumValue, BoolValue, StringValue, ArrayValue, StructValue, PointerValue) and compiler intrinsics.

11. Backend-Provided (LLVM Atomic) Intrinsics

The intrinsics::llvm::* namespace (LLVMNamespace, GlobalFunctions.cpp:2779) exposes atomic primitives that only work when a backend_context exists. On the C/TCC backend many of them are unsupported and draw an error telling you to use inline asm or %runtime_value() instead (see lang/libs/atomic/src/atomic.ch and preprocess/2c/2cASTVisitor.cpp:8147+).

IntrinsicClassParametersReturns
intrinsics::llvm::atomic_fence(order, scope)InterpretLLVMAtomicFence (:2411)int, intvoid
intrinsics::llvm::atomic_load(ptr, order, scope)InterpretLLVMAtomicLoad (:2459)%runtime<*any>, int, intany
intrinsics::llvm::atomic_store(ptr, value, order, scope)InterpretLLVMAtomicStore (:2507)%runtime<*mut any>, %maybe_runtime<any>, int, intvoid
intrinsics::llvm::atomic_cmp_exch_weak(ptr, expected, value, order1, order2, scope)InterpretLLVMAtomicCmpExchWeak (:2561)6 argsbool
intrinsics::llvm::atomic_cmp_exch_strong(ptr, expected, value, order1, order2, scope)InterpretLLVMAtomicCmpExchStrong (:2619)6 argsbool
intrinsics::llvm::atomic_op(op, ptr, value, order, scope)InterpretLLVMAtomicOperation (:2677)int, ptr, value, int, intany
intrinsics::llvm::atomic_signal_fence(order)InterpretLLVMAtomicSignalFence (:2735)intvoid (compiler-only fence)

Key implementation details:

  • Every method validates arity, checks that order/scope/op integers are within BackendAtomicMemoryOrder/BackendAtomicSyncScope/BackendAtomicOp (compiler/lab/backend/atomics.h), and errors if backend_context is null.
  • atomic_fence, atomic_store and atomic_signal_fence return the backend's renderable value (empty RawLiteral on C, null on LLVM) so the call site renders correctly.
  • The corresponding BackendContext virtuals are declared in compiler/lab/BackendContext.h:60-98 and implemented per backend.

12. Non-Registered / Reserved Names

  • InterpretDestructCallSite (:1970, name "destruct_call_site") is defined but never added to IntrinsicsNamespace::nodes — it is currently unreachable from Chemical code.
  • intrinsics::unreachable() and intrinsics::wrap(...) do not exist. unreachable is a keyword/UnreachableStmt (lexer/Lexer.cpp:79, parser/structures/ForBlock.cpp:46), and intrinsics::wrap appears only in an intentionally-invalid negative test (lang/compiled/bug.ch) — do not rely on either.

Availability Matrix (comptime / runtime / interpretation)

Every intrinsic is implemented by a C++ FunctionDeclaration::call() that runs at compile time — either while the interpreter executes a comptime function/block during symres, or while a backend evaluates a comptime call during codegen. The table below says where each family is usable and what it needs.

Intrinsic family--interpret (pure interpretation)Comptime during codegenEmitted runtime codeRequirements / notes
print, println, to_string, expr_print, expr_println✅✅❌Writes to std::cout/returns a compile-time StringValue; not a runtime print
is_interpretation, is_comptime, is_runtime, is_tcc_based, is_clang✅✅foldedContract flag drives comptime if branch pruning (TypeVerify.cpp:1339-1431)
satisfies, is, value_satisfies, is_same_type, type_to_string✅✅❌Pure compile-time type queries
size, defined, supports✅✅❌supports needs a backend to give meaningful answers
get_raw_location, get_raw_loc_of, get_line_no, get_char_no, get_caller_line_no, get_caller_char_no, get_call_loc, decode_location, get_loc_file_path, get_current_file_path✅✅❌Read LocationManager / call stack
get_module_scope, get_module_name, get_module_dir, get_target, get_build_dir, get_compiler_path, get_libs_dir, version, get_backend_name✅✅❌Need build_compiler/backend_context; return "" otherwise
get_child_fn, get_single_marked_decl_ptr, get_tests✅✅❌Reflection over resolved declarations; need build_compiler->controller
error✅✅❌Hard compile-time diagnostic
forget➖✅❌Calls backend_context->forget; no backend → null deref guard needed
mem::copy➖✅❌Calls backend_context->mem_copy; needs a backend
emit, raw, multiple, return_struct✅/➖✅✅emit writes raw backend text (C) / no-op (LLVM); raw/multiple are codegen carriers
expr_str_block_value✅✅❌Generates a BlockValue of stream.writeX calls; needs impls index
get_lambda_*, sizeof_lambda_captured, alignof_lambda_captured➖✅✅Produce ExtractionValues consumed by closure codegen
llvm::atomic_*❌✅✅Require backend_context; C backend supports only via inline asm on atomic_op/signal_fence, other ops error

Legend: ✅ works, ➖ limited/needs a backend, ❌ not usable there. "Comptime during codegen" means the intrinsic is callable from a comptime function/block that a backend evaluates (LLVM or 2c) — the common path for std::atomic and std::string.append.

Adding a New Intrinsic

Step-by-Step

  1. Create the class in ast/utils/GlobalFunctions.cpp (there is no header):
class InterpretMyFeature : public FunctionDeclaration {
public:
    // Declare parameters as class members
    FunctionParam myParam;

    explicit InterpretMyFeature(TypeBuilder& cache, ASTNode* parent_node)
        : FunctionDeclaration(
            "my_feature",                                // Name
            {cache.getIntType(), ZERO_LOC},              // Return type (int)
            false,                                        // Not variadic
            parent_node,
            ZERO_LOC,
            AccessSpecifier::Public,
            true                                          // compiler declaration
    ), myParam("input", {cache.getStringType(), ZERO_LOC}, 0, nullptr, false, this, ZERO_LOC) {
        set_compiler_decl(true);   // Mark as compiler-provided
        params = { &myParam };     // Register parameters
    }

    Value *call(InterpretScope *call_scope, ASTAllocator& allocator,
                FunctionCall *call, Value *parent_val,
                bool evaluate_refs) override {
        // Validate arguments
        if(call->values.size() != 1) {
            call_scope->error("my_feature requires 1 argument", call);
            return nullptr;
        }

        // Evaluate the argument
        auto input = call->values[0]->evaluated_value(*call_scope);

        // Check the value kind
        if(input->val_kind() != ValueKind::String) {
            call_scope->error("expected a string argument", call);
            return nullptr;
        }

        // Return an integer
        auto& str = input->get_the_string();
        return new (allocator.allocate<IntNumValue>())
            IntNumValue(str.size(), call_scope->global->typeBuilder.getIntType(), ZERO_LOC);
    }
};
  1. Add a member to IntrinsicsNamespace (:2822) — or to LLVMNamespace/MemNamespace for a sub-namespace. Add the same member to the constructor initializer list (:2891) and to the nodes = { ... } list (:2909). Missing either leaves the intrinsic unreachable.

  2. Wire per-job dependencies if any. If the intrinsic needs ImplementationsIndex, CoreNodes, or the annotation controller, store them on the class and set them in rebind_container (:3311) and create_container (:3342) — these run once per job/target.

  3. For backend-specific codegen, add a pure virtual to BackendContext (compiler/lab/BackendContext.h) and implement it in LLVMBackendContext (compiler/backend/LLVMBackendContext.h/LLVM.cpp) and ToCBackendContext (preprocess/2c/2cBackendContext.h/2cASTVisitor.cpp). Guard backend_context == nullptr and return a sensible fallback.

  4. No Chemical declaration is required — the namespace is built from C++. It becomes callable as intrinsics::my_feature("hello") after symres re-declares the namespace.

  5. Add tests in lang/tests/common/src/comptime/intrinsics.ch, lang/tests/common/src/comptime/basic.ch, or lang/tests/src/basic/comptime/intrinsics.ch. For reflection, a comptime func + test(...) pair is standard. For codegen intrinsics, test under both --tcc and --interpret.

  6. If you add a new CompilerFeatureKind, remember intrinsics::supports's string dispatch is a hardcoded switch (:595-608) — update it (and consider adding the enum name) or the feature can only be queried by integer.

Parameter Pattern

// Simple parameter:
FunctionParam myParam(
    "name",                                    // Parameter name
    { cache.getType(), ZERO_LOC },             // Type + location
    paramIndex,                                 // Parameter index
    defaultValue,                               // Default value (or nullptr)
    isSelf,                                     // Is it &self?
    parentFunction,                             // Owning function
    encodedLocation
);

Common Return Patterns

// Return integer
new (allocator.allocate<IntNumValue>())
    IntNumValue(value, type, location);

// Return boolean
new (allocator.allocate<BoolValue>())
    BoolValue(value, call_scope->global->typeBuilder.getBoolType(), ZERO_LOC);

// Return string
new (allocator.allocate<StringValue>())
    StringValue(string_view, call_scope->global->typeBuilder.getStringType(), ZERO_LOC);

// Return null
new (allocator.allocate<NullValue>())
    NullValue(call_scope->global->typeBuilder.getNullPtrType(), ZERO_LOC);

// Return void (no value)
return nullptr;

No Interpreter-Only Standard Library

There is no separate interpreter standard library any more (see section 10): comptime runs on MIRInterpreter over the same MIR the backends consume, so it uses the real std::string / std::vector<T> implementations. The AST layer only folds literals and intrinsics.

How RepresentationVisitor powers printing

intrinsics::print/println/to_string/expr_print*/type_to_string all use RepresentationVisitor (preprocess/RepresentationVisitor.h, flag at :33) with interpret_representation = true, which suppresses quotes/escapes around strings and chars (preprocess/RepresentationVisitor.cpp:681-699). For expr_print* the ExpressiveString's alternating literal/expression segments are evaluated with interpret_representation = true (see the Interpreter skill for InterpretExprPrintLn).

Gotchas

  • intrinsics::llvm::* require a backend. During symres (or LSP) backend_context may be null, and these intrinsics error out. Guard with intrinsics::supports("atomic") / intrinsics::get_backend_name() as std::atomic does.
  • supports's string set is tiny and hashed. Only "float128", "atomic", "inlineasm"/"InlineAsm" are recognized as strings. "volatile" exists in CompilerFeatureKind but is not in the dispatch. Prefer the enum-int form for precision.
  • defined is top-level, not intrinsics::defined. Call it as defined("NAME"). The target-data object is def.x86_64, def.linux, etc.
  • intrinsics::vector<T> ≠ std::vector<T>. The interpreter vector is a separate type; .get() returns a copy and get_ptr/iteration are unsupported.
  • Interpreter pointer bounds are enforced, compiled mode isn't. Intrinsics that hand raw pointers around may behave differently in --interpret.
  • is_interpretation()/is_comptime()/is_runtime() affect type checking. They carry ContractFlag::IsInterpretation; TypeVerifier::VisitIfStmt toggles argument verification off inside the interpretation branch (TypeVerify.cpp:847-848, 1367-1431). This is deliberate — code valid only at comptime (e.g. passing &mut to *mut) is allowed there.
  • Atomic order/scope enums must match compiler/lab/backend/atomics.h. BackendAtomicMemoryOrder skips 3 (NotAtomic=0, Unordered=1, Monotonic=2, Acquire=4, Release=5, AcquireRelease=6, SequentiallyConsistent=7).
  • Variadic intrinsics declare any.../%maybe_runtime<any> params, so the type checker is lenient; the C++ call() must validate arity and kinds and call call_scope->error(...).
  • get_tests<T> / decode_location<T> require a StructDecl generic argument, not an arbitrary type.
  • get_child_fn<T> only finds direct children of T's members container, and cannot see extension/impl methods.
  • get_tests state comes from AnnotationController collection 0, which is cleared between jobs (clear_marked_or_collected). Tests must be declared in the same job.
  • intrinsics::multiple passes false for the variadic constructor flag and then calls setIsVariadic(true) (:2102-2113) — a reminder that variadic-ness can be set two ways.
  • error(msg) is not a runtime panic — it is a compile-time diagnostic.
  • %runtime_value, %runtime, %maybe_runtime, %expressive_string are value/type syntax handled by the lexer/parser, not functions in GlobalFunctions.cpp.

Future Goals for Reflection

Based on the project's roadmap:

1. Better Compiler Reflection

  • Field enumeration: Allow iterating struct/union/variant members at compile time
  • Type properties: Query type alignment, size, offset of members
  • Interface checks: Find all types that implement a given interface
  • Call chain introspection: Access full call stack at compile time
  • get_marked_decls<T>(): enumerate all decls carrying a marker (today only get_single_marked_decl_ptr + get_tests exist)

2. Comptime Evaluation

Comptime code now runs on the MIR interpreter and uses the real std::string / std::vector<T> / std::unordered_map first-class implementations, so the old InterpretVector container (deleted with the AST interpreter) does not need a replacement. What remains future work is only the symres-time expression folder: it folds literals (IntNumValue, StringValue, ArrayValue, StructValue, …), not container methods.

3. Automatic Serializer/Deserializer Generation

  • Use get_child_fn<T> and marker lookups to find struct fields at compile time
  • Generate to_json() and from_json() functions automatically
  • See the JSON Serialization skill for the current #json approach via json_cbi.

Illustrative future API (from the original roadmap; not yet implemented — intrinsics::get_members<T>() does not exist today):

// Future API:
func to_json<T>(val : T) : json::JsonValue {
    // Generated at compile time using compiler reflection
    comptime {
        var result = json::Object()
        for(field in intrinsics::get_members<T>()) {
            result[field.name] = to_json(field.value)
        }
        return result
    }
}

4. Automatic Generic Impl Declaration Instantiation

  • When the user uses myType.method(), if method comes from a generic impl declaration, automatically instantiate and resolve it
  • Deprecate the need for nested impl blocks

Related Skills

  • Interpreter Internals (.agents/skills/interpreter/SKILL.md) — The MIR interpreter, the comptime driver, and the residual AST expression folder
  • Symbol Resolution (.agents/skills/symres/SKILL.md) — How symbols are resolved; relevant for intrinsics::* namespace declaration and get_child_fn
  • Compiler API (.agents/skills/compiler_api/SKILL.md) — The C++ ASTNode/Value/BaseType hierarchy and lang/libs/compiler/ binding wrappers used when implementing intrinsics
  • Annotations (.agents/skills/annotations/SKILL.md) — AnnotationController, @test.* markers, get_tests/get_single_marked_decl_ptr data source
  • AST Framework (.agents/skills/ast_framework/SKILL.md) — FunctionDeclaration, StructDefinition, StructValue base classes and the ASTAllocator arena
  • CBI Plugin API (.agents/skills/cbi_plugin_api/SKILL.md) — How macro plugins (which heavily exercise get_raw_location, emit, raw, get_child_fn) are built
  • Intrinsics & Reflection (this file) — GlobalFunctions.cpp registration, all intrinsic implementations
  • LLVM Backend (.agents/skills/llvm_backend/SKILL.md) — LLVMBackendContext and the sret/atomic codegen intrinsics drive
  • C Codegen (2c) (.agents/skills/c_codegen/SKILL.md) — ToCBackendContext and emit/mem_copy/atomic behavior on the TCC backend

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

Comprehensive guide to the Chemical compiler's annotation system (`@name`, `@name(args)`) — how annotations are lexed, parsed, registered in the `AnnotationController`, stored and queried centrally, consumed by the compiler and CBI plugins, and how to add a new one. Load when working on annotations, `AnnotationController`, compiler plugins that define/read annotations, or node attribute flags such as `@extern`, `@test`, `@make`, `@deprecated`, `@no_mangle`, `@inline`, and `@volatile`.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

The C++ AST node framework of the Chemical compiler — the ASTAny/ASTNode/Value/BaseType hierarchy, node/value/type kind enums and range predicates, the visitor dispatch framework, the arena allocator, TypeBuilder canonicalization, member containers, and cross-reference resolution. Load when adding or modifying AST nodes, writing compiler passes/codegen visitors, debugging kind mismatches or arena/lifetime issues, or extending the compiler API bindings.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

The Chemical compiler benchmark & release analytics system — how the static GitHub Pages dashboard works, the JSON data model (manifest/daily/release records), the bash collection scripts (bench-*.sh), the GitHub Actions workflows (benchmark-daily/release), and the critical pitfalls (hidden upload dirs, worktree submodules, gh-pages-only data). Load when working on the dashboard, benchmark collection, or analytics workflows.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

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.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

All documentation related to building and running Project or Tests

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

Comprehensive guide to the C translation backend (2c) — how Chemical AST is translated to C code, key patterns, gotchas, and optimization strategies.

The English summary is being prepared. Showing the original description.

chemicallang/chemical31Updated Oct 26, 2026

See all skills by chemicallang

Report a problem with this skill