Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Welcome to Chemical, a modern, statically typed systems programming language with a clean syntax and seamless interoperability with C libraries.

  • File extension: .ch
  • Module descriptor: chemical.mod

In this book, you will learn how to:

  1. Scaffold a new Chemical project
  2. Write and organize your source code in Markdown-friendly files
  3. Import and use modules (including the C standard library via cstd)
  4. Build and run your Chemical applications

Grab a chemical binary

You should download chemical executable from the main page or from github releases

There are two types of chemical binaries:

1 - based on LLVM & Clang (generated code is faster)

2 - based on Tiny CC (compilation is a little faster)

Both are capable of generating C code and running it and producing binaries from it

Hello, World!

  1. Create a directory for your project and enter it:

    mkdir hello_chemical && cd hello_chemical
    
  2. Create a src/ folder and add main.ch:

    src/main.ch
    
  3. In src/main.ch, write your first program:

    public func main() : int {
        printf("Hello, World!\n");
        return 0;
    }
    
  4. Create the chemical.mod file at the project root to describe sources and dependencies:

    chemical.mod
    
    module main
    
    # Tell the compiler to include all .ch files in `src/`
    source "src"
    
    # Import the C standard library module
    import cstd
    

Your project structure now looks like:

hello_chemical/
├── chemical.mod
└── src/
    └── main.ch
  1. Build and run your program:
  • Windows:

    chemical.exe chemical.mod -o main.exe --mode debug_complete
    .\main.exe
    
  • Linux/macOS:

    chemical chemical.mod -o main --mode debug_complete && ./main
    

Tip: If you prefer not to import cstd, you can declare extern functions inline:

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

That's it! You’ve just compiled and run your first Chemical program. In the next chapter, we’ll dive deeper into project structure and configuration.

Language Overview

Chemical is a native, statically‑typed programming language designed to be safe and easy to use while giving developers low‑level control. If you’re familiar with languages like TypeScript, Rust, or C, many of Chemical’s constructs will feel familiar.

This chapter provides a concise guide to Chemical’s core syntax and features, including:

  • Variable and constant declarations

  • Primitive and compound types

  • Functions and generics

  • Comments and documentation

  • Control‑flow constructs

  • Enumeration and variant types

  • Data structures: arrays, structs, and interfaces

  • Namespaces, extension methods, and unsafe blocks

  • Type aliases


Variables & Constants

Use var for mutable bindings and const for immutable:

var first = 0       // mutable
const second = 1    // immutable

Optionally, annotate types with : Type:

var count: int = 42
const name: *char = "Chemical"

Built‑In Types

Chemical provides a range of primitive types:

  • Boolean & Characters: bool, char, uchar
  • Integers: short, ushort, int, uint, long, ulong
  • Arbitrary‑Precision: bigint, ubigint
  • Floating Point: float, double
  • Function Type: (a : int, b : int) => int

Pointers and reference types also exist

  • Pointer Types: *int, *mut int
  • Reference Types: &int, &mut int

These types form the foundation for more complex data structures and interop with C via cstd.


Functions

Functions in chemical start with func keyword, Here's a function that computes sum of two integers

func sum(a : int, b : int) : int {
    return 10;
}

Extension methods would be discussed below after structs

Lets now see a generic function

func <T> print(a : T, b : T) : T {
    return a + b;
}

Calling function pointers

func call_it(lambda : () => int) : int {
    return lambda()
}

Comments

Chemical supports both single line and multiline comments

a single line comment starts with two forward slashes

// Here's my single line comment

multi line comments start with /* and end with */

/* 
    Here's my multi line comment
*/

Multi line Comments cannot be nested


Control Flow

Chemical supports a rich set of control constructs:

  • if / else

    if (condition) {
        // then‑branch
    } else {
        // else‑branch
    }
    
  • loop (infinite)

    loop {
        // runs until `break`
    }
    
  • for

    for (var i = 0; i < 10; i++) {
        printf("Iteration %d\n", i)
    }
    
  • while

    while (someCondition) {
        // ...
    }
    
  • do while

    do {
        // ...
    } while (someCondition)
    
  • switch

    switch (thing) {
        1        => { /* … */ }
        2        => { /* … */ }
        3, 4, 5  => { /* … */ }
        default  => { /* … */ }
    }
    

Null Value

In C++ there's nullptr keyword which allows you to quickly check a pointer if it's null, similarly we have null keyword

if(pointer == null) {
    // the pointer is null here
}

Enums

Enumerations (enums) are declared with the enum keyword and are fully scoped:

