跳到正文
cccskills
免费在 GitHub 公开

Language Syntax

Documentation of Syntax and APIs for Chemical Programming Language, A must read before implementing something in chemical programming language.s

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

chemicallang/chemical星标 31(GitHub 31 + cccskills 0)
集成工具AWS

实测评分 未实测

查看安装方法

包含的文件(1)

  • SKILL.md51.1 KB

SKILL.md(原文)

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

Chemical Language Syntax Reference

This document provides a comprehensive reference for Chemical language syntax, designed to help AI systems generate correct Chemical code. All syntax described here is based on analysis of the parser implementation and existing codebase.

Table of Contents

  1. Core C-like Features
  2. Standard Library
  3. Advanced Features
  4. Async / Await
  5. Best Practices for AI Generation

Basic Structure

File Extensions

  • Source files: .ch
  • Module descriptor: chemical.mod

Module Declaration

module module_name

source "src"  // Include all .ch files in src directory (recursive)
import module_name  // Import other modules

Important: No import statements are required at the top of .ch files. Any .ch file can access anything in the current module and from any modules imported in chemical.mod. Import statements are only valid in build.lab or chemical.mod files.

Namespaces

Chemical supports C++-style namespaces for organizing types and functions and for avoiding name collisions.

Declaring a namespace

namespace cool {
    struct Pair2 {
        var a : int
        var b : int
    }

    func pair2_sum(p : *Pair2) : int {
        return p.a + p.b;
    }
}

Qualified access with ::

Use namespace_name::identifier to refer to types/functions inside a namespace from outside that namespace.

var p : cool::Pair2 { a : 0, b : 0 }
var x = cool::pair2_sum(&p)

Exposing a namespace to other modules

You must make the namespace public if you want to expose anything inside it to other modules. This is a hard rule. Without it you'll get symbol resolution errors. By default every namespace is internal, Everything inside it is only accessible from inside the same module.

Anything inside the namespace (struct/function/variant) must be also made public if you want to expose it to other modules.

Extending (re-opening) a namespace

The same namespace can appear multiple times in the same file; later blocks extend the namespace and can use earlier declarations.

namespace cool {
    func pair2_mul(p : *Pair2) : int {
        return p.a * p.b;
    }
}

Unqualified use from inside the same namespace

Within a namespace X { ... } block, names inside X can be referenced without X::.

namespace cool {
    func pair2_sum_call(p : *Pair2) : int {
        return pair2_sum(p);
    }
}

using declarations

Bring a single symbol into the current scope:

using closed_bro::bring_me_in;

func demo() : int {
    return bring_me_in();
}

Bring all symbols from a namespace into the current scope:

using namespace all_closed;

func demo2() : int {
    return check_im_closed();
}

Practical guidance (especially for codegen and edits)

  • Prefer qualification: Use X::name in generated code when possible; it reduces ambiguity and avoids name collisions.
  • Use using sparingly: using namespace X; is convenient, but it can hide where a symbol comes from and can create conflicts if multiple namespaces define the same identifier.
  • Be consistent when editing: A typo in the namespace name (e.g. cool vs coool) silently creates a different namespace and breaks lookups.
  • Type aliases in namespaces: Type aliases can live in a namespace and are referenced with :: the same way as types.
// Mutable variables
var name : type = value
var name = value  // Type inference

// Immutable constants
const name : type = value
const name = value  // Type inference

// Compile-time variables
comptime var name : type = value
comptime const name : type = value

Variable Initialization Rules

  • Local variables must be initialized at declaration
  • Global/top-level variables can be declared without initialization if type is specified
  • A variable declared without an initializer is uninitialized. It is written with plain var x : Type (NO unsafe keyword — the old unsafe var syntax was removed). Accessing an uninitialized variable (reading it, taking its address/reference, or writing a member/index) is an error unless wrapped in unsafe(...). A full assignment x = value is treated as first initialization. Struct member declarations are exempt. Global @extern declarations and vars inside an unsafe { } block are also exempt.
  • const variables must be initialized at declaration
  • Type inference available when value is provided

Examples

var x : int = 42
var message = "Hello, World!"
const PI = 3.14159
comptime var COMPTIME_VAL = 100

Basic Types

Primitive Types

// Chemical integer types
i8, i16, i32, i64, int128
u8, u16, u32, u64, uint128

// C-compatible types
char, short, int, long, longlong
uchar, ushort, uint, ulong, ulonglong

// Floating point
float, double, longdouble, float128

// Other types
bool, void, any

Pointers and References

var ptr : *int          // Pointer to int
var ptr_ptr : **int     // Pointer to pointer to int
var func_ptr : () => int // Function pointer
var ref : &int          // Reference to int

Common Mistake

Using const in pointer type is not recommended, for example

// Invalid Code (const doesn't exist)
var ptr : *const int

Every pointer is by default non-mutable, you use *mut int for mutability

*char in Chemical means const char* in C

How to take a pointer of an object

Use &raw obj or &raw mut obj to take a pointer or mutable pointer respectively

func i_take_ptr(ptr : *mut Point) {}
func i_send_ptr(p : Point) {
    i_take_ptr(&raw mut p)
}

How to take a reference

func i_take_ref(ptr : &mut Point) {}
func i_send_ref(p : Point) {
    i_take_ref(&mut p)
}

Arrays

var arr : [10]int       // Fixed-size array
var multi : [10][20]int // Multi-dimensional array

Control Flow

If Statements

if(condition) {
    // then block
} else if(other_condition) {
    // else if block
} else {
    // else block
}

// If as expression
// must not use braces to specify values
var result = if(condition) value1 else value2

Switch Statements

