跳到正文
cccskills
免费在 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.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical星标 31(GitHub 31 + cccskills 0)

实测评分 未实测

查看安装方法

包含的文件(1)

  • SKILL.md41.0 KB

SKILL.md(原文)

安装前可以查看交给 AI 代理的指令内容。

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

评价

还没有评价。欢迎分享你的使用感受。

评论

无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。

还没有评论。来写第一条吧。

提交后会立即公开。请勿填写邮箱、电话等个人信息。URL 最多 2 个。

0 / 1000 字

此表单受 reCAPTCHA 保护,适用 Google 的隐私权政策和服务条款。

同一仓库的技能

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`.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

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.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

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.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

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.

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

Building

免费

All documentation related to building and running Project or Tests

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

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

中文简介正在准备中。当前显示原文说明。

chemicallang/chemical312026年10月26日 更新

查看 chemicallang 的全部技能

报告此技能的问题