enum Fruits {
    Mango,
    Banana,
}

Access them by qualifying with the enum name:

let f = Fruits.Mango   // ✅
let g = Mango          // ❌ invalid: must write `Fruits.Mango`

Arrays

Array can hold multiple values and provide indexed access, Just use [] to create an array


var arr = [ first_value, second_value ]
var first_value = &arr[0] // pointer to the first value


Structs

Structs hold grouped data and methods. Use public to make them visible across modules:

public struct Point {
    var x: int
    var y: int

    func sum(&self): int {
        return x + y
    }
}

Lets create an object of this struct and call sum on it

var point = Point { x : 10, y : 20 }
var sum = point.sum()

You can omit the type when it can be inferred

func create_point() : Point {
    return { x : 10, y : 20 }
}

Constructors and Destructors

Chemical provides a way to write constructors and destructors for a struct, here the function that has annotation @make is a constructor and function with annotation @delete is a destructor

struct HeapData {

    var data : *void

    @make
    func make() {
        data = malloc(sizoef(Data))
    }

    @delete
    func delete(&self) {
        free(data)
    }

}

Inheritance

You can use inheritance to build struct definitions

struct Animal {}

struct Dog : Animal {}

struct Fish : Animal {}

You can inherit a single struct, can implement multiple interfaces

Extension Methods

Add methods after the struct definition:

func (p: &Point) div(): int {
    return p.x / p.y
}

Extension methods only support reference types that point to a container (struct / variant / static interface)


Interfaces

Define an interface of method signatures:

interface Printer {
    func print(&self, a: int)
}

Implement it in two ways:

Inline

struct ImplPrinter : Printer {
    @override
    func print(&self, a: int) {
        printf("Printed: %d", a)
    }
}

impl Block

impl Printer for ImplPrinter {
    func print(&self, a: int) {
        printf("Printed: %d", a)
    }
}

Static Interfaces

interfaces can be made static using @static annotation above them, This means interfaces will be implemented once

@static
interface Organizer {
    func organize(&self)
}

Extension methods are only possible on static interfaces, Normal interfaces cannot support extension methods


Namespaces

Similar to c++ namespaces

public namespace mine {
    
    public var global_variable : int = 0

    public struct Point {
        var x : int
        var y : int
    }

}

Access members like this

func temp() {
    var p = mine::Point { x: 10, y: 10 }
}

Using statement

You can use the using keyword to bring symbols for a namespace into current scope

using namespace std;

or just a single symbol

using std::string_view

Variants & Pattern‑Matching

Variants are tagged unions:

variant Optional {
    Some(value: int),
    None(),
}

Create and return them:

func create_optional(condition: bool): Optional {
    if (condition) {
        return Optional.Some(10)
    } else {
        return Optional.None()
    }
}

Match on them with switch (no case keyword):

func check_optional(opt: Optional) {
    switch (opt) {
        Some(value) => { printf("%d", value) }
        None        => { /* nothing */ }
    }
}

You can easily check a variant using is keyword

func is_this_some(opt: Optional): bool {
    return opt is Optional.Some
}

Members can be extracted easily and safely using this syntax

func get_value() : int {
    // default value is -1 if its not `Some`
    var Some(value) = opt else -1
    printf("the value is : %d\n", value);
}

It supports different else cases

func print_value() {
    // return early without printing if its not `Some`
    var Some(value) = opt else return;
    printf("the value is : %d\n", value);
}

func get_value() : int {
    // compiler assumes its always `Some`
    var Some(value) = opt else unreachable;
    printf("the value is : %d\n", value);
}

Unsafe Blocks

By default, Chemical enforces safety checks. To perform unchecked or low‑level operations, wrap code in unsafe:

unsafe {
    var value = *ptr    // raw pointer dereference
}

The compiler will emit an error if you attempt pointer dereference or other unsafe ops outside of an unsafe block.


Type statements (typealias)

Type alias allows us to alias a type, Lets see

type MyInt = int

Now you can use MyInt instead of int

func check_my_int(i : MyInt) : bool

Generics

In this chapter we explore generics in chemical

Generics are unstable feature of the compiler, Generics are a little unsafe (at runtime)

Generic Functions

The generics in chemical are a little like templates in C++ at the moment

func <T> print(a : T, b : T) {
    printf("%d, %d", a, b);
}

Yes, you can do generic dispatch

func <T : Dispatch> call() {
    T::method()
}

You can also do conditional compilation

func <T> size() : T {
    if (T is short) {
        return 2
    } else if(T is int) {
        return 4
    } else if(T is bigint) {
        return 8
    } else {
        return sizeof(T)
    }
}