switch(value) {
    case1 => {
        // case 1 block
        // break statement NOT allowed
    }
    case2 => {
        // case 2 block
    }
    default => {
        // default block
    }
}

// Switch as expression
var result = switch(value) {
    case1, case2 => value
    default => default_value
}

Loops

// While loop
while(condition) {
    // loop body
}

// Do-while loop
do {
    // loop body
} while(condition)

// For loop
for(var i = 0; i < 10; i++) {
    // loop body
}

// Loop as expression
var result = loop {
    // loop body
    break value
}

Break and Continue

for(var i = 0; i < 10; i++) {
    if(condition) {
        break
    }
    if(other_condition) {
        continue
    }
}

Functions

Basic Function Definition

func function_name(param1 : type1, param2 : type2) : return_type {
    // function body
    return value
}

// Public functions (exposed to other modules)
public func public_function(param : type) : return_type {
    // implementation
}

Function Parameters

// Regular parameters
func func(param : type) : return_type

// Default values
func func(param : type = default_value) : return_type

// Variadic parameters, only for c extern defined functions
func func(param : type, ...) : return_type

Structs

Basic Struct Definition

// public before would expose to other modules
struct StructName {
    // Members with access specifiers
    public var member1 : int
    private const member2 : std::string
    
    // Default values
    // A member is by default public (like C++ struct)
    var member3 : float = 0.0
    
    // Methods
    func method_name(&self) : return_type {
        // method body
    }
}

How to initialize a struct

Chemical has three struct annotation combinations that affect initialization:

AnnotationsT{} works?T.make() works?T{field: val} works?
@direct_init onlyYes (all fields required)NoYes (all fields required)
@make onlyNoYesNo
@direct_init + @makeYes (all fields required)YesYes (all fields required)
NeitherYes (all fields required)NoYes (all fields required)

Critical: A struct with @make but without @direct_init cannot use {} syntax at all. The compiler errors with "struct has a constructor, use @direct_init to allow direct initialization". You must use T.make() instead.

// @direct_init only:
var s = StructName { member1 : 0, member2 : 0 }  // All fields required

// @make only:
var s = StructName.make()          // Works
// var s = StructName{}             // ERROR: struct has a constructor
// var s = StructName{member1: 0}  // ERROR: struct has a constructor

// @direct_init + @make:
var s = StructName{}               // Works (all fields zeroed)
var s = StructName{member1: 0}     // Works (all fields required)
var s = StructName.make()          // Works
Calling the constructor

When calling the constructor, You must not use the name of the function of the constructor, for example

// compiler will automatically determine the constructor
// like c++
var p = Point(10, 20)

If you use the name of the constructor function, chemical won't complain, but for example library author changes the name of the constructor, that would cause your project to break at compilation.

Creating a zeroed struct
// this will set all values to zero
// if the struct is destructible or has a constructor, this may not work, and compiler may force you to use annotations
var s = zeroed<StructName>()
Common Mistake

When initializing struct, members without a constructor or a default value MUST be initialized, otherwise there would be compilation errors.

vector<T>{} standalone init

vector<T>{} aggregate init only works inside @direct_init struct init context. Standalone var x = vector<T>{} fails. Use the constructor:

// WRONG (outside struct init):
// var file_data = vector<u8>{}

// CORRECT:
var file_data = vector<u8>()

Struct Members

  • Fields: var name : type or const name : type
  • Methods: func name(params) : return_type
  • Constructors: Use @constructor or @make annotation

Destructors

struct ManagedResource {
    var resource : *void
    
    @delete
    func destruct(&self) {
        unsafe {
            // dealloc statement just calls free under the hood
            // use dealloc instead of free, because it can be optimized out
            // when users are using a garbage collected runtime
            dealloc self.resource
            
            // even better would be using delete
            // delete ptr; which calls the destructor (if any) and then calls free
        }
    }
}

Access Specifiers

  • public
    • Exposed to other modules
  • private
    • Top Level decls only accessible in current file
    • Struct members/functions only accessible in current struct
  • internal
    • Only exposed to the current module
  • protected
    • Top level decls not exposed to other modules but public at link time
    • Struct members/functions visible to inheriting structs only

By default every top level decl (struct/function/variant/namespace) is internal

by default every struct member/function is public

Unsafe Code

Unsafe Blocks

func unsafe_operation() {
    unsafe {
        // Unsafe operations here
        var ptr = malloc(100) as *int
        *ptr = 42
        // dealloc statement just calls free under the hood
        dealloc ptr;
        // even better would be using delete
        // delete ptr; which calls the destructor (if any) and then calls free
    }
}

Memory Management

func manual_memory() {
    unsafe {
        var ptr = malloc(100) as *int
        *ptr = 42
        
        // Use pointer
        // dealloc statement just calls free under the hood
        dealloc ptr;

        // for structs use delete instead of dealloc
        // delete ensures that destructors are called
    }
}

New and Dealloc

func dynamic_allocation() {
    unsafe {
        var ptr = new int  // Allocate and construct
        *ptr = 42
        dealloc ptr      // Deallocate

        // for structs use delete instead of dealloc
        // delete ensures that destructors are called
    }
}

Uninitialized Variable Declarations

A variable declared without an initializer (e.g. var cmd : char[2048]) is uninitialized. This is written with plain var/const — the old unsafe var / unsafe const declaration syntax was removed and is now a hard parser error. Uninitialized declarations are commonly needed for buffers filled later (by sprintf, popen, read, etc.) or for intentionally zero-initialized module-level globals.

The compiler performs definite-assignment analysis. Accessing the variable while uninitialized is an error unless wrapped in unsafe(...) (expression) or unsafe { } (block):

