Walrus is a tiny and simple programming language designed for simplicity. Its syntax draws inspiration from Go, Rust, and TypeScript.
- Expressive: Just look at the code and you can tell what's going on without jumping around codes.
- Beginner friendly
- Distinct behavior for every things (most of the cases). Like in other language type cast often uses int(2.4) or (int)2.4; which looks like function calls. We have separate keyword 'as' for type casting like typescript keeping it as unambiguous as we can.
- Less over complications: we removed OOP instead used structs which support methods, access modifiers and has interfaces with duck typing. We have simple approach.
- Tokenizes the source code for parsing.
- Handles syntactic analysis for:
- Builtin types
- Signed Integers:
i8
,i16
,i32
,i64
,i128
- Unsigned Intergers:
u8
,u16
,u32
,u64
,u128
- Floats:
f32
,f64
- String:
str
- Null:
null
- Void:
void
- Map:
map[key]value
- Maybe:
maybe{type}
- Signed Integers:
- Variable Declaration and Assignment
- Mutable variables with
let
- Constant variables with
const
- Multiple variable declarations in one line
- Mutable variables with
- Expressions
- Unary:
-
,!
- Additive:
+
,-
- Multiplicative:
*
,/
,%
,^
- Grouping:
( )
- Type casting using
as
- Unary:
- Data Structures
- Arrays: Indexing and assignment
- Maps: Indexing and key-value assignments
- Conditionals
if
,else if
,else
- Functions
- Declaration, calls, return values
- Optional parameters
- First-class functions and closures
- User-Defined Types
- Structs: Property access and assignment
- Interfaces: Definition, implementation, and usage
- Operators
- Increment/Decrement: Prefix and Postfix
- Assignment:
+=
,-=
,*=
,/=
,%=
- Additional Constructs
- Switch statements
- For loops (syntax under development)
- Rich Error Reporting
- Displays multiple errors during parsing and type checking
- Builtin types
Note: Currently the parser is based on pratt parsing. Structs{}, maps{}, if expr {}, else {}, have same structure so it's hard to distinguish them. If you have any idea to distinguish them, please let us know.
- Ensures type safety across all constructs.
- Handles all parser-supported features except for loops and imports (in progress).
- Planned for future releases.
To run the compiler, you need to have go installed. You can download it from here
To test a walrus file, you need to open main.go
and change the filePath
variable to the path of the file you want to test.
func main() {
filePath := "filename.wal"
// ... rest of the code
}
Then run the following command
go run main.go
Or, if you're on windows then you can run the batch file run.bat
./run
To run the tests, run the following command
go test ./...
Or, if you're on windows then you can run the batch file test.bat
./test
To install local build, go to language-support
directory and run the following command
npm install -g vsce
If you don't have node installed, you can download it from here Then run the following command
vsce package
code --install-extension walrus-<full-output-name>.vsix
Or open vscode, go to extensions and click on the three dots on the top right corner and click on 'Install from VSIX' and select the generated vsix file.
Or, you can install the extension from the marketplace here Or, Search for 'Walrus' in the vscode extensions marketplace.
// Declare a variable with let or const keyword
let a := 10; // The variable is mutable and its type is inferred from the value e.g. i32
const pi := 3.14; // constant variable with type f32
// Declare a variable with type
let b: i32 = 20; // The variable is mutable and its type is i32
const c: f32 = 3.14; // constant variable with type f32
let unsigned: u32 = 10; // Unsigned integer of 32 bits
You can also declare multiple variables in a single line
//multiple variable declaration in one line
// with type
let t1: i32 = 43, t2: f32 = 3.5, t3: str;
// without type
let t4 := 43, t5 := 3.14, t6 := "hello";
let a := 10;
a = 20; // Assign a new value to a
let a := 10;
let b := 20;
let c := a + b; // c = 30
let d := a * b; // d = 200
let e := a / b; // e = 0.5
let f := a % b; // f = 10
let g := a ^ b; // g = 100000000000000000000
let h := -a; // h = -10
let i := !true; // i = false
let a := 10;
let b := 20;
let c := (a + b) * 2; // c = 60
Type must be explicit in walrus. Type cast is done using 'as' keyword. For example, we cannot assign to a 8 bit integer to a 32 bit integer and vice versa. We need to cast the type. But when we declare a variable with explicit type, the value is implicitly casted to the type.
let a := 10;
let b := a as f32; // b = 10.0 as float32
let a := [1, 2, 3, 4, 5]; // Array of integers. One dimensional array
let b := a[0]; // b = 1
a[0] = 10; // a = [10, 2, 3, 4, 5]
// Array of arrays
let c := [[1, 2], [3, 4], [5, 6]]; // Array of arrays of integers
let d := c[0][0]; // d = 1
c[0][0] = 10; // c = [[10, 2], [3, 4], [5, 6]]
let myMap : map[str]i32 = map[str]i32 {
"a" => 10,
"b" => 20,
"c" => 30
};
let a := myMap["a"]; // a = 10
myMap["a"] = 20; // myMap = {"a": 20, "b": 20, "c": 30}
Maybe is a type which can be null or the type specified. It is used to handle null values.
let a: maybe{i32} = null; // a can be null or an integer
a = 10; // a can be assigned an integer but the value is still maybe{i32}
a = null; // a can be assigned null
let b := 12;
//Now to use the value of a, we need to check if it is null or not
safe a {
// when a is not null, this block will be executed
b = 23 + a; // b = 33
} otherwise {
// when a is null, this block will be executed
b = 23;
}
type Person struct{
name: str;
age: i32;
}
//Assign the type with @Name syntax. So we can distinguish between type and variable.
let p := Person {
name: "John",
age: 20
};
// We could also assign the type with type inference
let p : Person = Person { // Here 'Person' is the type, @Person is the type instance
name: "John",
age: 20
};
let p := Person {
name: "John",
priv age: 20 // Private property
};
let name := p.name; // name = "John"
let age := p.age; // Error: Cannot access private property
let a := 10;
let b := 20;
if a > b {
print("a is greater than b");
} else if a < b {
print("a is less than b");
} else {
print("a is equal to b");
}
fn add(a: i32, b: i32) -> i32 {
ret a + b;
}
let sum := add(10, 20); // sum = 30
// function with optional parameters
fn add(a: i32, b: i32, c?: i32 = 0) -> i32 {
ret a + b + c;
}
let sum := add(10, 20); // sum = 30
// functions are first class citizens so we can assign them to variables
let adder := add;
let sum := adder(10, 20); // sum = 30
// function with closure
fn add(a: i32) -> fn(i32) -> i32 {
ret fn(b: i32) -> i32 {
ret a + b;
};
}
let adder := add(10);
let sum := adder(20); // sum = 30
Closures in simple terms are functions which are defined inside another function. They can access the variables of the parent function. So when you return a function from a function, it is called a closure.
fn closure(a: i32) -> fn (b: i32) -> i32 {
//return a function which takes an integer and returns the sum of a and b
ret fn (b: i32) -> i32 {
ret a + b;
};
}
const addRes := add(1);
const closureRes1 := closure(1); // returns a function which returns a + b
const closureRes2 := closureRes1(2); // returns a + b
const closureRes3 := closure(1)(2); // one liner version of the above two lines
types are user defined data types. They can be structs, or a function signature or a wrapper around a built-in type.
type Circle struct {
radius: f32;
}
type FnType fn(a: i32, b: i32) -> i32;
type WrapperInt i32;
let c := Circle {
radius: 10.0
};
let f: FnType = fn(a: i32, b: i32) -> i32 {
ret a + b;
};
let w: WrapperInt = 10;
let a := 10;
let b := ++a; // b = 11, a = 11
let c := a++; // c = 11, a = 12
let d := --a; // d = 11, a = 11
let e := a--; // e = 11, a = 10
let a := 10;
a += 10; // a = 20
a -= 10; // a = 10
a *= 10; // a = 100
a /= 10; // a = 10
a %= 10; // a = 0
Syntax is not finalized yet
let a := 10;
switch a {
case 10: {
print("a is 10");
}
case 20: {
print("a is 20");
}
default: {
print("a is neither 10 nor 20");
}
}
Interfaces are a way to define a contract that a type must implement. It is a way to achieve polymorphism in the language.
type Shape interface {
fn area() -> f32;
}
type Printable interface {
fn print();
}
type Person struct {
name: str;
age: i32;
}
impl Person {
fn print() {
print("Name: ", this.name, " Age: ", this.age);
}
}
let p := Person {
name: "John",
age: 20
};
fn printPerson(p: Printable) {
p.print();
}
- For loops
- Imports and modules
- Generics
- Advanced code generation
Stay tuned for updates and contribute to the project!