Generic Structs

The syntax for generic structs is as follows

struct Point<T> {
    var a : T
    var b : T
    func print(&self){
        printf("%d, %d", a, b);
    }
}

func usage() {
    var p = Point<int> { a : 10, b : 20 }
    p.print()
}

Generic Variants

Generic variants are similar to generic structs

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

func usage() {
    var opt = Optional.Some<int>(10)
    var Some(value) = opt else -1
    printf("%d", value)
}

Command Line Basics

You can absolutely build a chemical source file without a chemical.mod or build.lab file

Create a file main.ch with contents

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

public func main() : int {
    printf("Hello World");
    return 0;
}

You can use the following command to convert it into an executable

chemical main.ch -o main.exe && ./main.exe

This should print Hello World in command line

However this doesn't allow us multiple modules, You also cannot import other modules in this single source file (this may change)

How Modules work in Chemical

Chemical has a very minimal module system, A module in chemical contains source files and a build file

There are two kinds of build files

  • chemical.mod (Used in most projects)
  • build.lab (Used in mostly advanced projects)

chemical.mod

Lets look at contents of a simple chemical.mod file, which prints Hello World however using imported declaration from cstd module

chemical.mod

module main

// this file must be present relative to this file
// you can also mention a path to directory to include all nested .ch files
source "main.ch"

// this imports the cstd module
import cstd

main.ch

public func main() : int {
    printf("Hello World");
    return 0;
}

Here the main.ch file is sibling to the chemical.mod file, The directory structure is following

my_module

  • main.ch
  • chemical.mod

When you invoke the compiler you must use the build file chemical.mod or build.lab file as the argument, for example

chemical chemical.mod -o main.exe && ./main.exe

Adding a relative module

We created a chemical.mod file above, we can make these changes to add another module named mylib

chemical.mod

module main

// this file must be present relative to this file
source "main.ch"

// this imports the cstd module
import cstd
import "./mylib"

You should add a directory mylib sibling of main.ch which should contain a chemical.mod file

Important Command Line Options

Command OptionDescription
-conly generate an object file
-vturn on verbose output
-bmmeasure the time taken by compilation
--build-dirthis configures build directory
--ltoenable link time optimization
--no-cachedisable using cache
-target specify the target for which code is being generated
--assertionsturn on assertions to verify generated code

Release Modes

There are present these five release modes (more to be added)

  • debug
    • Default debug mode
    • No Optimizations are performed
    • Embeds debug information in executable
  • debug_quick
    • No Optimizations are performed
    • Does NOT embed debug information
    • Tries to output and run the executable as fast as possible
  • debug_complete
    • No Optimizations are performed
    • Embeds debug information
    • Generates slower code for better debugging
    • Takes more time to compile
    • Debugs every aspect of compilation, for example compiler binding libraries are also compiled in debug mode
  • release_small
    • Optimizations are performed
    • generate code to prefer size over performance
  • release_fast
    • Optimizations are performed
    • Release mode, generate code to prefer performance over size

The release modes act as base configuration for your exeuctable. For example, you can use -g with any of the modes to embed debug information

To use a mode you must use --mode parameter in command line

chemical chemical.mod -o main.exe --mode debug_complete

Translating Chemical To C

You can translate chemical to C, Chemical compiler outputs clean, readable C code that is performant, we are constantly improving the C translation

Use the following command to translate just one file to C

chemical main.ch -o main.c

Translating C To Chemical (experimental)

You can translate C to chemical, Chemical compiler parses C code using Clang and outputs chemical code

However currently only headers are emitted (no function bodies), Also avoid complex code

chemical main.c -o main.ch

Translating chemical.mod To build.lab

You can also translate chemical.mod file to build.lab like this

chemical chemical.mod -o build.lab

The chemical compiler behind the scenes converts all .mod files to lab files, This is improves performance and allows to import .mod files in lab files easily

Translating build.lab To C

Similarly a build.lab can also be translated to C using

chemical build.lab -o build.c

Emitting LLVM IR And Assembly

You can emit llvm ir for a chemical source file as well, Which you can use to analyze generated code

Here are the following command line options available for this

Command OptionDescription
--out-ll specify a output path for llvm ir
--out-asm specify a output path for assembly
--out-ll-allauto generate llvm ir for every module being compiled
--out-asm-allauto generate assembly for every module being compiled

Apart from these, there are these options which further help

Command OptionDescription
--debug-irthe ir is debug, this allows for generation of invalid ir
--build-diralthough this configures build directory, but that's where out-ll-all go