// OK — uninitialized, declared WITHOUT `unsafe`
var cmd : char[2048]
var s : [32]u16

// ERROR: taking a pointer to an uninitialized variable
var p = &raw mut s
// FIX:
var p = unsafe(&raw mut s)

// A full assignment is first initialization (no destruction of garbage):
cmd = "command"

Exemptions:

  • Global @extern declarations (defined elsewhere).
  • var/const declared inside an unsafe { } block.
  • Struct member declarations (fields inside a struct/class body) — do NOT add unsafe there.

Compiler errors:

[TypeVerify] error: use of uninitialized variable 'x' before it is initialized (use of)

unsafe(expr) is a compile-time-only safety marker: it never changes runtime behaviour (including destruction). When migrating existing code, do not add unsafe to the declaration — instead give the variable an initializer, or wrap the specific access of the uninitialized value in unsafe(...).

Overview of dealloc, destruct and delete

  • dealloc just calls free
  • destruct calls destructor only (no free)
  • delete calls the destructor then cals free

Variants and Pattern Matching

Basic Variant Definition

variant VariantName {
    Case1(param1 : type1, param2 : type2)
    Case2(param : type)
    Case3()  // Empty case
}

Pattern Matching

Strict Rule: Pattern Matching only works with variants

Variable Pattern Matching
variant Option<T> {
    Some(value : T)
    None()
}

func get_value(opt : Option<int>) : int {
    var Some(value) = opt else -1
    return value
}
Pattern Matching with Return
func get_value_or_return(opt : Option<int>) : int {
    var Some(value) = opt else return 0
    return value
}
Pattern Matching with Unreachable
func unwrap(opt : Option<int>) : int {
    var Some(value) = opt else unreachable
    return value
}
Pattern Matching in If Statements
func check_option(opt : Option<int>) : bool {
    if(var Some(value) = opt) {
        return value > 0
    }
    return false
}
Switch Pattern Matching
func process_variant(variant : MyVariant) : int {
    switch(variant) {
        Case1(value) => {
            return value * 2
        }
        Case2 => return -1
    }
}
Type Testing with is
func is_some_case(opt : Option<int>) : bool {
    return opt is Option.Some
}

func is_none_case(opt : Option<int>) : bool {
    return opt is Option.None
}

Note: These core C-like features are sufficient to write good, performant Chemical code. Stick to these features for better reliability and performance.

Standard Library

The Chemical standard library provides essential data structures and utilities. Below are the key APIs that AI systems should know.

std::string

Important: You cannot use the + operator between std::string or *char. Chemical doesn't perform support addition of strings with +.

Constructor

var s = std::string()  // Empty string

Methods

// Append a single character
s.append('x')

// Append a string view
s.append_view(std::string_view("wow"))
s.append_view("hello world")  // string_view has implicit constructor for *char

// Append another std::string
s.append_string(other_string)

// Append another std::string (by converting to view)
s.append_view(other_string.to_view())

Note about destructible containers

Chemical does implicit moves, which means a variable becomes inaccessible if moved.

var s = std::string("hello world")
var y = s

// using s is invalid here
// s.append_view("x") <-- causes compilation error
// because std::string has a destructor
// you can copy a string explicitly using
// var copy = s.copy()

Usage Examples

var s = std::string()
s.append('H')
s.append_view("ello")
s.append(' ')
s.append_view("World")

// WRONG - This will not work:
// var result = s + " more text"  // + operator not overloaded

// CORRECT:
s.append_view(" more text")

std::string_view

A lightweight view into string data without ownership.

Constructor

var view = std::string_view("hello")  // From *char literal
var view2 = std::string_view(string_obj)  // From std::string

Usage

func process_text(text : std::string_view) {
    // Process text without copying
}

process_text("hello")  // Works due to implicit constructor

Searching

find(needle) / find_last(needle) take a &std::string_view and return size_t, using std::NPOS ((0 as size_t) - 1, i.e. SIZE_MAX) as the not-found sentinel:

var pos = haystack.find(&needle)
if(pos == std::NPOS) { /* not found */ }

Never compare the result against -1u. -1u is a 32-bit 4294967295; NPOS is a 64-bit SIZE_MAX. On a 64-bit target they are never equal, so a pos == -1u "not found" check silently falls through and the huge pos gets used as an offset/length — out-of-bounds reads and crashes. Always use std::NPOS (or (0 as size_t) - 1).

std::vector

Dynamic array container similar to C++ std::vector.

Constructor

var vec = std::vector<int>()  // Empty vector of int
var vec2 = std::vector<std::string>()  // Empty vector of strings

Methods

// Add elements
vec.push(42)
vec.push(100)

// Access elements
var first = vec[0]  // No bounds checking
var safe_first = vec.at(0)  // With bounds checking

// Size
var size = vec.size()

// Check if empty
var empty = vec.empty()

// Clear all elements
vec.clear()

// Reserve capacity
vec.reserve(100)  // Allocate space for 100 elements

Usage Examples

var numbers = std::vector<int>()
for(var i = 0; i < 10; i++) {
    numbers.push(i * i)
}

var strings = std::vector<std::string>()
strings.push(std::string())
strings[0].append_view("first")

std::unordered_map

Hash map container for key-value pairs.

Does not support [key] operator for lookups or assignment.

Warning: The iterator() method scans buckets — iteration order is not deterministic. Use ordered_map if you need predictable insertion-order iteration.

Constructor

var map = std::unordered_map<std::string, int>()
var map2 = std::unordered_map<int, std::string>()

Methods

// Insert key-value pairs
map.insert("key", 42)

