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 either zig
or gcc
installed (they are used for linking to libc),
Then compile and run your code!
capy run examples/hello_world.capy
Variables are declared with name : type = value
wizard_name: str = "Gandalf";
// if you leave out the type, the compiler will figure it out for you
wizard_name := "Gandalf";
Variables can be made immutable by using ::
instead of :=
.
This prevents other code from changing the variable in any way.
age : i32 : 21;
age = 22; // ERROR! age is immutable
// just like before, the compiler will figure out the type if you leave it out
age :: 21;
Certain other languages have const
definitions AND immutable variables.
Capy combines these two concepts together. They are both defined with ::
.
Capy's definition of what const means will be explained later.
Variables can shadow each other. This means that later definitions will replace earlier definitions with the same name.
foo := true;
core.println(foo); // prints "true"
foo :: 5;
core.println(foo); // now prints "5"
foo := "Hullo :3";
core.println(foo); // now prints "Hullo :3"
The value of a variable can be omitted and a default/zero value will be supplied.
milk : f64; // without a value, this defaults to 0.0
letter : char; // without a value, this defaults to '\0'
number : i32; // without a value, this defaults to 0
// etc. for the other types (excluding pointers, slices, enums, and error unions)
Casting is done with type.(value)
age_int: i32 = 33;
age_float := f32.(age_int); // casts i32 -> f32
letter := char.(age_int); // casts f32 -> char
Arrays can be created with type.[value1, value2, value3, ...]
the_numbers: [6]i32 = i32.[4, 8, 15, 16, 23, 42];
the_numbers[2] = 10;
The type of this above array is [6]i32
.
[6]i32
is called a static array because its size can never change.
If you won't know the size of the array until runtime, slices can be used instead.
Slices can reference any array, no matter the size. To get a slice of i32
s, you'd write []i32
list_one: [6]i32 = i32.[4, 8, 15, 16, 23, 42];
list_two: [3]i32 = i32.[2, 4, 8];
my_slice: []i32 = list_one;
// my_slice can be reassigned to reference an array of some other size
my_slice = list_two;
As seen above, static arrays will automatically fit themselves into slice types if the underlying type
(in this case, i32
) matches.
If you want to get the underlying static array of a slice, you must manually cast it.
list_one: [3]i32 = i32.[2, 4, 8];
my_slice: []i32 = list_one; // this is automatic
list_two: [3]i32 = [3]i32.(my_slice); // this is a manual cast of []i32 -> [3]i32
Pointers are created with ^value
or ^mut value
In Capy, pointers are either mutable or immutable, like Rust.
foo := 5;
bar := ^mut foo;
bar^ = 10;
core.println(foo); // prints "10"
Unlike Rust however, there are currently no borrow checking rules like "either one mutable reference or multiple immutable references".
I say currently but to be honest I'm not sure if they even should be added down the road.
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. They can be put inside variables, passed to functions, printed, etc.
As an example, structs are usually defined by creating a new immutable variable and assigning a struct { .. }
value to it.
Person :: struct {
name: str,
age: i32
};
gandalf := Person.{
name = "Gandalf",
age = 2000,
};
// birthday!
gandalf.age += 1;
The struct { .. }
value above represents the type definition itself.
Any type can be assigned to any variable.
This system means that type aliasing is dead simple.
My_Int :: i32;
foo : My_Int = 42;
bar : i32 = 12;
bar = foo; // This works because My_Int == i32
Immutable variables like Person
, My_Int
, etc., can be used in place of a type annotation (as seen above).
It's important to note that there are certain rules about when a variable can be used as a type. It largely depends on whether the variable is const, or, "known at compile-time."
If a variable isn't const, the compiler will produce an error because it's impossible to compile a variable whose type might change at runtime.
My_Int := i32; // notice how this variable is mutable `:=`
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 use
::
and not:=
) - It must contain a literal value, a reference to another const variable, or a
comptime
block.
Beyond just being used for types, Const variables can also be used for enum discriminants (explained later) and array sizes.
Types can be created with the distinct
keyword, which creates a new type that has the same underlying semantics of its sub type
Seconds :: distinct i32;
foo : Seconds = 42;
bar : i32 = 12;
bar = foo; // ERROR! Seconds != i32
This can be useful for making sure one doesn't mix up Seconds
for Minutes
for e.g.
Enums are incredibly useful for dealing with varying state. In Capy enums look like this:
Dessert :: enum {
Ice_Cream,
Chocolate_Cake,
Apple_Pie,
Milkshake,
};
order_list := Dessert.[
Dessert.Chocolate_Cake,
Dessert.Ice_Cream,
Dessert.Milkshake,
];
Enums can have additional data associated with each variant, and this data can be extracted using switch
statements
Web_Event :: enum {
Page_Load,
Page_Unload,
Key_Press: char,
Paste: str,
Click: struct {
x: i64,
y: i64,
},
};
event_queue : []Web_Event = .[
Web_Event.Page_Load,
Web_Event.Key_Press.('x'),
Web_Event.Paste.("hi hi hi :)"),
Web_Event.Click.{
x = 20,
y = 80
}
];
switch e in event_queue[0] {
.Page_Load => core.println("page loaded"),
.Page_Unload => core.println("page unloaded"),
.Key_Press => {
// type_of(e) == char
core.println("pressed key: ", e);
},
.Paste => {
// type_of(e) == str
core.println("pasted: ", e);
},
.Click => {
// type_of(e) == struct { x: i64, y: i64 }
core.println("clicked at x=", e.x, ", y=", e.y);
}
}
As you can see, switches declare an argument (e
, in this case), and the type of that argument changes depending on the branch.
One of the unique things about Capy's enums is that each variant of the enum is actually its own unique type.
When you define the variants Web_Event.Click
, Web_Event.Paste
, Dessert.Chocolate_Cake
, etc. inside an enum {}
block
you are actually creating entirely new distinct types. You can reference and instantiate these types just like any other type.
special_click_related_code :: (click_event: Web_Event.Click) {
core.println(click_event.x);
}
It's very similar to creating distincts. The only real difference is that enums allow you to mix the different variants together.
Being able to operate on each variant as its own type can be quite useful, and doing things like this in Rust can be verbose.
Extra Information About Enums
If you're doing FFI and you need to specify the discriminant you can do that with |
Error :: enum {
IO: str | 10,
Caught_Fire | 20,
Exploded | 30,
Buggy_Code | 40,
}
In memory, the discriminant is always a u8 that comes after the payload of the enum itself.
Reflection can be used to see what the byte offset of the discriminant is. core/meta.capy
examples/enums_and_switch_statements.capy
contains more examples
Error handling can really make or break a language.
A programming language's error handling system will be used constantly, whether it's exceptions, errors as values, aborting, or whatever else. Making sure it's expressive and easy to use is very important.
In Capy's case, the chosen error handling system is heavily inspired by Zig's, with a few key differences.
An optional type can be declared with ?type
message : ?str = nil;
message = "hello";
Optionals represent the presence or absence of a value, which might change at runtime.
Capy does not have null pointers.
They've been called a "billion dollar mistake" and from my own personal experience they're such a pain to deal with.
All pointer types, ^i32
, ^bool
, etc. are forbidden from ever being null.
Instead, nullable pointers must be explicitly declared by using optional types
foo : i32 = 42;
// This is a regular pointer.
// You can use this freely in your program and dereference it at will
regular_ptr : ^i32 = ^foo;
// This is a nullable pointer.
// You must explicitly check it before dereferencing it
nullable_ptr : ?^i32 = ^foo;
// `nil` acts like a null pointer here
nullable_ptr = nil;
Of course, sometimes it's not enough just to know that a value isn't there. Sometimes you need to know why the value isn't there.
An Error Union type can be used to represent either a successful value or an error.
Error unions are created with <error type>!<success type>
.
For example:
do_work :: (task_number: i32) -> str!f32 {
// ...
}
What str!f32
means is that this function, do_work
, will normally return an f32
,
but if do_work
runs into some kind of problem that prevents it from functioning properly,
a value of type str
will be returned instead.
This is very similar to Zig's method of error handling, although the problem with Zig's system is that it heavily restricts
what kind of errors you can return. Imagine parsing a 12KB json file and just getting InvalidCharacter
.
The above example had a str
error type, but in Capy the errors can really be whatever you want.
Custom_Error :: enum {
Input_Too_Big,
Bad_Input: struct {
why: str,
},
Caught_Fire,
};
try_to_double :: (num: u64) -> Custom_Error!u64 {
if num == 5 {
return Custom_Error.Bad_Input.{ why = "I don't like it" };
}
if num > 10 {
return Custom_Error.Input_Too_Big;
}
num * 2
}
Enums, optionals, and error unions, are all called sum types in Capy because they represent data whose "true" type will vary between one of several at runtime.
The "true" type of an enum can be any one of the variants at runtime.
The "true" type of an optional can be either the success type or the type nil
at runtime.
The "true" type of an error union can be either the success type or the error type at runtime.
Switches aren't just for enums, they can be used with any sum type. Each arm of the switch statement represents one of the possible "true" types of the given data.
// switching on an optional
message : ?str = "Hello, World!"
switch inner in message {
str => {
// type_of(inner) == str
core.println("The message is: ", inner);
}
nil => {
// type_of(inner) == nil
// note: `nil` is a type, just like `i32` or `void`
// the `nil` keyword represents both the type `nil`
// and the value `nil`
core.println("There is no message :(");
}
}
// switching on an error union
switch inner in try_to_double(5) {
u64 => {
// type_of(inner) == u64
core.println("successfully doubled!");
core.println("the result is = ", inner);
}
Custom_Error => {
// type_of(inner) == Custom_Error
core.println("Uh oh, there was an error: ", inner);
}
}
You can also use the compiler directives #is_variant
and #unwrap
to quickly assert the "true" type of a given sum type
// With an optional
message : ?str = nil;
if #is_variant(message, str) {
val := #unwrap(message, str);
}
// With an error union
result := try_to_double(11);
if #is_variant(result, u64) {
doubled := #unwrap(result, u64);
}
// these also work for enums
Note: if the first argument is an optional, both #unwrap(message, str)
and #unwrap(message)
are equivalent
The one place where optionals and error unions differ from enums is that optionals and error unions allow you to use the .try
keyword.
.try
is an operator which will return an error/nil value if it finds one, and otherwise will continue execution like normal.
For example:
do_networking :: () -> My_Error_Type!u32 {
body := get_request("https://example.com").try;
version_number := parse_number(body).try;
save_number_to_disk(version_number).try;
200
}
get_request :: (url: str) -> My_Error_Type!str {}
parse_number :: (text: str) -> My_Error_Type!u64 {}
save_number_to_disk :: (number: u64) -> My_Error_Type!void {}
The above code is equivalent to this:
do_networking :: () -> My_Error_Type!u32 {
body := switch inner in get_request("https://example.com") {
str => inner,
My_Error_Type => {
return inner;
},
};
version_number := switch inner in parse_number(body) {
u64 => inner,
My_Error_Type => {
return inner;
},
};
switch inner in save_number_to_disk(version_number) {
void => {},
My_Error_Type => {
return inner;
}
}
200
}
get_request :: (url: str) -> My_Error_Type!str {}
parse_number :: (text: str) -> My_Error_Type!u64 {}
save_number_to_disk :: (number: u64) -> My_Error_Type!void {}
The above example uses .try
with error unions, but .try
works for optionals as well.
Extra Information About Optionals & Error Unions
Optionals and Error Unions are stored in memory as tagged unions, just like enums.
The "success" discriminant is 1, and the "error" (or "nil") discriminant is 0
Regular pointers are considered "non-zero" in Capy, and the size of a non-zero type is equal to the size of its optional.
This means that e.g. size_of(?^i32) == size_of(^i32)
but size_of(?i32) != size_of(i32)
Since the inner type can never be zero, an inner value of zero is used to represent the "nil" state. This allows non-zero optionals to have zero memory cost.
With that, here are all the possible types in Capy:
- Signed integers (
i8
,i16
,i32
,i64
,i128
,isize
) - Unsigned integers (
u8
,u16
,u32
,u64
,u128
,usize
) - Floating-point numbers (
f32
,f64
) bool
str
char
- Static arrays (
[6]i32
,[3]f32
,[10]bool
, etc.) - Slices (
[]i32
,[]f32
,[]bool
, etc.) - Pointers (
^i32
,^f32
,^mut bool
, etc.) - Distincts (
distinct i32
,distinct f32
,distinct bool
, etc.) - Structs (
struct { a: i32, b: i32 }
,struct { foo: str }
, etc.) - Enums (
enum { Foo: i32, Bar: str, Baz: bool }
, etc.) - Enum variants (each variant of an enum is treated as a special kind of distinct type)
- Functions (
() -> void
,(x: i32) -> bool
, etc.) - Files (when you import a file, that file is actually its own type, like a struct)
type
(types are first-class and "i32
" when used as a value has the typetype
)any
(a reference type, explained later)rawptr
,mut rawptr
(opaque pointers, like void* in C)rawslice
(an opaque slice)void
One of the most powerful features 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_i32(2, 1);
array[1] = math.pow_i32(2, 2);
array[2] = math.pow_i32(2, 3);
array
};
One of the most sacred and holy promises Capy tries to keep is: "any code which can run at runtime, shall also be runnable 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.
Compile-time execution is very useful, and can even be used to do things like arbitrarily calculating a particular type
My_Type :: comptime {
if random_num() % 2 == 0 {
i32
} else {
i64
}
};
x : My_Type = 42;
Something more pragmatic than the above (but far too complex to fit in a readme) might be an ORM that automatically downloads the latest schema and uses it to assemble struct types.
As this feature continues to be fleshed out, this will become the basis of Capy's compile-time generic system.
Additionally, at some point I'd like to make it so code within the comptime block can directly interface with the compiler, like a build.zig file, but within a comptime { .. }
block.
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.capy
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;
switch info in meta.get_type_info(array_type) {
.Array => {
core.assert(info.len == 3);
core.assert(info.sub_ty == i32);
}
_ => {}
}
The size of an integer type,
int_type := i16;
switch info in meta.get_type_info(int_type) {
.Int => {
core.assert(info.bit_width == 16);
core.assert(info.signed == true);
}
_ => {}
}
The members of a struct,
struct_type := struct {
foo: str
};
switch info in meta.get_type_info(struct_type) {
.Struct => {
first := info.members[0];
core.assert(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 in both.
This functionality powers the any
type, which can represent any possible value.
count : any = 5;
should_start : any = true;
greeting : any = "Hi";
core.println(count);
core.println(should_start);
core.println(greeting);
any
is a reference type, and internally contains a type ID and a rawptr.
Functions like core.println
use reflection on this type ID to determine how to print the given rawptr.
Reflection is extremely useful, and allows for things like a debug
or serialize
function that doesn't need to be implemented manually for all types (like Rust).
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);
// the foo file is closed, and *then* the file manager is freed
Speaking of breaks and returns, there's no better place to talk about them than right here
In Capy, break
acts like a goto
in C, jumping to the last labeled block, if statement, or loop
{
// doing stuff...
`my_block_label: {
// imagine code here...
if true {
// inside an if statement...
{
break;
}
}
} // <- the `break` will jump to the end of this block
}
break
can also be used with a value to easily return something from a block expression.
foo := `foo_calc: {
if 2 < 5 {
break `foo_calc 42;
}
5
};
return
works similarly to a break
, except that instead of jumping to the last labeled scope,
it jumps to the very first scope regardless of if its a function or not.
global_variable_1 :: true;
global_variable_2 :: comptime {
if global_variable_2 {
return 42;
}
5
};
There's not much to say about continue
. It works exactly like you'd expect it to.
Lambdas, or anonymous functions, are extremely useful in all the programming languages that have them.
Capy has only one way of creating functions:
(param1: type1, param2: type2, ...) -> return_type { <code here> }
These function values can be passed around and given to other functions, they can be assigned to variables, etc.
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);
apply_2_and_3((x: i32, y: i32) -> i32 {
(10*x + 10*y) / 2
});
This singular, combined syntax for lambdas and named functions allows for far more consistency and easier code evolution than the two separate syntaxes many languages are forced to go with.
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 { ... };
Capy uses compiler directives to do special operations that couldn't be achieved with code.
#import
and #mod
are used to refer to other files in your program. Just like types, They are first-class values.
#import
refers to a source file, relative to the current file, whereas #mod
refers to a specific module in the global modules directory.
my_file :: #import("some_file.capy");
core :: #mod("core");
The modules directory can be changed via the --mod-dir
compiler flag, and if it lacks a "core" subdirectory one will automatically be downloaded from this repository.
Another directive is #unwrap
, which asserts that an enum is a certain variant, and panics otherwise.
some_event : Web_Event = Web_Event.Click.{
x = 20,
y = 80
};
clicked : Web_Event.Click = #unwrap(some_event, Web_Event.Click);
#unwrap
requires the first argument to be a sum type value, and the second argument to be the expected "true" type.
The second argument is not required if the first argument is an optional.
#is_variant
returns true
if a sum type is the specified "true" type
The first argument must be a sum type value, and the second argument must be the expected "true" type.
Unlike #unwrap
, the second argument is always required.
Binary Operators:
Name | Symbols | Types |
---|---|---|
Arithmetic | + , - , * , / |
integers, floats |
Modulo | % |
integers |
Exclusive OR | ~ |
integers, floats |
Bit Shift | << , >> |
integers |
Binary AND/OR | & , | |
integers, floats, booleans |
Logical AND/OR | && , || |
booleans |
Order Comparison | < , > , <= , >= |
integers, floats, booleans |
Equality Comparison | == , != |
(all types other than pointers, slices, and any ) |
Prefix Operators:
Name | Symbols | Types | Example |
---|---|---|---|
Negation and Positivity | - , + |
integers, floats |
|
Binary NOT | ~ |
integers, floats |
|
Logical NOT | ! |
booleans | |
Array Type Declaration | [] |
works for any type | [size]type |
Optional Type Declaration | ? |
works for any type | ?type |
Pointer Type Declaration | ^ |
works for any type | ^type |
Distinct Type Declaration | distinct |
works for any type | distinct type |
Postfix Operators:
Name | Symbols | Types | Example |
---|---|---|---|
Array Index | [] |
arrays, slices |
array[index] |
Function Call | () |
functions | function(arg1, arg2, ...) |
Dereference | ^ |
pointers | pointer^ |
Propagation | .try |
error unions, optionals |
error_union.try |
Cast | .() |
works for any type | type.(value) |
Array Instantiation | .[] |
works for any type | type.[value1, value2, ...] |
Struct Instantiation | .{} |
works for any struct type | type.{ field = value } |
All operators that work for one type will also work for a distinct of that type.
Note that array/struct instantiation can be done without an explicit type. This creates an anonymous array or struct
bob := .{
name = "Bob",
age = 20,
};
things := .[1, 2, 3];
anonymous arrays and structs will automatically convert to any strongly typed array or struct they encounter.
The examples
directory contains a lot more than has been gone over in this section detailing the language,
and it gives a much better idea of what the language looks like in practice.
Currently, either zig
or gcc
must be installed for the compiler to work.
They are 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 extern 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. Although things like this are being worked on.
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 testing the limits of the language and improving the compiler in so many ways. Due to her help the language has become much more complete than I would've been able to accomplish myself.
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
.
This project was made by NotAFlyingGoose :)
Capy is open to contributions! See CONTRIBUTING.md
on how you can contribute.
Even if you're not at all interested in contributing, this file explains how Capy's codebase works, which might be a good read if you're interested in compilers.
The Capy Programming Language is licensed under the Apache License (Version 2.0) with Runtime Library Exception or the MIT license, at your option. See LICENSE-APACHE and LICENSE-MIT for details.
It is Copyright (c) The Capy Programming Language Contributors.
The Runtime Library Exception makes it clear that end users of the Capy compiler donβt have to attribute their use of Capy in their finished binary application, game, or service. End-users of the Capy Programming Language should feel unrestricted to create great software. The full text of this exception follows:
As an exception, if you use this Software to compile your source code and
portions of this Software are embedded into the binary product as a result,
you may redistribute such product without providing attribution as would
otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.
This exception can be found at the bottom of the LICENSE-APACHE file. This is the same exception used by the Swift programming language.
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.
I would honestly rather pay an artist to make a proper logo, but I wouldn't know how to do that.