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)

PathContents
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.txtCMake build configuration
ROADMAP.mdVersion-by-version feature roadmap

Suggested Repository Layout (Your Repo)

PathContents
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.

Comments

Shine supports single-line and multi-line comments.

SyntaxDescription
// commentSingle-line comment; extends to the end of the line
/* comment */Multi-line (block) comment; can span multiple lines
// This is a single-line comment.

/*
    This is a block comment.
    It can span multiple lines.
*/

fn int main() {
    write("Comments work!");  // inline comment
    r/0;
}

Block comments do not nest. A /* inside a block comment closes the comment at the first */.

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.

TypeMeaning
voidNo return value
intGeneric integer (alias for i32)
i8 / u88-bit signed / unsigned integer (byte)
i16 / u1616-bit signed / unsigned integer
i32 / u3232-bit signed / unsigned integer
i64 / u6464-bit signed / unsigned integer
*TPointer to a value of type T (e.g. *i32, **i32)
StructNameA 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.

CategoryOperators
Arithmetic+ - * /
Comparison== != > >= < <=
Assignment=
Address-of / deref& (address-of), * (declare pointer type / dereference)

Precedence

Operators are evaluated from lowest to highest precedence as follows:

PrecedenceOperators
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;
}
KeywordEquivalent to
if / elseStandard conditional branching, chains with else if
loop(cond) { }while / for (combined into one construct)
stopbreak
contcontinue

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:

EscapeMeaning
\nNewline
\tTab
\"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

BuiltinDescription
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]
FlagDescription
-o <output>Set the output file path. Defaults to <input>.exe on Windows, or <input>.o with -32 or -c.
-32Compile to 32-bit freestanding ELF object (skips linking)
-cCompile 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

ErrorCause
cannot assign to immutable variableAssigning to a let variable or a function parameter
variable already declaredRedeclaring a variable in the same scope
undeclared identifierUsing a variable or function that has not been declared
unknown typeUsing a type name that does not exist
stop/cont outside of a loopUsing stop or cont outside a loop
bad escape sequenceUsing an unsupported string escape (e.g. \r)
struct has no fieldAccessing a field that does not exist on a struct
expected type, gotType mismatch in a variable declaration or assignment

Roadmap

v0.1.0 first steps ✓ done

  • Lexer, parser, codegen for fn/return/call/literals
  • write builtin via puts
  • 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 as cont) 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

  • -32 flag for 32-bit compilation
  • -c flag (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, alongside r/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.pause out 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)
  • defer statements 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.