// Access elements
var value = map.get_ptr("key")  // Returns a pointer, null if not found

// Check if key exists
var exists = map.contains("key")

// Remove elements
map.erase("key")

// Size
var size = map.size()

// Clear all elements
map.clear()

Usage Examples

var word_count = std::unordered_map<std::string, int>()
word_count.insert("hello", 1)
word_count.insert("world", 2)
if(word_count.contains("hello")) {
    word_count.insert("hello", (*word_count.get_ptr("hello")) + 1)
}

std::ordered_map

Insertion-order-preserving hash map. Same API as unordered_map but iteration follows insertion order via an internal doubly-linked list.

Key constraint: Key : Hashable + Eq (same as unordered_map).

How it works

  • Hash table (same bucket-chain structure as unordered_map) for O(1) lookups
  • Each node has order_prev / order_next pointers forming a linked list in insertion order
  • insert appends new keys to the tail; insert of an existing key updates the value without changing position
  • erase splices the node out of both the hash chain and the linked list
  • Iterator walks order_next from head to tail (no bucket scanning)
  • Reverse iterator walks order_prev from tail to head

Methods

Same as unordered_map: insert, find, contains, get_ptr, erase, clear, size, empty, isEmpty

Iteration

for(var entry in map)                 // insertion order
for(var entry, i in map)              // with index
for(var& entry in map)                // by reference
for(var entry in map reversed)        // reverse insertion order

Constructor

var map = std::ordered_map<std::string, int>()
var map2 = std::ordered_map<int, std::string>()

Usage Examples

var map = std::ordered_map<std::string, int>()
map.insert("first", 1)
map.insert("second", 2)
map.insert("third", 3)

// Iteration is guaranteed to yield: first→1, second→2, third→3
for(var entry in map) {
    // entry.key, entry.value
}

// Erase a key
map.erase("second")  // remaining order: first→1, third→3

// Update (position unchanged)
map.insert("first", 100)  // still at position 1

std:: Option and Result Types

The standard library provides commonly used variant types for error handling and optional values.

std::Option

// Already available in standard library
var opt = std::Option<int>.Some(42)
var empty = std::Option<int>.None()

// Use with pattern matching
var Some(value) = opt else -1

std::Result

// Already available in standard library  
var success = std::Result<int, std::string>.Ok(42)
var error = std::Result<int, std::string>.Err("failed")

// Use with pattern matching
var Ok(value) = success else -1

Performance Guidelines

  1. *Prefer char for simple strings - Use *char for C-style string operations when possible
  2. Use string_view for read-only access - Avoid copying when you just need to read string data
  3. Reserve vector capacity - Pre-allocate memory when you know the expected size
  4. Avoid excessive string concatenation - Use append_view() instead of + operator
  5. Use appropriate containers - Choose vector for sequential access, unordered_map for key-value lookup
  6. Do not try to use moved variables - Chemical performs implicit moves of containers having destructors

Advanced Features

These features provide additional functionality but should be used judiciously. For most use cases, the core C-like features and standard library are sufficient.

Generics

Generic Type Parameters

struct GenericStruct<T> {
    var value : T
}

func <T> generic_func(param : T) : T {
    return param
}

Generic Constraints

func <T : SomeInterface> constrained_func(param : T) : T {
    return param
}

Default Type Parameters

struct DefaultGeneric<T = int> {
    var value : T
}

func <T = std::string> default_generic_func(param : T) : T {
    return param
}

Generic Instantiation

var int_struct = GenericStruct<int> { value : 0 }
var string_struct = GenericStruct<std::string>()
var default_struct = DefaultGeneric{ value : 0 }  // Uses int as default

Lambda Functions

When writing a function type, You should use colon arrow '=>' before the return type

var lambda : (param : type) => return_type = null ✅

When writing the lambda as an expression, You must not write return_type after =>

var lamb = () => int { return 33 } ❌

var lamb = () : int => 33 ✅

var lamb = () : int => { return 33 } ✅

Return type determined from the returned expression

var lamb = () => 33 ✅

var lamb = () => { return 33 } ✅

Lambda with return type

Here's a lambda with a function type

var lamb : () => int = () => 33;

Basic Lambda

var lambda = (param : type) : return_type => {
    return param * 2
}

The return_type after the colon is optional, You can do this

var lambda = (param : type) => {
    return param * 2
}

Capturing Lambda

When using capturing lambda, a container type like std::function is required, The lambda decays into that container type Without the container type, capturing of variables is not possible.

var x = 10
var capturing_lambda : std::function<(p : int) => int> = |x|(param : int) : int => {
    return param + x
}

Lambda without Parameters

var no_params = () : int => {
    return 42
}

Function Types

// Function pointer type
var func_ptr : (a : int, b : std::string) => bool

// Lambda type with capture - only capturing functions can capture variables
var lambda_type : std::function<(param : type) => return_type>

Annotations

Built-in Annotations

@deprecated          // Marks item as deprecated
@extern             // External definition
@no_mangle          // Don't mangle name
@static             // Static member
@implicit           // Implicit parameter
@constructor        // Constructor method
@test               // Test function
@inline             // Inline function
@noinline           // Don't inline
@volatile           // Mark variable as volatile (prevents compiler optimization)
@make               // Enable T.make() constructor (struct)
@direct_init        // Enable T{} and T{field: val} syntax (struct)
@delete             // Destructor method (struct)
@compiler.interface // CBI interface exposed to compiler

Annotation Usage

@deprecated("Use new_function instead")
func old_function() {
    // implementation
}

@extern public func printf(format : *char, ...) : int

@constructor
func init(&self, param : int) {
    // constructor implementation
}

