Another programming language implementation (hopefully this time will be better).
import io;
public class Person(private name: string, private age: int) {
private address: string = "1 High Road";
public getAddress() -> string {
return this.address;
}
public moveHouse(address: Option<string>) -> void {
this.address = address.value() ?? this.address; // either new address or old address
}
public birthday() -> int {
this.age += 1;
return this.age;
}
public getName() -> string {
return this.name;
}
}
fn main() -> void {
let p: Person = new Person("Ethan", 17);
io.println("Hello, $1! You live at $2.", p.getName(), p.getAddress());
io.println("Happy $1 birthday, $2!", p.birthday(), p.getName());
}Note:
Optionand??in the example above are parsed but not yet implemented end to end.
import <name>; looks for a module as <name>.crdm or <name>/main.crdm, searching
next to the importing file first and then the standard library. Only public functions
are visible to importers.
// geometry/main.crdm
import math;
public fn clamped(v: int, lo: int, hi: int) -> int {
return math.min(math.max(v, lo), hi);
}
fn helper() -> int { return 1; } // private to this module// app.crdm
import io;
import str;
import geometry;
fn main() {
io.println(str.fromInt(geometry.clamped(99, 0, 10)));
}Use import <module> as <name>; to bind a module to a different name. Import cycles are
detected and reported with the full chain.
To use individual exports without a module prefix, select them with .{...}:
import math.{abs, sqrt as root};
import io.{println};
import str.{fromFloat};
fn main() {
let value: float = root(9.0) + abs(-2);
println(fromFloat(value));
}Selective imports work with public functions, classes, and traits, including
generics. Use as on an individual name to give it an alias. Lists may span lines
and have a trailing comma. They must contain at least one name.
Imports belong at module scope. A selective import binds only the listed names;
add import math; separately if you also want the math namespace. Duplicate
import bindings and clashes with top-level declarations are errors; local variables
and parameters may shadow selected names. Imported names are not re-exported.
The standard library is just a set of modules that happen to live on the search path.
It is written in Cardamom, in std/:
| Module | Provides |
|---|---|
io |
print, println, input |
str |
len, charAt, charCodeAt, fromASCII, fromInt, fromFloat, toInt, toFloat, substring, repeat, contains |
math |
abs, min, max, pow, sqrt |
raylib (optional) |
Native windows, drawing, input, timing, and screenshots; requires raylib when used |
cmp |
operator-backed Eq and Comparable, primitive/array implementations, generic comparisons, min, max |
ops |
arithmetic, bitwise, shift, and unary operator traits with separate operand/result types |
fmt |
structural Printable, primitive/array implementations, generic text, print, println |
hash |
structural Hash, primitive implementations, generic hashing |
convert |
generic From<T>/Into<T> conversion traits and into<T, U> |
collections |
bounded generic Hashmap<K, V> |
Adding a function or trait implementation means editing std/<module>/main.crdm — no compiler changes.
Only the functions a program calls or uses as values are emitted, so importing a module costs nothing for the parts you do not use.
The search path is, in order: the importing file's directory, $CARDAMOM_STD, std/
next to the compiler binary, and the source checkout.
Most of the library is ordinary Cardamom, but the leaves have to reach C++ eventually.
@cpp splices code into the generated function body and @include adds a header:
public fn println(content: string) {
@include("<iostream>");
@cpp("std::cout << content << std::endl;");
}
// Built on top, in plain Cardamom:
public fn repeat(s: string, times: int) -> string {
let out: string = "";
let i: int = 0;
while (i < times) {
out += s;
i += 1;
}
return out;
}fn extern name(..) -> T {} remains available to declare a function you link yourself.
Functions can take type parameters. Type arguments are inferred from the call, or given explicitly:
fn identity<T>(x: T) -> T {
return x;
}
fn firstOr<T>(xs: T[], fallback: T) -> T {
if (xs.len() > 0) {
return xs[0];
}
return fallback;
}
fn main() {
identity(5); // T inferred as int
identity("hello"); // a second instantiation
identity<int>(7); // explicit, reuses the first
let xs: int[] = [1, 2];
firstOr(xs, 0);
}Generics are monomorphised: each distinct set of type arguments produces its own specialised function, so the generated C++ contains no templates and type errors are reported by Cardamom rather than by the C++ compiler.
Classes take type parameters too:
public class Option<T>(private value: T, private present: int) {
public unwrapOr(fallback: T) -> T {
if (this.present == 1) {
return this.value;
}
return fallback;
}
}
fn some<T>(v: T) -> Option<T> {
return new Option<T>(v, 1);
}
fn main() {
let a: Option<int> = some(41);
let b: Option<string> = new Option("", 0); // type argument inferred
a.unwrapOr(0);
b.unwrapOr("empty");
}Instantiation is transitive and only the instantiations a program actually uses are
emitted, so Box<T> used inside wrap<T> produces exactly the specialisations wrap
is called at.
Generic functions, classes, methods, and implementations cross module boundaries.
Nested generic types use the ordinary spelling, including adjacent closing brackets:
Box<Box<int>>.
Traits are structural: a class satisfies a trait whenever it has compatible public
methods, without an explicit declaration. where clauses expose those methods in a
generic body and validate every concrete instantiation:
trait Printable {
text() -> string;
}
fn render<T>(value: &T) -> string where T: Printable {
return value.text();
}Explicit implementations add trait behavior to primitives or foreign types, and may also be generic and constrained:
impl Printable for int {
public text() -> string { return str.fromInt(this); }
}
impl<T> Printable for Box<T> where T: Printable {
public text() -> string { return this.value.text(); }
}Generic bounds use static monomorphised dispatch. For runtime dispatch, a shared
borrow written &dynamic Trait carries a data pointer and a shared method table:
import fmt.{Printable};
import io;
fn show(value: &dynamic Printable) {
io.println(value.text());
}
fn main() {
let number: int = 42;
let label: string = "Cardamom";
show(as &dynamic Printable (&number));
show(as &dynamic Printable (&label));
}Both calls use the same show function. Casts use Cardamom's prefix as Type value
syntax. Explicit implementations and read-only public class methods work, including
concrete generic classes and traits such as &dynamic convert.Into<int>. Import
aliases refer to the same trait and method table.
This first version supports borrowed, read-only objects with restricted lifetimes:
- Create a handle from an explicit immutable borrow of an owned, named, concrete
value. Temporaries, fields, indexes, existing borrows,
this, and unresolved generic values cannot be used as cast sources. - Use handles directly as call arguments or method receivers. Named functions and
methods may take them as parameters and forward them to other dynamic parameters.
Storing, returning, capturing, or using handles as generic arguments is unsupported;
function values with dynamic parameters and
externdynamic signatures are also unsupported. Baredynamic Traitand&mut dynamic Traitare rejected. - Dynamic traits require concrete owned type arguments. Their methods cannot be
static or generic, use
Selfin signatures, return borrows, or include other dynamic objects in their signatures. Methods must be called directly.
These restrictions prevent handles from escaping their calls without introducing a
general lifetime checker. @cpp remains an unchecked native escape hatch.
Operators use these contracts too: ==/!= use cmp.Eq, ordering uses
cmp.Comparable, and arithmetic/bitwise operations use traits from ops.
For example, where T: ops.Add<T, T> makes left + right valid in a generic body.
Public structural methods and explicit implementations both work, including
cross-module and nested generic implementations. Compound assignments reuse the
binary contracts and require a result assignable to their destination.
See Operators and traits for the contract table, examples, array comparisons, evaluation rules, and current limitations.
A named function can be used wherever a fn type is expected:
fn twice(x: int) -> int { return x * 2; }
fn apply(f: fn(int) -> int, v: int) -> int { return f(v); }
fn main() {
let f: fn(int) -> int = twice;
let fs: (fn(int) -> int)[] = [twice, f];
apply(twice, 5);
}&T is an immutable borrow and &mut T a mutable one. They lower to const T& and
T&:
fn bump(x: &mut int) -> void {
x += 1; // visible to the caller
}
fn readonly(x: &int) -> int {
return x + 1; // reading only
}
fn main() {
let n: int = 1;
bump(n); // n is now 2
readonly(n);
readonly(5); // an immutable borrow accepts a temporary
}The rules the checker enforces:
- assigning through a
&Tis an error; use&mut T - a
&mut Targument must be a variable, index or field, not a temporary - a
&Tcannot be passed where a&mut Tis required (the reverse is fine) - members and indexing reach through a borrow, so
xs.len()works forxs: &int[]
Class methods that never write to this are emitted as const, which is what lets
them be called through a &T.
Empty array literals take their type from the context they appear in, so let xs: int[] = [];
and total([]) both work; a literal with nothing to infer from is an error.
Names that are C++ keywords but not Cardamom keywords (double, template, union, ...)
are usable as ordinary identifiers and renamed during code generation.
I am currently developing this programming language as a hobby
The VS Code/Cursor extension adds .crdm syntax
highlighting, embedded C++ highlighting for @cpp(...), comment toggling, bracket
matching, and snippets.
Build and install it locally with Node.js 20 or newer:
cd editors/vscode
npm ci --ignore-scripts
npm test
npm run package
code --install-extension cardamom-0.1.0.vsixFor Cursor, replace code with cursor. You can also use Extensions: Install
from VSIX... in the editor's Command Palette and select the generated package.
Open a .crdm file and its language mode should be Cardamom.
cargo build --release
cp ./target/release/cardamom ./cardamom./cardamom <file> # compile the file and generate ./output
./cardamom <file> -out # compile the file and generate ./output and ./output.cpp
./cardamom <file> -o app -- -O2 # choose an executable and pass C++ compiler flags
./cardamom <file> --cxx clang++ # override the C++ compiler (also configurable with CXX)Arguments after -- are passed individually to the C++ compiler, so native
headers and libraries can be supplied with -I, -L, -l, and platform linker
flags. Quote paths containing spaces. -out / --keep-cpp keeps the generated
source at <output>.cpp; a failed native compilation also keeps it for debugging.
The default C++ compiler is $CXX when set, otherwise g++, using C++17.
The boids example has flocking, motion trails, mouse attraction and repulsion, and pause/reset controls. Its simulation is written in Cardamom and uses the optional raylib module for drawing and input.
python3 scripts/boids.py # build raylib locally, compile, and open the demo
python3 scripts/boids.py --check # run the simulation checks without a windowThe helper needs Python 3, Cargo, a C++ compiler, Git, and CMake. Dependencies are
built under target/; see the example's README for platform setup and controls.