![]()
Prim is a (re)learning experience about programming languages and compilers.
The language attempts a balance between readability and strict type safety.
Language Design Overview:
- Statically Typed: Types are checked at compile-time, and types must either be declared or inferred.
- Strong Typing: Variables are bound to specific types, and implicit type conversion is likely minimal.
- Modern Syntax: Use of concise function definitions (like lambdas) and inferred types.
- Structured Types: Supports defining complex types via structs, allowing for object-like behavior.
Compiler includes the typical stages of static compilation:
- Parsing source files into syntax trees.
- Performing binding (semantic analysis) on the parsed syntax.
- Collecting and reporting diagnostics.
- Handling incremental compilation by reusing information from previous compilations.
The compiler is written in C# and there is also a REPL (running on a C# interpreter for the language).
- The language uses explicit typing (e.g., i32 for 32-bit integers, f32 for 32-bit floats) and supports type inference where the type is deduced from the assigned value.
- Primitive types include:
- signed integers: i8, i16, i32 ,i64, isz
- unsigned integers: u8, u16, u32 ,u64, usz
- floating-point numbers: f16, f32, f64
- strings: str
- booleans: bool
- other: any, never, unit
- unions
- options
- arrays
- Types are known and enforced at compile time.
- The pattern for declaring and assigning variables, functions, and structs is uniform, following the structure:
name : type = value;typecan be inferred by the compiler:
name := "this is a string" // type "str" is inferred for name- Variables can be declared with explicit types or inferred types.
a : i32 = 29; // explicit type i32 assigned to a
b := "a string"; // type inferred from the assigned value - stra :: "cannot reassign a"; // immutable
b := "can reassign b"; // mutable- Functions are just variables with a lamda type.
sum : (i32, i32) -> i32 = (a, b) -> { return a + b; } // block body
double : (i32) -> i32 = x -> x * 2; // single-expression body
double := (x: i32) -> x * 2; // type inference (**not yet supported**)- Functions as first-class values, meaning they can be assigned to variables, be passed as arguments to other functions or returned from them.
- Arrays are fixed-length. This is specified using the syntax
Type[size], meaning the array has a predefined length that cannot be changed after initialization. - Arrays are initialized using square brackets ([]) containing a list of values.
array := [1, 2]; // array of two i32 values, initialized with the values 1 and 2.
// inferred type is i32[2] though this is assignable to i32[] (**not yet supported**)When length is supported in arrays we can have:
- The type and length are both part of the array's signature. For example,
i32[2]andi32[3]are considered different types and cannot be used interchangeably. - Since arrays have fixed lengths, the compiler can enforce bounds checking both at compile-time and runtime.
a : i32 = 42; // a pointer to the variable b of type i32
ptr : i32* = &a;
ptr := &a; // type inference
b := *ptr; // dereferencing the pointer to get the value of a*- A union represents a value type that can be one of multiple types
- Appending
?is short hand for a union of that type with the unit type.
i32OrStr: i32 | str = "value";
i32OrStr = 29;
a : i32? = null;
a = 29;- The language supports user-defined structured types for grouping related fields.
Point2D :: type { x: f32; y: f32; } // defining a 2D point structure
point := Point2D { x = 0, y = 1 }; // creating an instance with literal field assignments- Definitions.
- Imports (not yet supported)
math :: module;
Point2D :: type { x: f32; y: f32; } // defining a 2D point structure
point := Point2D { x = 0, y = 1 }; // creating an instance with literal field assignmentsIn another file it would look like this:
import math // as M
point := math.Point2D { x = 0, y = 1 }; - Most constructs in the language are expressions.
a :: if (x > 0) "greater than 0" else "less than or equal to 0"; // type of a is str
b :: if (x > 0) "greater than 0"; // type of b is str | unith := 0;
a :: while (h <= 10) {
if (flipCoin() == "tails") break false;
h += 1;
continue true;
}- Drop parenthesis on if and while. We can most likely know where the expression ends.
- Add for
- Add defer