Table Of Contents#
- Overview
- Sage’s History
- What Can I Use Sage For?
- Community
- Code Examples And Syntax
- Learn More
- About The Author
Overview#
Sage is a small programming language with a unique blend of features from Rust and Python. It’s statically typed, portable, and easy-to-use.
print
out functions, types, and any runtime value. enum
s are more powerful, and can be used for better error management.
Immutability is by default, with enforced typechecking and mutability rules. Sage also includes a module system for organizing code.Sage can be used for anything from operating systems development, to web development.
Sage’s History#
Check out the video below to learn more about the history of Sage’s development! It visualizes all the modifications made to the compiler since its creation. Sage began in 2022, and it’s come a long way since then!
What Can I Use Sage For?#
Sage can be used in the web, or for low-level systems programming. Here’s an operating system with its entire userspace written in Sage!
A Shell Written In Sage For Sage-OS#
Here’s a shell written in Sage!
It lets users manipulate the filesystem, run programs, and interact with the operating system.
A PowerPoint App Written In Sage For Sage-OS#
Here’s a PowerPoint app written in Sage for Sage-OS. It loads images from the filesystem and uses the device drivers to receive keyboard commands and render the images to the screen!
Community#
Check out our community page to get in contact with other users and developers! Let us know if you have any thoughts or comments about the language!
Code Examples And Syntax#
Without further ado, let’s see what Sage’s syntax looks like!
Hello World!#
Sage code is like Python – you can just start writing without a main
function!
To print
something, just use print
on whatever arguments you want!
println("Hello world!");
Hello world!
print
also works on custom data structures.
println("Hello! ", {x = 5, y = 6, z = 7}, " ", [1, 2, 3, 4]);
Hello! {x=5, y=6, z=7} [1, 2, 3, 4]
Functions#
You can write functions in Sage using the fun
keyword. Here’s an example of the gcd
function implemented in Sage!
// Calculate the greatest common divisor of two numbers
// using Euclid's algorithm
fun gcd(a: Int, b: Int): Int {
if b == 0 {
return a;
}
return gcd(b, a % b);
}
println(gcd(12, 15));
3
Standard Library Imports#
You can get started with the standard library by importing some modules!
// Import some stuff!
from std.fallible import Option, Result, panic;
from std.collections import Vec, HashMap, List;
from std.io import *;
// Create a vector and push stuff onto it!
let mut v = Vec.make<Int>();
v.push(1);
v.push(2);
v.push(3);
v.print();
[1, 2, 3]
Structs And Methods#
Here’s how we can define custom struct
types, and imbue them with methods we can call!
// Create a structure named Point, with members `x` and `y`
struct Point {
x: Int,
y: Int
}
impl Point {
// A method for creating a new point
fun new(x: Int, y: Int): Point {
return {x = x, y = y};
}
// Shift this point by a given amount in the X
// and Y directions
fun translate(&mut self, dx: Int, dy: Int) {
self.x += dx;
self.y += dy;
}
}
// Create a mutable point at (4, 5)
let mut p = Point.new(4, 5);
// Print the point
println(p);
// Translate the X by -5, and the Y by 10
p.translate(-5, 10);
// Print the translated point
println(p);
{x=4, y=5}
{x=-1, y=15}
Sum Types#
Sage supports generic algebraic datatypes, and typechecks them with structural equality! This lets you define types linked-lists in a much more canonical way than most languages.
enum List<Elem> {
Cons {
data: Elem,
next: &List<Elem>
},
Nil
}
let l = List<Int> of Cons {
data=5,
next=new List<Int> of Nil
};
match l {
of Cons {data, next} => {
println("l is cons with data=", data, " and next=", next);
},
of Nil => {
println("Got nil");
}
}
l is cons with data=5 and next=&(38999)
Const-Generics#
Sage’s typesystem is also equipped to handle types with const
parameters. This can come in handy for many use-cases. For example, this feature allows you to typecheck matrix dimensions in matrix multiplications!
// Define a constant sized matrix with a generic element type
// and parameterized width and height.
struct Matrix<T, const Rows: Int, const Cols: Int> {
arr: [[T * Cols] * Rows]
}
// Add some methods to our matrix
impl Matrix<T, Rows, Cols> {
// Create a new matrix populated with initial values
fun new(x: T): Matrix<T, Rows, Cols> {
return {arr=[[x] * Cols] * Rows};
}
// Get a value from a matrix
fun get(&self, row: Int, col: Int): &T {
return &self.arr[row][col];
}
// Multiply with another matrix
fun mul<const NewCols: Int>(
&self,
other: &Matrix<T, Cols, NewCols>,
zero: T,
add: fun(T, T) -> T,
mul: fun(T, T) -> T
): Matrix<T, Rows, NewCols> {
let mut result = Matrix.new<T, Rows, NewCols>(zero);
// Perform the actual matrix multiplication on the rows and cols
// This uses the naive algorithm for matrix multiplication
for let mut j=0; j<NewCols; j+=1; {
for let mut i=0; i<Rows; i+=1; {
let mut sum = zero;
for let mut k=0; k<Cols; k+=1; {
sum = add(sum, mul(self.arr[i][k], other.arr[k][j]));
}
result.arr[i][j] = sum;
}
}
result
}
}
Virtual Machine Instruction Set#
Sage’s instruction set is tiny enough to port in an afternoon, but efficient enough to compile high level code efficiently. The virtual machine is a simple Turing-tape based architecture, not a stack or register based virtual machine. This is one of the contributing factors to Sage’s portability.
Instruction | C Equivalent |
---|---|
while | while (reg[0]) { |
if | if (reg[0]) { |
else | } else { |
end | } |
set N_0, N_1, ..., N_X | reg[0] = N_0; reg[1] = N_1; ... reg[x] = N_X; |
call | funs[reg[0]](); |
ret | return; |
load N | memcpy(reg, tape_ptr, N * sizeof(cell)); |
store N | memcpy(tape_ptr, reg, N * sizeof(cell)); |
move N | tape_ptr += N; |
where | reg[0].p = tape_ptr; |
deref | push(tape_ptr); tape_ptr = *tape_ptr; |
refer | tape_ptr = pop(); |
index N | for (int i=0; i<N; i++) reg[i].p += tape_ptr->i; |
offset O, N | for (int i=0; i<N; i++) reg[i].p += O; |
swap N | for (int i=0; i<N; i++) swap(reg + i, tape_ptr + i); |
add N | for (int i=0; i<N; i++) reg[i].i += tape_ptr[i].i; |
sub N | for (int i=0; i<N; i++) reg[i].i -= tape_ptr[i].i; |
mul N | for (int i=0; i<N; i++) reg[i].i *= tape_ptr[i].i; |
div N | for (int i=0; i<N; i++) reg[i].i /= tape_ptr[i].i; |
rem N | for (int i=0; i<N; i++) reg[i].i %= tape_ptr[i].i; |
or N | for (int i=0; i<N; i++) reg[i].i ||= tape_ptr[i].i; |
and N | for (int i=0; i<N; i++) reg[i].i &&= tape_ptr[i].i; |
not N | for (int i=0; i<N; i++) reg[i].i = !reg[i].i; |
bitand N | for (int i=0; i<N; i++) reg[i].i &= tape_ptr[i].i; |
bitor N | for (int i=0; i<N; i++) reg[i].i |= tape_ptr[i].i; |
bitxor N | for (int i=0; i<N; i++) reg[i].i ^= tape_ptr[i].i; |
lsh N | for (int i=0; i<N; i++) reg[i].i <<= tape_ptr[i].i; |
l-rsh N | for (int i=0; i<N; i++) reg[i].i = (uint64_t)reg[i].i >> tape_ptr[i].i; |
a-rsh N | for (int i=0; i<N; i++) reg[i].i >>= tape_ptr[i].i; |
gez N | for (int i=0; i<N; i++) reg[i].i = reg[i].i >= 0; |
Learn More#
You can read my blog post (~20 minute read) about the programming language to learn more about the implementation!
Here’s a 23 minute YouTube video that covers how compilers work, and delves into Sage!
Join the Discord server to chat about Sage!
How do I contribute?#
If you want to contribute, you can open an issue or a pull request. Adding backends for other architectures is a great way to contribute! We also need a VSCode syntax highlighting extension!
About The Author#
I’m a computer science PhD student at the University of Tennessee, Knoxville🍊. Rust is my favorite language, and I’ve written many other compilers. This is the last project I started as a teenager, and I was the only author to touch any of the code up to version v0.0.2-alpha
(12/25/2023)!