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:
- Scaffold a new Chemical project
- Write and organize your source code in Markdown-friendly files
- Import and use modules (including the C standard library via
cstd
) - 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!
-
Create a directory for your project and enter it:
mkdir hello_chemical && cd hello_chemical
-
Create a
src/
folder and addmain.ch
:src/main.ch
-
In
src/main.ch
, write your first program:public func main() : int { printf("Hello, World!\n"); return 0; }
-
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
- 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 Option | Description |
---|---|
-c | only generate an object file |
-v | turn on verbose output |
-bm | measure the time taken by compilation |
--build-dir | this configures build directory |
--lto | enable link time optimization |
--no-cache | disable using cache |
-target | specify the target for which code is being generated |
--assertions | turn 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 Option | Description |
---|---|
--out-ll | specify a output path for llvm ir |
--out-asm | specify a output path for assembly |
--out-ll-all | auto generate llvm ir for every module being compiled |
--out-asm-all | auto generate assembly for every module being compiled |
Apart from these, there are these options which further help
Command Option | Description |
---|---|
--debug-ir | the ir is debug, this allows for generation of invalid ir |
--build-dir | although 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;
})
}