Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

GLaDOS Logo

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

TechnologyVersionPurpose
HaskellGHC 9.6.3Implementation language
StackLTS 22.6Build system
MegaparsecLatestParser library
Hspec≥ 2.7.0Testing framework
BinaryLatestBytecode 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 - Success
  • 84 - 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

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:

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:

TypeDescriptionExamples
int64-bit signed integer0, 42, -10, 1000
float64-bit double-precision floating-point3.14, -0.5, 1.0, 2.5e10
boolBoolean valuetrue, false
voidNo 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:

  1. *, / (Multiplicative)
  2. +, - (Additive)
  3. <, >, <=, >=, ==, != (Comparison)
  4. && (Logical AND)
  5. || (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, and void
  • Type inference with var keyword (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:

  1. Unary: -, ! (right-to-left associativity)
  2. Multiplicative: *, /
  3. Additive: +, -
  4. Comparison: <=, >=, ==, !=, <, >
  5. Logical AND: &&
  6. 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, not 1)
  • Division by zero results in runtime error (like integers)
  • Floating-point arithmetic follows IEEE 754 standard
  • No automatic conversion between int and float

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: true or false
  • 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

  1. Argument Count: Must match parameter count exactly
  2. Argument Types: Must match parameter types exactly (in order)
  3. 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:

  • TypeInfer is used internally by the compiler during type checking for intermediate expressions
  • User code cannot explicitly use the TypeInfer type
  • All variable declarations require explicit type annotations

Type Checking Algorithm

The type checker performs:

  1. Environment Building: Collect all function signatures
  2. Function Validation: Check each function body
  3. Expression Type Inference: Determine type of each expression
  4. Type Constraint Verification: Ensure all operations are type-safe
  5. Initialization Tracking: Verify variables are initialized before use
  6. 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:

  1. Type Soundness: Well-typed programs cannot have type errors at runtime
  2. Initialization Safety: Variables cannot be read before being initialized
  3. Return Safety: Functions always return a value of the correct type
  4. 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:

  1. Unary Operators: Single-operand operations (negation, logical not)
  2. Arithmetic Operators: Mathematical operations on integers and floats
  3. Comparison Operators: Relational comparisons returning booleans
  4. 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 int or float
  • 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:

InputResult
truefalse
falsetrue

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 0 or 0.0 is 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:

LeftRightResult
truetruetrue
truefalsefalse
falsetruefalse
falsefalsefalse

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:

LeftRightResult
truetruetrue
truefalsetrue
falsetruetrue
falsefalsefalse

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):

PrecedenceOperatorsDescriptionAssociativity
1 (highest)-, !Unary (negation, not)Right-to-left
2*, /MultiplicativeLeft-to-right
3+, -AdditiveLeft-to-right
4<, >, <=, >=RelationalLeft-to-right
5==, !=EqualityLeft-to-right
6&&Logical ANDLeft-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: --run mode 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:

  1. Lexical Analysis: Source code is broken into tokens (keywords, operators, identifiers, literals)
  2. 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:

  1. Type Inference: Determine the type of every expression
  2. Type Compatibility: Verify operations use correct types
  3. Initialization Tracking: Ensure variables are initialized before use
  4. Return Path Analysis: Verify all code paths return correct type
  5. 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:

  1. Stack Operations: PushInt, PushFloat, PushBool, Pop
  2. Local Variables: GetLocal <index>, SetLocal <index>
  3. Arithmetic: AddInt, AddFloat, SubInt, SubFloat, MulInt, MulFloat, DivInt, DivFloat
  4. Unary: NegInt, NegFloat, NotBool
  5. Comparison: EqInt, EqFloat, LtInt, LtFloat, etc.
  6. Logical: AndBool, OrBool
  7. Control Flow: Jump <offset>, JumpIfFalse <offset>, Call <funcIndex>
  8. 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:

  1. Each module compiled with function prototypes for external references
  2. Linker resolves function calls across module boundaries
  3. 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

  1. Main.hs: Reads source code from stdin or file
  2. Parser/Core.hs: Parses source code into AST
  3. Security/TypeChecker.hs: Validates type safety
  4. Compiler/Core.hs: Compiles AST to IR (indexed bytecode)
  5. Bytecode/Serialize.hs (optional): Serializes IR to .gbc file
  6. Linker (optional): Links multiple modules together
  7. VM/Interpreter.hs: Executes IR program
  8. Main.hs: Returns result (exit code 0 for success, 84 for error)

Compiler Modes