@test
func test_feature() : bool {
    return true
}

Conditional Compilation

comptime if(def.windows) {
    // windows specific code
    // the syntax is still checked
    // however type checking is skipped if def.windows is false
}

Inline Assembly

Chemical supports GCC extended inline assembly via the asm keyword:

// Simple form — template only
asm("nop")

// Extended form — with outputs, inputs, clobbers
var old_val : u64 = 0
var success : bool = false
asm("lock cmpxchgq %3, %2\n\tsete %1"
    : "=a"(old_val), "=q"(success), "+m"(*ptr)
    : "r"(desired), "0"(old_val)
    : "cc", "memory")

// Compiler barrier (no hardware fence, just prevents reordering)
asm("" ::: "memory")

Syntax: asm("template" : outputs : inputs : clobbers)

  • Outputs: "constraint"(expr) — where asm writes results. "=a" = rax, "=q" = any GP reg, "+m" = read/write memory
  • Inputs: "constraint"(expr) — values passed to asm. "r" = any reg, "0" = same reg as operand 0, "m" = memory
  • Clobbers: "memory", "cc" — tells compiler what asm may trash

Important: asm(...) is a statement, not an expression. It cannot be used inline in expressions. Use output operands to write to local variables, then use those variables.

Pattern for @retained functions with inline asm:

@retained func my_asm_helper(ptr : *mut u64, val : u64) : u64 {
    var old_val : u64 = 0
    asm("lock xaddq %0, %1" : "=r"(old_val), "+m"(*ptr) : "0"(val) : "cc", "memory")
    return old_val
}

Called from comptime functions via %runtime_value(func(...)):

public comptime func my_operation(x : %runtime<*mut u64>, val : u64) : u64 {
    comptime if(intrinsics::get_backend_name() == "C") {
        return %runtime_value(my_asm_helper(x, val)) as u64
    }
}

Compile-time Features

Compile-time Functions

// only if function can run at compile time
comptime func calculate_at_compile_time() : int {
    return 42 * 2
}

const RESULT = calculate_at_compile_time()

Compile-time Conditionals

comptime if(condition) {
    // Compile-time branch
} else {
    // Other compile-time branch
}

Interoperability

C Function Declarations

@extern public func printf(format : *char, ...) : int
@extern public func malloc(size : usize) : *void
@extern public func free(ptr : *void) : void

C Struct Definitions

@extern
struct CStruct {
    var field1 : int
    var field2 : *char
}

Type Casting

var int_val = 42
var float_val = int_val as float
var ptr_val2 = &raw mut int_val as *mut float

Move Semantics

func move_example() {
    var original = MyStruct()
    // implicit moves
    var moved = original  // Move ownership
    
    // original is no longer valid here
}

Generic Structs and Variants

Generic Structs

struct GenericStruct<T, U> {
    var value1 : T
    var value2 : U
    
    func <V> generic_method(param : V) : V {
        return param
    }
}

Generic Variants

variant GenericVariant<T> {
    Some(value : T)
    None()
}

variant MultiGeneric<T, U> {
    First(a : T, b : U)
    Second(value : T)
    Empty()
}

Variant with Default Type Parameter

variant VariantWithDefault<T = int> {
    Some(value : T)
    None()
}

Variant Methods

variant MyVariant {
    Case1(value : int)
    Case2(text : std::string)
    
    func get_value(&self) : int {
        switch(self) {
            Case1(value) => return value
            Case2 => return -1
        }
    }
}

is / !is

x is SomeVariant.Member is a runtime check of the variant's discriminant, so it only means something when the right-hand side denotes a variant member:

if(value is MyVariant.Case1) { ... }
if(value !is MyVariant.Case1) { ... }

Against any type with no tags to read — a primitive, a pointer, a function type — the answer is decided at compile time instead of crashing the code generator: false for is and true for !is (nothing can ever be an instance of that type). T is U where the left side is a type is a separate compile-time identity/extends comparison.

Async / Await

Chemical has first-class async/await built on a Future/Poll/Waker protocol. async is additive: every synchronous API keeps working unchanged, and an async variant gets an _async suffix (X::foo → X::foo_async).

Declaring and awaiting

async func fetch(x : int) : int {
    var a = await work(x)     // await a FutureHandle<int> → int
    return a + 1              // the body returns T, not a handle
}

public func main() : int {
    return async::block_on<int>(fetch(41))
}
  • async func f(...) : T is a normal function whose static type is FutureHandle<T> — symres wraps the declared return type. You never write FutureHandle<T> yourself unless hand-authoring the ABI.

  • await expr evaluates a FutureHandle<T> to T, suspending the task when the future is not ready. It may appear only inside an async func (or an async closure/block); anywhere else is a compile error (await can only be used inside an async function, async closure, or async block).

  • await as a bare statement is not parsed. Always bind the result:

    await fut()            // WRONG (statement form does not parse)
    var v = await fut()    // CORRECT
    
  • Async methods are allowed. Async destructors and @extern async functions are rejected (@extern async must declare an explicit FutureHandle<T> ABI). A void async function's result maps to the zero-sized core::async::Unit.

  • async func main() : int (optionally main(argc, argv)) gets a synchronous int main trampoline that drives the future to completion; int/void/Unit results and eager (no-await) entries all work.

  • A module that declares async func must import async — that library supplies chemical_async_frame_alloc/_free. core alone only has the protocol types.

The protocol (core::async, re-exported by async)

import async re-exports the compiler-facing protocol through namespace async:

