Greyalien is a new, experimental systems language intended to be:
- Statically typed and compiled (in its full form).
- Memory-safe by default, with explicit
unsafefor low-level work. - Concurrency-friendly, with lightweight tasks and channels.
- Effect-aware, so side effects are visible in type signatures.
- Pleasant to use, with algebraic data types, pattern matching, traits, and generics.
This document describes the conceptual v0 spec. The included interpreter implements only a small, untyped subset.
-
Surpass C++ and Rust for new projects:
- No undefined behavior in safe Greyalien.
- Clear, modern module system.
- Ownership and lifetimes made simpler via lexical regions.
-
First-class concurrency:
- Lightweight tasks (
spawn). - Typed channels.
- Data-race freedom enforced by the type system.
- Lightweight tasks (
-
Effect-aware design:
- Effects like I/O, randomness, and global mutation are tracked in
function signatures via
effectannotations.
- Effects like I/O, randomness, and global mutation are tracked in
function signatures via
The current interpreter does not implement the type/ownership/effect system; it focuses on surface syntax and basic evaluation.
Each .grl file starts with a module declaration:
module my.project.module
Module names are hierarchical and match directory structure.
- Built-in scalar types:
Int,Bool,String. typefor records.enumfor algebraic data types.- Generics with type parameters.
- Traits and implementations via
trait/impl.
Greyalien uses a single-owner model to make memory safety explicit and teachable. The goal is predictable rules with diagnostics that explain "why" and "how".
- Every value has exactly one owner at a time.
- Moving a value transfers ownership to a new binding.
- After a move, the previous binding is invalid.
- Values are dropped when their owner goes out of scope.
- Small scalar types (e.g.,
Int,Bool) areCopyby default. - Non-
Copyvalues move on assignment or pass-by-value. - Explicit
cloneis required for non-Copyduplication.
&Tis a shared (read-only) borrow.&mut Tis an exclusive (read/write) borrow.- You can have many shared borrows or one mutable borrow, but not both.
- Shared borrows cannot mutate, mutable borrows are exclusive.
- Borrows are tied to lexical regions inferred from scope.
- A borrow cannot outlive its owner.
- Reborrowing is allowed but cannot extend the lifetime beyond the owner.
- Use-after-move errors point to the move site and the invalid use.
- Borrow conflicts explain both the active borrow and the conflicting access.
- Error messages include suggestions such as "borrow instead of move".
Move and use-after-move:
fn main() {
let data = make_buffer();
let moved = data;
print(moved);
print(data); // error: use-after-move
}
Shared vs mutable borrow conflict:
fn main() {
let mut x = 1;
let r1 = &x;
let r2 = &x;
let m = &mut x; // error: cannot borrow mutably while shared borrows are active
print(r1);
print(r2);
}
Valid scoped mutable borrow:
fn main() {
let mut x = 1;
{
let m = &mut x;
*m = 2;
}
print(x); // ok: mutable borrow ended
}
- Built-in
spawnfor tasks. - Channels for message passing.
- Structured concurrency: tasks tied to scopes.
Effects make side effects explicit in type signatures. Greyalien aims for a small effect system that is easy to read and easy to teach.
Functions default to pure. Effectful functions declare a set of effects:
fn read_file(path: String) -> Result<String, io::Error> effect [io]
- Pure functions cannot call effectful functions.
- A caller must include all effects of its callees.
- The effect set is part of the function type.
- If a function body uses an effectful operation, the effect is inferred.
- Explicit annotations are allowed and must be satisfied by the body.
io: console and file IO.net: networking.time: clocks and timers.rng: randomness.unsafe: explicit unsafe operations.
fn add(a: Int, b: Int) -> Int { a + b }
fn read_name() -> String effect [io] {
return input(); // effectful builtin
}
fn main() -> Unit effect [io] {
let name = read_name();
print("Hello " + name);
}
Calling an effectful function from a pure one is an error:
fn pure() -> Int {
let name = read_name(); // error: effect [io] not allowed here
return 0;
}
- Should effects be row-polymorphic (e.g.,
effect [io | e])? - How should effect handlers or capabilities be modeled?
- Are effects attached to expressions, blocks, or only function boundaries?
The included interpreter supports:
- A single module per file (optional
moduleheader). - Enum definitions:
enum Color { Red, Green, Blue }. - Import declarations:
import math_utils;orimport math_utils as math;. - Export declarations:
export { add, Color, Red };. - Function definitions:
fn name(args) { ... } - Function signatures and
letbindings may include optional type annotations. letbindings,setassignments, andreturnstatements inside function bodies.whileloops.forloops over integer ranges (optionalbystep).breakandcontinue.- Expressions:
- Integers, strings, booleans (
true,false) - Record literals:
{x: 1, y: 2} - List literals:
[1, 2, 3] - Unary
-,! - Binary
+,-,*,/,==,!=,<,<=,>,>=,&&,|| - Field access:
expr.field - Indexing:
expr[index] - Parentheses
ifexpressions withelse:if cond { expr; } else { expr; }matchexpressions with literal patterns, bindings, and_wildcard:match expr { 1 => { expr; } x => { x; } _ => { expr; } }
- Integers, strings, booleans (
- Function calls with positional arguments.
- Built-in
printfor console output (0 or more args).
Execution starts at fn main() with no arguments.
program ::= module_decl? (import_decl | export_decl)* (enum_def | fn_def)*
module_decl ::= "module" IDENT
import_decl ::= "import" IDENT ("as" IDENT)? ";"
export_decl ::= "export" "{" export_list "}" ";"
export_list ::= IDENT ("," IDENT)*
enum_def ::= "enum" IDENT "{" enum_variants "}"
enum_variants ::= enum_variant ("," enum_variant)*
enum_variant ::= IDENT ("(" type_ref ")")?
fn_def ::= "fn" IDENT "(" param_list? ")" return_type? block
param_list ::= param ("," param)*
param ::= IDENT (":" type_ref)?
return_type ::= "->" type_ref
block ::= "{" statement* "}"
statement ::= let_stmt
| set_stmt
| for_stmt
| break_stmt
| continue_stmt
| return_stmt
| while_stmt
| expr_stmt
let_stmt ::= "let" IDENT (":" type_ref)? "=" expr ";"
set_stmt ::= "set" IDENT "=" expr ";"
return_stmt ::= "return" expr ";"
while_stmt ::= "while" expr block
for_stmt ::= "for" IDENT "in" expr range_op expr ("by" expr)? block
range_op ::= ".." | "..="
break_stmt ::= "break" ";"
continue_stmt ::= "continue" ";"
expr_stmt ::= expr ";"
expr ::= match_expr
| if_expr
| logical_or
if_expr ::= "if" expr block "else" block
match_expr ::= "match" expr "{" match_arm+ "}"
match_arm ::= pattern "=>" block ";"?
pattern ::= INT | STRING | TRUE | FALSE | "_" | binding_pattern | enum_pattern
binding_pattern ::= IDENT
enum_pattern ::= IDENT ("(" pattern ")")?
| IDENT "." IDENT ("(" pattern ")")?
logical_or ::= logical_and ("||" logical_and)*
logical_and ::= equality_expr ("&&" equality_expr)*
equality_expr ::= relational_expr (("==" | "!=") relational_expr)*
relational_expr ::= additive_expr (("<" | "<=" | ">" | ">=") additive_expr)*
additive_expr ::= multiplicative_expr (("+" | "-") multiplicative_expr)*
multiplicative_expr ::= unary (("*" | "/") unary)*
unary ::= "-" unary
| "!" unary
| postfix
postfix ::= primary (call_suffix | field_suffix | index_suffix)*
call_suffix ::= "(" arg_list? ")"
field_suffix ::= "." IDENT
index_suffix ::= "[" expr "]"
primary ::= INT
| STRING
| TRUE
| FALSE
| record_literal
| list_literal
| IDENT
| "(" expr ")"
record_literal ::= "{" field_list? "}"
field_list ::= field ("," field)*
field ::= IDENT ":" expr
list_literal ::= "[" expr_list? "]"
expr_list ::= expr ("," expr)*
arg_list ::= expr ("," expr)*
type_ref ::= IDENT
| IDENT "." IDENT
else if is accepted as sugar for else { if ... }.
mainis the entry point.- Each function call creates a new scope with its own variables.
- Imports load sibling
.grlfiles by name (e.g.,import math_utils;loadsmath_utils.grl). import name as alias;binds the module namespace toalias;import name;binds it toname.export { ... };lists the functions, enums, and enum variants that are visible to other modules.- Exporting an enum name does not export its variants; list variants explicitly to allow
module.Variantaccess. - Module namespaces support qualified access to exported functions and enum variants (for example,
math.add(1, 2)ormath.Some(1)). - Unexported definitions are private to their module.
- If no
exportdeclarations are present, the module exports nothing. - Unqualified names resolve only within the current module.
letdefines a binding in the current scope (redefining in the same scope is an error).setupdates an existing binding in the nearest enclosing scope.returnexits the function with a value.ifexpressions evaluate to the value of either branch.ifblocks evaluate in a child scope, soletbindings inside do not leak.matchexpressions evaluate the first arm whose pattern matches the subject.- Match arms evaluate in child scopes like
ifblocks. - Identifier patterns bind a new variable unless they match a known enum variant.
- Enum definitions introduce new types and variants.
- Enum variants are values available in expressions and
matchpatterns. - Enum variants may carry a single payload value.
- Payload constructors use call syntax (e.g.,
Some(1)). - Payload patterns use
Variant(pattern)(e.g.,Some(1)orSome(_)). - Payload bindings use
Variant(name)to bind (e.g.,Some(x)). - Enum patterns may be qualified with a module alias (e.g.,
math.Some(x)). - Type annotations may refer to exported enums via module qualification (e.g.,
colors.Color). - Record literals evaluate to records with named fields.
- Field access reads a record field; missing fields are a runtime error.
- List literals evaluate to lists with ordered elements.
- Indexing reads a list element by integer index.
whileloops evaluate the condition before each iteration and run in the current scope.forloops iterate over integer ranges:start .. endexcludesend.start ..= endincludesend.- If
startis greater thanend, the loop counts down by 1. by stepoverrides the step size (must be a non-zero integer).
breakexits the nearest loop;continueskips to the next iteration.
Type annotations:
- Optional annotations are supported on parameters, returns, and
letbindings. - Supported type names:
Int,Bool,String,Unit, plus enum names.
Truthiness for if:
- Booleans:
falseis false,trueis true. - Integers:
0is false, non-zero is true. - Strings: empty is false, non-empty is true.
- Other values: use the host language truthiness (Python in this prototype).
Operators:
+concatenates if either operand is a string, otherwise it adds integers.-,*,/,<,<=,>,>=require integers.!,&&,||require booleans;&&/||short-circuit./performs integer division with truncation toward zero.==and!=require matching operand types.- All comparison operators yield booleans.
Errors:
- A minimal static type checker runs before execution and reports type errors.
- Type errors include line/column locations where available.
- Type mismatches are reported by the type checker (with runtime checks as a backstop).
- Division by zero raises a runtime error.
forranges require integers;break/continueoutside loops are runtime errors.- Record literals require unique field names.
- Field access on non-record values is a runtime error.
- Indexing requires integer indices and list operands.
- Indexing out of bounds is a runtime error.
- Non-exhaustive
matchexpressions are runtime errors.
This spec is intentionally lightweight to match the interpreter. A full compiler would extend this with types, ownership, effects, and a standard library.