The GLaDOS compiler supports multiple modes of operation:

ModeUsageDescription
Default./glados < program.cRead from stdin, compile and execute, output result
AST Mode./glados --ast < program.cParse and display the Abstract Syntax Tree
IR Mode./glados --ir < program.cParse, compile and display human-readable IR
Compile Mode./glados --compile < program.c > out.gbcCompile to bytecode and write to stdout
Compile Object./glados --compile-obj < module.c > module.gboCompile to relocatable object file for linking
Link Mode./glados --link mod1.gbo mod2.gbo -o prog.gbcLink multiple object files into executable
Run Mode./glados --run program.gbcExecute bytecode from FILE (skips parsing/compilation)
Help./glados --helpDisplay help message
Version./glados --versionDisplay 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

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:

  1. Lexical Analysis (Lexing): Convert source code into tokens
  2. 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 like 42, -10
  • FloatLit (Located Double): Floating-point literals like 3.14, -0.5
  • BoolLit (Located Bool): Boolean literals true, 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:

  • try combinator: Backtrack on failure
  • <|> operator: Try alternative parsers
  • optional: 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

  1. Composability: Build complex parsers from simple ones
  2. Error Messages: Detailed, helpful error reporting
  3. Performance: Efficient parsing with minimal overhead
  4. Type Safety: Strong typing prevents many parser bugs
  5. 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:

  1. Security Analysis: Check for division by zero and other issues
  2. Type Checking: Verify type correctness
  3. 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:

  1. Type inference: Determine the type of every expression
  2. Type compatibility: Verify operations use correct types
  3. Initialization tracking: Ensure variables are initialized before use
  4. Return path analysis: Verify all code paths return correct type
  5. 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:

See also:

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 0 to paramCount - 1
  • Local variables occupy indices paramCount to localVarCount - 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 0 loads first parameter/local

SetLocal Int

  • Pops a value from stack and stores it in local variable at given index
  • Example: SetLocal 2 stores to third local

Integer Arithmetic

AddInt

  • Pops b, pops a, pushes a + b (integers)

SubInt

  • Pops b, pops a, pushes a - b (integers)

MulInt

  • Pops b, pops a, pushes a * b (integers)

DivInt

  • Pops b, pops a, pushes a / b (integers)
  • Runtime error if b is 0

Float Arithmetic

AddFloat

  • Pops b, pops a, pushes a + b (floats)

SubFloat

  • Pops b, pops a, pushes a - b (floats)

MulFloat

  • Pops b, pops a, pushes a * b (floats)

DivFloat

  • Pops b, pops a, pushes a / b (floats)
  • Runtime error if b is 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, pops a, pushes boolean a == b

NeqInt

  • Pops b, pops a, pushes boolean a != b

LtInt

  • Pops b, pops a, pushes boolean a < b

GtInt

  • Pops b, pops a, pushes boolean a > b

LeInt

  • Pops b, pops a, pushes boolean a <= b

GeInt

  • Pops b, pops a, pushes boolean a >= b

Float Comparison

EqFloat

  • Pops b, pops a, pushes boolean a == b

NeqFloat

  • Pops b, pops a, pushes boolean a != b

LtFloat

  • Pops b, pops a, pushes boolean a < b

GtFloat

  • Pops b, pops a, pushes boolean a > b

LeFloat

  • Pops b, pops a, pushes boolean a <= b

GeFloat

  • Pops b, pops a, pushes boolean a >= b

Logical Operations

AndBool

  • Pops b, pops a, pushes boolean a && b
  • Note: Both operands are always evaluated (not short-circuit in IR)

OrBool

  • Pops b, pops a, pushes boolean a || b
  • Note: Both operands are always evaluated (not short-circuit in IR)

Control Flow

Jump Int

  • Unconditional jump by relative offset
  • Example: Jump 5 jumps forward 5 instructions
  • Example: Jump -3 jumps 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:

  1. Evaluate condition x > 0
  2. JumpIfFalse 3: if false, jump forward 3 instructions (skip then branch)
  3. Then branch: get x and return
  4. Jump 2: skip else branch
  5. 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:

  1. Initialize i = 0
  2. Loop condition: i < 10
  3. JumpIfFalse 6: exit loop if condition false
  4. Loop body: i = i + 1
  5. 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:

  1. Function names → Function table index
  2. Variable names → Local variable index
  3. 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:

  1. IR is passed to bytecode serializer (see Bytecode Generation)
  2. 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:

  1. Source code is parsed and type-checked
  2. Compiled to IR with symbolic function calls
  3. Exports and imports are tracked
  4. Object file is serialized to disk

