Viz-Compiler is a custom-built native compiler for VizLang, a minimal C-like language designed as a complete, pedagogically clear compiler construction reference. It compiles VizLang source through every classical stage — Lexing → Parsing → Semantic Analysis → IR Generation → Optimization → x86-64 Native Code — exposing each phase's internal representation via interactive web visualizers.
-8(%rbp)). This ensures state is always verifiable.sort() that map directly to pre-compiled assembly routines for algorithmic education.Source (.viz) → Lexer (Flex) → Parser (Bison) → Semantic (AST Walk) → IR Gen (Quads) → Optimizer (Prop/Fold/Strength/DCE) → x86-64 ASM
VizLang is a strict subset of C. Every valid VizLang program is structurally close to C, but relies on integers, strict scoping, and explicit memory pointers.
VizLang operates exclusively on 64-bit (8-byte) integers. To perfectly align with the x86-64 hardware stack, every variable, pointer, and array element occupies exactly 8 bytes of memory. Variables must be explicitly declared.
// 64-bit Integer (8 bytes)
int score = 100;
int x;
x = 50;
VizLang supports explicit memory addressing using the address-of (&) and dereference (*) operators. Pointers also consume exactly 8 bytes.
int target = 42;
int *ptr = ⌖ // Get 8-byte memory address
*ptr = 99; // Mutate target via pointer
Arrays are allocated as contiguous blocks on the stack frame. The size must be an integer literal. There is no runtime bounds checking.
int arr[5]; // Uninitialized fixed-size array
int primes[3] = {2, 3, 5}; // Array with initializer list
arr[0] = primes[2]; // Indexing (Read & Write)
| Category | Operators | Notes |
|---|---|---|
| Arithmetic | + - * / | Standard integer math. Evaluated left-to-right. |
| Relational | == != < > <= >= | Evaluates to 1 (true) or 0 (false). |
| Logical | && || | C-style short-circuit evaluation supported by IR generator. |
| Memory | & * | Address-of and Dereference operators. |
VizLang supports complex procedural generation through functions. Functions must declare their return type (int or void). The semantic analyzer guarantees that all code paths in an int function reach a return statement.
Standard arguments (like integers) are passed by value. The function receives a copy of the data. Modifying the argument inside the function does not affect the original variable.
// Recursive Factorial Function
int factorial(int n) {
if (n <= 1) { return 1; }
// 'n' is a local copy for this frame
return n * factorial(n - 1);
}
When an array is passed as an argument, the semantic analyzer forces Array Decay. Instead of copying the entire array, it is passed by reference as an 8-byte memory pointer.
// Notice the empty brackets: arr[]
// This tells the compiler it is a pointer
void mutateArray(int arr[], int index) {
arr[index] = 999;
// This modifies the caller's array!
}
To modify a standard variable inside a function, you must pass its memory address using the Address-of (&) operator. The function receives it as a Pointer (*) and mutates it using the Dereference operator.
// Function expects an 8-byte memory pointer
void addBonus(int *scorePtr) {
*scorePtr = *scorePtr + 500; // Mutate caller's data
}
int main() {
int myScore = 100;
// Pass the memory address, NOT the value
addBonus(&myScore);
print(myScore); // Outputs 600
return 0;
}
Because Viz-Compiler is an educational tool, it includes special grammar rules designed to teach fundamental computer science concepts.
// If-Else Branching
if (x == 10) {
print("Ten");
} else {
print("Other");
}
// Standard Loops
while (x < 10) { x = x + 1; }
for (int i = 0; i < 5; i = i + 1) {
print(i);
}
// The Repeat Loop (Evaluates limit exactly once)
repeat (5 * 2) {
print("This prints 10 times!");
}
These commands bypass standard function calls and are injected directly as distinct IR instructions or external assembly jumps.
// I/O Operations
print(x); // Prints variable
print("Text"); // Prints string literal
input(x); // Reads integer from stdin
// Hardware & Memory Hooks
printarray(arr); // Dumps entire array contents
sort(arr, 5); // Here 5 was the Size of the array .
The Intermediate Representation (IR) flattens the AST into 3-Address Code (Quads) generated by ir.c. This acts as the bridge to Assembly.
| Opcode | Arg1 | Arg2 | Result | Semantics |
|---|---|---|---|---|
IR_MOV | Source | — | Dest | Result = Arg1 |
IR_ADD / SUB | Left | Right | Dest | Result = Arg1 +/- Arg2 |
IR_MUL / DIV | Left | Right | Dest | Result = Arg1 */÷ Arg2 |
IR_SHL / SHR | Value | Shift | Dest | Result = Arg1 <</>> Arg2 (Used by Optimizer) |
IR_LT / GT / EQ | Left | Right | Dest | Sets Result to 1 or 0 based on comparison |
IR_IF_FALSE | Condition | — | Label | Jump to Result if Arg1 == 0 |
IR_GOTO | Label | — | — | Unconditional jump to Arg1 |
IR_LABEL | Name | — | — | Jump target marker |
IR_CALL | Func Name | — | Dest | Executes function, stores return in Result |
IR_PARAM / ARG | Var/Value | Index | — | Pushes argument to stack before a CALL |
IR_ARR_DECL | Name | Size | — | Allocates memory for array |
IR_ARR_LOAD | Array Name | Index | Dest | Result = Array[Index] |
IR_ARR_STORE | Array Name | Index | Value | Array[Index] = Result (Value) |
The optimize_ir() pipeline in ir.c performs a multi-pass sweep over the IR Quads to improve execution speed before Code Generation.
opt_propagate): Tracks variables assigned to literal values. If t0 = 5, it replaces future uses of t0 with the number 5.opt_fold): Scans for IR_ADD, IR_SUB, IR_MUL, and IR_DIV where both arguments are numbers. It executes the math during compilation and replaces the Quad with a single IR_MOV.opt_strength): Scans for IR_MUL or IR_DIV by a power of 2. It replaces them with IR_SHL (Shift Left) or IR_SHR (Shift Right), mapping to CPU bitwise operators which execute significantly faster.opt_dead): Scans forward to see if a Compiler Temporary (tN) is ever used as an Arg1 or Arg2. If it is never used, the assignment Quad is deleted entirely.This is a complete, valid VizLang program demonstrating functions, recursion, array decay, and mathematical logic. Our compiler can translate this all the way down to executable x86-64 assembly!
// Recursive Binary Search Implementation
int binarySearch(int arr[], int l, int r, int key) {
int mid;
// Base Case: Not found
if (l > r) {
return 0 - 1;
}
// Find middle (Optimized to bit-shift by IR)
mid = (l + r) / 2;
// Check if key is present at mid
if (arr[mid] == key) {
return mid;
} else {
// Recurse left or right
if (key < arr[mid]) {
return binarySearch(arr, l, mid - 1, key);
} else {
return binarySearch(arr, mid + 1, r, key);
}
}
}
int main() {
int data[5] = {10, 20, 30, 40, 50};
int target = 30;
int result;
print("Searching for target...");
// Call the function
result = binarySearch(data, 0, 4, target);
if (result != 0 - 1) {
print("Found at index:");
print(result);
} else {
print("Not Found");
}
return 0;
}