TypeMeaning
Poll<T>Poll.Ready(v) / Poll.Pending()
Future<T>interface: func poll(&mut self, cx : *mut Context) : Poll<T>
FutureHandle<T>move-only runtime future (frame + FutureTable<T>*); its @delete cancels — frame destructors run exactly once
FutureTable<T>vtable: poll(frame, cx), drop(frame)
Context / Waker / WakerVTabletask wake callback
Unitzero-sized result of a void future

std::concurrent::Future<T> (thread-pool) is a different type — do not confuse it with core::async::Future<T>. Use spawn_blocking to bridge.

Runtime API (import async → async::…)

APIPurpose
block_on<T>(h) : Trun the executor until h is ready and return its value
spawn<T>(h) : FutureHandle<T>enqueue a task; await the returned join handle for its result
spawn_local<T>(h) : FutureHandle<T>enqueue on the current/UI-thread executor
spawn_blocking<T>(f : std::function<() => T>) : FutureHandle<T>run blocking work on the shared thread pool
sleep(ms : u64) / yield_now()timer / yield future (FutureHandle<Unit>)
select<T>(a, b)race two same-typed futures
timeout_or<T>(h, ms, fallback) : FutureHandle<T>resolve to fallback on timeout
block_on_timeout<T>(h, ms) : std::Option<T>synchronous timed wait
channel<T>() : Channel<T>mpsc (sender/receiver, send, try_recv, awaitable recv_or(fallback), clone_sender)
readable(fd) / writable(fd) / AsyncFdfd readiness for CRT file descriptors (POSIX select(2); Windows probes with PeekNamedPipe). Windows net sockets do not use this — they go through IOCP (see below)
async::test::block_on<T>(h)deterministic executor used by tests

Usage: var ch = async::channel<int>(); ch.sender.send(7); var v = await ch.receiver.recv_or(-1). select/timeout_or require both futures to have the same payload type (composite-generic vtables are not supported yet, B20).

Library _async surface

  • net::AsyncSocket, net::async_listener, dial_async/accept_async/recv_async/send_async.
  • tls::{tls_connect_async, ssl_handshake_async, ssl_read_async, ssl_write_async, tls_accept_async}.
  • http::{request_async, get_async, post_async, put_async, patch_async, delete_async, head_async}; http::server::serve_coro. Async client results are a caller-owned *mut HttpResult (is_ok/status_code/body_view/error_view) — delete it.
  • fs::{read_entire_file_async, write_text_file_async, atomic_write_async}.
  • process::{execute_async, spawn_async, wait_async, try_wait_async, kill_process_async, write_stdin_async, close_stdin_async, is_running_async, sleep_ms_async}.
  • environment::{get_async, set_async, unset_async}.
  • server::serve_files_async.
  • UI: window::window_run_async/window_pump, webview::webview_run_async. These drive the executor on the GTK/Win32 loop, so a continuation after await resumes on the UI thread. Wrap blocking work in spawn_blocking.