Combine object files into executable program:

./glados --link module_a.gbo module_b.gbo -o program.gbc

What the linker does:

  1. Load all object files
  2. Build symbol table mapping function names to indices
  3. Check for duplicate symbols
  4. Check for undefined symbols
  5. Resolve symbolic calls to actual indices
  6. Find main function
  7. 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:

  1. Converting Call <index> to CallRef <name>
  2. Extracting exports (all functions in module)
  3. Detecting imports (external calls)

Link Objects:

linkObjects :: [ObjectFile] -> Either String IRProgram

Links multiple objects into executable program by:

  1. Building global symbol table
  2. Checking for errors (duplicates, undefined symbols)
  3. Resolving symbolic references
  4. Finding main function
  5. 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 operations
  • io.c: Input/output utilities
  • logic.c: Business logic
  • main.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

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 -> VMState
  • pop :: VMState -> VMResult (Value, VMState)

Locals

Indexed local variables stored as a list (array):

locals :: [Value]

Indexing:

  • Parameters occupy indices 0 to paramCount - 1
  • Local variables occupy indices paramCount to localVarCount - 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 Value
  • setLocal :: 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, JumpIfFalse for 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:

  1. Fetch: Get instruction at code[pc]
  2. Decode: Pattern match on instruction type
  3. Execute: Dispatch to appropriate handler
  4. Increment: pc = pc + 1 (unless instruction modified PC)
  5. Repeat: Loop until Return or Halt

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:

  1. Get function from program by index
  2. Pop paramCount arguments from stack
  3. Execute function with arguments
  4. 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:

