Shine Wiki
Everything currently documented and demonstrated about Shine; a small, low level language that compiles straight to native machine code through LLVM. No runtime, no VM.
Introduction
Shine is a small compiled language with its own hand-written lexer, recursive-descent parser, and LLVM-based code generator. Source files compile down to a native object file, which is linked straight into an executable. There is no interpreter, no bytecode, no VM, and no garbage collector.
Shine programs link against libc and nothing else. The project is developed by KerbalMissile and is open source under the GPL-3.0 license on GitHub.
Project Status
Shine is under active development. As of the current build, the compiler compiles functions with parameters, calls, string/int literals, variables, operators, comparisons, control flow, real types, pointers, and structs. Arrays are next up.
v0.1.0
Lexer, parser, codegen for functions, return, calls, literals, and the write builtin.
v0.2.0
let / var variables, assignment, arithmetic and comparison operators.
v0.3.0
Control flow: if / else, loop, stop, cont.
v0.4.0
write() for int expressions, user_input() for reading integers.
v0.5.0
Better default compiler error messages and general error-handling improvements.
v0.6.0
Real type hierarchy, fixed-width integers (i8 up to i64, u8/u32), pointers (& / *), and a type-checking pass.
v0.7.0
-32 flag for 32-bit compilation, -c compile-only flag, colored success/failure driver output.
v0.8.0
Structs, struct literals, nested field access, and field access through pointers.
v0.9.0 in progress
Fixed-size arrays, indexing, array/pointer decay, and possibly simple slices.
Getting Started
Compiling a Shine file is a single command:
shinec yourfile.shine -o output.exe
Your Shine code is then compiled and linked, ready to run as output.exe.
Building the Compiler from Source (Windows, MSYS2/MinGW)
Shine's compiler itself is built with CMake + Ninja against LLVM, inside an MSYS2 MINGW64 shell. Every command below must be run from that shell.
# install the toolchain
pacman -S mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake \
mingw-w64-x86_64-llvm mingw-w64-x86_64-ninja
# configure and build
cmake -B build -G Ninja
cmake --build build
# run the unit tests
./build/tests/shine_tests.exe
# compile and run the hello world example
./build/shinec.exe examples/hello.shine -o hello.exe
./hello.exe
Repository Layout (Compiler Repo)
| Path | Contents |
|---|---|
include/shine/ | Compiler headers |
src/ | Lexer, parser, codegen, driver (main.cpp) |
tests/ | Unit tests |
examples/ | Sample .shine programs |
assets/ | Project assets (logo, etc.) |
scripts/ | Build/dev scripts |
CMakeLists.txt | CMake build configuration |
ROADMAP.md | Version-by-version feature roadmap |
Suggested Repository Layout (Your Repo)
| Path | Contents |
|---|---|
src/ | Subfolders, .shine files, etc |
assets/ | Project assets (logo, etc) |
Program Structure
Every Shine program is a set of functions declared with fn. Execution starts at main, which returns an integer to the OS via r/ or return/ (the return operator, r/ and return/ are both valid, your choice). A minimal program looks like this:
fn int main() {
write("Hello, World!");
r/0;
}
Functions without an explicit return at the end automatically return 0 (or void for void functions). This is implicit and not an error.
Types
Shine has a full fixed-width type hierarchy, backed by a dedicated type-checking pass. Plain int remains an alias for i32 for backwards compatibility.
| Type | Meaning |
|---|---|
void | No return value |
int | Generic integer (alias for i32) |
i8 / u8 | 8-bit signed / unsigned integer (byte) |
i16 / u16 | 16-bit signed / unsigned integer |
i32 / u32 | 32-bit signed / unsigned integer |
i64 / u64 | 64-bit signed / unsigned integer |
*T | Pointer to a value of type T (e.g. *i32, **i32) |
StructName | A user-defined struct type |
The literal 0 is accepted wherever a pointer type is expected, acting as a null pointer constant (same as in C).
Variables: let and var
let(type) declares an immutable value; var(type) declares a value that can be reassigned later.
// let creates an immutable value.
let(int) base = 10;
// var creates a value that can be changed later.
var(int) total = 4;
total = total + base * 2;
Function parameters are always immutable. You cannot reassign a parameter inside the function body; only var declared locals can be changed.
Operators
Standard arithmetic and comparison operators are supported. Comparisons currently evaluate to 0 or 1 as i32 values; there is no boolean type.
| Category | Operators |
|---|---|
| Arithmetic | + - * / |
| Comparison | == != > >= < <= |
| Assignment | = |
| Address-of / deref | & (address-of), * (declare pointer type / dereference) |
Precedence
Operators are evaluated from lowest to highest precedence as follows:
| Precedence | Operators |
|---|---|
| 1 (lowest) | == != |
| 2 | < <= > >= |
| 3 | + - |
| 4 | * / |
| 5 (highest) | & (unary), * (unary) |
Control Flow
Shine unifies while and for into a single loop(condition) { } construct. Loop control uses stop (break) and cont (continue) instead of the usual keywords.
fn int sumTo(n: int) {
var(int) total = 0;
var(int) i = 0;
loop (i <= n) {
if (i == 3) {
i = i + 1;
cont; // skip the rest of this iteration
}
total = total + i;
i = i + 1;
}
r/total;
}
fn int main() {
var(int) count = 0;
loop (1) { // loop(1) is an infinite loop
count = count + 1;
if (count == 5) {
stop; // break out of the loop
}
}
r/count;
}
| Keyword | Equivalent to |
|---|---|
if / else | Standard conditional branching, chains with else if |
loop(cond) { } | while / for (combined into one construct) |
stop | break |
cont | continue |
Functions
Functions are declared with fn returnType name(param: type, ...) { ... }. Parameters are passed as real LLVM arguments and referenced by name in the body. A function returns using r/ (shorthand for return; the longer return/ form also appears in later examples).
fn int identity(x: int) {
r/x;
}
fn int pick_second(a: int, b: int) {
r/b;
}
fn int main() {
r/pick_second(identity(1), 2); // nested calls work
}
Both r/ and return/ work as the return keyword. The / is part of the keyword, not a separate token.
Strings
String literals are enclosed in double quotes. Shine supports the following escape sequences:
| Escape | Meaning |
|---|---|
\n | Newline |
\t | Tab |
\" | Double quote |
\\ | Backslash |
write("Line one\nLine two"); // prints on two lines
write("Tab\there"); // prints with a tab
write("She said \"hello\""); // escaped quotes
write("Backslash: \\"); // escaped backslash
Any other escape sequence (e.g. \r, \0) produces a compile error: bad escape sequence.
Pointers
Pointers use & to take the address of a value, and * both to declare a pointer type and to dereference. Pointer-to-pointer types like **i32 are supported in the type hierarchy.
fn void increment(p: *i32) {
*p = *p + 1;
}
fn i32 main() {
var(i32) x = 10;
var(*i32) px = &x;
increment(px);
write(*px); // 11
write(x); // 11, same storage
var(**i32) ppx = &px;
write(**ppx); // 11
r/0;
}
Structs
Structs are declared with struct Name { field: type, ... }, built with struct-literal syntax, and support both direct and pointer-based field access. Trailing commas after the last field are optional.
struct Vec2 {
x: i32,
y: i32,
}
struct Player {
pos: Vec2,
health: i32,
name: *i32, // placeholder until a real string/array type exists
}
fn Vec2 add(a: Vec2, b: Vec2) {
r/Vec2 { x: a.x + b.x, y: a.y + b.y }; // struct literal
}
fn void damage(p: *Player, amount: i32) {
(*p).health = (*p).health - amount; // field access through a pointer
}
Builtins
| Builtin | Description |
|---|---|
write(expr) | Prints a string literal or int-valued expression, lowers to puts. A compiler builtin for now; the roadmap plans to move it into a real standard library. |
user_input(prompt) | Prints prompt, reads an integer from the user, and returns it. |
terminal.pause(user_action) | Pauses the terminal so output is visible before the program exits, waiting for a user action. |
Full Examples
Every example currently shipped with the language, in order of complexity.
hello.shine
The classic first program. Prints a greeting and returns cleanly.
fn int main() {
write("Hello, World!");
terminal.pause(user_action);
// Return 0
r/0;
}
test.shine
Exercises the write builtin and function calls, including a nested call passed as an argument.
fn int identity(x: int) {
// Return the input unchanged so we can verify parameter passing.
r/x;
}
fn int pick_second(a: int, b: int) {
// Ignore the first argument and return the second one.
r/b;
}
fn int main() {
write("Shine v0.1.0");
write("compiled and linked successfully");
terminal.pause(user_action);
// Call another function with a nested call and return its result.
r/pick_second(identity(1), 2);
}
v0.2.0 example
Demonstrates let, var, assignment, operator precedence, and comparisons.
fn int main() {
// let creates an immutable value.
let(int) base = 10;
// var creates a value that can be changed later.
var(int) total = 4;
// Operators work inside expressions, with * before +.
total = total + base * 2;
write("Shine v0.2.0 example");
write("let, var, assignment, operators, and comparisons worked");
terminal.pause(user_action);
// Comparisons return 0 or 1 for now.
r/total >= 24;
}
v0.3.0 example
Introduces loop, if/else, cont, and stop. These are Shine's control flow.
fn int sumTo(n: int) {
var(int) total = 0;
var(int) i = 0;
loop (i <= n) {
if (i == 3) {
i = i + 1;
cont;
}
total = total + i;
i = i + 1;
}
r/total;
}
fn int main() {
var(int) x = sumTo(10);
if (x > 40) {
write("big");
} else {
write("small");
}
var(int) count = 0;
loop (1) {
count = count + 1;
if (count == 5) {
stop;
}
}
r/count + x;
}
v0.4.0 example
write() now accepts int-valued expressions, and user_input() reads an integer from the user.
fn int square(n: int) {
r/n * n;
}
fn int main() {
// write() now accepts int-valued expressions, not just string literals.
write(42);
write(square(7));
// user_input() prints the prompt, reads an int, and returns it.
var(int) x = user_input("Enter a number: ");
write(x * 2);
r/0;
}
calc.shine
A small calculator that puts everything so far together: user_input, var, if/else if/else chains, and terminal.pause.
fn int main() {
// declare all the variables
var(int) num1 = user_input("Pick a number ");
var(int) num2 = user_input("Pick a second number ");
var(int) op = user_input("What operator do you want? 1 = +, 2 = -, 3 = *, 4 = / ");
var(int) product = 0;
var(int) validop = 1;
if (op == 1) {
product = num1 + num2;
}
else if (op == 2) {
product = num1 - num2;
}
else if (op == 3) {
product = num1 * num2;
}
else if (op == 4) {
product = num1 / num2;
} else {
validop = 0;
write("No valid number entered for the operator. Please retry and enter a valid input.");
}
if (validop == 1) {
write(product);
}
terminal.pause(user_action);
r/0;
}
v0.6.0 example
Fixed-width integer types and pointers. Use & to take an address, * to declare a pointer type or dereference.
// v0.6.0: real types, fixed-width ints, pointers.
fn i32 add(a: i32, b: i32) {
r/a + b;
}
fn void increment(p: *i32) {
*p = *p + 1;
}
fn i32 main() {
// Fixed-width int locals. "int" alone still means i32 for back-compat.
let(i8) small = 5;
let(u8) byte = 255;
let(i16) medium = 1000;
let(i32) normal = add(3, 4);
let(u32) unsigned = 4000000000;
let(i64) big = 9000000000;
write(normal);
write(big);
// Pointers: & to take an address, * to declare a pointer type / deref.
var(i32) x = 10;
var(*i32) px = &x;
increment(px);
write(*px); // 11
write(x); // 11, same storage
// Pointer to pointer, just to exercise the type hierarchy.
var(**i32) ppx = &px;
write(**ppx); // 11
r/0;
}
v0.8.0 example
Structs, struct literals, and field access. They work both directly and through a pointer.
// v0.8.0: structs.
struct Vec2 {
x: i32,
y: i32,
}
struct Player {
pos: Vec2,
health: i32,
name: *i32, // placeholder until a real string/array type exists
}
fn Vec2 add(a: Vec2, b: Vec2) {
// Struct literal, built from field access on the params.
r/Vec2 { x: a.x + b.x, y: a.y + b.y };
}
fn void damage(p: *Player, amount: i32) {
// Field access/assignment through a pointer-to-struct.
(*p).health = (*p).health - amount;
}
fn i32 main() {
var(Vec2) origin = Vec2 { x: 0, y: 0 };
var(Vec2) delta = Vec2 { x: 3, y: 4 };
var(Vec2) moved = add(origin, delta);
write(moved.x); // 3
write(moved.y); // 4
var(Player) hero = Player { pos: moved, health: 100, name: 0 };
var(*Player) pHero = &hero;
damage(pHero, 30);
write(hero.health); // 70, mutated through the pointer
write(hero.pos.x); // 3, nested field access
return/0;
}
CLI Reference
The compiler is invoked with:
shinec <input.shine> [-o <output>] [-32] [-c]
| Flag | Description |
|---|---|
-o <output> | Set the output file path. Defaults to <input>.exe on Windows, or <input>.o with -32 or -c. |
-32 | Compile to 32-bit freestanding ELF object (skips linking) |
-c | Compile only; emit an object file and skip linking |
The compiler uses a bundled MinGW linker when available, falling back to g++ on Windows or cc on other platforms.
Error Handling
Errors are reported in the format file:line:col: error: message, with the offending source line printed below.
example.shine:3:5: error: undeclared identifier 'totl' (did you mean 'total'?)
total = totl + 1;
The compiler suggests corrections for misspelled identifiers and function names using Levenshtein distance, as long as the distance is within max(2, name.length / 3).
Common Errors
| Error | Cause |
|---|---|
cannot assign to immutable variable | Assigning to a let variable or a function parameter |
variable already declared | Redeclaring a variable in the same scope |
undeclared identifier | Using a variable or function that has not been declared |
unknown type | Using a type name that does not exist |
stop/cont outside of a loop | Using stop or cont outside a loop |
bad escape sequence | Using an unsupported string escape (e.g. \r) |
struct has no field | Accessing a field that does not exist on a struct |
expected type, got | Type mismatch in a variable declaration or assignment |
Roadmap
v0.1.0 first steps ✓ done
- Lexer, parser, codegen for fn/return/call/literals
writebuiltin viaputs- Object file emission + link, builds hello.shine
- Function parameters, identifier expressions, per-function argument passing
v0.2.0 variables and operators ✓ done
let(type) name = expr;var(type) name = expr;- Assignment to
var + - * /, comparisons
v0.3.0 control flow ✓ done
- if/else, while, for, etc.; while/for are combined into
loop(condition) {} - break (written as
stop) and continue (written ascont) loop controls
v0.4.0 inputs & non string literals ✓ done
- User inputs, written as
user_input("TEXT"); it prints the prompt, reads an int from stdin, and returns it write()supports non-string literals; any int-valued expression can be passed to it
v0.5.0 error handling ✓ done
- Better default error messages for the compiler
- General improvements to error handling
v0.6.0 real types ✓ done
- Type hierarchy instead of string-named TypeRef
- Pointers, fixed-width ints
- Type-checking pass
v0.7.0 build/driver improvements ✓ done
-32flag for 32-bit compilation-cflag (compile only, skip linking)- Success/failure messages colored (green/red) in the driver output
v0.8.0 structs ✓ done
struct Name { field: type, ... }declarations- Field access (
x.field) and field assignment (x.field = expr;) - Struct values as function params/returns
- Struct literal / initialization syntax
return/0;is also valid, alongsider/0;
v0.9.0 arrays in progress
- Fixed-size arrays
- Indexing (
arr[i]), with a decided and documented bounds-check behavior - Array/pointer decay interaction
- Simple slices (ptr + length) if time allows, otherwise deferred past v1.0.0
v0.10.0 standard library planned
- Minimal extern mechanism, just enough to call into libc (
printf,scanf,malloc) - Move
write/user_input/terminal.pauseout of the compiler and into Shine-source stdlib functions - Basic
String/Buffer-style type built on structs + arrays
v0.11.0 error handling planned
- Result/error-union style return values
- Propagation syntax, or at minimum pattern-matching on Result
v0.12.0 multi-file modules planned
import/module resolution- Symbol visibility (pub/private)
- Multi-translation-unit linking in the driver
v1.0.0 out of beta planned
- Syntax freeze, written language spec/reference (not just README examples)
- Full test coverage pass, basic parser/lexer fuzzing
- Documented versioning/back-compat policy going forward
- README status line updated to reflect 1.0, not active-development beta
Later planned
- Full C FFI (extern function declarations, calling into arbitrary native libs)
deferstatements for guaranteed cleanup- Function pointers and callbacks
- Closures with explicit capture (allocator-controlled, no GC)
- Allocator story in the stdlib (arena, pool, bump allocators)
- Package manager + package ecosystem (Cargo/pip-style)
- Ability to generate .dll's
- UI Tool
- Freestanding codegen mode (no libc, no CRT startup, custom entry point/linker script)
TBD may or may not happen
- Simple interpreter
License
Shine is open source, licensed under GPL-3.0. See the LICENSE file in the repository for full terms.
Comments
Shine supports single-line and multi-line comments.
// comment/* comment */Block comments do not nest. A
/*inside a block comment closes the comment at the first*/.