Gotchas and current limits

  • Await a stored handle by value: var j = async::spawn(...); var v = await j is correct (the compiler clears j's drop flag). Never await the same handle twice.
  • Future payloads: ints, pointers, variants, plain structs, and large struct variants (Result<Response, std::string>) all work — B23 and B26 were fixed. Remaining LLVM/2c limits, worked around in the libraries but not fixed: reading a struct-typed parameter in the body (B24, 2c), awaiting a combinator inside a task polled on the executor (B25), and composite-generic vtable types like FutureTable<Result<T, E>> (B20). The shipped libraries still pass raw fds/handles and return flat pointer boxes; follow that pattern when hand-authoring a Future.
  • Async closures (async |x|(...) => { ... }) are parsed but not lowered — the compiler diagnoses them (async closures are not yet supported; use a named async func instead). Use a named async func.
  • No interpreter/comptime async: the interpreter treats await as transparent (it just evaluates the inner expression). Futures that are not ready on the first poll cannot suspend at comptime.
  • A blocking call left inside an async func stalls the executor; route it through spawn_blocking.

These limits are tracked, actionable work. See lang/docs/async-remaining-work.md for each item's symptom, root cause, current workaround, and definition of done (B20/B24/B25, async debug info, async closures, TLS transport, POSIX epoll). B23 and B26 are fixed.

Platform note (Windows): net's async accept/recv/send are driven by a process-wide IOCP completion port (lang/libs/net/win/iocp.ch), not the thread pool and not the CRT-fd reactor: each op posts an overlapped WSARecv/WSASend/AcceptEx and a dispatcher thread wakes the awaiting coroutine on completion. Async TLS rides on top of that, so it is event-driven on Windows too. Only dial_async (blocking connect) uses spawn_blocking.

Library Development Gotchas

These patterns were discovered while implementing pure-Chemical libraries.

Struct initializers use exact, case-sensitive field names

T { field: value } matches members by name (case-sensitive). A wrong name/case is a symres error, not a silent default:

struct GUID { var Data1 : DWORD; var Data4 : [8]BYTE }
// ERROR: couldn't find value for member 'Data1'
// var g : GUID = GUID { data1 : 1, data4 : [...] }
var g : GUID = GUID { Data1 : 1, Data4 : [0x8C as BYTE, /* ... */] }   // OK

Array elements must match the field's element type too — [8]int does not satisfy [8]BYTE, so cast each element (0x8C as BYTE).

C ABI / COM vtable structs must match the exact field order

When a C/COM vtable is modelled as a struct of function pointers and called through, declaration order is the ABI: every slot must be present, in the exact SDK order, or a call invokes the wrong function. For COM, the IUnknown slots (QueryInterface, AddRef, Release) come first, then base-interface methods, then derived ones. Don't bolt on suffixed workarounds (SetClientGuid2) — insert the real method at its correct index. The receiver is passed as the first argument: obj.vtbl.Show(obj as *mut void, hwnd).

.data() Returns Immutable Pointer

vector<T>.data() returns *T (immutable). To write through the pointer, cast to *mut T:

var sptr = vec.data() as *mut i16
sptr[0] = 10000
sptr[1] = -10000

.size() Not .length()

string and vector use .size() — there is no .length() method.

Float Literals

0.5 is double in Chemical. For float parameters, use 0.5f:

public func audio_volume(audio : *mut Audio, factor : float) { ... }
audio_volume(&raw mut a, 0.5f)   // WRONG: 0.5 is double
audio_volume(&raw mut a, 0.5)    // TypeCheck error: double does not satisfy float

if Expressions Not Inline

Chemical does not support if as an expression inline in function arguments:

// WRONG:
// var max_samples = if(a > b) { a } else { b }

// CORRECT:
var max_samples : size_t
if(a > b) { max_samples = a } else { max_samples = b }

if Requires else

Every if block requires an else block. No bare if without else.

Variant-Captured Strings: append_string(&msg)

When capturing a string from a variant pattern, pass it by reference to append_string:

switch(self) {
    InvalidFormat(msg) => {
        var s = string("Error: ")
        s.append_string(&msg)    // NOT append_string(msg)
        return s
    }
}

using Declarations in Test Files

Test files at global scope need using declarations to access standard library types:

using std::Result;    // For Result.Err / Result.Ok patterns
using std::string;    // For string() constructor
using std::vector;    // For vector<T>() constructor

Internal Functions Not Accessible Across Packages

Functions without public (internal by default) cannot be called from test modules in other packages. Mark test-relevant helper functions public:

// archive/src/endian.ch — WRONG: internal, can't be called from tests
// func read_u16_le(data : *u8, offset : size_t) : u16 { ... }

// CORRECT:
public func read_u16_le(data : *u8, offset : size_t) : u16 { ... }

memcpy With Pointer Variables

When a variable is already *mut T, never use &raw mut var with memcpy. &raw mut var takes the address of the pointer variable itself (on the stack), not what it points to:

func zip_find_entry(output : *mut ArchiveEntry, ...) {
    // WRONG — &raw mut output is *mut (*mut ArchiveEntry), stack corruption!
    // memcpy(&raw mut output, &raw entry, sizeof(ArchiveEntry))

    // CORRECT — output is already *mut ArchiveEntry:
    memcpy(output, &raw entry, sizeof(ArchiveEntry))
}

Result Pattern in @test Functions

@test
public func my_test(env : &mut TestEnv) {
    var result = some_func()
    if(result is Result.Err) { env.error("should succeed"); return }
    var Ok(value) = result else unreachable
    // Now use value safely
}

No const in Pointer Types

*const T is not valid Chemical. Use *T for immutable pointers.

Right Shift Type Matching

The right operand of shift operators must match the left operand type:

var val : u32 = 128u
var shifted = val >> 2u   // CORRECT: both u32
// var shifted = val >> 2  // WRONG: int does not satisfy uint

Best Practices for AI Generation

Core Principles

  1. Write C-like code for performance - Chemical is very C-like, strings are *char. For performant code, stick to C-like features with Chemical syntax.
  2. Use structs and destructors for better API - Combine C-style performance with modern safety through destructors.
  3. Add variants and pattern matching selectively - Use these for specific modern features, but don't overuse them.
  4. No magic behind the scenes - Chemical doesn't perform hidden allocations.

Bad AI Patterns to Avoid

  1. Don't use + operator with strings - std::string and *char don't support + operator. Use append() functions specifically designed.
  2. No import statements in .ch files - Import statements are only valid in build.lab or chemical.mod files.
  3. No defer statements - Chemical doesn't support defer (may be added in future).

Code Quality Guidelines

  1. Always specify types for struct members - Don't rely on inference in struct definitions
  2. Use proper access specifiers - Be explicit about public/private/internal
  3. Initialize variables - Local variables must be initialized at declaration
  4. Handle all variant cases - When using switch with variants, cover all cases
  5. Use pattern matching - Prefer pattern matching over manual variant checking
  6. Mark unsafe code - Always wrap unsafe operations in unsafe blocks
  7. Include proper annotations - Use @extern for C functions
  8. Follow naming conventions - Use PascalCase for types, camelCase for functions
  9. Handle memory properly - Use destructors for resource cleanup
  10. Use generics appropriately - Don't over-genericize without need

Performance Guidelines

  1. Prefer core C-like features - For best performance, stick to the basics
  2. Use pointers when appropriate - Chemical supports pointers for low-level control
  3. Leverage destructors for RAII - Get modern safety with C-like performance
  4. Use variants judiciously - Great for certain use cases but not a replacement for all enums
  5. Standard library when beneficial - Use std containers when they provide clear advantages

Critical Gotchas (From Implementation)

1. &key in parameter means reference, not address-of

When a function signature uses &Key as a parameter type, it means reference to Key — pass the value directly, don't prefix with &:

func contains(&self, key : &Key) : bool    // key is a reference

// CORRECT:
map.contains(10)

// WRONG — taking address of r-value literal:
map.contains(&10)   // ERROR: cannot apply operator '&' to r-value

The same applies to erase(key), get_ptr(key), find(key, &mut out).

Only use explicit & when the parameter type is a raw pointer (*Key or *mut Key).

1b. &arr[0] gives a reference, not a pointer — use &raw for raw pointers

Array indexing arr[i] produces a reference to the element (&T), not a raw pointer (*T). To get a raw pointer for passing to C functions, use &raw or &raw mut:

var buf : char[2048]

// WRONG — produces &char, not *char:
// popen(&buf[0], "r")           // TypeCheck error: &char does not satisfy *char
// sprintf(&buf[0], "%s", arg)   // TypeCheck error: &char does not satisfy *mut char

// CORRECT — use &raw for raw pointers:
var pipe = popen(&raw buf[0], "r")          // *char (immutable)
sprintf(&raw mut buf[0], "%s", arg)         // *mut char (mutable)

This applies to all C interop (sprintf, popen, fgets, fwrite, fopen, etc.) and any function expecting *char or *mut char.

1c. String literals are *char (immutable), arrays need &raw mut for *mut char

String literals (e.g., "hello") have type *char and can be passed directly to functions expecting *char. However, char arrays are mutable local variables and require &raw mut to get *mut char:

@extern public func sprintf(buf : *mut char, fmt : *char, ...) : int

var s = "hello"              // *char — pass directly
var buf : char[64]
sprintf(&raw mut buf[0], "%s", s)  // buf needs &raw mut for *mut char

2. Multi-line expressions break inside lambdas

The parser errors on expressions spanning multiple lines inside a lambda body:

// WRONG — parser error:
test("name", () => {
    return a == 1 && b == 2
        && c == 3           // ERROR here
})

// CORRECT — use if/return statements:
test("name", () => {
    if(a != 1) return false
    if(b != 2) return false
    if(c != 3) return false
    return true
})

3. reversed is a keyword — cannot use as field name

The reversed keyword used in for(var entry in map reversed) conflicts with user-defined identifiers. If you name a struct field reversed, the compiler errors:

error: couldn't find value for member 'reversed'

Use a different name like reverse_dir or avoid the name entirely.

4. vector<T> does NOT support operator[]

You cannot use vec[0], vec[1], etc. to index into a std::vector. There is no index operator overload. To access elements, use .get(i) or .get_ptr(i):

var vec = std::vector<int>()
vec.push(10)
vec.push(20)

// WRONG:
// var x = vec[0]     // ERROR

// CORRECT:
var x = vec.get(0)
var px = vec.get_ptr(0)   // returns *mut int

.get() on an element type with a destructor (std::string, structs) is a trap. It returns a bitwise copy that shares the element's heap buffer; when the temporary goes out of scope its destructor frees that buffer, and the std::vector later frees it again → double-free / heap corruption (STATUS_HEAP_CORRUPTION, c0000374). Use .get_ptr(i) for anything with a destructor:

// WRONG — copies + destroys shared buffer
// var s = vec_of_strings.get(i)
// var view = s.to_view()

// CORRECT
var view = vec_of_strings.get_ptr(i).to_view()

5. Implementing for-in requires Iterable/ReversibleIterable

To make a custom type iterable with for(var x in obj), implement core::iterable::Iterable<T, Cursor>:

impl core::iterable::Iterable<MyNode, MyCursor> for MyStruct {
    func begin(&self) : MyCursor { ... }
    func valid(&self, c : MyCursor) : bool { ... }
    func current(&self, c : MyCursor) : &MyNode { ... }
    func next(&self, c : MyCursor) : MyCursor { ... }
}

For reversed support, additionally implement ReversibleIterable:

impl core::iterable::ReversibleIterable<MyNode, MyCursor> for MyStruct {
    func rbegin(&self) : MyCursor { ... }     // start from end
    func previous(&self, c : MyCursor) : MyCursor { ... }  // walk backward
    func count(&self) : size_t { ... }
}

The cursor struct does not need a reversed flag — next() is used for forward iteration, previous() for reverse. They are never called in the same loop.

6. Hash table + linked list pattern for ordered maps

To build an insertion-order-preserving map:

  • Each node has separate pointers for the hash chain (hash_next) and the order chain (order_prev / order_next)
  • head / tail pointers on the map track the ordered list
  • insert appends new nodes to the tail (tail.order_next = newNode; tail = newNode)
  • erase splices the node out of the order list (prev.order_next = next; next.order_prev = prev)
  • resize rehashes by walking the order list (not the buckets), preserving the linked list pointers unchanged
  • The @delete destructor walks the order list (not buckets) for cleanup, then frees the table

Common Patterns

to_view pattern

This is invalid code

take_view(create_string().to_view())

with code like this you get error

error: function call is on a temporary that is destroyed at expression end, please store the temporary in a variable

To fix you must use

var str = create_string()
take_view(str.to_view())

Option Type Pattern

std::Option type is available in standard library

variant Option<T> {
    Some(value : T)
    None()
}

func <T> unwrap_or(opt : Option<T>, default : T) : T {
    var Some(value) = opt else default
    return value
}

Result Type Pattern

std::Result type is available in standard library

variant Result<T, E> {
    Ok(value : T)
    Err(error : E)
}

func <T, E> map_result(result : Result<T, E>, mapper : (T) => T) : Result<T, E> {
    switch(result) {
        Ok(value) => Result.Ok(mapper(value))
        Err(error) => Result.Err(error)
    }
}

Builder Pattern

struct Builder {
    var field1 : int = 0
    var field2 : std::string = ""
    
    func with_field1(value : int) -> Builder {
        field1 = value
        return this
    }
    
    func with_field2(value : std::string) -> Builder {
        field2 = value
        return this
    }
    
    func build() -> MyStruct {
        return MyStruct(field1, field2)
    }
}

This syntax reference provides the foundation for generating correct Chemical code. The language emphasizes safety, performance, and C interoperability while maintaining modern language features.

评价

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

评论

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

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

提交后会立即公开。请勿填写邮箱、电话等个人信息。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 的全部技能

报告此技能的问题