SkillAgentSearch skills...

Capy

🍊 A statically typed, compiled programming language, largely inspired by Jai, Odin, and Zig.

Install / Use

/learn @capy-language/Capy
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

<p align=center><img src="./resources/capybara.png" alt="capy icon" height="150"/></p>

The Capy Programming Language

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

Getting Started

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

Basics

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 i32s, 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

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.

  1. It must be immutable (it must use :: and not :=)
  2. It must contain a literal value, a reference to another const variable, a comptime block, or a reference to a comptime parameter.

Beyond just being used for types, Const variables can also be used for enum discriminants (explained later) and array sizes.

Distinct types

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

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.

<details> <summary>Extra Information About Enums</summary>

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


</details>

Optionals & Error handling

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;
View on GitHub
GitHub Stars95
CategoryDevelopment
Updated29d ago
Forks7

Languages

Rust

Security Score

100/100

Audited on Mar 3, 2026

No findings