A programming language made to explore Compile-Time Execution and Runtime Reflection, largely inspired by Jai, Odin, and Zig.
core :: mod "core";
greeting :: "Hello, World!";
main :: () -> i32 {
core.println(greeting);
// exit with code 0
0
}
From examples/hello_world.capy
First clone this repo,
git clone https://github.com/capy-language/capy.git
cd capy
Then install the capy command with cargo,
cargo install --path crates/capy
Make sure you have gcc
installed,
Then compile and run your code!
capy run examples/hello_world.capy
Variables are declared with name : type : value
or name : type = value
.
The first is immutable (can't be changed), and the second is mutable (can be changed).
name : str : "Terry";
age : i32 = 42;
age = 43; // age can be changed
The type can also be omitted.
name :: "Terry";
age := 42;
Certain other languages have const
definitions along with immutable variables. Capy combines these two together.
An immutable variable does not necessarily need to be known at compile time, it can be a runtime store like any other.
But there might be certain circumstances in which it must be const, which will be expanded on later.
These bindings and variables can also shadow each other,
foo := true;
foo :: 5;
foo := "Hullo :3";
The value can be omitted and a default/zero value will be supplied.
Fixed arrays are declared as follows,
my_array : [6]i32 = i32.[4, 8, 15, 16, 23, 42];
my_array[2] = 10;
Slices are a type of pointer that can represent an array of any possible size. They look very similar but lack a length within the square brackets.
my_slice : []i32 = i32.[1, 2, 3];
my_slice : []i32 = i32.[4, 5, 6, 7, 8];
my_slice[2] = 10;
Arrays can be implicitly cast to slices, but casting a slice to an array must be done explicitly.
// start with an array
my_array : [3]i32 = i32.[2, 4, 8];
// automatically turn it into a slice
my_slice : []i32 = my_array;
// manually turn it back into an array
my_array : [3]i32 = my_slice as [3]i32;
Pointers can be either mutable or immutable, similar to Rust.
foo := 5;
bar :: ^mut foo;
bar^ = 10;
Unlike Rust however, there are currently no borrow checking rules like "either one mutable reference or many const references".
Mutable pointers greatly improve the readability of code, and allow one to see at a glance the side-effects of a function.
Types are first-class in Capy, which means structs are values that can be assigned to a variable like any other,
Person :: struct {
name: str,
age: i32
};
gandalf := Person.{
name = "Gandalf",
age = 2000,
};
// birthday!
gandalf.age = gandalf.age 1;
Types can also be created with the distinct
keyword, which creates a new type with the same underlying semantics of it's sub type.
Imaginary :: distinct i32;
x : Imaginary = 42;
y : i32 = 12;
y = x; // ERROR! Imaginary != i32 :(
You can alias a type by simply assigning it to a variable.
My_Int :: i32;
x : My_Int = 42;
y : i32 = 12;
y = x; // yay! My_Int == i32 :)
It is important to note that in order to actually use My_Int
as a type, it must be const, or, "known at compile-time."
Otherwise, the compiler will throw an error as it's impossible to compile a variable (x
in this case) whose type might change at runtime.
My_Int := i32;
if random_num() % 2 == 0 {
My_Int = i64;
}
x : My_Int = 42; // ERROR! My_Int's value might change at runtime! uncompilable!
There are two requirements which determine if a variable is const.
- It must be immutable.
- It must either contain a literal value, a reference to another const variable, or a
comptime
block.
Beyond type annotations, the size of an array is also expected to be const, and this value can be calculated using comptime
.
To see all the different types, you can look through core/meta.capy
,
which contains reflection related code and documentation for all of Capy's types.
One of the most powerful parts of Capy is its arbitrary compile-time execution. This allows you to run any code at compile-time, returning whatever data you wish.
math :: mod "core".math;
powers_of_two := comptime {
// array with default value (all zeros)
array : [3]i32;
array[0] = math.pow(2, 1);
array[1] = math.pow(2, 2);
array[2] = math.pow(2, 3);
array
};
One of the most sacred promises Capy tries it's best to keep is any code that can be run at runtime, can also be run at compile-time.
There are no special const
functions to be found here. Mine for crypto, play a video game, or anything else your heart desires within a comptime
block.
Or at least, that's the end goal. A few wrinkles haven't been fully ironed out yet, like returning pointers and functions from comptime
blocks.
Types work well with compile-time execution, and can be arbitrarily calculated by whatever code you want,
My_Type :: comptime {
if random_num() % 2 == 0 {
i32
} else {
i64
}
};
x : My_Type = 42;
This is obviously not a very useful example. But as this feature continues to be fleshed out, this will become the basis of Capy's compile-time generic system.
Reflection is another powerful feature of Capy, and powers the language's runtime generic system.
All types in a Capy program become 32 bit IDs at runtime. The meta
file of the core
module contains reflection related code for inspecting
these IDs and getting information such as the length of an array type,
array_type := [3]i32;
info := meta.get_array_info(array_type);
core.assert(info.len == 3);
core.assert(info.ty == i32);
The size of an integer type,
int_type := i16;
core.assert(meta.size_of(int_type) == 2);
core.assert(meta.align_of(int_type) == 2);
The members of a struct,
struct_type := struct {
foo: str
};
info := meta.get_struct_info(struct_type);
first := info.members[0];
core.assert(core.str_eq(first.name, "foo"));
core.assert(first.ty == str);
core.assert(first.offset == 0);
And anything else you'd like to know about your types.
This information is supplied in a few global arrays at both runtime and compile-time, meaning that reflection works within both.
This functionality powers the core.Any
type, which can represent any possible value.
count : i32 = 5;
should_start : bool = true;
greeting : str = "Hi";
// core.Any contains a type ID and an opaque pointer (like `void*` in C).
// The type ID allows `core.println` to know how to display the given pointer.
core.println(core.Any.{
ty = i32,
data = ^count,
});
core.println(core.Any.{
ty = bool,
data = ^should_start,
});
core.println(core.Any.{
ty = str,
data = ^greeting,
});
This is pretty verbose, so the compiler will automatically cast values to a core.Any
if needed,
core.println(5);
core.println(true);
core.println("Hello");
This isn't hard coded for the core.Any
struct, but works for any struct with the following members:
struct {
type_id: type,
opaque_pointer: ^any, // `^any` and `^mut any` are opaque pointers (they have no associated type)
}
As you can probably guess, core.println
internally uses a lot of reflection to determine what to actually print to the screen when given a core.Any
.
Reflection is extremely useful, and allows for things like a debug
function that doesn't need to be implemented manually for all types (like Rust), or making it easy to
serialize and deserialize structs.
If comptime
powers Capy's compile-time generic system, reflection powers Capy's runtime generic system.
In the future reflection will be made to embrace functions. When user-defined annotations are added, this will result in automation far more powerful than Rust macros.
The defer statement allows you to code in the future by moving the given expression to the end of the current scope.
The expression in a defer is guarenteed to run, regardless of any breaks or returns.
{
my_file := open_file("foo.txt");
defer close_file(my_file);
// do a bunch of stuff with file
} // `close_file` gets run here
Defers are "first in, last out". So later defers will run before earlier defers.
file_manager := alloc_manager();
defer free_manager(file_manager);
file_manager.foo := open_file("foo.txt");
defer close_file(file_manager.foo);
// foo is freed, and then the file manager is freed
Every Capy program must contain a main
function. It is the entry point of the program.
This function's signature can be written in multiple ways; it can return either void
or an integer type.
// this is valid
main :: () { ... };
// this is also valid
main :: () -> u32 { ... };
/// this isn't :(
main :: () -> bool { ... };
In Capy, almost everything is first-class, and that includes functions. Functions can be put within variables and bindings just like any other value.
add :: (x: i32, y: i32) -> i32 {
x y
};
mul :: (x: i32, y: i32) -> i32 {
x * y
};
apply_2_and_3 :: (func: (x: i32, y: i32) -> i32) -> i32 {
func(2, 3)
};
apply_2_and_3(add);
apply_2_and_3(mul);
Lambdas, or anonymous functions, are extremely useful in many programming languages. This one singular lambda syntax allows for far more consistency and easier code evolution than the two separate syntaxes for lambdas and functions many languages are forced to go with.
Capy contains an import
and mod
expression. These are first class values that refer to other files in your program.
An import
refers to a source file, relative to the current file, whereas a mod
refers to a specific mod.capy
file in the global modules directory.
my_file :: import "some_file.capy";
core :: mod "core";
The modules directory can be changed via the --mod-dir
flag, and if it lacks a "core" subfolder one will automatically be downloaded from this repository.
The examples
folder contains a lot more, and it gives a much better idea of what the language looks like in practice.
Currently, gcc
must be installed for the compiler to work.
It is used for linking to libc and producing a proper executable.
If you want to use libc functions, define them with extern
(look in core/libc.capy
for examples).
Variadic functions do not work. You could try explicitly defining a function like printf
to take 3 arguments,
but this won't work for floats, which are passed into variadic functions differently depending on the calling convention.
Cranelift is currently working on adding variadic support, so that will be added in the future.
While the end goal is to make any code than can run outside of a comptime
block be allowed to run within a comptime
block,
this is easier said than done. printf
in particular cannot be run at compile-time.
Especially as support for linked libaries increases, it'll be harder to keep this promise.
If you find any bugs in the compiler, please be sure to make an issue about it and it'll be addressed as soon as possible.
Big shout out to Luna Razzaghipour, the structure of this entire codebase is largely based on gingerbread and eldiro. Her help in teaching how programming languages really work is immeasurable and I'm very thankful.
Big shout out to lenawanel, she's been an enormous help in finding bugs and testing the limits of the language. Due to her help the language has really expanded to new heights.
Big shout out to cranelift. Trying to get LLVM on windows was just way too much effort for me and cranelift made all my dreams come true.
I know the cranelift documentation isn't the greatest, so if anyone wants to use this repo to see how I've implemented higher-level features such as arrays, structs, first class functions, etc. then it's all in crates/codegen
.
Capy is open to contributions! See CONTRIBUTING.md
on how you can contribute. This file also explains how Capy's codebase actually works internally.
The Capy Programming Language is distributed under the terms of both the MIT license and the Apache License (Version 2.0). See LICENSE-APACHE and LICENSE-MIT for details.
The capybara logo was AI generated by imagine.art, who own the rights to it. It can be used in this non-commercial setting with attribution to them.