PCInstructionStackLocalsNotes
0PushInt 5[5][]Push 5
1PushInt 3[5, 3][]Push 3
2AddInt[8][]Pop 3, pop 5, push 8
3SetLocal 0[][8]Pop 8, store to local 0
4GetLocal 0[8][8]Load local 0, push 8
5Return[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 = []
PCInstructionStackAction
0GetLocal 0[10]Load a
1GetLocal 1[10, 5]Load b
2GtInt[True]10 > 5 = True
3JumpIfFalse 3[]Pop True, don't jump
4GetLocal 0[10]Load a
5Return[10]Return 10

Result: VInt 10

Execution Trace for max(3, 8):

Initial state:

  • locals = [3, 8] (a=3, b=8)
PCInstructionStackAction
0GetLocal 0[3]Load a
1GetLocal 1[3, 8]Load b
2GtInt[False]3 > 8 = False
3JumpIfFalse 3[]Pop False, jump to PC 6
6GetLocal 1[8]Load b
7Return[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:

PCInstructionLocalsStackNotes
0-3Initialize[0, 0][]sum=0, i=0
4-6Check i < 3[0, 0][True]0 < 3
7JumpIfFalse 10[0, 0][]Don't jump (True)
8-11sum = sum + i[0, 0][]sum = 0+0 = 0
12-15i = i + 1[0, 1][]i = 1
16Jump -12[0, 1][]Jump to PC 4
4-6Check i < 3[0, 1][True]1 < 3
8-11sum = sum + i[1, 1][]sum = 0+1 = 1
12-15i = i + 1[1, 2][]i = 2
16Jump -12[1, 2][]Jump to PC 4
4-6Check i < 3[1, 2][True]2 < 3
8-11sum = sum + i[3, 2][]sum = 1+2 = 3
12-15i = i + 1[3, 3][]i = 3
16Jump -12[3, 3][]Jump to PC 4
4-6Check i < 3[3, 3][False]3 < 3 = False
7JumpIfFalse 10[3, 3][]Jump to PC 17
17GetLocal 0[3, 3][3]Load sum
18Return[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:

FunctionPCInstructionStackNotes
main0PushInt 21[21]Push argument
main1Call 0-Call double with 21
double0GetLocal 0[21]Load parameter x=21
double1PushInt 2[21, 2]Push 2
double2MulInt[42]21 * 2 = 42
double3Return[42]Return 42
main1(after call)[42]Result pushed
main2Return[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)

PCInstructionStackLocalsNotes
0PushFloat 3.14[3.14][2.0, 0.0]
1SetLocal 1[][2.0, 3.14]pi = 3.14
2GetLocal 1[3.14][2.0, 3.14]Load pi
3GetLocal 0[3.14, 2.0][2.0, 3.14]Load radius
4MulFloat[6.28][2.0, 3.14]3.14 * 2.0
5GetLocal 0[6.28, 2.0][2.0, 3.14]Load radius
6MulFloat[12.56][2.0, 3.14]6.28 * 2.0
7Return[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 Vector for true O(1) indexing
  • Code: Could use Vector for true O(1) instruction fetch

Implementation Notes

Module Structure

VM/HelperFunc.hs:

  • VMState and CallFrame definitions
  • 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 Vector later

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:

  1. Compiling GLaDOS source → IR
  2. Executing IR with test inputs
  3. 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

  1. Read the error message carefully - it tells you exactly what's wrong
  2. Check the line number - the error is usually on or near that line
  3. Look at variable types - most errors are type mismatches
  4. Verify all code paths return - use else branches
  5. Initialize variables before using them
  6. Match function signatures - check parameter counts and types
  7. 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?

CriterionStackCabalRaw GHCOur Choice
Reproducible BuildsLTS snapshotsPartial (with freeze files)ManualStack
Dependency ManagementAutomatic resolutionManual constraintsNoneStack
Ease of SetupOne commandModerateComplexStack
Project IsolationSeparate environmentsWith sandboxesGlobalStack
Learning CurveLowModerateHighStack

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?

CriterionMegaparsecParsecAlex/HappyParser GeneratorsOur Choice
Error MessagesExcellentGoodPoorPoorMegaparsec
ComposabilityCombinator-basedCombinator-basedSeparate toolsGenerated codeMegaparsec
Learning CurveLowLowModerateHighMegaparsec
FlexibilityHighHighLowLowMegaparsec
Type SafetyStrongStrongModerateWeakMegaparsec
MaintenanceActiveLegacyActiveVariesMegaparsec
  1. Built-in Lexer Support: Text.Megaparsec.Char.Lexer handles whitespace and comments automatically

  2. 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?

CriterionHspecHUnitQuickCheckTastyOur Choice
ReadabilityDescriptiveModerateLowModerateHspec
BDD Styledescribe/itAssertionsPropertiesMixedHspec
Output FormattingExcellentBasicBasicGoodHspec
Learning CurveLowLowModerateModerateHspec
Property TestingIntegrates QuickCheckNoNativeYesHspec
Parallel ExecutionYesNoNoYesHspec

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?

Criterioncontainersunordered-containersvectorlistsOur Choice
PerformanceO(log n)O(1) averageO(1) indexO(n)containers
OrderingSortedUnorderedIndexedOrderedcontainers
ImmutabilityPersistentPersistentImmutableImmutablecontainers
MemoryEfficientEfficientVery EfficientPoorcontainers
Pattern MatchingGoodModerateLimitedExcellentcontainers

Our Usage:

  • Map: Symbol tables, type environments, function tables
  • Set: Tracking initialized variables, imported modules
  • Seq: 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?

CriterionByteString[Word8]Vector Word8Our Choice
PerformanceExcellentPoorExcellentByteString
MemoryPackedWastefulPackedByteString
I/OOptimizedSlowManualByteString
Binary IntegrationNativeManualManualByteString
EcosystemMatureN/AGrowingByteString

Key Benefits:

Our Usage:

  • .glc bytecode files
  • .glo object 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?

CriterionDeepSeqseqBangPatternsOur Choice
Deep EvaluationRecursiveShallowShallowDeepSeq
Type SafetyNFData classNoNoDeepSeq
ComposabilityDerivesManualManualDeepSeq
ControlExplicitExplicitImplicitDeepSeq

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?

Criterionhspec-discoverManual mainTasty-discoverOur Choice
AutomationFully automaticManualAutomatichspec-discover
Convention*Spec.hs filesCustomCustomhspec-discover
MaintenanceZeroHighLowhspec-discover
IntegrationNative HspecN/ADifferent APIhspec-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?

CriterionprocessSystem.CmdShellCmdRaw forkOur Choice
Type SafetyStrongWeakModerateUnsafeprocess
Error HandlingExplicitWeakModerateManualprocess
PortabilityCross-platformLimitedLimitedUnix onlyprocess
ControlHighLowModerateTotalprocess
Output CaptureEasyManualEasyManualprocess

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?

CriterionQuickCheckExample-BasedSmallCheckHedgehogOur Choice
CoverageExcellentManualComplete (small)ExcellentQuickCheck
Random TestingYesNoExhaustiveYesQuickCheck
ShrinkingAutomaticN/AN/ABetterQuickCheck
IntegrationHspec nativeYesYesManualQuickCheck
MaturityVery matureN/AMatureNewerQuickCheck

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:

  1. Separation of Concerns:

    • Megaparsec: Core parsing functionality
    • Parser-combinators: Reusable combinators
    • Keeps Megaparsec lightweight
  2. 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 lists
  • between: Parentheses, braces, brackets
  • choice: Multiple alternatives
  • option: 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?

CriterionBinaryCerealStoreCustomOur Choice
SimplicityHighHighModerateLowBinary
PerformanceGoodGoodBetterVariesBinary
EcosystemMatureMatureNewerN/ABinary
Type SafetyDerive instancesDerive instancesDerive instancesManualBinary

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:

  1. Explicit in Type Signatures: Function types show they can fail
  2. Composable with Monads: Easy chaining with do notation or >>=
  3. Forced Error Handling: Compiler ensures you handle errors
  4. 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 releases
  • dev: 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:

LibraryPurposeWhy Not Alternative?
baseStandard libraryRequired foundation
megaparsecParser combinatorsBetter errors than Parsec, more flexible than generators
parser-combinatorsParser utilitiesStandard combinators for megaparsec
containersMap, Set, SeqDeterministic ordering, efficient immutable operations
bytestringBinary dataStandard for binary I/O
binarySerializationSimple automatic derivation
deepseqForce evaluationPrevents space leaks
hspecBDD testingReadable test output
hspec-discoverTest discoveryZero-maintenance test suite
QuickCheckProperty testingFinds edge cases automatically
processExternal programsEnd-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:

  1. Learning: Clear, understandable implementations
  2. Safety: Type safety and error handling
  3. Productivity: Good tooling and quick iteration
  4. 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 FeatureImplementationDetection TimeStatus
Static Type SystemFull type checking with inferenceCompile-time✓ Implemented
Initialization TrackingFlow-sensitive analysisCompile-time✓ Implemented
Division by Zero (literals)AST analysisCompile-time✓ Implemented
Return Path VerificationControl flow analysisCompile-time✓ Implemented
Type Inferencevar keyword with initializationCompile-time✓ Implemented
Stack SafetyVM bounds checkingRuntime✓ Implemented
Function Call SafetyArgument count & type validationCompile-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)

  1. Parser (Parser/Core.hs): Builds AST with source positions for error reporting
  2. Type Inference (Security/TypeInference.hs): Resolves var declarations
  3. Type Checker (Security/TypeChecker.hs): Validates types and initialization
  4. Compiler (Compiler/Core.hs): Generates safe bytecode only for valid programs

Runtime Safety (VM Layer)

  1. Stack Bounds Checking (VM/HelperFunc.hs): Prevents stack overflow/underflow
  2. Local Variable Bounds (VM/HelperFunc.hs): Array bounds checking for locals
  3. Division by Zero (VM/InstructionHandlers.hs): Runtime check for dynamic values
  4. Type-Safe Operations: VM maintains type tags (VInt, VBool, VFloat)

Design Rationale

Why Compile-Time Over Runtime?

  1. Zero Performance Overhead: No runtime checks for proven-safe code
  2. Early Error Detection: Bugs caught before deployment
  3. Better Developer Experience: Immediate feedback during development
  4. 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

FeatureCRustOur Language
Type SafetyWeak, allows castingStrong with lifetimesStrong, no unsafe casts
Memory SafetyManual, error-proneBorrow checkerNo pointers (safe by design)
InitializationUndefined behaviorEnforcedCompile-time tracked
Null SafetyNull pointersOption typeNo null (no pointers)
Integer OverflowSilent wraparoundPanic in debugSilent (same as C)
Division by ZeroRuntime crashPanicCompile-time for literals, runtime otherwise
Return CheckingUndefined behaviorEnforcedCompile-time verified

Limitations and Trade-offs

What We DON'T Prevent:

  1. Integer Overflow: Would require either runtime checks (performance cost) or different arithmetic semantics
  2. Runtime Division by Zero: Only catch literal zeros; dynamic values checked at runtime
  3. Infinite Loops: Halting problem - theoretically impossible to solve completely
  4. 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:

  1. Tail Call Optimization: Prevent stack overflow in recursive functions
  2. Bounds-Checked Arrays: Safe indexing when arrays are added
  3. Pattern Matching: Exhaustiveness checking for safer control flow
  4. Effect System: Track side effects for pure functional programming

Considered but Deferred:

  1. Gradual Typing: Allow mixing static and dynamic typing
  2. Linear Types: Resource management without GC
  3. 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