Introduction
Welcome to the GLaDOS programming language documentation!
What is GLaDOS?
GLaDOS (Generic Language and Data Operand Syntax) is a statically-typed, compiled programming language that emphasizes safety and performance. It features:
- Static type checking - Catch errors before runtime
- C-like syntax - Familiar and easy to learn
- Bytecode compilation - Efficient execution
- Stack-based VM - Fast and predictable
Quick Example
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
int main() {
int result = factorial(5);
return result;
}
Project Goals
GLaDOS aims to recreate HolyC (the programming language created by Terry A. Davis for TempleOS) with our own fun twists. While maintaining the spirit of a simple, powerful systems language, we've added modern safety features and improvements.
Technologies Used
| Technology | Version | Purpose |
|---|---|---|
| Haskell | GHC 9.6.3 | Implementation language |
| Stack | LTS 22.6 | Build system |
| Megaparsec | Latest | Parser library |
| Hspec | ≥ 2.7.0 | Testing framework |
| Binary | Latest | Bytecode serialization |
Why Haskell?
Haskell's strong type system and functional programming paradigms make it an excellent choice for building compilers and type checkers. The language naturally lends itself to writing correct, maintainable compiler infrastructure. That said, the choice of Haskell was also imposed by our school, EPITECH, as part of the curriculum requirements for this project.
Installation
This guide will help you set up GLaDOS on your system.
Prerequisites
System Requirements
- Operating System: Linux (Ubuntu/Debian recommended)
- Memory: At least 4GB RAM (8GB recommended for compilation)
- Disk Space: ~2GB for Haskell toolchain and dependencies
Required Dependencies
1. Install Stack (Haskell Build Tool)
curl -sSL https://get.haskellstack.org/ | sh
2. Verify Installation
stack --version
# Should output something like: Version 2.13.1
3. Install System Dependencies
For Ubuntu/Debian:
sudo apt-get update
sudo apt-get install build-essential curl libffi-dev libffi8ubuntu1 \
libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5
For CentOS/RHEL/Fedora:
sudo dnf install gcc gcc-c++ gmp-devel make ncurses-devel zlib-devel
Installation
1. Clone the Repository
git clone git@github.com:EpitechPGE3-2025/G-FUN-500-TLS-5-1-glados-2.git
cd glados
2. Build GLaDOS
First time setup (installs GHC and dependencies):
stack setup
stack build
Build the compiler:
# Using Makefile (recommended)
make
# Or using Stack directly
stack build --copy-bins --local-bin-path . && mv ./glados-exe ./glados
The compiled binary will be named glados in your project root.
3. Verify Installation
./glados --help
# Or test with a simple program
echo 'int main() { return 42; }' | ./glados
Usage
GLaDOS supports multiple modes of operation:
# Compile and execute (default)
./glados < program.c
# Display Abstract Syntax Tree
./glados --ast < program.c
# Display human-readable IR
./glados --ir < program.c
# Compile to bytecode
./glados --compile < program.c > program.gbc
# Execute bytecode file
./glados --run program.gbc
# Show help
./glados --help
Exit Codes:
0- Success84- Error (parse, type, or runtime error)
Development Workflow
Running Tests
# Run all tests
stack test
# Run with verbose output
stack test --test-arguments="--format=progress"
# Run with coverage
stack test --coverage
Clean Build
make fclean && make
# Or
make re
Next Steps
- Write Your First Program
- Check out Examples for more programs
Your First Program
Now that you have GLaDOS installed, let's write your first program!
Hello World
Create a file called hello.c:
int main() {
int x = 5;
int y = 10;
int result = x + y;
return result;
}
Compile and Run
./glados < hello.c
Expected output:
VInt 15
Congratulations! You've just compiled and run your first GLaDOS program. The program adds two numbers and returns the result.
Next Steps
Now that you've written your first programs, you can:
- Explore more Examples
- Learn the full Syntax Guide
- Understand the Type System
Examples
This page contains practical examples demonstrating various GLaDOS features.
Basic Programs
Hello World (Simple Return)
int main() {
return 42;
}
Output: 42
Simple Addition
int add(int a, int b) {
return a + b;
}
int main() {
return add(5, 3);
}
Output: 8
Variable Declaration and Assignment
int main() {
int x = 10;
int y = 20;
int z = x + y;
return z;
}
Output: 30
Control Flow Examples
If-Else Statement
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
int main() {
int result = max(10, 20);
return result;
}
Output: 20
Nested If Statements
int abs(int x) {
if (x < 0) {
return -x;
} else {
return x;
}
}
int main() {
return abs(-15);
}
Output: 15
While Loop
int sum_to_n(int n) {
int sum = 0;
int i = 1;
while (i <= n) {
sum = sum + i;
i = i + 1;
}
return sum;
}
int main() {
return sum_to_n(10); // 1+2+3+...+10
}
Output: 55
Recursive Functions
Factorial
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
int main() {
int result = factorial(5);
return result;
}
Output: 120 (5! = 5×4×3×2×1)
Fibonacci
int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
int main() {
return fib(10);
}
Output: 55
Power Function
int power(int base, int exp) {
if (exp == 0) {
return 1;
}
return base * power(base, exp - 1);
}
int main() {
return power(2, 8); // 2^8
}
Output: 256
Boolean Logic
Comparison Functions
bool isPositive(int x) {
return x > 0;
}
bool isNegative(int x) {
return x < 0;
}
int main() {
bool result = isPositive(5);
if (result) {
return 1;
}
return 0;
}
Output: 1
Range Checking
bool isInRange(int x, int low, int high) {
return (x >= low) && (x <= high);
}
int main() {
bool result = isInRange(5, 1, 10);
if (result) {
return 1;
}
return 0;
}
Output: 1
Logical Operations
int main() {
bool a = 5 < 10; // true
bool b = 10 == 10; // true
bool c = a && b; // true
if (c) {
return 1;
}
return 0;
}
Output: 1
Function Composition
Nested Function Calls
int double(int x) {
return x * 2;
}
int quadruple(int x) {
return double(double(x));
}
int main() {
return quadruple(5);
}
Output: 20
Helper Functions
int square(int x) {
return x * x;
}
int sumOfSquares(int a, int b) {
return square(a) + square(b);
}
int main() {
return sumOfSquares(3, 4); // 3² + 4² = 9 + 16
}
Output: 25
Iterative vs Recursive
Factorial (Iterative)
int factorial_iter(int n) {
int result = 1;
while (n > 0) {
result = result * n;
n = n - 1;
}
return result;
}
int main() {
return factorial_iter(5);
}
Output: 120
Factorial (Recursive)
int factorial_rec(int n) {
if (n <= 1) {
return 1;
}
return n * factorial_rec(n - 1);
}
int main() {
return factorial_rec(5);
}
Output: 120
Advanced Examples
Greatest Common Divisor (GCD)
int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a - (a / b) * b; // a mod b
a = temp;
}
return a;
}
int main() {
return gcd(48, 18);
}
Output: 6
Count Digits
int countDigits(int n) {
int count = 0;
if (n == 0) {
return 1;
}
while (n != 0) {
n = n / 10;
count = count + 1;
}
return count;
}
int main() {
return countDigits(12345);
}
Output: 5
Reverse Number
int reverse(int n) {
int reversed = 0;
while (n != 0) {
int digit = n - (n / 10) * 10; // n mod 10
reversed = reversed * 10 + digit;
n = n / 10;
}
return reversed;
}
int main() {
return reverse(12345);
}
Output: 54321
Expression Statements
Function calls can be used as statements (result discarded):
int print(int x) {
return x; // Pretend this prints
}
int main() {
print(42); // Expression statement
print(100);
return 0;
}
Output: 0
Syntax Guide
This guide provides a practical introduction to GLaDOS syntax with examples.
Program Structure
Every GLaDOS program consists of one or more functions. The main function is the entry point.
int main() {
return 0;
}
Functions
Function Declaration
Functions are declared with a return type, name, parameters, and body:
<return_type> <name>(<parameters>) {
<statements>
}
Example:
int add(int a, int b) {
return a + b;
}
Function Parameters
Functions can take zero or more parameters:
int getFortyTwo() {
return 42;
}
int multiply(int x, int y) {
return x * y;
}
Return Statements
Every non-void function must return a value on all code paths:
int abs(int x) {
if (x < 0) {
return -x;
} else {
return x;
}
}
int bad(int x) {
if (x > 0) {
return 1;
}
}
Function Prototypes (Forward Declarations)
Functions can be declared before they are defined using prototypes. This enables mutual recursion and allows you to organize code with definitions after usage.
Syntax:
<return_type> <function_name>(<parameters>);
Examples:
int helper(int x);
bool isValid(int value);
int helper(int x) {
return x * 2;
}
bool isValid(int value) {
return helper(value) > 0;
}
Use Cases:
- Mutual recursion between functions
- Organize code with definitions after main
- Separate interface from implementation
Mutual Recursion Example:
int isEven(int n);
int isOdd(int n);
int isEven(int n) {
if (n == 0) {
return 1;
}
return isOdd(n - 1);
}
int isOdd(int n) {
if (n == 0) {
return 0;
}
return isEven(n - 1);
}
Important Notes:
- Prototype signature must match the actual function definition
- Parameters can be named in prototypes, but names are ignored
- Prototypes are particularly useful for mutually recursive functions
Variables
Declaration
Variables must be declared with a type:
int x;
bool flag;
Initialization
Variables can be initialized when declared:
int x = 10;
bool isReady = true;
Assignment
Variables can be assigned after declaration:
int x;
x = 5;
x = x + 1;
Types
GLaDOS supports four primitive types:
| Type | Description | Examples |
|---|---|---|
int | 64-bit signed integer | 0, 42, -10, 1000 |
float | 64-bit double-precision floating-point | 3.14, -0.5, 1.0, 2.5e10 |
bool | Boolean value | true, false |
void | No return value (functions only) | N/A |
Type Safety
GLaDOS is strictly typed. You cannot mix types:
int x = true;
int y = 5 + true;
int x = 42;
bool flag = false;
Operators
Arithmetic Operators
int a = 10 + 5;
int b = 10 - 5;
int c = 10 * 5;
int d = 10 / 5;
Note: Division by zero is detected at compile-time for literals:
int x = 10 / 0;
Comparison Operators
All comparison operators return bool:
bool a = 5 == 5;
bool b = 5 != 3;
bool c = 5 < 10;
bool d = 5 > 10;
bool e = 5 <= 5;
bool f = 5 >= 10;
Logical Operators
bool a = true && false;
bool b = true || false;
Operator Precedence
From highest to lowest:
*,/(Multiplicative)+,-(Additive)<,>,<=,>=,==,!=(Comparison)&&(Logical AND)||(Logical OR)
Use parentheses to override precedence:
int x = 2 + 3 * 4;
int y = (2 + 3) * 4;
Control Flow
If Statements
if (condition) {
}
If-Else Statements
if (condition) {
} else {
}
Example:
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
While Loops
while (condition) {
}
Example:
int factorial(int n) {
int result = 1;
while (n > 0) {
result = result * n;
n = n - 1;
}
return result;
}
Important: The condition must be a bool:
while (x) {
x = x - 1;
}
while (x > 0) {
x = x - 1;
}
For Loops
for (init; condition; increment) {
}
Example:
int sum(int n) {
int total = 0;
for (int i = 1; i <= n; i = i + 1) {
total = total + i;
}
return total;
}
All clauses are optional:
for (;;) {
if (done) {
return;
}
}
for (int i = 0; i < 10; i = i + 1) {
}
int i = 0;
for (; i < 10; i = i + 1) {
}
int i = 0;
for (; i < 10;) {
i = i + 1;
}
Init clause can be declaration or assignment:
for (int i = 0; i < 10; i = i + 1) {
}
int i;
for (i = 0; i < 10; i = i + 1) {
}
Important: The condition must be a bool (when provided):
for (int i = 0; i < 10; i = i + 1) {
}
int i = 0;
for (; i;) {
i = i - 1;
}
Comments
GLaDOS supports C-style comments:
// Single-line comment
/*
Multi-line
comment
*/
int x = 42;
Semicolons
Following in the footsteps of King Terry Davis and HolyC, semicolons are optional after statements in GLaDOS. We know this might make some of you anxious, but the program will run either way - even if they're mixed up in the same function!
int x = 5;
int y = 10;
return x + y;
int x = 5
int y = 10
return x + y
int x = 5;
int y = 10
return x + y;
if (x > 0) {
return x
}
if (x > 0) {
return x;
}
Use them, don't use them, mix them - GLaDOS doesn't judge. Code the way Terry intended: free.
Important Exception:
Semicolons are required in for loop syntax to separate the three clauses:
for (int i = 0; i < 10; i = i + 1) {
}
for (int i = 0; i < 10; i = i + 1) {
}
The semicolons between init, condition, and increment clauses in for loops are mandatory, even though semicolons after statements inside the loop body remain optional.
Function Calls
Functions are called by name with arguments:
int result = add(5, 3);
int answer = factorial(10);
Recursion
Functions can call themselves:
int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
Expression Statements
Any expression can be used as a statement (useful for function calls with side effects):
int print(int x) {
return x;
}
int main() {
print(42);
return 0;
}
Formal Grammar (BNF)
This is the formal grammar specification for GLaDOS in Backus-Naur Form (BNF).
ε veut dire string vide Emily 👍
Program Structure
<program> ::= <top_level_list>
<top_level_list> ::= <top_level> | <top_level> <top_level_list>
<top_level> ::= <function> | <function_prototype>
<function> ::= <type> <identifier> "(" <parameters> ")" "{" <statement_list> "}"
<function_prototype> ::= <type> <identifier> "(" <parameters> ")" ";"
Parameters
<parameters> ::= ε | <parameter> | <parameter> "," <parameters>
<parameter> ::= <type> <identifier>
Statements
<statement_list> ::= ε | <statement> <statement_list>
<statement> ::= <declaration> ";"
| <assignment> ";"
| <if_statement>
| <while_statement>
| <for_statement>
| <return_statement> ";"
Statement Types
<declaration> ::= <type> <identifier>
| <type> <identifier> "=" <expression>
| "var" <identifier> "=" <expression>
<assignment> ::= <identifier> "=" <expression>
<if_statement> ::= "if" "(" <expression> ")" "{" <statement_list> "}" <else_clause>
<else_clause> ::= ε | "else" "{" <statement_list> "}"
<while_statement> ::= "while" "(" <expression> ")" "{" <statement_list> "}"
<for_statement> ::= "for" "(" <for_init> ";" <for_condition> ";" <for_increment> ")" "{" <statement_list> "}"
<for_init> ::= ε | <declaration> | <assignment>
<for_condition> ::= ε | <expression>
<for_increment> ::= ε | <assignment>
<return_statement> ::= "return" <expression>
Note on Semicolons:
While the grammar shows semicolons after statements (declaration, assignment, return), they are optional in the actual implementation. The parser accepts statements with or without trailing semicolons.
However, semicolons are required in for loop syntax to separate the three clauses (init, condition, increment), as shown in the <for_statement> rule above.
Expressions
<expression> ::= <bool>
| <number>
| <float>
| <identifier>
| <unary_op> <expression>
| <expression> <arith_op> <expression>
| <expression> <bool_op> <expression>
| "(" <expression> ")"
Types and Operators
<type> ::= "int" | "float" | "bool" | "void"
<unary_op> ::= "-" | "!"
<arith_op> ::= "+" | "-" | "*" | "/"
<bool_op> ::= "==" | "!=" | "<" | ">" | "<=" | ">=" | "&&" | "||"
Literals and Identifiers
<bool> ::= "true" | "false"
<number> ::= <digit>+
<float> ::= <digit>+ "." <digit>+
<identifier> ::= <letter> <identifier_rest>
<identifier_rest> ::= ε | <letter_or_digit> <identifier_rest>
<letter_or_digit> ::= <letter> | <digit>
<letter> ::= "a" | "b" | ... | "z" | "A" | "B" | ... | "Z" | "_"
<digit> ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
Language Features
The grammar supports:
- Functions with parameters and return values
- Function Prototypes (forward declarations) for mutual recursion
- Types:
int,float,bool, andvoid - Type inference with
varkeyword (requires initialization) - Statements: variable declaration, assignment, if/else, while, for, return
- Expressions: integers, floats, booleans, identifiers, unary operators, arithmetic operators, boolean operators
- Comments: C-style
//line comments and/* */block comments
Example Programs
Simple Function
int add(int a, int b) {
return a + b;
}
Control Flow
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
While Loops
int factorial(int n) {
int result = 1;
while (n > 0) {
result = result * n;
n = n - 1;
}
return result;
}
For Loops
int sum(int n) {
int total = 0;
for (int i = 1; i <= n; i = i + 1) {
total = total + i;
}
return total;
}
Function Prototypes (Mutual Recursion)
int isEven(int n);
int isOdd(int n);
int isEven(int n) {
if (n == 0) {
return 1;
}
return isOdd(n - 1);
}
int isOdd(int n) {
if (n == 0) {
return 0;
}
return isEven(n - 1);
}
Float Arithmetic
float circleArea(float radius) {
float pi = 3.14159;
return pi * radius * radius;
}
Void Functions
void printValue(int x) {
if (x > 0) {
return;
}
}
Unary Operators
int negate(int x) {
return -x;
}
bool invert(bool flag) {
return !flag;
}
Type Inference with var
int compute(int x, int y) {
var sum = x + y; // Inferred as int
var product = x * y; // Inferred as int
var average = sum / 2; // Inferred as int
return average;
}
float calculate(float a, float b) {
var result = a * b; // Inferred as float
return result;
}
Operator Precedence
From highest to lowest precedence:
- Unary:
-,!(right-to-left associativity) - Multiplicative:
*,/ - Additive:
+,- - Comparison:
<=,>=,==,!=,<,> - Logical AND:
&& - Logical OR:
||
Parentheses can be used to override precedence.
Type System
GLaDOS features a static, strong type system that ensures type safety at compile time. This document provides a comprehensive reference for the type system.
Primitive Types
GLaDOS supports four primitive types:
Integer Type (int)
- Size: 64-bit signed integer
- Range: -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
- Haskell Representation:
Int64 - Literals: Decimal integer literals (e.g.,
0,42,-100,999999)
Examples:
int x = 42;
int negative = -10;
int zero = 0;
int large = 1000000;
Operations:
- Arithmetic:
+,-,*,/ - Comparison:
==,!=,<,>,<=,>= - No implicit conversion to/from bool or float
Float Type (float)
- Size: 64-bit double-precision floating-point
- Range: Approximately ±1.7 × 10^308 (IEEE 754 double precision)
- Precision: ~15-17 significant decimal digits
- Haskell Representation:
Double - Literals: Decimal floating-point literals (e.g.,
3.14,-0.5,1.0,2.5e10)
Examples:
float pi = 3.14159;
float negative = -2.5;
float small = 0.001;
float scientific = 1.5e-10;
Operations:
- Arithmetic:
+,-,*,/ - Comparison:
==,!=,<,>,<=,>= - No implicit conversion to/from int or bool
Important Notes:
- Float literals must include a decimal point (e.g.,
1.0, not1) - Division by zero results in runtime error (like integers)
- Floating-point arithmetic follows IEEE 754 standard
- No automatic conversion between
intandfloat
Type Safety with Floats:
float x = 3.14;
float y = 2.0;
float z = x + y;
int a = 5;
float b = a;
float c = 3.14 + 5;
Boolean Type (bool)
- Values:
trueorfalse - Haskell Representation:
Bool - Usage: Conditions, logical operations, comparison results
Examples:
bool flag = true;
bool isReady = false;
bool result = 5 > 3;
Operations:
- Logical:
&&(AND),||(OR) - Comparison:
==,!= - No implicit conversion to/from int or float
Void Type (void)
- Purpose: Indicates functions that do not return a value
- Usage: Only valid as a function return type
- Haskell Representation:
TypeVoid
Examples:
void printMessage() {
}
void doSomething(int x) {
if (x > 0) {
return;
}
}
Restrictions:
void x;
void result = someFunction();
void helper(int a, int b) {
}
Important Notes:
- Void functions do not need explicit return statements
- Can use
return;(without value) for early exit - Cannot be used as variable type
- Cannot be used as parameter type
- Type checker does not require return path analysis for void functions
Type Safety
Static Type Checking
All types are checked at compile time before the program runs. This catches type errors early:
int x = true;
float y = 5;
int x = 42;
float y = 3.14;
bool flag = true;
Strong Typing
GLaDOS does not allow implicit type conversions:
int x = 5 + true;
float y = 3.14 + 5;
if (5) {
return 1;
}
int z = 3.14;
if (5 > 0) {
return 1;
}
float a = 3.14;
float b = a + 2.0;
Type Inference
GLaDOS supports type inference using the var keyword. Variables declared with var must be initialized, and their type is inferred from the initialization expression:
var x = 42; // Inferred as int
var y = 3.14; // Inferred as float
var z = true; // Inferred as bool
int x = 42; // Explicit type declaration (also valid)
Requirements for var:
- Must be initialized at declaration (required)
- Type is inferred from the initialization expression
- Provides compile-time type safety
Note: The compiler uses an internal TypeInfer type during type checking for intermediate expressions to enable the var keyword functionality.
Function Types
Functions have a type signature consisting of:
- Return type: Type of value returned
- Parameter types: Types of all parameters (in order)
Function Signature Syntax
<return_type> <function_name>(<param_type> <param_name>, ...)
Examples:
int square(int x) {
return x * x;
}
int add(int a, int b) {
return a + b;
}
float multiply(float x, float y) {
return x * y;
}
bool isGreater(int a, int b) {
return a > b;
}
int getFortyTwo() {
return 42;
}
void printNumber(int x) {
}
Type Checking Rules
- Argument Count: Must match parameter count exactly
- Argument Types: Must match parameter types exactly (in order)
- Return Type: All return statements must match declared return type
int add(int a, int b) {
return a + b;
}
int main() {
int x = add(5);
int y = add(true, 5);
int z = add(5, 3);
return z;
}
Type Rules
Variable Declaration
Variables must be declared with a type before use:
int x;
x = 42;
int y = 10;
float pi = 3.14;
bool flag = true;
Variable Initialization
GLaDOS tracks whether variables are initialized before use:
int x;
return x;
int x = 42;
return x;
int y;
y = 10;
return y;
Assignment Type Checking
Assignments must match the variable's declared type:
int x = 5;
x = 10;
x = true;
Expression Type Rules
Every expression has a type determined by its components:
Arithmetic Expressions
Rule: Both operands must be the same numeric type (int or float), result is same type
int a = 5 + 3;
int b = 10 - 2;
int c = 4 * 5;
int d = 20 / 4;
float e = 3.14 + 2.86;
float f = 10.5 - 3.2;
float g = 2.5 * 4.0;
float h = 9.0 / 3.0;
int i = 5 + true;
int j = 5 + 3.14;
float k = 3.14 + 5;
Comparison Expressions
Rule: Both operands must be same type, result is bool
bool a = 5 == 5;
bool b = 5 != 3;
bool c = 5 < 10;
bool d = 3.14 == 3.14;
bool e = 2.5 < 3.7;
bool f = 1.0 >= 0.5;
bool g = true == false;
bool h = 5 == true;
bool i = 3.14 == 5;
bool j = 5 < 3.14;
Logical Expressions
Rule: Both operands must be bool, result is bool
bool a = true && false;
bool b = true || false;
bool c = (5 > 3) && (10 < 20);
bool d = 5 && 10;
bool e = true && 5;
Control Flow Type Rules
If Statement Condition
Rule: Condition must be type bool
if (5 > 3) {
return 1;
}
bool flag = true;
if (flag) {
return 1;
}
if (5) {
return 1;
}
While Loop Condition
Rule: Condition must be type bool
while (x > 0) {
x = x - 1;
}
while (x) {
x = x - 1;
}
Return Statement Type Rules
Rule: Return expression type must match function return type
int getValue() {
return 42;
}
float getPi() {
return 3.14159;
}
bool isReady() {
return true;
}
void doSomething() {
}
int getBad() {
return true;
}
float getWrong() {
return 5;
}
void voidFunc() {
return 42;
}
All Paths Must Return
Rule: Every non-void function code path must return a value of the correct type
int bad(int x) {
if (x > 0) {
return 1;
}
}
int good(int x) {
if (x > 0) {
return 1;
} else {
return 0;
}
}
int alsoGood(int x) {
if (x > 0) {
return 1;
}
return 0;
}
void printValue(int x) {
if (x > 0) {
return;
}
}
Note: The return path analysis only applies to non-void functions. Void functions are not required to have return statements on all paths.
Type Error Messages
The type checker provides detailed error messages:
Type error at line 5: expected int but got bool
Type error at line 8: expected float but got int
Return type mismatch at line 10: expected int but got bool
Return type mismatch at line 15: expected void but got int
Type error in arithmetic at line 7: expected int but got bool
Type error in arithmetic at line 12: cannot mix int and float
Function 'add' expects 2 arguments but got 1 at line 15
Type mismatch in function argument at line 12: expected int but got bool
Type mismatch in function argument at line 18: expected float but got int
Each message includes:
- Error category (Type error, Return type mismatch, etc.)
- Source location (line number)
- Expected type vs Actual type
Type System Implementation
Internal Representation
Types are represented in Haskell as:
data Type
= TypeInt
| TypeFloat
| TypeBool
| TypeVoid
| TypeInfer
deriving (Show, Eq)
Notes:
TypeInferis used internally by the compiler during type checking for intermediate expressions- User code cannot explicitly use the
TypeInfertype - All variable declarations require explicit type annotations
Type Checking Algorithm
The type checker performs:
- Environment Building: Collect all function signatures
- Function Validation: Check each function body
- Expression Type Inference: Determine type of each expression
- Type Constraint Verification: Ensure all operations are type-safe
- Initialization Tracking: Verify variables are initialized before use
- Return Path Analysis: Ensure all paths return correct type
Type Environment
The type checker maintains:
- Global Environment: Function signatures
- Local Environment: Local variable types and initialization status
- Expected Return Type: Current function's return type
Limitations and Design Choices
No User-Level Type Inference
Variables must have explicit type declarations. This is by design for simplicity and clarity.
Note: The compiler internally uses a TypeInfer type during type checking for intermediate expressions, but this is not exposed to user code. All variable and parameter declarations require explicit type annotations.
No Implicit Conversions
No automatic conversions between types (e.g., int to bool, int to float, or vice versa). This prevents common bugs.
Examples of rejected implicit conversions:
int x = 5;
float y = x;
float a = 3.14;
int b = a;
bool flag = 1;
int count = true;
All type conversions must be explicit (note: GLaDOS currently does not provide explicit conversion functions, so values must be literals of the correct type).
No User-Defined Types
GLaDOS does not support:
- Structs or records
- Enums
- Type aliases
- Generic/parametric types
This keeps the language simple and focused.
Void Return Type
Functions can have void return type when they don't return a value:
void helper(int x) {
}
void earlyExit(bool condition) {
if (condition) {
return;
}
}
Important:
- Void can only be used as a function return type
- Void functions don't need explicit return statements
- Can use
return;(without expression) for early exit - Cannot declare void variables or parameters
- Type checker doesn't enforce return path analysis for void functions
No Null or Optional Types
There is no concept of null, undefined, or optional values. All variables must be initialized.
Type Safety Guarantees
GLaDOS's type system guarantees:
- Type Soundness: Well-typed programs cannot have type errors at runtime
- Initialization Safety: Variables cannot be read before being initialized
- Return Safety: Functions always return a value of the correct type
- Operation Safety: Operators are only applied to compatible types
These guarantees eliminate entire classes of bugs at compile time.
Operators
This page provides a complete reference for all operators in GLaDOS, including precedence, associativity, and type rules.
Operator Categories
GLaDOS supports four categories of operators:
- Unary Operators: Single-operand operations (negation, logical not)
- Arithmetic Operators: Mathematical operations on integers and floats
- Comparison Operators: Relational comparisons returning booleans
- Logical Operators: Boolean logic operations
Unary Operators
Unary operators take a single operand and produce a result.
Negation (-)
Syntax: -<expr>
Type: -int → int or -float → float
Description: Negates a numeric value (changes sign)
Examples:
int x = -5;
int y = -(10 + 3);
int z = -x;
float a = -3.14;
float b = -(2.5 * 2.0);
float c = -a;
Type Rules:
- Operand must be
intorfloat - Result type matches operand type
- Cannot negate boolean values
int x = -5;
float y = -3.14;
bool z = -true;
Logical NOT (!)
Syntax: !<expr>
Type: !bool → bool
Description: Inverts a boolean value
Truth Table:
| Input | Result |
|---|---|
| true | false |
| false | true |
Examples:
bool a = !true;
bool b = !false;
bool c = !(5 > 3);
bool d = !(x && y);
Type Rules:
- Operand must be
bool - Result is always
bool - Cannot negate numeric values
bool x = !true;
int y = !5;
float z = !3.14;
Arithmetic Operators
All arithmetic operators require both operands to have the same numeric type (int or float) and produce a result of the same type.
Addition (+)
Syntax: <expr> + <expr>
Type: int + int → int or float + float → float
Description: Adds two numbers of the same type
Examples:
int a = 5 + 3;
int b = 10 + 20;
int c = -5 + 10;
int d = x + y;
float e = 3.14 + 2.86;
float f = 1.5 + 0.5;
float g = pi + radius;
Type Safety:
int x = 5 + 3.14;
float y = 3.14 + 2;
Subtraction (-)
Syntax: <expr> - <expr>
Type: int - int → int or float - float → float
Description: Subtracts the second number from the first
Examples:
int a = 10 - 3;
int b = 5 - 10;
int c = x - 1;
float d = 10.5 - 3.2;
float e = 2.0 - 0.5;
float f = total - cost;
Type Safety:
int x = 10 - 3.5;
float y = 10.5 - 3;
Multiplication (*)
Syntax: <expr> * <expr>
Type: int * int → int or float * float → float
Description: Multiplies two numbers of the same type
Examples:
int a = 5 * 3;
int b = 10 * -2;
int c = n * n;
float d = 3.14 * 2.0;
float e = 1.5 * 4.0;
float f = radius * radius;
Type Safety:
int x = 5 * 2.0;
float y = 3.14 * 2;
Division (/)
Syntax: <expr> / <expr>
Type: int / int → int or float / float → float
Description:
- Integer division: Truncates toward zero
- Float division: Produces floating-point result
Examples:
int a = 10 / 2;
int b = 15 / 4;
int c = -10 / 3;
float d = 10.0 / 3.0;
float e = 22.0 / 7.0;
float f = area / width;
Type Safety:
int x = 10 / 2.0;
float y = 10.0 / 2;
Important Notes:
- Division by zero with literal
0or0.0is detected at compile time - Division by zero with variables causes a runtime error
int x = 10 / 0;
float y = 10.0 / 0.0;
int divide(int a, int b) {
return a / b;
}
int main() {
return divide(10, 0);
}
Workaround for safety:
int safeDivide(int a, int b) {
if (b == 0) {
return 0;
}
return a / b;
}
float safeDivideFloat(float a, float b) {
if (b == 0.0) {
return 0.0;
}
return a / b;
}
Comparison Operators
All comparison operators require both operands to have the same type and produce a bool result.
Equal (==)
Syntax: <expr> == <expr>
Type: T == T → bool (where T is int, float, or bool)
Description: Tests if two values of the same type are equal
Examples:
bool a = 5 == 5;
bool b = 10 == 3;
bool c = true == false;
bool d = x == y;
bool e = 3.14 == 3.14;
bool f = 1.5 == 2.0;
bool g = pi == radius;
Type Safety:
bool x = 5 == 3.14;
bool y = true == 1;
Not Equal (!=)
Syntax: <expr> != <expr>
Type: T != T → bool (where T is int, float, or bool)
Description: Tests if two values of the same type are not equal
Examples:
bool a = 5 != 3;
bool b = 10 != 10;
bool c = true != false;
bool d = 3.14 != 2.0;
bool e = 1.5 != 1.5;
bool f = x != y;
Type Safety:
bool x = 5 != 3.14;
bool y = false != 0;
Less Than (<)
Syntax: <expr> < <expr>
Type: int < int → bool or float < float → bool
Description: Tests if the first numeric value is less than the second
Examples:
bool a = 5 < 10;
bool b = 10 < 5;
bool c = 5 < 5;
bool d = x < 100;
bool e = 3.14 < 4.0;
bool f = 10.5 < 2.0;
bool g = temperature < threshold;
Type Safety:
bool x = 5 < 3.14;
bool y = 3.14 < 5;
Greater Than (>)
Syntax: <expr> > <expr>
Type: int > int → bool or float > float → bool
Description: Tests if the first numeric value is greater than the second
Examples:
bool a = 10 > 5;
bool b = 5 > 10;
bool c = 5 > 5;
bool d = 10.5 > 3.14;
bool e = 2.0 > 10.5;
bool f = value > maximum;
Type Safety:
bool x = 10 > 3.14;
bool y = 3.14 > 10;
Less Than or Equal (<=)
Syntax: <expr> <= <expr>
Type: int <= int → bool or float <= float → bool
Description: Tests if the first numeric value is less than or equal to the second
Examples:
bool a = 5 <= 10;
bool b = 10 <= 10;
bool c = 15 <= 10;
bool d = 3.14 <= 4.0;
bool e = 10.5 <= 10.5;
bool f = current <= limit;
Type Safety:
bool x = 5 <= 3.14;
bool y = 3.14 <= 10;
Greater Than or Equal (>=)
Syntax: <expr> >= <expr>
Type: int >= int → bool or float >= float → bool
Description: Tests if the first numeric value is greater than or equal to the second
Examples:
bool a = 10 >= 5;
bool b = 10 >= 10;
bool c = 5 >= 10;
bool d = 10.5 >= 3.14;
bool e = 5.0 >= 5.0;
bool f = score >= passingGrade;
Type Safety:
bool x = 10 >= 3.14;
bool y = 3.14 >= 5;
Logical Operators
All logical operators require both operands to be bool and produce a bool result.
Logical AND (&&)
Syntax: <expr> && <expr>
Type: bool && bool → bool
Description: Returns true if both operands are true
Truth Table:
| Left | Right | Result |
|---|---|---|
| true | true | true |
| true | false | false |
| false | true | false |
| false | false | false |
Examples:
bool a = true && true;
bool b = true && false;
bool c = false && false;
bool d = (5 > 3) && (10 < 20);
bool e = (x > 0) && (x < 100);
Note on Evaluation: GLaDOS uses eager evaluation for logical operators. Both operands are always evaluated before the operation is performed. This means:
// NOT safe in GLaDOS - both sides are evaluated
bool result = (b != 0) && (a / b > 5); // Will evaluate a/b even if b==0
// Safe alternative - use nested if statements
bool result = false;
if (b != 0) {
result = (a / b > 5);
}
Logical OR (||)
Syntax: <expr> || <expr>
Type: bool || bool → bool
Description: Returns true if either operand is true
Truth Table:
| Left | Right | Result |
|---|---|---|
| true | true | true |
| true | false | true |
| false | true | true |
| false | false | false |
Examples:
bool a = true || true;
bool b = true || false;
bool c = false || false;
bool d = (x < 0) || (x > 100);
Note on Evaluation:
Like &&, the || operator uses eager evaluation. Both operands are always evaluated before the operation is performed.
Operator Precedence
Operators are evaluated in the following order (highest to lowest precedence):
| Precedence | Operators | Description | Associativity |
|---|---|---|---|
| 1 (highest) | -, ! | Unary (negation, not) | Right-to-left |
| 2 | *, / | Multiplicative | Left-to-right |
| 3 | +, - | Additive | Left-to-right |
| 4 | <, >, <=, >= | Relational | Left-to-right |
| 5 | ==, != | Equality | Left-to-right |
| 6 | && | Logical AND | Left-to-right |
| 7 (lowest) | ` | ` |
Precedence Examples
int a = 2 + 3 * 4;
bool b = 5 > 3 && 10 < 20;
bool c = true || false && false;
int d = -5 + 3;
bool e = !true && false;
int f = -2 * 3;
Using Parentheses
Parentheses () can override precedence:
int a = (2 + 3) * 4;
bool b = 5 > (3 && 10);
bool c = (true || false) && false;
Best Practice: Use parentheses for clarity even when not strictly required:
bool result = a > 0 && b < 10 || c == 5;
bool result = ((a > 0) && (b < 10)) || (c == 5);
Type Compatibility Rules
Arithmetic Operators Type Rules
int x = 5 + 3;
int y = 5 + true;
int z = false * 10;
Comparison Operators Type Rules
bool a = 5 < 10;
bool b = true == false;
bool c = 5 == true;
bool d = 10 < false;
Logical Operators Type Rules
bool a = true && false;
bool b = (5 > 3) || (10 == 10);
bool c = 5 && 10;
bool d = true && 42;
Complete Examples
Arithmetic Expression
int calculate(int x, int y) {
int sum = x + y;
int diff = x - y;
int product = x * y;
int quotient = x / y;
int result = sum * 2 + diff;
return result;
}
Comparison Chain
bool isInRange(int x, int low, int high) {
return (x >= low) && (x <= high);
}
Complex Boolean Expression
bool isValid(int age, bool hasLicense, bool isResident) {
return (age >= 18) && hasLicense && isResident;
}
Precedence in Action
int main() {
int x = 10;
int y = 5;
int a = x + y * 2;
bool b = x > 5 && y < 10;
int c = (x + y) * 2;
return a + c;
}
Common Mistakes
Mixing Types
if (x && y) {
return 1;
}
if ((x > 0) && (y > 0)) {
return 1;
}
Division by Zero
int x = 100 / 0;
int divide(int a, int b) {
return a / b;
}
divide(100, 0);
int safeDivide(int a, int b) {
if (b == 0) {
return 0;
}
return a / b;
}
Forgetting Parentheses
bool result = a > 0 && b < 10 || c == 5;
bool result = ((a > 0) && (b < 10)) || (c == 5);
Operator Implementation
Operators are implemented in the VM using type-specific stack-based instructions:
- Arithmetic:
AddInt,AddFloat,SubInt,SubFloat,MulInt,MulFloat,DivInt,DivFloat - Unary:
NegInt,NegFloat,NotBool - Comparison:
EqInt,EqFloat,NeqInt,NeqFloat,LtInt,LtFloat,GtInt,GtFloat,LeInt,LeFloat,GeInt,GeFloat - Logical:
AndBool,OrBool
All operators pop their operands from the stack and push the result back onto the stack.
Compilation Overview
GLaDOS is a compiled language that transforms source code through multiple stages to produce executable bytecode. This page provides a high-level overview of the compilation pipeline.
Compilation Pipeline
The GLaDOS compiler follows a multi-stage architecture with multiple entry and exit points:
Source Code (.c file) ────────────────────────┐
↓ │
[1. Parsing] │
(Lexing + Parsing via Megaparsec) │
↓ │
Abstract Syntax Tree (AST) ──────> --ast (output AST)
↓ │
[2. Type Checking] │
(Security/TypeChecker.hs) │
↓ │
Type-Checked AST │
↓ │
[3. IR Compilation] │
(Compiler/Core.hs) │
↓ │
IR Program ──────────────────────> --ir (output IR)
↓ │
[4. Bytecode Serialization] │
(Optional - for .gbc files) │
↓ │
Bytecode (.gbc) ──────────────────> --compile (output .gbc)
│ │
├──> --run FILE ──────────┐ │
│ (Skip parsing, load │ │
│ bytecode directly) │ │
↓ ↓ │
[5. Linking] (Optional) │
(For multi-module programs) │
↓ │
Linked Program │
↓ │
[6. VM Execution] <───────────────────────────┘
(VM/Interpreter.hs)
↓
Result (exit code) ───────────────> Default mode (output result)
Key Points:
- Multiple output modes: Can output at AST, IR, bytecode, or execution result stages
- Bytecode entry point:
--runmode skips parsing/compilation and loads bytecode directly - Lexing and parsing are combined in a single stage using Megaparsec
- Type checking is a critical stage that validates type safety before compilation
- IR compilation generates WebAssembly-like indexed bytecode
- Bytecode serialization is optional, only needed for saving to disk
- Linking is optional, only needed for multi-module programs
Stage 1: Parsing (Lexing + Parsing)
Input: Source code (string)
Output: Abstract Syntax Tree (AST)
Module: Parser/Core.hs (using Megaparsec)
Megaparsec combines lexical analysis and parsing into a single stage. The parser directly consumes source code and produces an AST, handling tokenization internally.
What happens internally:
- Lexical Analysis: Source code is broken into tokens (keywords, operators, identifiers, literals)
- Syntactic Analysis: Tokens are structured into an AST tree
Lexer Features:
- Whitespace and comments are ignored
- Keywords are recognized (
int,float,bool,void,if,while,for,return, etc.) - Operators are tokenized (
+,-,*,/,==,&&,!, etc.) - Identifiers and literals are captured with source positions
Parser Features:
- Recursive descent parsing with parser combinators
- Operator precedence handling
- Error recovery and detailed error reporting
- Expression nesting support
Example AST for int add(int a, int b) { return a + b; }:
Program
[ Function
(FuncDef
{ funcName = "add"
, funcReturnType = TypeInt
, funcParams = [("a", TypeInt), ("b", TypeInt)]
, funcBody =
[ Return
(BinOp Add
(Var "a")
(Var "b")
)
]
}
)
]
Stage 2: Type Checking
Input: Abstract Syntax Tree (AST)
Output: Type-Checked AST (or type error)
Module: Security/TypeChecker.hs
Critical Stage: The type checker validates the program's type safety before compilation. This prevents invalid programs from being compiled and executed.
Type Checking Operations:
- Type Inference: Determine the type of every expression
- Type Compatibility: Verify operations use correct types
- Initialization Tracking: Ensure variables are initialized before use
- Return Path Analysis: Verify all code paths return correct type
- Function Validation: Check function calls and signatures
Example Type Errors Caught:
int x = true;
int y;
return y;
if (x > 0) {
return 1;
}
int result = add(5);
Why "Security" Module? Type checking is a security feature that prevents:
- Type confusion vulnerabilities
- Use of uninitialized memory
- Missing return values causing undefined behavior
- Function signature mismatches
See Type Checking for detailed documentation.
Stage 3: IR Compilation
Input: Type-Checked AST
Output: IR Program (list of CompiledFunction)
Module: Compiler/Core.hs
The compiler translates the validated AST into an intermediate representation (IR) - a WebAssembly-like indexed bytecode:
Key Architectural Features:
- Indexed Locals: Variables accessed by integer index, not name
- Type-Specific Instructions: Separate instructions for int, float, bool operations
- Offset-Based Jumps: Relative jumps by instruction offset, not labels
- Function Indexing: Functions called by index, not name
IR Instruction Categories:
- Stack Operations:
PushInt,PushFloat,PushBool,Pop - Local Variables:
GetLocal <index>,SetLocal <index> - Arithmetic:
AddInt,AddFloat,SubInt,SubFloat,MulInt,MulFloat,DivInt,DivFloat - Unary:
NegInt,NegFloat,NotBool - Comparison:
EqInt,EqFloat,LtInt,LtFloat, etc. - Logical:
AndBool,OrBool - Control Flow:
Jump <offset>,JumpIfFalse <offset>,Call <funcIndex> - Utility:
Return,Halt
Example:
int add(int a, int b) {
return a + b;
}
Compiled IR:
CompiledFunction {
funcName = "add",
paramCount = 2, -- a and b are params
localVarCount = 0, -- no local variables
code = [
GetLocal 0, -- Get parameter 'a' (index 0)
GetLocal 1, -- Get parameter 'b' (index 1)
AddInt, -- Type-specific int addition
Return
]
}
Local Variable Indexing:
- Parameters are indexed first:
0, 1, 2, ... - Local variables follow:
paramCount, paramCount+1, ...
See IR Generation for complete instruction set documentation.
Stage 4: Bytecode Serialization (Optional)
Input: IR Program (CompiledFunction list)
Output: Bytecode file (.gbc)
Module: Bytecode/Serialize.hs
This stage is optional and only used when saving compiled programs to disk.
Purpose:
- Save compiled IR to
.gbc(GLaDOS Bytecode) files - Enable separate compilation and distribution
- Load pre-compiled programs for faster execution
Bytecode Format:
- Binary serialization of IR instructions
- Compact representation for storage
- Can be loaded and executed without recompilation
Usage:
./glados --compile < program.c > program.gbc
./glados --run program.gbc
Stage 5: Linking (Optional)
Input: Multiple compiled modules (.gbc files) Output: Linked IR Program Module: Linker system (v1.3+)
This stage is optional and only used for multi-module programs.
Purpose:
- Combine separately compiled modules
- Resolve external function references
- Create single executable program
How it works:
- Each module compiled with function prototypes for external references
- Linker resolves function calls across module boundaries
- Produces single unified IR program
Usage:
./glados --link module1.gbc module2.gbc -o program.gbc
See Linker Documentation for details (when available).
Stage 6: VM Execution
Input: IR Program (CompiledFunction list)
Output: Program result (exit code)
Module: VM/Interpreter.hs
The virtual machine executes the IR program using a stack-based model:
VM State:
data VMState = VMState
{ stack :: [Value] -- Operand stack
, locals :: [Value] -- Indexed local variables
, pc :: Int -- Program counter
, callStack :: [CallFrame] -- Function call frames
}
Key Features:
- Indexed Locals: Variables stored in array by index, not Map by name
- Type Checking: Stack operations validate types (popInt, popFloat, popBool)
- Stack-Based: All operations use operand stack
- Call Frames: Each function call creates isolated frame
Execution Model:
1. Fetch instruction at PC
2. Execute instruction (modify stack/locals)
3. Increment PC
4. Repeat until Return or Halt
Example execution:
int main() {
int x = 5;
int y = 10;
return x + y;
}
VM trace:
PushInt 5 Stack: [5] Locals: []
SetLocal 0 Stack: [] Locals: [5] (x = 5)
PushInt 10 Stack: [10] Locals: [5]
SetLocal 1 Stack: [] Locals: [5, 10] (y = 10)
GetLocal 0 Stack: [5] Locals: [5, 10] (load x)
GetLocal 1 Stack: [5, 10] Locals: [5, 10] (load y)
AddInt Stack: [15] Locals: [5, 10] (5 + 10)
Return Result: 15
See VM Execution for complete execution details.
Error Handling
Errors can occur at each stage:
Parsing Errors
Parse error at line 5, column 10: unexpected token '{'
Parse error at line 10: unexpected end of input, expecting statement
Type Checking Errors
Type error at line 8: expected int but got bool
Type error at line 12: variable 'x' used before initialization
Type error at line 15: function 'add' expects 2 arguments but got 1
Type error: function must return a value on all code paths
Compilation Errors
Compile error: division by zero at line 20
Runtime Errors
Runtime error: Division by zero
Runtime error: Stack underflow
Runtime error: Type mismatch in operation
Compiler Architecture
Module Structure
GLaDOS uses a modularized architecture with separate directories for each major component:
glados/
├── src/
│ ├── AST/
│ │ └── AST.hs # AST data types
│ ├── Parser/
│ │ ├── Core.hs # Main parser export
│ │ ├── Lexer.hs # Lexer utilities
│ │ ├── Program.hs # Program parser
│ │ ├── Statement.hs # Statement parsers
│ │ └── Expression.hs # Expression parsers
│ ├── Security/
│ │ ├── Environment.hs # Type checking environment
│ │ ├── TypeChecker.hs # Type checking implementation
│ │ └── Types.hs # Type checking data types
│ ├── Compiler/
│ │ ├── Core.hs # Main compiler
│ │ ├── Environment.hs # Variable/function tables
│ │ ├── Expr.hs # Expression compilation
│ │ ├── Statement.hs # Statement compilation
│ │ └── Function.hs # Function compilation
│ ├── IR/
│ │ └── Types.hs # IR instruction types
│ ├── Bytecode/
│ │ └── Serialize.hs # Bytecode serialization
│ ├── VM/
│ │ ├── Interpreter.hs # VM execution
│ │ ├── HelperFunc.hs # VM helper functions
│ │ └── InstructionHandlers.hs # Instruction execution handlers
│ ├── Error/
│ │ └── Types.hs # Error types
│ └── Main.hs # Entry point
Data Flow
- Main.hs: Reads source code from stdin or file
- Parser/Core.hs: Parses source code into AST
- Security/TypeChecker.hs: Validates type safety
- Compiler/Core.hs: Compiles AST to IR (indexed bytecode)
- Bytecode/Serialize.hs (optional): Serializes IR to .gbc file
- Linker (optional): Links multiple modules together
- VM/Interpreter.hs: Executes IR program
- Main.hs: Returns result (exit code 0 for success, 84 for error)
Compiler Modes
The GLaDOS compiler supports multiple modes of operation:
| Mode | Usage | Description |
|---|---|---|
| Default | ./glados < program.c | Read from stdin, compile and execute, output result |
| AST Mode | ./glados --ast < program.c | Parse and display the Abstract Syntax Tree |
| IR Mode | ./glados --ir < program.c | Parse, compile and display human-readable IR |
| Compile Mode | ./glados --compile < program.c > out.gbc | Compile to bytecode and write to stdout |
| Compile Object | ./glados --compile-obj < module.c > module.gbo | Compile to relocatable object file for linking |
| Link Mode | ./glados --link mod1.gbo mod2.gbo -o prog.gbc | Link multiple object files into executable |
| Run Mode | ./glados --run program.gbc | Execute bytecode from FILE (skips parsing/compilation) |
| Help | ./glados --help | Display help message |
| Version | ./glados --version | Display version information |
Examples:
# Compile and execute (default mode)
./glados < program.c
# Show the AST for debugging
./glados --ast < program.c
# Show human-readable IR
./glados --ir < program.c
# Compile to bytecode file
./glados --compile < program.c > program.gbc
# Execute pre-compiled bytecode (fast - skips parsing)
./glados --run program.gbc
# Separate compilation with linking
./glados --compile-obj < module_a.c > module_a.gbo
./glados --compile-obj < module_b.c > module_b.gbo
./glados --link module_a.gbo module_b.gbo -o program.gbc
./glados --run program.gbc
These modes provide flexibility for:
- Debugging: View AST and IR representations
- Development: Separate compilation for faster incremental builds
- Distribution: Create standalone bytecode files
- Performance: Skip compilation when running pre-compiled bytecode
Implementation Details
Parser Combinators (Megaparsec)
GLaDOS uses Megaparsec for parsing:
- Combinator-based: Compose small parsers into larger ones
- Error messages: Detailed parse error reporting
- Backtracking: Try alternative parse paths
- Lexer support: Built-in token parsing utilities
Example parser combinator:
functionDef :: Parser FuncDef
functionDef = do
returnType <- typeParser
name <- identifier
params <- parens paramList
body <- braces statementList
return $ FuncDef name returnType params body
IR Design
The IR is a human-readable disassembly format:
- Simple to generate from AST
- Easy to debug and inspect
- Clear representation of program flow
- Can be saved and examined independently
Bytecode Format
Bytecode is the binary compiled format:
- Compact and efficient
- Fast to execute in VM
- Can be saved to disk
- No complex register allocation
VM Design
The VM uses a stack machine architecture:
- All operations use the stack
- Function calls push new frames
- Return pops frames
- Simple and efficient
Performance Characteristics
Compilation Time
- Lexing/Parsing: O(n) where n is source length
- IR Generation: O(n) where n is AST size
- Bytecode Generation: O(n) where n is IR size
- Total: Linear in source code size
Runtime Performance
- Stack operations: O(1)
- Variable access: O(1) with environment map
- Function calls: O(1) for call overhead
- Recursion: Limited by stack size (Haskell stack)
Exit Codes
GLaDOS follows standard EPITECH conventions:
- 0: Success (program compiled and ran successfully)
- 84: Error (parse error or runtime error)
Next Steps
To learn more about each compilation stage:
- Parsing: Detailed parser implementation
- IR Generation: IR instruction set and generation
- VM Execution: Virtual machine implementation
Parsing
This page details the GLaDOS parser implementation, which transforms source code into an Abstract Syntax Tree (AST) using Megaparsec parser combinators.
Overview
The parser is implemented in the src/Parser/ module and consists of two main phases:
- Lexical Analysis (Lexing): Convert source code into tokens
- Syntactic Analysis (Parsing): Build AST from tokens
Both phases are implemented using Megaparsec, a powerful parser combinator library for Haskell.
Lexical Analysis
Token Types
The lexer recognizes several categories of tokens:
Keywords:
keywords = ["int", "bool", "if", "else", "while", "return", "true", "false"]
Operators:
operators =
[ "+", "-", "*", "/" -- Arithmetic
, "==", "!=", "<", ">", "<=", ">=" -- Comparison
, "&&", "||" -- Logical
, "=" -- Assignment
]
Delimiters:
delimiters = ["(", ")", "{", "}", ";", ","]
Identifiers:
<identifier> ::= [a-zA-Z_][a-zA-Z0-9_]*
Integer Literals:
<integer> ::= [0-9]+
Boolean Literals:
<boolean> ::= "true" | "false"
Whitespace and Comments
The lexer automatically handles:
- Whitespace: Spaces, tabs, newlines
- Line comments:
// comment text - Block comments:
/* comment text */
All whitespace and comments are ignored during parsing.
Lexer Implementation
The lexer is built using Megaparsec's built-in lexer utilities:
-- Space consumer (handles whitespace and comments)
sc :: Parser ()
sc = L.space
space1
(L.skipLineComment "//")
(L.skipBlockComment "/*" "*/")
-- Lexeme parser (parses token and trailing whitespace)
lexeme :: Parser a -> Parser a
lexeme = L.lexeme sc
-- Symbol parser (parses specific string)
symbol :: String -> Parser String
symbol = L.symbol sc
-- Identifier parser
identifier :: Parser String
identifier = lexeme (p >>= check)
where
p = (:) <$> letterChar <*> many alphaNumChar
check x = if x `elem` keywords
then fail $ "keyword " ++ show x ++ " cannot be an identifier"
else return x
-- Integer parser
integer :: Parser Int64
integer = lexeme L.decimal
-- Keyword parser
keyword :: String -> Parser ()
keyword w = lexeme (string w *> notFollowedBy alphaNumChar)
Syntactic Analysis
Parser Combinators
Parser combinators allow building complex parsers from simple ones:
-- Sequence: parse p, then q
p <*> q
-- Alternative: try p, if it fails try q
p <|> q
-- Map: parse p and apply function f
f <$> p
-- Bind: parse p, use result in computation
p >>= \result -> ...
-- Many: parse p zero or more times
many p
-- Some: parse p one or more times
some p
-- Optional: parse p or return Nothing
optional p
Expression Parsing
GLaDOS uses operator precedence parsing for expressions:
-- Expression parser with precedence
expr :: Parser Expr
expr = makeExprParser term operatorTable
-- Operator precedence table
operatorTable :: [[Operator Parser Expr]]
operatorTable =
[ [ binary "*" Mul, binary "/" Div ] -- Precedence 6 (highest)
, [ binary "+" Add, binary "-" Sub ] -- Precedence 5
, [ binary "<" Lt, binary ">" Gt
, binary "<=" Le, binary ">=" Ge ] -- Precedence 4
, [ binary "==" Eq, binary "!=" Neq ] -- Precedence 3
, [ binary "&&" And ] -- Precedence 2
, [ binary "||" Or ] -- Precedence 1 (lowest)
]
-- Binary operator helper
binary :: String -> Op -> Operator Parser Expr
binary name op = InfixL (BinOp op <$ symbol name)
-- Term (atomic expression)
-- Note: Simplified pseudocode. Actual implementation uses BoolLit, NumLit, FloatLit
-- constructors with Located wrappers for source position tracking.
term :: Parser Expr
term = parens expr
<|> literal -- Parses to BoolLit/NumLit/FloatLit (Located)
<|> try functionCall
<|> identifier -- Parses to Var (Located String)
Key Points:
- Operators at the top of the table bind tighter (higher precedence)
- All operators are left-associative (
InfixL) - Terms include literals, variables, function calls, and parenthesized expressions
Statement Parsing
Statements are parsed using pattern matching:
statement :: Parser Statement
statement = choice
[ ifStatement
, whileStatement
, returnStatement
, try varDeclaration
, try assignment
, expressionStatement
]
-- If statement
ifStatement :: Parser Statement
ifStatement = do
keyword "if"
condition <- parens expr
thenBody <- braces (many statement)
elseBody <- optional (keyword "else" >> braces (many statement))
return $ If condition thenBody elseBody
-- While loop
whileStatement :: Parser Statement
whileStatement = do
keyword "while"
condition <- parens expr
body <- braces (many statement)
return $ While condition body
-- Return statement
returnStatement :: Parser Statement
returnStatement = do
keyword "return"
value <- expr
optional (symbol ";")
return $ Return value
-- Variable declaration
varDeclaration :: Parser Statement
varDeclaration = do
varType <- typeParser
varName <- identifier
value <- optional (symbol "=" >> expr)
optional (symbol ";")
return $ Decl varType varName value
-- Assignment
assignment :: Parser Statement
assignment = do
varName <- identifier
symbol "="
value <- expr
optional (symbol ";")
return $ Assign varName value
-- Expression statement (function call as statement)
expressionStatement :: Parser Statement
expressionStatement = do
e <- expr
optional (symbol ";")
return $ ExprStmt e
Function Parsing
Functions are the top-level program structure:
-- Function definition
functionDef :: Parser FuncDef
functionDef = do
returnType <- typeParser
name <- identifier
params <- parens paramList
body <- braces (many statement)
return $ FuncDef
{ funcName = name
, funcReturnType = returnType
, funcParams = params
, funcBody = body
}
-- Parameter list
paramList :: Parser [(String, Type)]
paramList = param `sepBy` symbol ","
where
param = (,) <$> typeParser <*> identifier
-- Type parser
typeParser :: Parser Type
typeParser = (TypeInt <$ keyword "int")
<|> (TypeFloat <$ keyword "float")
<|> (TypeBool <$ keyword "bool")
<|> (TypeVoid <$ keyword "void")
-- Program (list of functions)
program :: Parser Program
program = sc *> many functionDef <* eof
Function Calls
Function calls require special handling:
functionCall :: Parser Expr
functionCall = do
name <- identifier
args <- parens (expr `sepBy` symbol ",")
return $ Call name args
Note: try combinator is used to backtrack if parsing fails (useful for distinguishing variable from function call).
AST Structure
The parser produces an Abstract Syntax Tree with the following structure:
Program Node
data Program = Program [FuncDef]
Function Definition Node
data FuncDef = FuncDef
{ funcName :: String
, funcReturnType :: Type
, funcParams :: [(String, Type)]
, funcBody :: [Statement]
}
Statement Nodes
data Statement
= Decl Type String (Maybe Expr) -- int x = 5;
| Assign String Expr -- x = 10;
| If Expr [Statement] (Maybe [Statement]) -- if (...) {...} else {...}
| While Expr [Statement] -- while (...) {...}
| For (Maybe Statement) -- for (init; cond; incr) {...}
(Maybe Expr)
(Maybe Statement)
[Statement]
| Return Expr -- return x;
| ExprStmt Expr -- functionCall();
For Loop Structure:
- First
Maybe Statement: Optional initialization (declaration or assignment) Maybe Expr: Optional condition expression- Second
Maybe Statement: Optional increment statement [Statement]: Loop body
Expression Nodes
data Expr
= BoolLit (Located Bool) -- true, false (with source position)
| NumLit (Located Int) -- 42, -10 (with source position)
| FloatLit (Located Double) -- 3.14, -0.5 (with source position)
| Var (Located String) -- x (with source position)
| BinOp Op (Located Expr) (Located Expr) -- a + b
| UnOp UnOp (Located Expr) -- -x, !flag
| Call (Located String) [Expr] -- foo(a, b)
Note: All expressions are wrapped with Located to track source positions for better error messages.
Binary Operators
data Op
= Add | Sub | Mul | Div -- Arithmetic
| Eq | Neq | Lt | Gt | Le | Ge -- Comparison
| And | Or -- Logical
Unary Operators
data UnOp
= Neg -- Negation: -x
| Not -- Logical NOT: !x
Literals
Literals are now represented directly in expression nodes rather than a separate Literal type:
NumLit (Located Int): Integer literals like42,-10FloatLit (Located Double): Floating-point literals like3.14,-0.5BoolLit (Located Bool): Boolean literalstrue,false
Types
data Type
= TypeInt -- int
| TypeFloat -- float
| TypeBool -- bool
| TypeVoid -- void
| TypeInfer -- Internal use only
Note: TypeInfer is used internally by the compiler for type inference and should not appear in parsed AST.
Located Wrapper
All AST nodes that require error reporting are wrapped with source position information:
data Located a = Located
{ pos :: SourcePos -- Source file position
, value :: a -- Actual value
}
data SourcePos = SourcePos
{ sourceName :: String -- File name
, sourceLine :: Int -- Line number
, sourceColumn :: Int -- Column number
}
Purpose: The Located wrapper enables precise error messages by tracking where each construct appears in the source code.
Example Error Message:
Type error at line 10, column 5:
Variable 'x' used before initialization
Without Located, the compiler could only report "Variable 'x' used before initialization" without indicating where in the code this occurred.
Error Handling
Parse Errors
Megaparsec provides detailed error messages:
-- Example error messages
Parse error at line 5, column 10:
unexpected '{'
expecting ';', ')', or identifier
Parse error at line 10:
unexpected end of input
expecting statement
Error Recovery
The parser uses:
trycombinator: Backtrack on failure<|>operator: Try alternative parsersoptional: Make parts of syntax optional (e.g., semicolons)
Source Position Tracking
Megaparsec automatically tracks:
- Line number
- Column number
- File name (if provided)
This information is included in error messages.
Parser Usage
Parsing Source Code
-- Parse from string
parseGLaDOS :: String -> Either String Program
parseGLaDOS input =
case parse program "<stdin>" input of
Left err -> Left (errorBundlePretty err)
Right ast -> Right ast
Example
Input:
int add(int a, int b) {
return a + b;
}
Output AST:
Program
[ FuncDef
{ funcName = "add"
, funcReturnType = TypeInt
, funcParams = [("a", TypeInt), ("b", TypeInt)]
, funcBody =
[ Return
(BinOp Add
(Var "a")
(Var "b")
)
]
}
]
Parser Features
Semicolon Flexibility
GLaDOS makes semicolons optional after statements:
optionalSemi :: Parser ()
optionalSemi = optional semi >> return ()
This allows both styles:
int x = 5;
return x;
int x = 5
return x
Exception: Semicolons are required in for loop syntax to separate the clauses:
void semi
The for loop parser uses void semi (not optionalSemi) for the clause separators, making them mandatory.
Expression Precedence
The operator table ensures correct precedence:
int x = 2 + 3 * 4;
bool b = 5 > 3 && 10 < 20;
Nested Expressions
The parser handles arbitrary nesting:
int result = ((a + b) * c) / (d - e);
bool flag = (x > 0) && ((y < 10) || (z == 5));
Recursive Functions
Functions can call themselves:
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
Parser Limitations
Limited Error Recovery
The parser stops at the first error. It doesn't attempt to recover and continue parsing.
Implementation Details
Megaparsec Advantages
- Composability: Build complex parsers from simple ones
- Error Messages: Detailed, helpful error reporting
- Performance: Efficient parsing with minimal overhead
- Type Safety: Strong typing prevents many parser bugs
- Flexibility: Easy to extend with new syntax
Parser Performance
- Time Complexity: O(n) where n is input length
- Space Complexity: O(n) for AST storage
- Backtracking: Minimal, limited to specific cases (function calls vs variables)
Testing the Parser
Example Test Cases
-- Test: Simple function
testSimpleFunction = "int main() { return 42; }"
-- Test: Complex expression
testExpression = "int x = 2 + 3 * 4 - 5 / 2;"
-- Test: Control flow
testControlFlow = "if (x > 0) { return 1; } else { return 0; }"
-- Test: Recursion
testRecursion = "int fib(int n) { if (n <= 1) { return n; } return fib(n-1) + fib(n-2); }"
Running Tests
stack test --test-arguments="--match Parser"
Next Steps
After parsing, the AST is passed to:
- Security Analysis: Check for division by zero and other issues
- Type Checking: Verify type correctness
- IR Generation: Convert to bytecode
See:
Type Checking
This page details the GLaDOS type checking system, which ensures type safety and correctness before code execution.
Overview
The type checker is implemented in src/Security/TypeChecker.hs and performs:
- Type inference: Determine the type of every expression
- Type compatibility: Verify operations use correct types
- Initialization tracking: Ensure variables are initialized before use
- Return path analysis: Verify all code paths return correct type
- Function validation: Check function calls and signatures
Type Checking Architecture
Two-Pass Approach
The type checker uses a two-pass algorithm:
Pass 1: Environment Building
buildGlobalEnv :: Program -> TypeEnv
buildGlobalEnv (Program funcs) = Map.fromList
[ (funcName f, (funcReturnType f, map snd (funcParams f)))
| f <- funcs
]
Collects all function signatures into a global environment for type checking function calls.
Pass 2: Function Body Checking
checkFunction :: GlobalEnv -> FuncDef -> Either String ()
checkFunction globalEnv func = do
let localEnv = initLocalEnv (funcParams func)
let returnType = funcReturnType func
checkStatements globalEnv localEnv returnType (funcBody func)
verifyAllPathsReturn (funcBody func)
Validates each function body with proper type constraints.
Type Environments
Global Environment
Maps function names to their signatures:
type GlobalEnv = Map String (Type, [Type])
-- Map from function name to (return type, parameter types)
-- Example:
-- "add" -> (TypeInt, [TypeInt, TypeInt])
-- "isPositive" -> (TypeBool, [TypeInt])
Local Environment
Tracks local variables and their initialization status:
type LocalEnv = Map String (Type, InitStatus)
data InitStatus
= Initialized -- Variable has a value
| Uninitialized -- Variable declared but not initialized
-- Example:
-- "x" -> (TypeInt, Initialized)
-- "y" -> (TypeBool, Uninitialized)
Expression Type Checking
Type Inference
Every expression has an inferred type:
inferExprType :: GlobalEnv -> LocalEnv -> Expr -> Either String Type
-- Literal
inferExprType _ _ (Lit (IntLit _)) = Right TypeInt
inferExprType _ _ (Lit (BoolLit _)) = Right TypeBool
-- Variable
inferExprType _ env (Var name) =
case Map.lookup name env of
Nothing -> Left $ "Undefined variable '" ++ name ++ "'"
Just (ty, Uninitialized) -> Left $ "Variable '" ++ name ++ "' used before initialization"
Just (ty, Initialized) -> Right ty
-- Binary operation
inferExprType globalEnv env (BinOp op left right) = do
leftType <- inferExprType globalEnv env left
rightType <- inferExprType globalEnv env right
checkBinOp op leftType rightType
-- Function call
inferExprType globalEnv env (Call fname args) = do
(returnType, paramTypes) <- lookupFunction globalEnv fname
argTypes <- mapM (inferExprType globalEnv env) args
checkArgumentTypes fname paramTypes argTypes
return returnType
Binary Operator Type Rules
Different operators have different type requirements:
checkBinOp :: BinOp -> Type -> Type -> Either String Type
-- Arithmetic operators: int × int → int
checkBinOp Add TypeInt TypeInt = Right TypeInt
checkBinOp Sub TypeInt TypeInt = Right TypeInt
checkBinOp Mul TypeInt TypeInt = Right TypeInt
checkBinOp Div TypeInt TypeInt = Right TypeInt
checkBinOp op TypeInt TypeInt = Left $ "Type error: arithmetic operation requires int operands"
-- Comparison operators: T × T → bool (same types)
checkBinOp Eq t1 t2 | t1 == t2 = Right TypeBool
checkBinOp Neq t1 t2 | t1 == t2 = Right TypeBool
checkBinOp Lt TypeInt TypeInt = Right TypeBool
checkBinOp Gt TypeInt TypeInt = Right TypeBool
checkBinOp Le TypeInt TypeInt = Right TypeBool
checkBinOp Ge TypeInt TypeInt = Right TypeBool
-- Logical operators: bool × bool → bool
checkBinOp And TypeBool TypeBool = Right TypeBool
checkBinOp Or TypeBool TypeBool = Right TypeBool
-- Type mismatch
checkBinOp op t1 t2 = Left $ "Type error: incompatible types " ++ show t1 ++ " and " ++ show t2
Statement Type Checking
Variable Declaration
checkVarDecl :: GlobalEnv -> LocalEnv -> Type -> String -> Maybe Expr -> Either String LocalEnv
checkVarDecl globalEnv env declaredType varName Nothing =
-- Declaration without initialization
Right $ Map.insert varName (declaredType, Uninitialized) env
checkVarDecl globalEnv env declaredType varName (Just initExpr) = do
-- Declaration with initialization
exprType <- inferExprType globalEnv env initExpr
when (exprType /= declaredType) $
Left $ "Type error: expected " ++ show declaredType ++ " but got " ++ show exprType
Right $ Map.insert varName (declaredType, Initialized) env
Example:
int x; // OK: x is TypeInt, Uninitialized
int y = 42; // OK: y is TypeInt, Initialized
int z = true; // ERROR: expected int but got bool
Assignment
checkAssignment :: GlobalEnv -> LocalEnv -> String -> Expr -> Either String LocalEnv
checkAssignment globalEnv env varName expr = do
(varType, _) <- lookupVariable env varName
exprType <- inferExprType globalEnv env expr
when (exprType /= varType) $
Left $ "Type error: cannot assign " ++ show exprType ++ " to " ++ show varType
-- Mark variable as initialized after assignment
Right $ Map.insert varName (varType, Initialized) env
Example:
int x;
x = 42; // OK: x becomes Initialized
x = true; // ERROR: cannot assign bool to int
If Statement
checkIf :: GlobalEnv -> LocalEnv -> Type -> Expr -> [Statement] -> Maybe [Statement] -> Either String ()
checkIf globalEnv env returnType condition thenBody elseBody = do
-- Check condition is bool
condType <- inferExprType globalEnv env condition
when (condType /= TypeBool) $
Left $ "Type error: if condition must be bool, got " ++ show condType
-- Check then branch
checkStatements globalEnv env returnType thenBody
-- Check else branch if present
case elseBody of
Nothing -> Right ()
Just stmts -> checkStatements globalEnv env returnType stmts
Example:
if (x > 0) { // OK: x > 0 is bool
return 1;
}
if (x) { // ERROR: x is int, not bool
return 1;
}
While Loop
checkWhile :: GlobalEnv -> LocalEnv -> Type -> Expr -> [Statement] -> Either String ()
checkWhile globalEnv env returnType condition body = do
-- Check condition is bool
condType <- inferExprType globalEnv env condition
when (condType /= TypeBool) $
Left $ "Type error: while condition must be bool, got " ++ show condType
-- Check body
checkStatements globalEnv env returnType body
Example:
while (x > 0) { // OK: x > 0 is bool
x = x - 1;
}
while (x) { // ERROR: x is int, not bool
x = x - 1;
}
Return Statement
checkReturn :: GlobalEnv -> LocalEnv -> Type -> Expr -> Either String ()
checkReturn globalEnv env expectedType expr = do
actualType <- inferExprType globalEnv env expr
when (actualType /= expectedType) $
Left $ "Return type mismatch: expected " ++ show expectedType ++ " but got " ++ show actualType
Example:
int getValue() {
return 42; // OK: returns int
return true; // ERROR: expected int but got bool
}
Function Call Validation
Argument Type Checking
checkFunctionCall :: GlobalEnv -> LocalEnv -> String -> [Expr] -> Either String Type
checkFunctionCall globalEnv env funcName args = do
-- Lookup function signature
(returnType, paramTypes) <- case Map.lookup funcName globalEnv of
Nothing -> Left $ "Undefined function '" ++ funcName ++ "'"
Just sig -> Right sig
-- Check argument count
when (length args /= length paramTypes) $
Left $ "Function '" ++ funcName ++ "' expects " ++ show (length paramTypes) ++
" arguments but got " ++ show (length args)
-- Infer argument types
argTypes <- mapM (inferExprType globalEnv env) args
-- Check each argument type
zipWithM_ (checkArgType funcName) (zip [1..] paramTypes) argTypes
return returnType
checkArgType :: String -> (Int, Type) -> Type -> Either String ()
checkArgType funcName (idx, expected) actual =
when (expected /= actual) $
Left $ "Type mismatch in function '" ++ funcName ++ "' argument " ++ show idx ++
": expected " ++ show expected ++ " but got " ++ show actual
Example:
int add(int a, int b) {
return a + b;
}
int main() {
return add(5, 3); // OK: 2 int arguments
return add(5); // ERROR: expects 2 arguments, got 1
return add(true, 5); // ERROR: argument 1 expected int but got bool
}
Initialization Tracking
Variable Usage Before Initialization
The type checker tracks whether variables are initialized:
-- When variable is declared without initialization
Map.insert varName (varType, Uninitialized) env
-- When variable is used
case Map.lookup varName env of
Just (ty, Uninitialized) ->
Left $ "Variable '" ++ varName ++ "' used before initialization"
Just (ty, Initialized) ->
Right ty
-- When variable is assigned
Map.insert varName (varType, Initialized) env
Example:
int x;
return x; // ERROR: x used before initialization
int y;
y = 10;
return y; // OK: y initialized before use
int z = 5;
return z; // OK: z initialized at declaration
Initialization Flow Analysis
The checker tracks initialization through control flow:
int x;
if (condition) {
x = 5; // x initialized in this branch
} else {
x = 10; // x initialized in this branch
}
return x; // OK: x initialized on all paths
int y;
if (condition) {
y = 5; // y initialized only here
}
return y; // ERROR: y might be uninitialized
Return Path Analysis
Verifying All Paths Return
Every non-void function must return on all code paths:
verifyAllPathsReturn :: [Statement] -> Either String ()
verifyAllPathsReturn stmts =
unless (allPathsReturn stmts) $
Left "Function must return a value on all code paths"
allPathsReturn :: [Statement] -> Bool
allPathsReturn [] = False
allPathsReturn stmts = any isReturn stmts || hasIfElseReturn stmts
isReturn :: Statement -> Bool
isReturn (Return _) = True
isReturn _ = False
hasIfElseReturn :: [Statement] -> Bool
hasIfElseReturn stmts = any checkIfElse stmts
where
checkIfElse (If _ thenBody (Just elseBody)) =
allPathsReturn thenBody && allPathsReturn elseBody
checkIfElse _ = False
Example:
// ✗ ERROR: Missing return
int bad1(int x) {
if (x > 0) {
return 1;
}
// No return here!
}
// ✗ ERROR: Empty body
int bad2() {
}
// ✓ OK: Return after if
int good1(int x) {
if (x > 0) {
return 1;
}
return 0;
}
// ✓ OK: Both branches return
int good2(int x) {
if (x > 0) {
return 1;
} else {
return 0;
}
}
Type Error Messages
Error Message Format
Type errors include detailed information:
typeError :: Int -> String -> String
typeError line msg = "Type error at line " ++ show line ++ ": " ++ msg
Examples:
Type error at line 5: expected int but got bool
Type error at line 10: variable 'x' used before initialization
Type error at line 15: undefined function 'foo'
Type error at line 20: function 'add' expects 2 arguments but got 1
Type error at line 25: type mismatch in function argument: expected int but got bool
Return type mismatch at line 30: expected int but got bool
Context-Specific Errors
Different error messages for different contexts:
- Variable errors: "Undefined variable 'x'", "Variable 'x' used before initialization"
- Type mismatches: "expected int but got bool"
- Function errors: "Undefined function 'foo'", "Function 'add' expects 2 arguments but got 1"
- Return errors: "Return type mismatch", "Function must return a value on all code paths"
- Operator errors: "Type error in arithmetic: expected int but got bool"
Type Checking Algorithm
Complete Algorithm
typeCheck :: Program -> Either String ()
typeCheck program@(Program funcs) = do
-- Pass 1: Build global environment
let globalEnv = buildGlobalEnv program
-- Verify main function exists
unless (Map.member "main" globalEnv) $
Left "Program must have a 'main' function"
-- Pass 2: Check each function
mapM_ (checkFunction globalEnv) funcs
checkFunction :: GlobalEnv -> FuncDef -> Either String ()
checkFunction globalEnv func = do
-- Initialize local environment with parameters
let localEnv = Map.fromList
[ (name, (ty, Initialized))
| (name, ty) <- funcParams func
]
-- Check function body
checkStatements globalEnv localEnv (funcReturnType func) (funcBody func)
-- Verify all paths return
verifyAllPathsReturn (funcBody func)
checkStatements :: GlobalEnv -> LocalEnv -> Type -> [Statement] -> Either String ()
checkStatements globalEnv env returnType stmts =
foldM_ (checkStatement globalEnv returnType) env stmts
checkStatement :: GlobalEnv -> Type -> LocalEnv -> Statement -> Either String LocalEnv
checkStatement globalEnv returnType env stmt =
case stmt of
Decl ty name init ->
checkVarDecl globalEnv env ty name init
Assign name expr ->
checkAssignment globalEnv env name expr
If cond thenBody elseBody -> do
checkIf globalEnv env returnType cond thenBody elseBody
return env
While cond body -> do
checkWhile globalEnv env returnType cond body
return env
Return expr -> do
checkReturn globalEnv env returnType expr
return env
ExprStmt expr -> do
_ <- inferExprType globalEnv env expr
return env
Type Checking Performance
Complexity Analysis
- Time Complexity: O(n) where n is the size of the AST
- Each expression/statement is visited once
- Environment lookups are O(log n) with Map
- Space Complexity: O(n) for environments and call stack
Optimization Opportunities
- Environment Sharing: Reuse environments when possible
- Early Exit: Stop at first error for faster failure
- Lazy Evaluation: Haskell's lazy evaluation helps
Testing Type Checker
Test Cases
-- Test: Type mismatch
testTypeMismatch = "int main() { int x = true; return x; }"
-- Test: Undefined variable
testUndefinedVar = "int main() { return x; }"
-- Test: Uninitialized variable
testUninitialized = "int main() { int x; return x; }"
-- Test: Missing return
testMissingReturn = "int getValue(int x) { if (x > 0) { return 1; } }"
-- Test: Function call errors
testWrongArgCount = "int add(int a, int b) { return a + b; } int main() { return add(5); }"
testWrongArgType = "int add(int a, int b) { return a + b; } int main() { return add(true, 5); }"
Running Tests
stack test --test-arguments="--match TypeChecker"
Next Steps
After type checking succeeds, the program moves to:
- IR Generation: Convert type-safe AST to bytecode
- VM Execution: Execute the bytecode
See also:
- Type System: Language-level type rules
- Error Messages: User-facing error documentation
IR Generation
This page details how GLaDOS translates the type-checked AST into Intermediate Representation (IR), a WebAssembly-like stack-based bytecode suitable for VM execution.
Overview
IR generation is implemented in src/Compiler/ modules and produces instructions defined in src/IR/Types.hs. The IR uses:
- Stack-based execution: All operations work with an operand stack
- Indexed locals: Variables accessed by integer index, not name
- Type-specific instructions: Separate operations for int, float, and bool
- Offset-based control flow: Jumps use relative instruction offsets
- Function indexing: Functions called by index into function table
IR Program Structure
An IR program consists of multiple compiled functions:
data IRProgram = IRProgram
{ functions :: [CompiledFunction]
, mainIndex :: Int
}
Each function is compiled separately:
data CompiledFunction = CompiledFunction
{ funcName :: String
, paramCount :: Int
, localVarCount :: Int
, code :: [Instruction]
}
Local Variable Indexing:
Locals are numbered starting from 0:
- Parameters occupy indices
0toparamCount - 1 - Local variables occupy indices
paramCounttolocalVarCount - 1
Example:
int add(int a, int b) {
int result = a + b;
return result;
}
Local indices:
a= local 0 (parameter)b= local 1 (parameter)result= local 2 (local variable)
IR Instruction Set
Complete instruction set from src/IR/Types.hs:
Stack Operations
PushInt Int
- Pushes an integer literal onto the stack
- Example:
PushInt 42
PushBool Bool
- Pushes a boolean literal onto the stack
- Example:
PushBool True
PushFloat Double
- Pushes a float literal onto the stack
- Example:
PushFloat 3.14
Pop
- Pops and discards the top value from the stack
- Used for expression statements
Local Variable Operations
GetLocal Int
- Pushes the value of local variable at given index onto the stack
- Example:
GetLocal 0loads first parameter/local
SetLocal Int
- Pops a value from stack and stores it in local variable at given index
- Example:
SetLocal 2stores to third local
Integer Arithmetic
AddInt
- Pops
b, popsa, pushesa + b(integers)
SubInt
- Pops
b, popsa, pushesa - b(integers)
MulInt
- Pops
b, popsa, pushesa * b(integers)
DivInt
- Pops
b, popsa, pushesa / b(integers) - Runtime error if
bis 0
Float Arithmetic
AddFloat
- Pops
b, popsa, pushesa + b(floats)
SubFloat
- Pops
b, popsa, pushesa - b(floats)
MulFloat
- Pops
b, popsa, pushesa * b(floats)
DivFloat
- Pops
b, popsa, pushesa / b(floats) - Runtime error if
bis 0.0
Unary Operations
NegInt
- Pops integer
a, pushes-a
NegFloat
- Pops float
a, pushes-a
NotBool
- Pops boolean
a, pushes!a
Integer Comparison
EqInt
- Pops
b, popsa, pushes booleana == b
NeqInt
- Pops
b, popsa, pushes booleana != b
LtInt
- Pops
b, popsa, pushes booleana < b
GtInt
- Pops
b, popsa, pushes booleana > b
LeInt
- Pops
b, popsa, pushes booleana <= b
GeInt
- Pops
b, popsa, pushes booleana >= b
Float Comparison
EqFloat
- Pops
b, popsa, pushes booleana == b
NeqFloat
- Pops
b, popsa, pushes booleana != b
LtFloat
- Pops
b, popsa, pushes booleana < b
GtFloat
- Pops
b, popsa, pushes booleana > b
LeFloat
- Pops
b, popsa, pushes booleana <= b
GeFloat
- Pops
b, popsa, pushes booleana >= b
Logical Operations
AndBool
- Pops
b, popsa, pushes booleana && b - Note: Both operands are always evaluated (not short-circuit in IR)
OrBool
- Pops
b, popsa, pushes booleana || b - Note: Both operands are always evaluated (not short-circuit in IR)
Control Flow
Jump Int
- Unconditional jump by relative offset
- Example:
Jump 5jumps forward 5 instructions - Example:
Jump -3jumps backward 3 instructions
JumpIfFalse Int
- Pops top of stack (must be boolean)
- If false, jumps by relative offset
- If true, continues to next instruction
Call Int
- Calls function at given index in function table
- Function pops its parameters from stack
- Function pushes return value onto stack
Return
- Returns from current function
- Top of stack is return value (for non-void functions)
- Void functions have no value on stack
Halt
- Ends program execution
- Used after main function returns
Type-Specific Compilation
The compiler selects instructions based on operand types:
Integer Expression
int x = 5 + 3;
Compiles to:
PushInt 5
PushInt 3
AddInt
SetLocal 0
Float Expression
float y = 2.5 * 4.0;
Compiles to:
PushFloat 2.5
PushFloat 4.0
MulFloat
SetLocal 0
Boolean Expression
bool flag = x > 10;
Compiles to:
GetLocal 0
PushInt 10
GtInt
SetLocal 1
AST to IR Translation
Expression Translation
Integer Literal:
42
→ PushInt 42
Float Literal:
3.14
→ PushFloat 3.14
Boolean Literal:
true
→ PushBool True
Variable Reference:
x
→ GetLocal <index>
Binary Operation:
a + b
→
GetLocal <a_index>
GetLocal <b_index>
AddInt
Unary Operation:
-x
→
GetLocal <x_index>
NegInt
Function Call:
add(5, 3)
→
PushInt 5
PushInt 3
Call <add_func_index>
Statement Translation
Variable Declaration:
int x = 10;
→
PushInt 10
SetLocal 0
Assignment:
x = 20;
→
PushInt 20
SetLocal 0
Return Statement:
return x + 1;
→
GetLocal 0
PushInt 1
AddInt
Return
Expression Statement:
factorial(5);
→
PushInt 5
Call <factorial_index>
Pop
Control Flow Translation
If Statement
Source:
if (x > 0) {
return x;
} else {
return 0;
}
IR:
GetLocal 0
PushInt 0
GtInt
JumpIfFalse 3
GetLocal 0
Return
Jump 2
PushInt 0
Return
Explanation:
- Evaluate condition
x > 0 JumpIfFalse 3: if false, jump forward 3 instructions (skip then branch)- Then branch: get x and return
Jump 2: skip else branch- Else branch: push 0 and return
While Loop
Source:
int i = 0;
while (i < 10) {
i = i + 1;
}
IR:
PushInt 0
SetLocal 0
GetLocal 0
PushInt 10
LtInt
JumpIfFalse 6
GetLocal 0
PushInt 1
AddInt
SetLocal 0
Jump -9
Explanation:
- Initialize
i = 0 - Loop condition:
i < 10 JumpIfFalse 6: exit loop if condition false- Loop body:
i = i + 1 Jump -9: jump back to condition
For Loop
Source:
for (int i = 0; i < 10; i = i + 1) {
sum = sum + i;
}
IR:
PushInt 0
SetLocal 1
GetLocal 1
PushInt 10
LtInt
JumpIfFalse 10
GetLocal 0
GetLocal 1
AddInt
SetLocal 0
GetLocal 1
PushInt 1
AddInt
SetLocal 1
Jump -13
Function Translation
Simple Function
Source:
int double(int x) {
return x * 2;
}
CompiledFunction:
CompiledFunction {
funcName = "double",
paramCount = 1,
localVarCount = 1,
code = [
GetLocal 0,
PushInt 2,
MulInt,
Return
]
}
Function with Locals
Source:
int add(int a, int b) {
int result = a + b;
return result;
}
CompiledFunction:
CompiledFunction {
funcName = "add",
paramCount = 2,
localVarCount = 3,
code = [
GetLocal 0,
GetLocal 1,
AddInt,
SetLocal 2,
GetLocal 2,
Return
]
}
Local indices:
- Local 0: parameter
a - Local 1: parameter
b - Local 2: local variable
result
Void Function
Source:
void printValue(int x) {
if (x > 0) {
return;
}
}
CompiledFunction:
CompiledFunction {
funcName = "printValue",
paramCount = 1,
localVarCount = 1,
code = [
GetLocal 0,
PushInt 0,
GtInt,
JumpIfFalse 1,
Return,
Return
]
}
Program Translation
Function Table
Functions are indexed for calling:
int helper(int x) {
return x * 2;
}
int main() {
return helper(21);
}
Function Table:
- Index 0:
helper - Index 1:
main
main function IR:
PushInt 21
Call 0
Return
Main Index
The IRProgram stores the index of the main function:
IRProgram {
functions = [helperCompiled, mainCompiled],
mainIndex = 1
}
Offset Calculation
Jump offsets are calculated relative to the current instruction.
Forward Jump
Code:
0: GetLocal 0
1: JumpIfFalse 3
2: PushInt 1
3: Return
4: PushInt 0
5: Return
At instruction 1, JumpIfFalse 3 jumps to instruction 4 (1 + 3).
Backward Jump
Code:
0: GetLocal 0
1: PushInt 10
2: LtInt
3: JumpIfFalse 4
4: GetLocal 0
5: PushInt 1
6: AddInt
7: SetLocal 0
8: Jump -8
9: ...
At instruction 8, Jump -8 jumps to instruction 0 (8 - 8).
Complete Examples
Example 1: Factorial (Recursive)
Source:
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
CompiledFunction:
CompiledFunction {
funcName = "factorial",
paramCount = 1,
localVarCount = 1,
code = [
GetLocal 0,
PushInt 1,
LeInt,
JumpIfFalse 2,
PushInt 1,
Return,
GetLocal 0,
GetLocal 0,
PushInt 1,
SubInt,
Call 0,
MulInt,
Return
]
}
Execution trace for factorial(3):
Stack: []
GetLocal 0 → Stack: [3]
PushInt 1 → Stack: [3, 1]
LeInt → Stack: [False]
JumpIfFalse 2 → Stack: [], PC = 6
GetLocal 0 → Stack: [3]
GetLocal 0 → Stack: [3, 3]
PushInt 1 → Stack: [3, 3, 1]
SubInt → Stack: [3, 2]
Call 0 → Stack: [3, 2] (recursive call)
...eventually returns with Stack: [3, 2]
MulInt → Stack: [6]
Return → Returns 6
Example 2: Sum Loop (Iterative)
Source:
int sum(int n) {
int total = 0;
int i = 0;
while (i < n) {
total = total + i;
i = i + 1;
}
return total;
}
CompiledFunction:
CompiledFunction {
funcName = "sum",
paramCount = 1,
localVarCount = 3,
code = [
PushInt 0,
SetLocal 1,
PushInt 0,
SetLocal 2,
GetLocal 2,
GetLocal 0,
LtInt,
JumpIfFalse 10,
GetLocal 1,
GetLocal 2,
AddInt,
SetLocal 1,
GetLocal 2,
PushInt 1,
AddInt,
SetLocal 2,
Jump -12,
GetLocal 1,
Return
]
}
Local indices:
- Local 0: parameter
n - Local 1: local variable
total - Local 2: local variable
i
Example 3: Float Arithmetic
Source:
float circleArea(float radius) {
float pi = 3.14159;
return pi * radius * radius;
}
CompiledFunction:
CompiledFunction {
funcName = "circleArea",
paramCount = 1,
localVarCount = 2,
code = [
PushFloat 3.14159,
SetLocal 1,
GetLocal 1,
GetLocal 0,
MulFloat,
GetLocal 0,
MulFloat,
Return
]
}
Note: All float operations use type-specific instructions.
Example 4: Boolean Logic
Source:
bool isInRange(int x, int low, int high) {
return (x >= low) && (x <= high);
}
CompiledFunction:
CompiledFunction {
funcName = "isInRange",
paramCount = 3,
localVarCount = 3,
code = [
GetLocal 0,
GetLocal 1,
GeInt,
GetLocal 0,
GetLocal 2,
LeInt,
AndBool,
Return
]
}
Compilation Environment
Variable Table
The compiler maintains a VarTable mapping variable names to local indices:
type VarTable = Map String Int
Example for function add(int a, int b):
varTable = Map.fromList [("a", 0), ("b", 1), ("result", 2)]
Function Table
The compiler maintains a FuncTable mapping function names to indices:
type FuncTable = Map String Int
Example:
funcTable = Map.fromList [("factorial", 0), ("main", 1), ("helper", 2)]
IR Properties
Stack-Based Architecture
- All operations consume operands from stack and push results
- No register allocation needed
- Simple to generate, simple to interpret
Type-Specific Operations
- Type information encoded in instructions (AddInt vs AddFloat)
- No runtime type checking needed
- VM execution is fast and predictable
Indexed Locals
- Variables accessed by integer index
- No runtime name lookup
- Fast and efficient
Offset-Based Control Flow
- No label resolution needed
- Direct jumps with known offsets
- Compact representation
Name Resolution
During compilation, all names are resolved to indices:
- Function names → Function table index
- Variable names → Local variable index
- Jump labels → Relative instruction offset
This happens during IR generation, so the VM never sees string names.
Performance Characteristics
Code Size
- Compact representation
- Instructions are small (most are 1-2 bytes when serialized)
- Local indices fit in single bytes for most functions
Generation Time
- Single-pass compilation after type checking
- Linear time in AST size
- No complex optimizations (yet)
IR as Compilation Target
The IR design is inspired by WebAssembly:
- Indexed locals (like WebAssembly)
- Type-specific instructions (like WebAssembly)
- Stack-based execution (like WebAssembly)
- Function indexing (like WebAssembly)
This makes it:
- Simple to generate
- Simple to interpret
- Simple to serialize
- Fast to execute
Next Steps
After IR generation:
- IR is passed to bytecode serializer (see Bytecode Generation)
- Bytecode is executed by VM (see VM Execution)
Linker System
The GLaDOS linker enables separate compilation and linking of multiple modules, allowing you to build large programs from independently compiled components.
Added in: Version 1.3
Overview
The linker system provides:
- Separate Compilation: Compile modules independently
- Symbol Resolution: Resolve function calls across module boundaries
- Import/Export Tracking: Track dependencies between modules
- Duplicate Detection: Prevent duplicate function definitions
- Missing Symbol Detection: Catch undefined function references
Architecture
Object File Format
Object files (.gbo - GLaDOS Binary Object) contain:
data ObjectFile = ObjectFile
{ objFunctions :: [RelocatableFunction] -- Compiled functions
, objExports :: [String] -- Functions this module provides
, objImports :: [String] -- External functions this module needs
}
Key Components:
- Exports: Functions defined in this module (available to other modules)
- Imports: External functions referenced by this module (must be provided by other modules)
- Relocatable Functions: Functions with symbolic (name-based) call references
Relocatable Functions
Unlike fully compiled functions, relocatable functions use symbolic references:
data RelocatableFunction = RelocatableFunction
{ rfName :: String
, rfParams :: Int
, rfLocals :: Int
, rfCode :: [RelocatableInstr] -- Instructions with symbolic calls
}
data RelocatableInstr
= Fixed Instruction -- Normal instruction
| CallRef String -- Symbolic function call (name, not index)
Why Relocatable?
In a fully compiled program, function calls use integer indices (Call 3). In relocatable code, calls use symbolic names (CallRef "helper"). The linker resolves these symbolic references to actual indices during linking.
Compilation Pipeline with Linker
Traditional Single-Module Compilation
Source Code
↓
[Parse]
↓
AST
↓
[Type Check]
↓
[Compile to IR]
↓
IR Program
↓
[VM]
↓
Result
Multi-Module Compilation with Linker
Module A Source Module B Source
↓ ↓
[Parse] [Parse]
↓ ↓
AST AST
↓ ↓
[Type Check] [Type Check]
↓ ↓
[Compile to Object] [Compile to Object]
↓ ↓
module_a.gbo module_b.gbo
↓ ↓
└────────┬───────────────┘
↓
[Linker]
↓
IR Program
↓
[VM]
↓
Result
Using the Linker
Step 1: Write Modules with Function Prototypes
Function prototypes enable separate compilation by declaring functions before they're defined.
module_a.c:
// Forward declaration for function in module_b
int helper(int x);
int process(int value) {
return helper(value * 2);
}
module_b.c:
// Forward declaration for function in module_a
int process(int value);
int helper(int x) {
return x + 10;
}
int main() {
return process(5);
}
Step 2: Compile Modules to Object Files
Compile each module separately:
./glados --compile-object < module_a.c > module_a.gbo
./glados --compile-object < module_b.c > module_b.gbo
What happens:
- Source code is parsed and type-checked
- Compiled to IR with symbolic function calls
- Exports and imports are tracked
- Object file is serialized to disk
Step 3: Link Object Files
Combine object files into executable program:
./glados --link module_a.gbo module_b.gbo -o program.gbc
What the linker does:
- Load all object files
- Build symbol table mapping function names to indices
- Check for duplicate symbols
- Check for undefined symbols
- Resolve symbolic calls to actual indices
- Find
mainfunction - Generate final IR program
Step 4: Execute Linked Program
./glados --run program.gbc
Or link and execute in one step:
./glados --link-and-run module_a.gbo module_b.gbo
Linking Process
1. Symbol Table Construction
The linker builds a global symbol table:
buildSymbolTable :: [ObjectFile] -> Either String (Map String Int)
Example:
Given modules with functions:
- Module A:
["process", "utility"] - Module B:
["helper", "main"]
Symbol table:
{ "process" -> 0
, "utility" -> 1
, "helper" -> 2
, "main" -> 3
}
2. Duplicate Detection
Check for duplicate function definitions:
checkDuplicates :: [String] -> Either String ()
Error Example:
Linker error: Duplicate symbols: ["process"]
This occurs when multiple modules define the same function.
3. Undefined Symbol Detection
Check that all imported symbols are defined:
checkUndefinedSymbols :: [ObjectFile] -> Map String Int -> Either String ()
Error Example:
Linker error: Undefined symbols: ["helper"]
This occurs when a module calls a function that no module provides.
4. Symbol Resolution
Convert symbolic references to indices:
resolveFunction :: Map String Int -> RelocatableFunction -> Either String CompiledFunction
Before (Relocatable):
RelocatableFunction {
rfName = "process",
rfCode = [
PushInt 5,
CallRef "helper", -- Symbolic reference
Return
]
}
After (Resolved):
CompiledFunction {
funcName = "process",
code = [
PushInt 5,
Call 2, -- Resolved to index 2
Return
]
}
5. Main Function Location
Find the entry point:
findMainFunction :: Map String Int -> Either String Int
The linker ensures exactly one main function exists.
Import/Export Tracking
Automatic Import Detection
Imports are automatically detected from function calls:
module_a.c:
int helper(int x); // Prototype
int process(int value) {
return helper(value); // Calls helper
}
Generated Object:
ObjectFile {
objFunctions = [process],
objExports = ["process"],
objImports = ["helper"] -- Automatically detected
}
Export List
Exports include all top-level functions in the module:
module_b.c:
int helper(int x) { return x + 10; }
int utility(int x) { return x * 2; }
int main() { return 0; }
Generated Object:
ObjectFile {
objFunctions = [helper, utility, main],
objExports = ["helper", "utility", "main"], -- All functions
objImports = [] -- No external references
}
Example: Multi-Module Program
math.c - Math Utilities
int square(int x) {
return x * x;
}
int cube(int x) {
return x * x * x;
}
logic.c - Logic Utilities
// Import from math module
int square(int x);
int isSquareLarge(int x) {
int sq = square(x);
return sq > 100;
}
main.c - Main Program
// Import from logic module
int isSquareLarge(int x);
// Import from math module
int cube(int x);
int main() {
if (isSquareLarge(15)) {
return cube(3);
}
return 0;
}
Compilation and Linking
# Compile each module
./glados --compile-object < math.c > math.gbo
./glados --compile-object < logic.c > logic.gbo
./glados --compile-object < main.c > main.gbo
# Link all modules
./glados --link math.gbo logic.gbo main.gbo -o program.gbc
# Run
./glados --run program.gbc
Dependency Graph:
main.c → logic.c → math.c
main.c → math.c
Symbol Resolution:
math.gbo exports: ["square", "cube"]
logic.gbo exports: ["isSquareLarge"], imports: ["square"]
main.gbo exports: ["main"], imports: ["isSquareLarge", "cube"]
Linker resolves:
- logic's "square" → math's "square"
- main's "isSquareLarge" → logic's "isSquareLarge"
- main's "cube" → math's "cube"
Linker Errors
Duplicate Symbol Error
Cause: Multiple modules define the same function
Example:
Module A defines: int foo() { return 1; }
Module B defines: int foo() { return 2; }
Error:
Linker error: Duplicate symbols: ["foo"]
Solution: Rename one of the functions or remove the duplicate
Undefined Symbol Error
Cause: Module calls function that no module provides
Example:
// module_a.c
int helper(int x); // Declared but never defined anywhere
int main() {
return helper(5);
}
Error:
Linker error: Undefined symbols: ["helper"]
Solution: Provide the missing function in one of the modules
No Main Function Error
Cause: None of the linked modules define main
Error:
Linker error: No main function found
Solution: Ensure one module defines int main()
Object File Format
Binary Serialization
Object files are serialized using Haskell's Data.Binary:
saveObjectToFile :: FilePath -> ObjectFile -> IO (Either String ())
loadObjectFromFile :: FilePath -> IO (Either String ObjectFile)
Format: Binary encoded with:
- Function metadata (names, param counts, local counts)
- Relocatable instruction lists
- Export and import symbol lists
File Extension
.gbo: GLaDOS Binary Object (relocatable object file).gbc: GLaDOS Bytecode (linked executable program)
Implementation Details
Module Structure
src/Linker/
├── Types.hs # ObjectFile, RelocatableFunction types
├── ObjectCompiler.hs # IR → Object conversion
├── Linker.hs # Linking logic
└── BinaryInstances.hs # Binary serialization instances
Key Functions
IR to Object:
irToObject :: IRProgram -> ObjectFile
Converts compiled IR to relocatable object file by:
- Converting
Call <index>toCallRef <name> - Extracting exports (all functions in module)
- Detecting imports (external calls)
Link Objects:
linkObjects :: [ObjectFile] -> Either String IRProgram
Links multiple objects into executable program by:
- Building global symbol table
- Checking for errors (duplicates, undefined symbols)
- Resolving symbolic references
- Finding main function
- Generating final IR program
Advantages of Separate Compilation
1. Faster Incremental Builds
Only recompile changed modules:
# Only module_a.c changed
./glados --compile-object < module_a.c > module_a.gbo
./glados --link math.gbo logic.gbo module_a.gbo -o program.gbc
2. Code Organization
Separate concerns into modules:
math.c: Mathematical operationsio.c: Input/output utilitieslogic.c: Business logicmain.c: Program entry point
3. Team Collaboration
Different team members work on different modules without conflicts.
4. Reusable Libraries
Create reusable object files:
# Compile once
./glados --compile-object < stdlib.c > stdlib.gbo
# Link with multiple programs
./glados --link stdlib.gbo program1.gbo -o program1.gbc
./glados --link stdlib.gbo program2.gbo -o program2.gbc
Limitations
1. No Circular Dependencies at Function Level
While modules can have circular dependencies (A imports from B, B imports from A), individual function calls cannot be circular without recursion.
2. Name-Based Symbol Resolution
The linker uses function names, so all exported functions must have unique names across all linked modules.
3. No Selective Exports
All top-level functions in a module are exported. There's no way to mark a function as "private" to the module.
Best Practices
1. Use Function Prototypes
Always declare external functions with prototypes:
int helper(int x); // Good: Clear declaration
int process() {
return helper(5); // Linker will resolve this
}
2. Organize by Functionality
Group related functions into modules:
math.c - Mathematical operations
string.c - String utilities
io.c - Input/output operations
main.c - Program entry
3. Minimize Cross-Module Dependencies
Reduce coupling between modules to make linking simpler and builds faster.
4. One Main Function
Ensure only one module defines main:
// main.c - Only this file has main
int main() {
return 0;
}
See Also
- Compilation Overview: Full compilation pipeline
- IR Generation: How IR is generated
- Function Prototypes: Syntax for forward declarations
VM Execution
This page details how the GLaDOS Virtual Machine executes IR bytecode. The VM is implemented in src/VM/ modules.
Overview
The GLaDOS VM is a stack-based virtual machine that executes the IR instructions generated by the compiler. Key characteristics:
- Stack-based architecture: Operands pushed/popped from value stack
- Indexed local variables: Fast array-based local storage
- Type-specific operations: Runtime type checking for safety
- Function call frames: Proper call/return mechanism
- Offset-based control flow: Relative jumps for branches/loops
VM State Structure
The VM maintains execution state in VMState:
data VMState = VMState
{ stack :: [Value]
, locals :: [Value]
, pc :: Int
, callStack :: [CallFrame]
}
Stack
The value stack holds:
- Operands for operations
- Intermediate computation results
- Function arguments
- Return values
Operations:
push :: Value -> VMState -> VMStatepop :: VMState -> VMResult (Value, VMState)
Locals
Indexed local variables stored as a list (array):
locals :: [Value]
Indexing:
- Parameters occupy indices
0toparamCount - 1 - Local variables occupy indices
paramCounttolocalVarCount - 1
Example for int add(int a, int b):
locals[0] = a (parameter)
locals[1] = b (parameter)
Example for int sum(int n):
locals[0] = n (parameter)
locals[1] = total (local variable)
locals[2] = i (local variable)
Operations:
getLocal :: Int -> VMState -> VMResult ValuesetLocal :: Int -> Value -> VMState -> VMResult VMState
Program Counter
The PC tracks the current instruction index:
pc :: Int
- Starts at 0 for each function
- Incremented after each instruction (unless instruction modifies it)
- Modified by
Jump,JumpIfFalsefor control flow - Must be within bounds
[0, length code - 1]
Call Stack
The call stack stores information for function returns:
callStack :: [CallFrame]
data CallFrame = CallFrame
{ returnPC :: Int
, returnFuncIdx :: Int
, savedLocals :: [Value]
}
Purpose:
- Save return address when calling function
- Restore caller's locals after return
- Enable recursive calls
Value Types
Runtime values are tagged:
data Value
= VInt Int
| VBool Bool
| VFloat Double
Type checking happens at runtime when instructions expect specific types.
Execution Model
Fetch-Decode-Execute Loop
The VM uses a classic fetch-decode-execute cycle:
runLoop :: IRProgram -> Int -> CompiledFunction -> VMState -> VMResult Value
runLoop program funcIdx func state
| pc state < 0 = Left "PC became negative"
| pc state >= length (code func) = Left "PC out of bounds"
| otherwise =
let instruction = code func !! pc state
oldPC = pc state
in case executeInstruction program instruction state of
Left err -> Left err
Right newState ->
case instruction of
Halt -> -- return result
Return -> -- return result
_ -> -- continue loop
Steps:
- Fetch: Get instruction at
code[pc] - Decode: Pattern match on instruction type
- Execute: Dispatch to appropriate handler
- Increment:
pc = pc + 1(unless instruction modified PC) - Repeat: Loop until
ReturnorHalt
Initialization
When calling a function:
execute :: IRProgram -> Int -> [Value] -> VMResult Value
execute program funcIdx args =
let func = functions program !! funcIdx
initialLocals = args ++ replicate (localVarCount func) (VInt 0)
initialState = VMState {
stack = [],
locals = initialLocals,
pc = 0,
callStack = []
}
in runLoop program funcIdx func initialState
Initialization:
- Locals: Parameters first, then uninitialized locals (default
VInt 0) - Stack: Empty
- PC: 0
- CallStack: Empty for main, populated for nested calls
Instruction Execution
Stack Operations
PushInt n:
push (VInt n) state
Stack: [...] → [n, ...]
PushBool b:
push (VBool b) state
Stack: [...] → [b, ...]
PushFloat f:
push (VFloat f) state
Stack: [...] → [f, ...]
Pop:
case pop state of
Right (_, newState) -> Right newState
Stack: [x, ...] → [...] (discard x)
Local Variable Operations
GetLocal idx:
case getLocal idx state of
Right val -> Right (push val state)
- Loads
locals[idx] - Pushes value onto stack
- Error if index out of bounds
Stack: [...] → [locals[idx], ...]
SetLocal idx:
case pop state of
Right (val, newState) -> setLocal idx val newState
- Pops value from stack
- Stores into
locals[idx] - Error if index out of bounds
Stack: [val, ...] → [...]
Integer Arithmetic
AddInt, SubInt, MulInt:
binaryIntOp :: (Int -> Int -> Int) -> VMState -> VMResult VMState
binaryIntOp op state =
case popInt state of
Right (b, state1) -> case popInt state1 of
Right (a, state2) -> Right (push (VInt (a `op` b)) state2)
Stack: [a, b, ...] → [a op b, ...]
DivInt:
case popInt state of
Right (b, state1) ->
if b == 0
then Left "Runtime error: Division by zero"
else case popInt state1 of
Right (a, state2) -> Right (push (VInt (a `div` b)) state2)
Stack: [a, b, ...] → [a / b, ...] (or error if b == 0)
Float Arithmetic
AddFloat, SubFloat, MulFloat:
binaryFloatOp :: (Double -> Double -> Double) -> VMState -> VMResult VMState
Stack: [a, b, ...] → [a op b, ...]
DivFloat:
- Check for division by 0.0
- Otherwise same as integer division
Stack: [a, b, ...] → [a / b, ...] (or error)
Unary Operations
NegInt:
case popInt state of
Right (n, newState) -> Right (push (VInt (-n)) newState)
Stack: [n, ...] → [-n, ...]
NegFloat:
case popFloat state of
Right (n, newState) -> Right (push (VFloat (-n)) newState)
Stack: [n, ...] → [-n, ...]
NotBool:
case popBool state of
Right (b, newState) -> Right (push (VBool (not b)) newState)
Stack: [b, ...] → [!b, ...]
Integer Comparison
EqInt, NeqInt, LtInt, GtInt, LeInt, GeInt:
compareInts :: (Int -> Int -> Bool) -> VMState -> VMResult VMState
compareInts cmp state =
case popInt state of
Right (b, state1) -> case popInt state1 of
Right (a, state2) -> Right (push (VBool (a `cmp` b)) state2)
Stack: [a, b, ...] → [a cmp b, ...] (boolean result)
Float Comparison
EqFloat, NeqFloat, LtFloat, GtFloat, LeFloat, GeFloat:
compareFloats :: (Double -> Double -> Bool) -> VMState -> VMResult VMState
Stack: [a, b, ...] → [a cmp b, ...] (boolean result)
Logical Operations
AndBool, OrBool:
binaryBoolOp :: (Bool -> Bool -> Bool) -> VMState -> VMResult VMState
binaryBoolOp op state =
case popBool state of
Right (b, state1) -> case popBool state1 of
Right (a, state2) -> Right (push (VBool (a `op` b)) state2)
Stack: [a, b, ...] → [a op b, ...]
Note: Both operands are always evaluated (no short-circuit at VM level).
Control Flow
Jump offset:
Right (state {pc = pc state + offset})
- Adds offset to PC
- No stack change
- PC is not auto-incremented after this instruction
JumpIfFalse offset:
case popBool state of
Right (condition, newState) ->
if condition
then Right newState
else Right (newState {pc = pc newState + offset})
- Pops boolean from stack
- If false: jump by offset
- If true: continue to next instruction
Stack: [bool, ...] → [...]
Call funcIdx:
case getFunctionAt program funcIdx of
Right targetFunc ->
let argCount = paramCount targetFunc
in case popNValues argCount [] state of
Right (args, stateAfterPop) ->
case executeFunc program funcIdx args of
Right result -> Right $ push result stateAfterPop
Process:
- Get function from program by index
- Pop
paramCountarguments from stack - Execute function with arguments
- Push return value onto stack
Stack: [arg1, arg2, ..., argN, ...] → [result, ...]
Return:
- Returns control to caller (or ends execution)
- Top of stack is return value
- Execution loop exits
Halt:
- Ends program execution
- Top of stack is program result
- Execution loop exits
Type Checking
The VM performs runtime type checking:
popInt:
popInt :: VMState -> VMResult (Int, VMState)
popInt state = case pop state of
Right (VInt n, newState) -> Right (n, newState)
Right (VBool _, _) -> Left "Type error: expected Int, got Bool"
Right (VFloat _, _) -> Left "Type error: expected Int, got Float"
popBool:
popBool :: VMState -> VMResult (Bool, VMState)
popBool state = case pop state of
Right (VBool b, newState) -> Right (b, newState)
Right (VInt _, _) -> Left "Type error: expected Bool, got Int"
Right (VFloat _, _) -> Left "Type error: expected Bool, got Float"
popFloat:
popFloat :: VMState -> VMResult (Double, VMState)
popFloat state = case pop state of
Right (VFloat f, newState) -> Right (f, newState)
Right (VInt _, _) -> Left "Type error: expected Float, got Int"
Right (VBool _, _) -> Left "Type error: expected Float, got Bool"
These ensure type safety even though IR is type-erased at the value level.
Execution Examples
Example 1: Simple Arithmetic
Source:
int x = 5 + 3;
return x;
IR:
PushInt 5
PushInt 3
AddInt
SetLocal 0
GetLocal 0
Return
Execution Trace:
| PC | Instruction | Stack | Locals | Notes |
|---|---|---|---|---|
| 0 | PushInt 5 | [5] | [] | Push 5 |
| 1 | PushInt 3 | [5, 3] | [] | Push 3 |
| 2 | AddInt | [8] | [] | Pop 3, pop 5, push 8 |
| 3 | SetLocal 0 | [] | [8] | Pop 8, store to local 0 |
| 4 | GetLocal 0 | [8] | [8] | Load local 0, push 8 |
| 5 | Return | [8] | [8] | Return 8 |
Result: VInt 8
Example 2: Conditional
Source:
int max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
IR:
0: GetLocal 0
1: GetLocal 1
2: GtInt
3: JumpIfFalse 3
4: GetLocal 0
5: Return
6: GetLocal 1
7: Return
Execution Trace for max(10, 5):
Initial state:
locals = [10, 5](a=10, b=5)stack = []
| PC | Instruction | Stack | Action |
|---|---|---|---|
| 0 | GetLocal 0 | [10] | Load a |
| 1 | GetLocal 1 | [10, 5] | Load b |
| 2 | GtInt | [True] | 10 > 5 = True |
| 3 | JumpIfFalse 3 | [] | Pop True, don't jump |
| 4 | GetLocal 0 | [10] | Load a |
| 5 | Return | [10] | Return 10 |
Result: VInt 10
Execution Trace for max(3, 8):
Initial state:
locals = [3, 8](a=3, b=8)
| PC | Instruction | Stack | Action |
|---|---|---|---|
| 0 | GetLocal 0 | [3] | Load a |
| 1 | GetLocal 1 | [3, 8] | Load b |
| 2 | GtInt | [False] | 3 > 8 = False |
| 3 | JumpIfFalse 3 | [] | Pop False, jump to PC 6 |
| 6 | GetLocal 1 | [8] | Load b |
| 7 | Return | [8] | Return 8 |
Result: VInt 8
Example 3: Loop
Source:
int sum = 0;
int i = 0;
while (i < 3) {
sum = sum + i;
i = i + 1;
}
return sum;
IR:
0: PushInt 0
1: SetLocal 0
2: PushInt 0
3: SetLocal 1
4: GetLocal 1
5: PushInt 3
6: LtInt
7: JumpIfFalse 10
8: GetLocal 0
9: GetLocal 1
10: AddInt
11: SetLocal 0
12: GetLocal 1
13: PushInt 1
14: AddInt
15: SetLocal 1
16: Jump -12
17: GetLocal 0
18: Return
Abbreviated Execution Trace:
| PC | Instruction | Locals | Stack | Notes |
|---|---|---|---|---|
| 0-3 | Initialize | [0, 0] | [] | sum=0, i=0 |
| 4-6 | Check i < 3 | [0, 0] | [True] | 0 < 3 |
| 7 | JumpIfFalse 10 | [0, 0] | [] | Don't jump (True) |
| 8-11 | sum = sum + i | [0, 0] | [] | sum = 0+0 = 0 |
| 12-15 | i = i + 1 | [0, 1] | [] | i = 1 |
| 16 | Jump -12 | [0, 1] | [] | Jump to PC 4 |
| 4-6 | Check i < 3 | [0, 1] | [True] | 1 < 3 |
| 8-11 | sum = sum + i | [1, 1] | [] | sum = 0+1 = 1 |
| 12-15 | i = i + 1 | [1, 2] | [] | i = 2 |
| 16 | Jump -12 | [1, 2] | [] | Jump to PC 4 |
| 4-6 | Check i < 3 | [1, 2] | [True] | 2 < 3 |
| 8-11 | sum = sum + i | [3, 2] | [] | sum = 1+2 = 3 |
| 12-15 | i = i + 1 | [3, 3] | [] | i = 3 |
| 16 | Jump -12 | [3, 3] | [] | Jump to PC 4 |
| 4-6 | Check i < 3 | [3, 3] | [False] | 3 < 3 = False |
| 7 | JumpIfFalse 10 | [3, 3] | [] | Jump to PC 17 |
| 17 | GetLocal 0 | [3, 3] | [3] | Load sum |
| 18 | Return | [3, 3] | [3] | Return 3 |
Result: VInt 3
Example 4: Function Call
Source:
int double(int x) {
return x * 2;
}
int main() {
return double(21);
}
IR for double:
0: GetLocal 0
1: PushInt 2
2: MulInt
3: Return
IR for main:
0: PushInt 21
1: Call 0
2: Return
Execution Trace:
Starting in main:
| Function | PC | Instruction | Stack | Notes |
|---|---|---|---|---|
main | 0 | PushInt 21 | [21] | Push argument |
main | 1 | Call 0 | - | Call double with 21 |
double | 0 | GetLocal 0 | [21] | Load parameter x=21 |
double | 1 | PushInt 2 | [21, 2] | Push 2 |
double | 2 | MulInt | [42] | 21 * 2 = 42 |
double | 3 | Return | [42] | Return 42 |
main | 1 | (after call) | [42] | Result pushed |
main | 2 | Return | [42] | Return 42 |
Result: VInt 42
Example 5: Float Operations
Source:
float circleArea(float radius) {
float pi = 3.14;
return pi * radius * radius;
}
IR:
0: PushFloat 3.14
1: SetLocal 1
2: GetLocal 1
3: GetLocal 0
4: MulFloat
5: GetLocal 0
6: MulFloat
7: Return
Execution Trace for circleArea(2.0):
Initial: locals = [2.0, 0.0] (radius=2.0, pi uninitialized)
| PC | Instruction | Stack | Locals | Notes |
|---|---|---|---|---|
| 0 | PushFloat 3.14 | [3.14] | [2.0, 0.0] | |
| 1 | SetLocal 1 | [] | [2.0, 3.14] | pi = 3.14 |
| 2 | GetLocal 1 | [3.14] | [2.0, 3.14] | Load pi |
| 3 | GetLocal 0 | [3.14, 2.0] | [2.0, 3.14] | Load radius |
| 4 | MulFloat | [6.28] | [2.0, 3.14] | 3.14 * 2.0 |
| 5 | GetLocal 0 | [6.28, 2.0] | [2.0, 3.14] | Load radius |
| 6 | MulFloat | [12.56] | [2.0, 3.14] | 6.28 * 2.0 |
| 7 | Return | [12.56] | [2.0, 3.14] | Return 12.56 |
Result: VFloat 12.56
Error Handling
The VM returns errors for:
Stack Underflow:
"Stack underflow: tried to pop from empty stack"
Type Mismatch:
"Type error: expected Int, got Bool"
"Type error: expected Float, got Int"
Division by Zero:
"Runtime error: Division by zero"
Invalid Local Index:
"Local index out of bounds: 5 (max: 2)"
"Invalid local index: -1 (negative)"
Invalid Function Index:
"Function index out of bounds: 10"
"Invalid function index: -1 (negative)"
PC Out of Bounds:
"PC out of bounds"
"PC became negative"
Performance Characteristics
Memory Usage
- Stack: Grows with expression nesting depth
- Locals: Fixed size per function (paramCount + localVarCount)
- CallStack: Grows with recursion depth
Instruction Cost
Most instructions are O(1):
- Stack operations: O(1)
- Arithmetic: O(1)
- Comparisons: O(1)
- Local access: O(1) (array indexing)
- Jumps: O(1) (PC update)
Function calls: O(1) + cost of called function
Optimization Opportunities
Current implementation uses lists (linked lists in Haskell):
- Stack: List works well (push/pop at head is O(1))
- Locals: Could use
Vectorfor true O(1) indexing - Code: Could use
Vectorfor true O(1) instruction fetch
Implementation Notes
Module Structure
VM/HelperFunc.hs:
VMStateandCallFramedefinitions- Basic stack operations (
push,pop) - Local variable operations (
getLocal,setLocal) - Type-safe pop functions (
popInt,popBool,popFloat) - Binary operation helpers
VM/Interpreter.hs:
- Main execution loop (
runLoop) - Instruction dispatcher (
executeInstruction) - Function executor (
execute)
VM/InstructionHandlers.hs:
- Individual instruction implementations
- Stack operations handler
- Variable operations handler
- Arithmetic operations handler
- Comparison operations handler
- Logical operations handler
- Control flow handler
Design Decisions
Why Lists for Locals?
- Simple implementation
- Good enough for most functions (few locals)
- Could be optimized to
Vectorlater
Why No Short-Circuit?
- Compiler evaluates both operands before logical ops
- Simplifies VM implementation
- Short-circuiting happens at IR generation level
Why Type-Tagged Values?
- Type safety at runtime
- Catch type errors that shouldn't happen (defensive)
- Clear error messages
Why Offset-Based Jumps?
- No label resolution needed
- Compact representation
- Fast execution (just add to PC)
VM Architecture
The GLaDOS VM design is similar to:
- WebAssembly: Indexed locals, stack-based
- JVM: Stack machine with typed operations
- Python bytecode: Stack-based with locals
But simpler:
- No objects or references
- No garbage collection (values are primitives)
- No exceptions (Either monad for errors)
- Straightforward execution model
Testing the VM
The VM can be tested by:
- Compiling GLaDOS source → IR
- Executing IR with test inputs
- Checking results against expected values
Example test:
let program = compileProgram "int main() { return 42; }"
result = execute program (mainIndex program) []
-- Expected: Right (VInt 42)
Next Steps
After VM execution:
- Results can be inspected
- Error messages help debugging
- Bytecode can be serialized for storage
- Future: JIT compilation or optimization passes
Error Messages
GLaDOS provides detailed error messages to help you debug your programs. This guide explains common errors and how to fix them.
Compile-Time Errors
All errors below are caught during compilation (before your program runs).
Type Errors
Type Mismatch in Assignment
int x = true;
Error:
Type error at line 1: expected int but got bool
Fix: Use matching types:
int x = 42;
bool flag = true;
Type Mismatch in Return
int getValue() {
return true; // Returning bool from int function
}
Error:
Return type mismatch at line 2: expected int but got bool
Fix:
int getValue() {
return 42;
}
Type Mismatch in Operations
int x = 5;
bool y = true;
int z = x + y;
Error:
Type error in arithmetic at line 3: expected int but got bool
Fix: Don't mix types in arithmetic:
int x = 5;
int y = 10;
int z = x + y; // Both operands are int
Type Mismatch in Conditionals
int x = 5;
while (x) { // int where bool expected
x = x - 1;
}
Error:
Type error at line 2: expected bool but got int
Fix: Use a comparison:
while (x > 0) {
x = x - 1;
}
Function Errors
Undefined Function
int main() {
return multiply(5, 3); // multiply not defined
}
Error:
Undefined function 'multiply' at line 2
Fix: Define the function before calling it:
int multiply(int a, int b) {
return a * b;
}
int main() {
return multiply(5, 3);
}
Wrong Argument Count
int add(int a, int b) {
return a + b;
}
int main() {
return add(5); // Missing one argument
}
Error:
Function 'add' expects 2 arguments but got 1 at line 6
Fix: Provide all required arguments:
return add(5, 3);
Wrong Argument Types
int add(int a, int b) {
return a + b;
}
int main() {
bool x = true;
return add(x, 5); // bool passed where int expected
}
Error:
Type mismatch in function argument at line 7: expected int but got bool
Fix: Use correct types:
int x = 10;
return add(x, 5);
Variable Errors
Undefined Variable
int main() {
return x; // x not defined
}
Error:
Undefined variable 'x' at line 2
Fix: Declare the variable first:
int main() {
int x = 42;
return x;
}
Uninitialized Variable
int main() {
int x;
return x; // x declared but not initialized
}
Error:
Variable 'x' used before initialization at line 3
Fix: Initialize before use:
int main() {
int x = 42;
return x;
}
Or assign a value:
int main() {
int x;
x = 42;
return x;
}
Variable Used Before Declaration
int main() {
int y = x + 1; // x used before declaration
int x = 5;
return y;
}
Error:
Undefined variable 'x' at line 2
Fix: Declare variables before using them:
int main() {
int x = 5;
int y = x + 1;
return y;
}
Return Statement Errors
Missing Return Statement
int getValue(int x) {
if (x > 0) {
return 1;
}
// Missing return for x <= 0
}
Error:
Function 'getValue' must return a value on all code paths
Fix: Add an else branch with return:
int getValue(int x) {
if (x > 0) {
return 1;
} else {
return 0;
}
}
Or a return after the if:
int getValue(int x) {
if (x > 0) {
return 1;
}
return 0;
}
Empty Function Body
int main() {
}
Error:
Function 'main' must return a value on all code paths
Fix: Add a return statement:
int main() {
return 0;
}
Division Errors
Division by Zero (Literal)
int main() {
int x = 10 / 0;
return x;
}
Error:
Division by zero at line 2
Fix: Don't divide by literal zero:
int main() {
int x = 10 / 2;
return x;
}
Note: Division by zero with variables is only caught at runtime:
int divide(int a, int b) {
return a / b; // OK at compile time
}
int main() {
return divide(10, 0); // Runtime error!
}
Parse Errors
Syntax Errors
Missing Semicolon
int main() {
int x = 5
int y = 10;
return x + y;
}
Error:
Parse error: unexpected token at line 3
Fix: Add semicolons (though GLaDOS is lenient):
int x = 5;
int y = 10;
Mismatched Braces
int main() {
int x = 5;
return x;
// Missing closing brace
Error:
Parse error: unexpected end of input
Fix: Close all braces:
int main() {
int x = 5;
return x;
}
Invalid Token
int main() {
int x = 5 @ 3; // @ is not a valid operator
return x;
}
Error:
Parse error: unexpected character '@' at line 2
Fix: Use valid operators:
int x = 5 + 3;
Runtime Errors
These errors occur during program execution:
Division by Zero (Runtime)
int divide(int numerator, int denominator) {
return numerator / denominator;
}
int main() {
int result = divide(10, 0);
return result;
}
Error:
Runtime error: Division by zero
Fix: Check for zero before dividing:
int divide(int numerator, int denominator) {
if (denominator == 0) {
return 0; // Or handle error appropriately
}
return numerator / denominator;
}
Stack Overflow (Deep Recursion)
int infinite(int x) {
return infinite(x + 1); // Never stops!
}
This may cause a stack overflow. Always ensure recursion has a base case.
Error Message Format
GLaDOS error messages follow this format:
<Error Type>: <Description> at <Source Position>
Where:
- Error Type: Type error, Parse error, Runtime error, etc.
- Description: What went wrong
- Source Position: File location (line and column)
Example:
Type error in arithmetic at input:5:12: expected int but got bool
Tips for Debugging
- Read the error message carefully - it tells you exactly what's wrong
- Check the line number - the error is usually on or near that line
- Look at variable types - most errors are type mismatches
- Verify all code paths return - use else branches
- Initialize variables before using them
- Match function signatures - check parameter counts and types
- Test incrementally - compile often to catch errors early
Exit Codes
GLaDOS uses standard Unix exit codes:
- 0: Success, no errors
- 84: Compilation or runtime error (EPITECH standard)
Design Decisions
Why we chose specific technologies and design approaches for GLaDOS
Overview
This document explains the rationale behind our technology and design choices. Each decision was made to balance learning goals, development speed, safety, and maintainability.
Build System: Stack
What is Stack?
Stack is a cross-platform build tool for Haskell projects that provides reproducible builds and dependency management.
Why Stack?
| Criterion | Stack | Cabal | Raw GHC | Our Choice |
|---|---|---|---|---|
| Reproducible Builds | LTS snapshots | Partial (with freeze files) | Manual | Stack |
| Dependency Management | Automatic resolution | Manual constraints | None | Stack |
| Ease of Setup | One command | Moderate | Complex | Stack |
| Project Isolation | Separate environments | With sandboxes | Global | Stack |
| Learning Curve | Low | Moderate | High | Stack |
Decision: Stack provides the best developer experience for a team project with educational goals.
Parser Library: Megaparsec
What is Megaparsec?
Megaparsec is a modern parser combinator library for Haskell, successor to Parsec.
Why Megaparsec?
| Criterion | Megaparsec | Parsec | Alex/Happy | Parser Generators | Our Choice |
|---|---|---|---|---|---|
| Error Messages | Excellent | Good | Poor | Poor | Megaparsec |
| Composability | Combinator-based | Combinator-based | Separate tools | Generated code | Megaparsec |
| Learning Curve | Low | Low | Moderate | High | Megaparsec |
| Flexibility | High | High | Low | Low | Megaparsec |
| Type Safety | Strong | Strong | Moderate | Weak | Megaparsec |
| Maintenance | Active | Legacy | Active | Varies | Megaparsec |
-
Built-in Lexer Support:
Text.Megaparsec.Char.Lexerhandles whitespace and comments automatically -
Type-Safe: Haskell's type system catches parser bugs at compile time
Decision: Megaparsec's composability and error messages make it ideal for educational purposes and rapid language iteration.
Testing Framework: Hspec
What is Hspec?
Hspec is a BDD (Behavior-Driven Development) testing framework for Haskell, inspired by Ruby's RSpec.
Why Hspec?
| Criterion | Hspec | HUnit | QuickCheck | Tasty | Our Choice |
|---|---|---|---|---|---|
| Readability | Descriptive | Moderate | Low | Moderate | Hspec |
| BDD Style | describe/it | Assertions | Properties | Mixed | Hspec |
| Output Formatting | Excellent | Basic | Basic | Good | Hspec |
| Learning Curve | Low | Low | Moderate | Moderate | Hspec |
| Property Testing | Integrates QuickCheck | No | Native | Yes | Hspec |
| Parallel Execution | Yes | No | No | Yes | Hspec |
Decision: Hspec's readability and BDD style make tests self-documenting, which is valuable for an educational project.
Core Libraries
Containers: Data Structures
What is Containers?
The containers library provides high-performance, immutable data structures for Haskell.
Why Containers?
| Criterion | containers | unordered-containers | vector | lists | Our Choice |
|---|---|---|---|---|---|
| Performance | O(log n) | O(1) average | O(1) index | O(n) | containers |
| Ordering | Sorted | Unordered | Indexed | Ordered | containers |
| Immutability | Persistent | Persistent | Immutable | Immutable | containers |
| Memory | Efficient | Efficient | Very Efficient | Poor | containers |
| Pattern Matching | Good | Moderate | Limited | Excellent | containers |
Our Usage:
Map: Symbol tables, type environments, function tablesSet: Tracking initialized variables, imported modulesSeq: Instruction sequences, statement lists
Decision: Containers provides the perfect balance of performance and immutability for compiler data structures.
Binary Data Handling
ByteString: Binary Data Handling
What is ByteString?
Efficient, immutable byte array representation for binary data.
Why ByteString?
| Criterion | ByteString | [Word8] | Vector Word8 | Our Choice |
|---|---|---|---|---|
| Performance | Excellent | Poor | Excellent | ByteString |
| Memory | Packed | Wasteful | Packed | ByteString |
| I/O | Optimized | Slow | Manual | ByteString |
| Binary Integration | Native | Manual | Manual | ByteString |
| Ecosystem | Mature | N/A | Growing | ByteString |
Key Benefits:
Our Usage:
.glcbytecode files.gloobject files- Binary serialization/deserialization
- File I/O operations
Decision: ByteString is the standard, battle-tested solution for binary data in Haskell.
Evaluation Control: DeepSeq
What is DeepSeq?
Library for controlling evaluation strictness in lazy Haskell.
Why DeepSeq?
| Criterion | DeepSeq | seq | BangPatterns | Our Choice |
|---|---|---|---|---|
| Deep Evaluation | Recursive | Shallow | Shallow | DeepSeq |
| Type Safety | NFData class | No | No | DeepSeq |
| Composability | Derives | Manual | Manual | DeepSeq |
| Control | Explicit | Explicit | Implicit | DeepSeq |
Key Benefits:
Our Usage:
- Force evaluation before writing bytecode
- Prevent space leaks in compilation
- Ensure error messages are fully evaluated
- Accurate benchmarking
Decision: DeepSeq prevents space leaks and ensures predictable memory behavior in our compiler.
Testing Infrastructure
Hspec-Discover: Automatic Test Discovery
What is Hspec-Discover?
Automatic test discovery tool for Hspec - finds all test files and generates test suite automatically.
Why Hspec-Discover?
| Criterion | hspec-discover | Manual main | Tasty-discover | Our Choice |
|---|---|---|---|---|
| Automation | Fully automatic | Manual | Automatic | hspec-discover |
| Convention | *Spec.hs files | Custom | Custom | hspec-discover |
| Maintenance | Zero | High | Low | hspec-discover |
| Integration | Native Hspec | N/A | Different API | hspec-discover |
Key Benefits:
Decision: Hspec-discover eliminates boilerplate and ensures we never forget to run a test.
Process: External Program Execution
What is Process?
Library for creating and interacting with system processes.
Why Process?
| Criterion | process | System.Cmd | ShellCmd | Raw fork | Our Choice |
|---|---|---|---|---|---|
| Type Safety | Strong | Weak | Moderate | Unsafe | process |
| Error Handling | Explicit | Weak | Moderate | Manual | process |
| Portability | Cross-platform | Limited | Limited | Unix only | process |
| Control | High | Low | Moderate | Total | process |
| Output Capture | Easy | Manual | Easy | Manual | process |
Our Usage:
- Run compiled programs in tests
- Test linker by calling glados-exe
- Integration tests for full pipeline
- Verify error messages from command-line tool
Decision: Process provides safe, portable process execution for comprehensive integration testing.
QuickCheck: Property-Based Testing
What is QuickCheck?
Property-based testing library that generates random test cases to verify properties.
Why QuickCheck?
| Criterion | QuickCheck | Example-Based | SmallCheck | Hedgehog | Our Choice |
|---|---|---|---|---|---|
| Coverage | Excellent | Manual | Complete (small) | Excellent | QuickCheck |
| Random Testing | Yes | No | Exhaustive | Yes | QuickCheck |
| Shrinking | Automatic | N/A | N/A | Better | QuickCheck |
| Integration | Hspec native | Yes | Yes | Manual | QuickCheck |
| Maturity | Very mature | N/A | Mature | Newer | QuickCheck |
Our Usage:
- Test compiler properties (type preservation)
- Verify VM correctness (semantics preservation)
- Test parser round-tripping (parse . pretty = id)
- Find edge cases in type checker
Decision: QuickCheck finds bugs that example-based tests miss, and integrates perfectly with our Hspec test suite.
Parser Combinators
Parser-Combinators: Megaparsec Utilities
What is Parser-Combinators?
Companion library to Megaparsec providing lightweight, general parser combinators.
Why Parser-Combinators?
Key Benefits:
-
Separation of Concerns:
- Megaparsec: Core parsing functionality
- Parser-combinators: Reusable combinators
- Keeps Megaparsec lightweight
-
Useful Combinators:
import Control.Monad.Combinators -- Many with separator paramList = param `sepBy` comma -- Between delimiters block = between (symbol "{") (symbol "}") statements -- Optional with default optionalSemicolon = option () semicolon
Our Usage:
sepBy,sepBy1: Comma-separated listsbetween: Parentheses, braces, bracketschoice: Multiple alternativesoption: Optional syntax elements
Decision: Parser-combinators provides standard combinators that keep our parser code clean and readable.
Binary Serialization: Binary Library
Why Haskell's Binary?
| Criterion | Binary | Cereal | Store | Custom | Our Choice |
|---|---|---|---|---|---|
| Simplicity | High | High | Moderate | Low | Binary |
| Performance | Good | Good | Better | Varies | Binary |
| Ecosystem | Mature | Mature | Newer | N/A | Binary |
| Type Safety | Derive instances | Derive instances | Derive instances | Manual | Binary |
Decision: Binary provides the simplest solution with good performance and ecosystem support.
Error Handling: Either vs Exceptions
Why Either Types?
parseProgram :: String -> Either String Program
typeCheck :: Program -> Either [TypeError] Program
compile :: Program -> Either CompilerError IRProgram
Benefits:
- Explicit in Type Signatures: Function types show they can fail
- Composable with Monads: Easy chaining with
donotation or>>= - Forced Error Handling: Compiler ensures you handle errors
- Testable: Easy to test error cases
Why Not Exceptions?
- Exceptions invisible in type signatures
- Can escape and crash program
- Harder to test systematically
- Less idiomatic in Haskell
Decision: Either types provide better type safety and explicit error handling, fitting Haskell's philosophy.
Git Workflow
Branch Strategy
main: Stable releasesdev: Development branch (we use this as our main working branch)dev_(feature)Feature branches: For experimental features
Why This Structure?
- Clear separation between stable and development
- Easy to experiment without breaking main branch
- Standard practice in industry
Compiler Configuration
GHC Warning Flags
We enable extensive warning flags to catch bugs early and maintain code quality.
Enabled Warnings
ghc-options:
- -Wall
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wmissing-export-lists
- -Wmissing-home-modules
- -Wpartial-fields
- -Wredundant-constraints
Why Each Warning?
-Wall (Enable All Standard Warnings)
- Catches unused variables, imports, and patterns
- Detects type defaulting and orphan instances
- Essential baseline for code quality
-Wcompat (Future Compatibility)
- Warns about upcoming breaking changes
- Helps with GHC version migration
- Prevents reliance on deprecated features
-Wincomplete-record-updates
updateName record = record { name = "new" }
-- GOOD: Pattern match ensures field exists
updateName record@Record{..} = record { name = "new" }
-Wincomplete-uni-patterns
-- BAD: Pattern match can fail
let Just x = maybeValue in x + 1
-- GOOD: Exhaustive pattern match
case maybeValue of
Just x -> x + 1
Nothing -> 0
-Wmissing-export-lists
-- BAD: Everything exported
module Parser where
-- GOOD: Explicit exports
module Parser (parseProgram, ParseError) where
- Prevents accidental API exposure
- Makes module interface explicit
- Helps maintain encapsulation
-Wpartial-fields
-- BAD: Partial record
data Point = Point2D { getX :: Int, getY :: Int }
| Point3D { getX :: Int, getZ :: Int }
-- GOOD: Total fields
data Point = Point2D Int Int | Point3D Int Int Int
-Wredundant-constraints
-- BAD: Ord not needed
sort :: (Ord a, Show a) => [a] -> [a]
-- GOOD: Only necessary constraints
sort :: Ord a => [a] -> [a]
Runtime Options
ghc-options:
- -threaded
- -rtsopts
- -with-rtsopts=-N
Decision: Strict warnings catch bugs at compile time, and runtime options enable performance optimization without code changes.
Base Library and Language Extensions
Base: Haskell Standard Library
Version Constraint: base >= 4.7 && < 5
Decision: Base library is required for any Haskell program; our constraint ensures stability while allowing modern GHC features.
Library Summary
Complete Dependency List
Here's a quick reference of all libraries used in GLaDOS:
| Library | Purpose | Why Not Alternative? |
|---|---|---|
| base | Standard library | Required foundation |
| megaparsec | Parser combinators | Better errors than Parsec, more flexible than generators |
| parser-combinators | Parser utilities | Standard combinators for megaparsec |
| containers | Map, Set, Seq | Deterministic ordering, efficient immutable operations |
| bytestring | Binary data | Standard for binary I/O |
| binary | Serialization | Simple automatic derivation |
| deepseq | Force evaluation | Prevents space leaks |
| hspec | BDD testing | Readable test output |
| hspec-discover | Test discovery | Zero-maintenance test suite |
| QuickCheck | Property testing | Finds edge cases automatically |
| process | External programs | End-to-end integration tests |
Dependency Graph
Application Layer:
├── Parser (megaparsec, parser-combinators)
├── Type Checker (containers)
├── Compiler (containers, bytestring)
├── VM (bytestring, binary, deepseq)
└── Tests (hspec, hspec-discover, QuickCheck, process)
Infrastructure:
├── Build System (Stack)
└── Compiler (GHC with strict warnings)
Conclusion
Our technology choices prioritize:
- Learning: Clear, understandable implementations
- Safety: Type safety and error handling
- Productivity: Good tooling and quick iteration
- Maintainability: Modular structure and clear code
Security Review
A statically-typed, compiled language with compile-time safety guarantees and runtime protections
Executive Summary
Our GLaDOS language implementation prioritizes compile-time safety to prevent entire classes of bugs that plague C programs. We enforce strict type checking, initialization tracking, and control flow verification before any code runs, resulting in zero runtime overhead for these safety features.
Security Features Overview
| Safety Feature | Implementation | Detection Time | Status |
|---|---|---|---|
| Static Type System | Full type checking with inference | Compile-time | ✓ Implemented |
| Initialization Tracking | Flow-sensitive analysis | Compile-time | ✓ Implemented |
| Division by Zero (literals) | AST analysis | Compile-time | ✓ Implemented |
| Return Path Verification | Control flow analysis | Compile-time | ✓ Implemented |
| Type Inference | var keyword with initialization | Compile-time | ✓ Implemented |
| Stack Safety | VM bounds checking | Runtime | ✓ Implemented |
| Function Call Safety | Argument count & type validation | Compile-time | ✓ Implemented |
The Problem: Unsafe C Behaviors
1. Type Confusion
// C allows this - silent truncation
int x = 3.7; // becomes 3, no warning
char* p = (char*)123; // arbitrary casting
2. Uninitialized Variables
// C allows this - undefined behavior
int x;
printf("%d", x); // Could print anything
3. Division by Zero
// C allows this - runtime crash
int result = 10 / 0;
4. Missing Return Statements
// C allows this - returns garbage
int calculate(int x) {
if (x > 0) return x * 2;
// Oops, forgot the else case
}
Our Solutions
1. Strong Static Type System
What we do: Enforce type correctness at compile time with no implicit conversions.
Example - Type Mismatch Detection:
int x = true; // Our language
Error:
Type error at line 1:5
expected: int
got: bool
Example - Safe Type Inference:
var x = 42; // Inferred as int
var y = 3.14; // Inferred as float
var z = x + y; // Error: cannot add int and float
2. Initialization Tracking
What we do: Track variable initialization through control flow paths using a flow-sensitive analysis.
Example:
int x;
if (condition) {
x = 10;
}
return x; // Error: x may not be initialized
Error:
Variable 'x' used before initialization at line 4:8
Safe Pattern:
int x = 0; // Explicit initialization
if (condition) {
x = 10;
}
return x; // OK: x is definitely initialized
3. Division by Zero Detection
What we do: Catch literal zero denominators at compile time.
Example:
int y = 10 / 0;
float z = 3.14 / 0.0;
Error:
Division by zero at line 1:9
Runtime Protection:
int divisor = getUserInput();
int result = 10 / divisor; // Runtime check in VM
4. Return Path Verification
What we do: Ensure all control paths in non-void functions return a value.
Example:
int calculate(int x) {
if (x > 0) {
return x * 2;
}
// Missing else branch
}
Error:
Function 'calculate' must return a value on all code paths
Correct Version:
int calculate(int x) {
if (x > 0) {
return x * 2;
} else {
return 0;
}
}
5. Function Call Safety
What we do: Validate argument counts and types at compile time.
Example:
int add(int a, int b) {
return a + b;
}
int main() {
int x = add(1); // Error: wrong arg count
int y = add(1, true); // Error: type mismatch
return 0;
}
Implementation Architecture
Compile-Time Checks (Zero Runtime Cost)
- Parser (
Parser/Core.hs): Builds AST with source positions for error reporting - Type Inference (
Security/TypeInference.hs): Resolvesvardeclarations - Type Checker (
Security/TypeChecker.hs): Validates types and initialization - Compiler (
Compiler/Core.hs): Generates safe bytecode only for valid programs
Runtime Safety (VM Layer)
- Stack Bounds Checking (
VM/HelperFunc.hs): Prevents stack overflow/underflow - Local Variable Bounds (
VM/HelperFunc.hs): Array bounds checking for locals - Division by Zero (
VM/InstructionHandlers.hs): Runtime check for dynamic values - Type-Safe Operations: VM maintains type tags (VInt, VBool, VFloat)
Design Rationale
Why Compile-Time Over Runtime?
- Zero Performance Overhead: No runtime checks for proven-safe code
- Early Error Detection: Bugs caught before deployment
- Better Developer Experience: Immediate feedback during development
- Predictable Performance: No hidden runtime costs
Why Not Full Dependent Types?
We deliberately chose a practical middle ground:
- What we have: Strong static types with inference
- What we don't: Full dependent types or theorem proving
- Rationale: Balances safety with usability and compilation speed
Security Model Comparison
| Feature | C | Rust | Our Language |
|---|---|---|---|
| Type Safety | Weak, allows casting | Strong with lifetimes | Strong, no unsafe casts |
| Memory Safety | Manual, error-prone | Borrow checker | No pointers (safe by design) |
| Initialization | Undefined behavior | Enforced | Compile-time tracked |
| Null Safety | Null pointers | Option type | No null (no pointers) |
| Integer Overflow | Silent wraparound | Panic in debug | Silent (same as C) |
| Division by Zero | Runtime crash | Panic | Compile-time for literals, runtime otherwise |
| Return Checking | Undefined behavior | Enforced | Compile-time verified |
Limitations and Trade-offs
What We DON'T Prevent:
- Integer Overflow: Would require either runtime checks (performance cost) or different arithmetic semantics
- Runtime Division by Zero: Only catch literal zeros; dynamic values checked at runtime
- Infinite Loops: Halting problem - theoretically impossible to solve completely
- Stack Overflow from Deep Recursion: Would need tail-call optimization (future work)
Design Trade-offs:
- No Implicit Conversions: More verbose but prevents subtle bugs
- Mandatory Initialization: Requires explicit defaults but prevents undefined behavior
- No Null/Pointers: Limits expressiveness but eliminates entire bug classes
Testing Our Security
Type System Tests
// All should fail compilation:
int x = 3.14; // float to int
bool b = 42; // int to bool
int y = true + false; // bool arithmetic
Initialization Tests
// Should fail:
int x;
int y = x + 1; // x not initialized
// Should pass:
int a = 0;
int b = a + 1; // a is initialized
Control Flow Tests
// Should fail:
int risky(bool flag) {
if (flag) return 42;
// missing else
}
// Should pass:
int safe(bool flag) {
if (flag) return 42;
else return 0;
}
Future Security Enhancements
Planned Improvements:
- Tail Call Optimization: Prevent stack overflow in recursive functions
- Bounds-Checked Arrays: Safe indexing when arrays are added
- Pattern Matching: Exhaustiveness checking for safer control flow
- Effect System: Track side effects for pure functional programming
Considered but Deferred:
- Gradual Typing: Allow mixing static and dynamic typing
- Linear Types: Resource management without GC
- Formal Verification: Mathematical proofs of correctness
Conclusion
Our language prevents entire classes of bugs that plague C programs through a carefully designed type system and compile-time analysis. By catching errors before runtime, we provide both safety and performance. While we don't catch everything (that would be impossible), we focus on high-impact, provably detectable errors that cause real problems in production systems.
The key insight: Most bugs can be prevented by design, not just detected at runtime.
Team Members
This page recognizes the contributors to the GLaDOS project.
Core Contributors
Thierry B.
Role: Compiler & Virtual Machine
Contributions:
- Project architecture and design
- Complete compiler implementation
- Virtual machine design and execution engine
- Error handling and formatting system
- Build system setup and configuration
- Technical documentation
Thierry designed and implemented the core compilation pipeline, from IR generation to bytecode execution. He established the project structure and build tooling that enabled efficient team collaboration. He also implemented comprehensive error handling and formatting throughout the compiler, ensuring clear and helpful error messages for developers.
Aymeric L.
Role: Parser, Security, Linker & Bonus Features
Contributions:
- Full modular parsing system
- Advanced security and type checking
- Linker system for separate compilation
- Bonus feature implementation
- Compiler refactoring and modularization
Aymeric built the complete parsing infrastructure using Megaparsec, implementing the full language grammar with proper error handling. He designed and implemented the comprehensive security layer including type checking, initialization tracking, and return path analysis. He also implemented the linker system enabling separate compilation and multi-module programs. Additionally, he refactored the compiler to eliminate illegal imports and improve modularity.
Sven R.
Role: TBD
Contributions: TBD
Tony F.
Role: TBD
Contributions: TBD
Aurelien P.
Role: TBD
Contributions: TBD
Project Statistics
- Language: Haskell (GHC 9.6.3)
- Completion Percentage: 99%
- Test Coverage: .c test files ready to be executed by .sh scripts + HSpec test files
- Doc Status: 100%
- Compilation Stages: 6 (Parsing → Type Checking → IR Generation → Bytecode Serialization → Linking → VM Execution)
- Compiler Modes: 9 (default, --ast, --ir, --compile, --compile-obj, --link, --run, --help, --version)
Acknowledgments
This project was developed as part of the Epitech curriculum, demonstrating advanced concepts in:
- Functional programming with Haskell
- Compiler design and implementation
- Type systems and static analysis
- Virtual machine architecture
- Language security features