Here's some examples using this options

chemical main.ch -o main.exe --out-ll main.ll

chemical chemical.mod -o main.exe --out-ll-all

chemical chemical.mod -o main.exe --build-dir build --debug-ir --out-ll-all

out-bc also exists for emitting bitcode

Conditional Compilation

Conditional Compilation through chemical.mod

This is the preferred way of conditional compilation as it saves compilation time

in chemical.mod file, you can use source statements

source "src"

We can also use a if condition with this source statement like this

// include win directory only when compiling for windows
source "win" if windows

// include pos directory only when compiling for posix
source "pos" if posix

currently support for conditionals is limited, You should use a build.lab file for proper support

Conditional Compilation inside source files (experimental)

You can also just do conditional compilation in source files like this

if(def.windows) {
    // code will only run on windows
}

Please note this syntax may change to something like this in future

$if(def.windows) {}

or

if comptime(def.windows) {}

Conditional Compilation inside build.lab

since chemical code is written inside build.lab, You should just use if(def.windows) to include source files

We'll go over this in the future, build.lab features are experimental

The Old Standard Library

Our standard library is not yet finished, that's why we refer to the current library as 'old'

The new standard library would be safe to use, using most features of the language, Currently

This standard library that is similar to C++ in naming is being used

To import this library use import std in the chemical.mod file

module main

import std

String View

string_view is a struct that is similar to C++ string_view, string_view contains a data pointer and a size, The actual string is allocated somewhere else, This is just a view into it, This can be easily sliced

Lets create and print a string_view

func print_my_view(view : std::string_view) {
    printf("%s\n", view.data());
}

func run() {
    // create by implicit constructor
    print_my_view("Hello World")
    // create explicitly
    print_my_view(std::string_view("Hello World2"))
}

A std::string_view has data and size functions

String

Here's how strings can be used

func take_str(str : std::string) {
    printf("%s", str.data());
}

func run() {
    take_str(std::string("my string"))
}

Some functions available in the string struct

func run(str : std::string) {
    
    // append some characters
    str.append('a')
    str.append('b')

    // check if empty
    if(str.empty()) {
        // its empty
    }

    // get the size of string
    var s = str.size()

    // check if equal to another string
    if(str.equals(std::string("other"))) {
        // yes its equal
    }

    // create a substring
    var sub = str.substring(10, 15)

    // copy the string
    var co = str.copy()

    // clear the string
    str.clear()

}

Vector

Here are some functions available in the vector struct

func create_vec() {
    var v = std::vector<int>()

    // get the item at index
    var item = v.get(1)
    var item_ptr = v.get_ptr(1)

    // push two items 0=>32, 1=>10
    v.push(32)
    v.push(10)

    // remove the second item
    v.remove(1)

    // remove the last item
    v.remove_last()

    // check if its empty
    if(v.empty()) {
        // its empty here
    }
    
    // get the size and capacity
    var s = v.size()
    var c = v.capacity()

    // clear all the items
    v.clear()

    // automatically destructs
}

Span

span is a struct that is kind of like a string_view

func run() {
    var span = std::span<int>()

    span.data(); // gets the pointer

    span.size(); // gets the size

    var item3 = span.get(3); // gets pointer to an integer at location 3

    // check if empty
    if(span.empty()) {
        // its empty
    }

}

We can create spans out of arrays

func run() {
    var arr = [10, 20, 30, 40, 50]
    var view = std::span<int>(arr)
}

or vectors

func create_span(std::vector<int>& vec) {
    var view = std::span<int>(vec)
    // now you can pass it to other functions
}

Unordered Map

Here's how to use an unordered map in chemical

func take(map : std::unordered_map<int, int>& map) {

}

func create() {
    var m = std::unordered_map<int, int>()

    // insert values with keys
    m.insert(10, 20)
    m.insert(30, 40)

    // check if key exists
    var check = m.contains(10)

    // find the value of the key
    var value2 = m.get_ptr(10)

    // erase a value
    m.erase(10)

    // get the size
    var s = m.size()

    // check if empty
    m.empty()

}

Capturing Functions

To use lambdas that are capturing, We use the std::function struct

func call_lamb(my_lamb : std::function<() => int>) {
    printf("%d", my_lamb())
}

func create_lamb() {
    var i = 33
    call_lamb(|i|() => {
        return i;
    })
}

The given list in pipes || contains identifiers that are captured

You can type & before them to capture them by reference

func create_lamb() {
    var i = 44
    call_lamb(|&i|() => {
        return i;
    })
}