Reference
Qoqi Language – Core Spec (v0.1 draft)
Focus: a small, statically checked core that compiles cleanly to the luvm register VM. HTML/routing/macros live above this layer.
0. Goals
- Static checks before execution
- Familiar syntax (TS/Rust/Go vibes)
- Straightforward lowering to luvm
1. Lexical Structure
- Source files: UTF-8
.qi, one file = one module (see Modules & Imports). - Identifiers:
Letter {Letter | Digit | _}, case-sensitive. - Soft keywords:
case,match,in,as,defer,import,pub,extern,struct,enum,trait,implare identifiers where the tokens around them say so, and keywords everywhere else. One is an identifier afterlet,const,fn,for,.or?., and before a token that only follows a value (.,?., an assignment,++,--,;,,,:,),],},->,in, a binary operator other than-,<and>). It is one before(or[too, exceptmatch,case,asandimpl, and before{afterif,while,matchorin. A statement that is the word alone (import;) keeps the keyword.inis the keyword only right after the variable of a loop (a name or)).waitis a word only where a name could not be: before a name, a number, a text ormatch; before(or[, and anywhere else, it is a name. The other keywords are reserved. - Keywords:
fn let const struct enum trait impl if else match case while for in break continue defer return true false null import as pub extern, plus the type namesInt Float Bool String Any Void Self. - Literals: ints (
123), floats (1.0,10e3), strings ("hi"), bools,null. No char literal in v0. A leading-is the unary operator, so-42is the negation of42. - String literals stay on one line and support the escapes
\n \t \r \0 \\ \" \$, and\u{...}: a character by its number, one to six hexadecimal digits (\u{e9},\u{1F600}). - Interpolation.
${expr}inside a string literal is replaced by the text of the expression's value, astoStringgives it:"total: ${n} items". The expression is any expression, including strings with interpolation of their own, and must close on the same line. The literal is then an expression of typeString, equal to the concatenation of its parts; the expressions are evaluated left to right, each time the string is.$not followed by{and{not preceded by$are ordinary characters;\$writes a dollar sign that does not start an interpolation. - Comments:
// line,/* block */(non-nested). A first line that starts with#!is a comment too, for scripts:#!/usr/bin/env qoqi.
2. Types
Union Types
- Syntax:
T1 | T2 | ... | Tn(e.g.Int | String) - A union type accepts a value of any listed type.
- Usage:
- Variable declaration:
let x: Int | String = ... - Function parameter:
fn foo(val: Int | Float) -> String { ... } - Function return:
fn parse(val: String) -> Int | Error { ... }
- Variable declaration:
- Compatibility:
- A value of type
Tcan be assigned to a union type variable ifTis included in the union. - Operations on a union type variable require type checking (e.g. via
matchor future refinement syntax). - Unions can be combined with optional types:
Int | String?. - Unions are not implicitly converted: a value must explicitly correspond to one of the types.
- Unions are statically checked.
- A value of type
letmutable,constimmutable; must initialize at declaration. Reassigning aconstis a static error (E2022).- Assignment:
x = x + 1;. Compound forms+=,-=,*=,/=,%=read the target, combine and store back; the target is evaluated once (xs[next()] += 1callsnextonce).x++;andx--;arex += 1;andx -= 1;. All of these are statements: they have no value. - Blocks create scope:
{ let x = 1; }. - Control flow:
if/elsewithBoolcondition.whilewithBoolcondition.for item in items { ... }over arrays or strings; a string yields its characters.for i, item in items { ... }also gives the position, from 0.for i in low..high { ... }counts fromlowup to, not including,high;low..=highincludes it. Both areInt, worked out once before the first iteration; nothing runs whenlowis not belowhigh. There and as amatchpattern a range is counted or compared with, and nothing is made for it. Anywhere else it is a value: aRangeofstd.ops(a struct:from,to,inclusive,by), which the program need not import to make.(0..10).step(2),.reversed(),.contains(n),.len(),.toArray(),.map(f),.filter(f); a loop over one counts without making an array. Its ends areInt(E2038).- The loop's variables cannot be assigned (
E2026). Assigning to the index would not move the loop, and assigning to the element would not change the collection; both look like they would. To change the collection writeitems[i] = ...; to control the position usewhile. Changing a value through the element is fine:for p in points { p.x = 0; }. break/continueonly in loops.
Enums
Full description: enums.md.
enum Shape { Circle(Float), Rect(Float, Float), Empty }: a type whose values are one of the variants, each with the values it holds. Variants may hold any types, the enum itself included.enum Option<T> { Some(T), None }is generic.- A variant may name its values:
Rect { width: Float, height: Float }. It is built with every value by name in any order,Rect { height: 1.0, width: 2.0 }, and matched by name,case Rect { width, height: 0.0 }:, where a value not named is_. Both are the forms by position with the values in the order declared, which remain valid. A missing, unknown or doubled name, or names for a variant declared by position, areE8008. - A value is written
Shape.Circle(2.0),Shape.Empty, or the variant alone,Circle(2.0),Empty, in expressions, patterns (case Circle(r):,case Node(Leaf(a), _):) and tests (value is Empty). A variant alone is of the enum among those the file declares or imports by name that has it, unless a variable, a parameter, a function, a constant or a type of that name is in scope. When several such enums have it, it is of the one expected: the declared type of a variable, a parameter or a return, the type of the value amatchor anistests, the type of the other items of an array literal, the other side of==. Otherwise it isE2041. A variant with values, not called, is the function that makes one. - Type arguments of a generic enum come from what is written (
Option<Int>.None), from the values (Option.Some(1)), then from the type expected where the value goes. If none determines one, it is an error (E2030). - A value has no fields; it is taken apart by
match, or byvalue is Patternwhen one variant is asked for. - Two values are equal (
==) when they are the same variant holding equal values. - Enums have methods and implement traits like structs:
impl Shape { ... },impl Named for Shape { ... }.
Constants
const LIMIT = 100;at the top level of a file, optionally with a type (const RATIO: Float = 1.5;) andpub.- The value is made of literals, operators and other constants; nothing else (
E2032). It is worked out when the program is compiled. - Constants may be declared in any order and refer to each other, but not, through each other, to themselves (
E2031). - A constant cannot be assigned (
E2022). A local variable or a parameter of the same name hides it. - A constant can also be a struct, an array or a value of an enum made of constants: it is made once, the first time it is used, and is read-only with all it holds. A variable outside functions is a
let(see Variables outside functions).
Behaviors (Traits)
- Definition:
trait ErrorLike { fn Error() -> String; } - Implementation:
struct MyStruct {} impl ErrorLike for MyStruct { fn Error() -> String { return "error"; } } - Usage: Behaviors define a set of methods that a type must implement. They are similar to interfaces in other languages.
- A trait may be implemented for a struct, an enum, or one of
Int,Float,String,Bool; once per type. - Default methods: a trait method with a body is copied into every impl that does not write it.
- Type parameters:
trait Source<T> { fn next() -> T?; }, implemented asimpl Source<Int> for Counterorimpl<T> Source<T> for List<T>.Source<Int>andSource<String>are different types. - Requirements:
<T: Ordered + Show>afterfn name,struct Name,enum Nameorimpl. Inside, the methods of those traits can be called on aT; where type arguments are given or inferred, each has to implement them (E2036). - The traits of
std.opsgive meaning to syntax on types that implement them:Equal(==,!=),Ordered(<,<=,>,>=),Add/Subtract/Multiply/Divide(+ - * /and their assignments),Show(println,toString,"${}"),Iterable<T>(for),Indexed<K, V>(a[k]),IndexedSet<K, V>(a[k] = v). See traits.md. - Struct fields may have a default:
port: Int = 8080;. A literal may leave such a field out; the expression is evaluated for each literal. - A method without a call, on a variable or a field path, is a function value bound to it:
ages.get. - Self Type:
Selfcan be used in trait definitions and implementations to refer to the implementing type.trait Cloneable { fn clone() -> Self; }
4. Expressions
- Primary: literals, identifiers, grouping
(expr), tuples(a, b). - Text over several lines:
"""...""". The line end after the opening quotes and the one before the closing quotes are dropped, and the indentation of the closing quotes is removed from every line."needs no escape inside; escapes and${...}work. - Unary:
-onInt/Float,!onBool. - Binary precedence (high → low):
* / %,+ -,<< >>,&,^,|,??,|>,< <= > >=,== !=,&&,||.??binds tighter than a comparison and than|>:count ?? 0 > 3compares the count, andname ?? "".trim()trims either.- On bits:
& | ^ << >>and unary~, onIntonly, with&= |= ^= <<= >>=. They bind tighter than comparisons:flags & MASK == 0tests the mask.>>keeps the sign. A shift by a negative count, or one that pushes bits past the 48 anInthas, is a runtime error. In the pattern of acase,|separates patterns (case 1 | 2:); in brackets it is the operator. - Whole numbers are also written by their bits:
0xFF,0b1010. - Arithmetic:
Int/Float. TwoIntgive anInt(7 / 2is3), twoFloataFloat. AnIntwith aFloatis worked out as twoFloat:7 / 2.0is3.5. - Comparison: numbers or strings →
Bool. - Equality: any type →
Bool. - Null-coalescing
a ?? b:a: T?,b: T|T?, resultT.
- On bits:
- Member/index:
user.name,items[0](Intindex; OOB is runtime error). Indexing a string yields a one-character string. - Unwrap:
value!after an expression of typeT?is aT; on aResult<T, E>it is aT. When there is no value (null, orErr) the program stops at that point aspanicstops it, with "'!' found null where a value was expected" or "'!' found a failure:" and the failure (described when it hasdescribe()).!on a type that cannot be null isE2034. - Methods of optionals:
impl<T> T? { ... }gives everyT?its methods,selfbeing theT?(it may be null).std.optiongives themmap,then,filter,mapOr,or,orElse,each,expect,okOr,isSome,isNone, without an import. A call on an optional finds these first; any other method ofTstill needs a null test,?.or!(E2017). - Dictionary literal:
{key: value, ...}. With texts for keys it is aDict<V>(std.dict), with keys of any other type aMap<K, V>(std.map); where aDictor aMapis expected, that one. Keys are of one type (E2001).{}needs the type it is to be (E2020). In the header ofif,while,forandmatcha{is the block, as for struct literals. - Slice: a range as the index of an array or a string gives a new array or string of the items or characters at the positions the range goes through:
items[1..3],items[1..=3],items[(0..9).step(3)], or any value of typeRange. Between the brackets a range may leave out its start (items[..2], from 0) or its end (items[1..], to the end). Positions outside the collection are left out rather than failing. A slice is not assignable (E2006). A range without its end anywhere else is a syntax error. - Length:
items.len()andtext.len()(oritems.len(),text.len()) give the number of elements of an array and of characters of a string.items.lengthis an error (E2033) that says what to write. - Methods on builtin types:
impl String { ... },impl Int,impl Float,impl Boolgive the type methods;impl<T> [T] { ... }gives them to every array,Tbeing the type of its items, andimpl [Name] { ... }to the arrays of one named type, which are looked at first.selfis the value. The call is decided when the program is compiled; through a trait (impl Trait for String,impl<T> Trait for [T]) it is found when the program runs.std.arrays,std.stringsandstd.mathdeclare such methods for everything they do to an array, a text or a number. These three modules are part of every program that calls one of their methods, without an import; the functions the methods are made of are private to them. A call of such a method is compiled as a call of that function, for the types it is used with. - Safe access
a?.b,a?.m(args):a: T?; null whenais null, otherwise the rest of the postfix chain applied toaas aT(a?.b.creadsconly whenais not null). The result is optional. An error (E2035) whenacannot be null. - Try
a?(postfix): fora: T?, the value as aT, orreturn null;from the enclosing function, which must return an optional or nothing. Fora: Result<T, E>, the value as aT, or a return of the failure, from a function that returnsResult<U, E>. Anything else isE2034. In a function expression the enclosing function is that expression. - If as a value:
if cond { a } else { b }where an expression is expected, withelse ifchained. Anelseis required. A branch is braces around an expression, after statements if any; one that always leaves (return,break,continue,panic) gives nothing. The branches are of one type, ornull(the type is then optional), or two types the place expects both of. Narrowing and the names ofvalue is Patternin the condition hold in the first branch. Only the branch taken is evaluated. At the start of a statementifis the statement. - Choice:
cond ? a : bisif cond { a } else { b }written short, with the same types, narrowing and evaluation:owed == null ? "in credit" : "owes ${owed}". It is looser than every operator and than a range, and read from the right:s >= 90 ? "A" : s >= 75 ? "B" : "C". A?right after a value with no:to match is thevalue?of a failure:r? - 1subtracts,r? > 0 ? a : btests whatr?gives. - Waiting:
wait wwaits untilwhas come and gives what it gives;wis a value of a type that implementsWaitable<T>ofstd.task(a task,channel.receive(),channel.send(v),time.after(2.seconds()),task.all(tasks)), anything else isE2044.waittakes the postfix expression after it, and binds tighter thanis,asand every binary operator:wait ch.receive() ?? 0,(wait job).expect("x").wait match { case ...: ... }waits for the first of its cases, eachcase let name = w:,case w is Pattern:orcase w:, and runs that one; as a statement or as a value, likematch. The cases on one channel take every value it can give,nullincluded (E2028). A waitable made and thrown away by a statement isE2043, a task excepted. The parser writeswait was the callstd.task.waitFor(w), importingstd.taskfor it. See tasks.md. - Match as a value:
match x { case p: expr; ... }where an expression is expected. The cases are expressions of one type (nullamong them makes it optional; the type expected at that place may take them all), or blocks that always leave, or areturn,breakorcontinuealone:default: return Err(e);. Without adefaultit must be complete over an enum (E2028); a case that gives nothing isE2040. - Tuples:
(a, b)of type(A, B), two to six values.pair.0,pair.1read by position;let (a, b) = pair;andfor (a, b) in pairsname the values,_skipping one. Compared by content, immutable, matched withcase (a, b):and checked for completeness like an enum (which is what a tuple is: see enums.md). - Calls:
callee(args...)with arity/type check. Method calls:value.method(args...). - Pipe:
value |> f(args...)isf(value, args...), andvalue |> fisf(value). What follows|>is one call, or a function (a name, a member, a function expression); a?after the call applies to its result. Left to right:a |> f |> g(x)isg(f(a), x). The parser writes the call, so every rule that applies to a call applies to it. - Array literals:
[1, 2, 3],[]. All elements have the same type. - Struct literals:
Point { x: 1, y: 2 }. In the header ofif,while,forandmatch, a{after a name starts the body, so a struct literal there needs parentheses:if (Point { x: 1 }).x == 1 { ... }. - Short functions:
x -> x + 1,(a, b) -> a + b,() -> now(),x -> { ...; return y; }. No types are written: the parameters take theirs from the function type expected where the expression stands (a parameter, a declared variable or field, a return type), and the result is that type's, or the type of the body when it is a type parameter still open. The body is one expression, one assignment, or a block. Without an expected function type it isE2037, except for one without parameters. - Anonymous functions:
fn(x: Int) -> Int { return x + 1; }. They can use the variables of the enclosing function; see Closures below. &&and||evaluate their right operand only if the left one does not decide the result.a ?? bevaluatesbonly ifaisnull.
5. Functions
- Form:
fn add(a: Int, b: Int) -> Int { return a + b; }. - Return type required (
Voidallowed). In-> T, all paths returnT; in-> Void,return;only. - Entry point convention:
fn main() -> Int { ... }, or statements outside any function (a script, see §11). - Generics:
fn map<T, U>(xs: [T], f: fn(T) -> U) -> [U] { ... }; instantiate withmap<Int, String>(nums, toStr).name<...>is read as type arguments only when the closing>is followed by(or{; otherwise<is the comparison operator. - Variadic arguments:
fn printf(fmt: String, args: ...Any) -> Void { ... }. The last parameter can be prefixed with...to accept zero or more arguments of that type. Inside the function the parameter is an array (args.len(),for a in args). Arguments are packed when the function is called by its name; a variadic function called through a variable receives its arguments as written. - Generic Variadic:
fn collect<T>(items: ...T) -> [T] { ... }. - Values of their own:
fn page(items: [String], number: Int = 1, size: Int = PAGE). Parameters with a value come after those without (E0001). A call to the function or method by its name may leave out any number of the last ones, which then take their values (E2009gives the range of counts accepted); the value is checked against the parameter's type, and is evaluated at the call, each time, in the scope of the function's module: it may name constants and functions, not parameters.
Methods
- Fields are as visible as their struct.
priv name: Type;makes a field its module's: another module reading, assigning, giving in a literal or matching it isE2042, and a literal there that leaves out aprivfield without a value of its own isE2042too. The fields of anextern structare never private.privis a word only before a field's name.std.reflectneither shows nor changes aprivfield. static fnin a plainimpl(not of a trait) declares a function of the type:Point.origin(),geo.Point.origin(),Box.of(1)for animpl<T> Box<T>, whose type parameters it takes. It has noself(E8012) and is not called on a value (E2015says how it is). It is as visible aspubmakes it;Point.nothing()isE2015.staticis a word only beforefnin animpl. A method that never namesselfis valid, with a warning (E8011).impl Trait for Type { fn ... }implements a trait;impl Type { fn ... }adds methods that belong to no trait.- For a generic struct the type parameters are introduced after
impland repeated on the struct:impl<T> Stack<T> { fn peek() -> T? { ... } },impl<T> Sized for Stack<T> { ... }. On aStack<Int>,peek()then returnsInt?. - A method can have type parameters of its own, given or inferred at the call like those of a generic function:
fn map<U>(f: fn(T) -> U) -> Stack<U>is called asstack.map<String>(toString)or juststack.map(toString). - Inside a method,
selfis the value the method was called on. It is implicit: methods do not list it among their parameters. - Method calls are fully checked: the method must exist on the receiver's struct or trait, the arguments must match its signature, and the call has the method's return type.
Selfin a signature is the struct the method belongs to. - A field and a method may share a name;
value.namereads the field andvalue.name(...)calls the method. A method cannot be used as a value without calling it. - A parameter typed with a trait accepts exactly the structs that implement it (checked at the call), and only the trait's methods can be called on it. Which struct's method runs is decided at runtime.
Methods of a type as values
Type.method, whereTypeis a struct, an enum, a trait orInt,Float,String,Bool, is a function value whose first parameter is the type and the rest are the method's:Point.plus: fn(Point, Point) -> Point. Calling it,Point.plus(a, b), isa.plus(b). On an enum a variant of that name comes first. A method with type parameters of its own, or one of a generic type, cannot be named so.
Variables outside functions
let name = value;(orlet name: Type = value;) at the top level of a module, or of an entry file that has amain, declares a variable of the file. Its functions read and assign it; it is private to its module. Every such variable is made beforemainruns, in the order written, a variable whose value uses another making that one first. Any other statement outside a function there isE8007. A null test does not narrow one. In a script (an entry file withoutmain), the top-level statements are its body and their variables are not seen by its functions.
Closures
- In
let name = fn(params) -> Type { body };, the long form with its types,nameis in scope inbody: the function can call itself. A short function (x -> ...) does not see the name it is given. - A function expression captures the variables it uses by reference: it sees later assignments made by the enclosing function, and its own assignments are visible outside. A captured variable lives on after the enclosing function returns.
- Each iteration of a loop has its own variables: closures created in different iterations keep the values of their own iteration, including the loop's variables (
for i, item in items). Within one iteration a closure and the loop body still share them. A variable declared outside the loop is shared by all of them.
6. Modules & Imports
Full description: modules.md.
- One module per
.qifile. Its name is its path from the folder of the program's entry file, with dots:physics/body.qiisphysics.body. A folder is not a module. - Paths always start at that folder; there are no relative imports.
import physics.body;thenbody.new(1),body.Body. The module is used through the last part of its path, or through an alias:import physics.body as pb;.import physics.body.{Body, new};brings names in directly.- A declaration is private to its module unless marked
pub(pub fn,pub struct,pub trait). Fields are as visible as their struct, unless writtenpriv. A method of a plainimplneedspub fnto be called from another module; methods that implement a trait are always visible. pub import physics.body.{Body};exports an imported name again.- A
pubfunction, or apubmethod of a type other modules can name, may only use in its parameters and its result the types of its module that arepub(E8009): a caller would otherwise hold a value whose type it cannot write. - Modules may import each other. Nothing runs when a module is loaded.
- A local variable hides a module of the same name.
extern fn name(params) -> Type;declares a function without a body, provided by the runtime under its full name (module path and name). Only the functions of the standard library exist there today.- The package
stdis the standard library, built into the compiler:import std.strings;. - Implicit imports: a file of the program that writes the name of a module of
stdbefore a dot (dict.empty<Int>()) is read as if it imported that module, unless it imports or declares something of that name at its top level. It is read as importingResult,Template,Dict,List,Setand the traits ofstd.ops(Equal,Ordered,Show,Add,Subtract,Multiply,Divide,Iterable,Indexed,IndexedSet) when it names them and declares and imports none of its own. The files ofstdwrite their imports. - The program starts in the
mainof the entry file, which cannot itself be imported. Only the entry file may have statements outside a function: in a module they areE8007. - A project file,
qoqi.json, can name the entry file, move the folder paths start at (root), and name folders of modules outside it (packages). It is not part of the language: a program means the same with or without it, given the same files at the same module paths.
7. Type Rules (grab bag)
if/whileconditions must beBool.for inrequires array or string.- Names every file has without an import: the builtins (
print,println,toString,panic), the methods of arrays, texts, numbers and optionals, and the modules and types of the library (see Implicit imports in §6), unless the file declares something of that name itself. - Equality.
==compares numbers as numbers, and texts, arrays, structs and values of enums by content: item by item, field by field, variant and values, recursively. A type that implementsEqualis compared by itsequals, also where it is held by an array, a struct or an enum. Functions are equal only to themselves.reflect.identical(a, b)asks whether two names are of the one value. - Constants:
const NAME = value;at the top level. The value is made of literals, operators, other constants, and structs, arrays and enum values made of those. A constant that is a struct, an array or a value of an enum is made the first time it is used and is that one value from then on, read-only with all it holds: assigning to one of its fields or items isE2022where the compiler can see it, and a runtime error through any other name. - An
Intis taken for aFloatwhere one is expected: an argument, a variable or field declaredFloat, a value returned as one, an item amongFloats ([1, 2.5]), one branch of anifwhose other is aFloat. It is converted there, so what is kept is aFloat. With an operator, anIntand aFloatgive aFloat, and compare as numbers (2 == 2.0). Never the other way: aFloatbecomes anIntonly throughfloor,ceil,roundortrunc. A type parameter is not guessed:largest([1, 2.5])works because the array is one ofFloat,max(1, 2.5)does not. - Optionals.
T?is "aTornull".nullonly fitsT?orAny. ATcan be used where aT?is expected, not the other way round. An optional cannot be operated on, indexed, or have fields read or methods called directly (E2017);value ?? fallbackyields aT. A test againstnullnarrows a local variable, a parameter or a field path (node.next) toTwhere the test guarantees it: in the body ofif x != null, afterif x == null { return; }(orbreak/continue), in awhile x != nullbody, and on the right ofx != null && .../x == null || .... Narrowing ends at an assignment to the variable, and does not apply inside lambdas or to a variable that a lambda assigns (it could be set tonullwhenever the lambda runs). What is known about a field also ends at the next assignment to any field and at the next call of something that may assign one, since those could change it through another reference. Calls that cannot assign a field (builtins, functions that only read) do not affect it; a call of a function value or of a function stored in a field always does. - Type tests.
value is Typeis aBoolcomputed when the program runs; where it is true, the tested variable or field has that type:if shape is Square { shape.side }. A test that can never be true (1 is String) is a compile error (E2021).matchaccepts types as patterns (case Int:,case [String]:,case Point:,case fn(Int) -> Int:), with the same narrowing inside each case. - Pattern tests.
value is P, wherePis the pattern of a variant or of a tuple as acasetakes it, is aBool: whether the value fits. The value is evaluated once. The names ofPare variables of what runs only when the test held: the operands to its right in a chain of&&, and, when the test is the condition of anifor awhileor an operand of the&&chain that is, the block that statement guards. They are not in scope in anelse. A test in any other place (under!, an operand of||, a value by itself or the last operand of a chain that is a value) may not name anything (E1003).Pis checked against the type of the value as a case is (E2029); a test that always holds is a warning (E2023). Where the test held, a tested variable or field is not null, and is of the enum if it was anAny, a trait or a union. Afteris,Name.Namenot followed by parentheses is a type whenNameis a module, and a tuple of names that are all types is a tuple type. - Patterns on enums.
case Shape.Rect(w, h):matches a variant and names the values it holds; the names are variables of that case. A position may also hold_(any value, unnamed), a literal (only that value) or the pattern of another variant, to any depth. Without parentheses (case Shape.Rect:) the variant is matched whatever it holds. Amatchon an enum value without adefaultmust handle every value the enum can have, looking inside nested patterns (E2028); a case that can never run is a warning (E2023). Assigning a non-null value to an optional variable narrows it too. - Guards.
if !(T) { ... }, whereTis a pattern test or an&&chain of them and other conditions, with noelseand a block that ends withreturn,break,continueor a call ofpanic: the names ofTare variables of the statements that follow theifin its block, which only run whenTheld. Inside the block they do not exist. - Patterns on structs.
case Point { x: 0, y }:matches a struct by its fields: a field with a pattern (a literal,_, a variant, another struct) is matched by it, a field written alone is named, the fields not written are not looked at. A value of a trait, a union orAnyis tested for the struct first, and is that struct in the case. Struct patterns nest in variant patterns and the other way. One whose every field is a name or_, recursively, takes every value of the struct for completeness. They are written aftercase, not afteris. The same shorthand builds a struct:Point { x, y }isPoint { x: x, y: y }. - Several patterns.
case A | B:runs for any of them. They may name values when each of them names the same names with the same types (E2029otherwise); each name is the value of the first pattern that matched, and a condition is asked of that pattern's values. - Null.
case null:matches null; the cases below it see the value as not null. A match that gives a value, on an optional, handles null withcase null:ordefault(E2028); a statement runs no case for null. Anyaccepts every value, and that is all it does: a value of typeAnycan be passed along, printed, converted withtoStringand compared with==. It cannot be used as a number, indexed, called, iterated or assigned to a more specific type without a cast.- Casts.
value as Typetreats a value as a more specific type: out ofAny, out of an optional or union, or from a trait to a struct implementing it. The cast is verified when it runs and stops the program if the value is something else. A cast that can never succeed (1 as String) is a compile error (E2021); one to a wider type is free.asbinds tighter than the binary operators:v as Int + 1is(v as Int) + 1. The check goes into the value: every element of an array, and the fields of a generic struct that depend on its type arguments. A function type is compared with the signature of the function: it must accept at least what the type passes and return what it promises. - Constants. A name declared with
constcannot be assigned (E2022). The value it refers to can still be changed:const xs = [1]; xs[0] = 2;is fine. - Cases. What a case of a match statement does is a block, or one statement without braces, which is the block of it:
case Err(e): return Err(e);,case Light.Red: stops += 1;,case 0: continue;.case a | b:runs for either pattern and names nothing (E2029).case p if cond:runs whenpmatches andcond, which sees the names ofp, holds; it is not counted when a match is checked for completeness.case low..high:andcase low..=high:match numbers (IntorFloat). - Defer.
defer stmt;anddefer { ... }run when the enclosing block is left, by any way out, a failure passing through included; the defers of a block run last first, after the value of areturnhas been worked out. The body cannot leave the defer:return,?, andbreak/continueof an outer loop areE2039. - Returns. A function with a return type other than
Voidmust return a value on every path (E2004).while truewithout abreak, and amatchwhose cases all return and which has adefaultor handles every variant of an enum, count as returning. Statements that can never run, after areturn,breakorcontinue, produce a warning (E2023). - Arrays. All elements share one type. A literal takes its element type from its first element, or from the place it is used when that place has a type:
let shapes: [Shape] = [rect, square];. An empty literal needs such a place:let xs: [Int] = [];(a barelet xs = [];isE2020).xs.push(v)requiresvto fit the element type. - Structs. A literal gives every field a value (
E2018), and only declared fields can be read or written (E2014). - Functions as values. A function fits a function type if it accepts at least what that type passes and returns what it promises, so
toString(fn(Any) -> String) can be used as afn(Int) -> String. - Type parameters are opaque inside a generic function: a
Tonly fitsT, and has no operators or members. At a call,f<Int>has its signature withIntsubstituted, so arguments and result are checked. - Type argument inference. The type arguments of a call may be left out when the arguments determine them:
first([1, 2])isfirst<Int>([1, 2]),map(names, toString)infers both parameters,stack.map(toString)infers the method's. A parameter the arguments leave open is taken from the type expected of the result:let xs: [Int] = none();,return none();,size(none()). If neither determines it, it has to be written (E3001, orE2024for methods). - Traits. An impl method matches the trait's by position of its type parameters, not by their names:
fn convert<U>(v: U) -> [U]implementsfn convert<T>(v: T) -> [T]. - Generic type args must be concrete types (no higher-kinded). Each instantiation gets its own monomorphized copy.
8. luvm Mapping (sketch)
- Values:
Int→TAG_INT,Float→double,Bool→TAG_BOOL,String/Struct/Array→heap ptr,Null→TAG_NULL,Void→none or TAG_UNDEF. - Locals/temps become registers per function chunk; params occupy first registers.
- Example lowering:
fn add(a: Int, b: Int) -> Int { let c = a + b; return c; } // a->R0, b->R1, c->R2 0: ADD R2, R0, R1 1: RET R2
9. Errors
- Static: unknown identifier, type mismatch, bad arity, assign to
const. - Warnings, with
qoqi run,check,testand in the editor: a variable, a parameter or a name of a pattern nothing uses (E1004), an import nothing uses (E8010). Names starting with_, the parameters of a trait's method and of a short function, and the library are not counted. - Runtime: array index out of bounds (read or write), integer overflow (an
Intis 48 bits and never wraps), division by zero, a failed cast. The program stops with a message naming the source line.
10. Minimal Stdlib (host-provided)
fn print(args: ...Any) -> Void;writes its arguments separated by spaces.fn println(args: ...Any) -> Void;same, then a newline.fn toString(v: Any) -> String;the textprintwould write.fn panic(values: ...Any) -> Void;stops the program with the values as its message (errors.md). It does not return: a function may end with it in place of areturn, and what follows it is unreachable.items.push(value)appends to an array. Assigning toitems[i]replaces an existing element;pushis how an array grows. It is a method of arrays like the others, and one instruction.
Values print as 42, 2.5, true, null, the text of a string, [1, 2] for arrays, {x: 1, y: 2} for structs and Rect(2, 3) or Empty for values of an enum.
11. Program Entry and Exit
- Execution starts at
fn main(), which takes no parameters (E6006otherwise). - A script has no
main: the statements written outside any function in the entry file are its main, and run in the order they are written. Declarations may come before, between or after them, and are known everywhere. A file has amainor statements outside a function, not both (E8007). - The variables of a script are those of its main: a closure made in the script sees them, a function or a method does not (
E1001, which says so). What a function needs is passed to it, or declared withconst. - A script stops at a
return;. When one of its returns gives a value, it is anInt, the exit code; reaching the end of the script exits with 0. - When
mainreturns anInt,qoqi runuses it as the process exit code (low 8 bits). Any other result exits with 0. - A runtime error prints the message with the source location and exits with 1.
- Two limits end a runaway program with an error rather than letting it take the machine down: calls may nest 10000 deep (
--max-depth=N), and the program may hold 256 MB at once (--heap-mb=N).
12. Grammar (EBNF sketch)
Program = { Import | [ "pub" ] Decl } EOF ;
Import = [ "pub" ] "import" ModulePath [ "." "{" Ident { "," Ident } "}" | "as" Ident ] ";" ;
ModulePath = Ident { "." Ident } ;
Decl = FnDecl | ExternDecl | ConstDecl | StructDecl | EnumDecl | TraitDecl | ImplDecl ; (* "pub" not allowed before ImplDecl *)
ConstDecl = "const" Ident [ ":" Type ] "=" Expr ";" ; (* Expr of literals, operators and constants *)
EnumDecl = "enum" Ident [BoundedParams] "{" [ Variant { "," Variant } [ "," ] ] "}" ;
StructDecl = "struct" Ident [BoundedParams] "{" { Ident ":" Type [ "=" Expr ] ( ";" | "," ) } "}" ;
TraitDecl = "trait" Ident [TypeParams] "{" { FnSignature ( ";" | Block ) } "}" ;
ImplDecl = "impl" [BoundedParams] [ TypeName [TypeArgs] "for" ] TypeName [TypeParams] "{" { [ "pub" ] FnDecl } "}" ;
BoundedParams = "<" BoundedParam { "," BoundedParam } ">" ; (* also after the name of a function *)
BoundedParam = Ident [ ":" TraitRef { "+" TraitRef } ] ;
TraitRef = TypeName [TypeArgs] ;
Variant = Ident [ "(" Type { "," Type } ")" ] ;
ExternDecl = "extern" "fn" Ident "(" [ParamList] ")" [ "->" Type ] ";" ;
TypeName = Ident [ "." Ident ] ; (* a type of an imported module *)
TraitDecl = "trait" Ident "{" { TraitMethod } "}" ;
TraitMethod = "fn" Ident "(" [ParamList] ")" "->" Type ";" ;
ImplDecl = "impl" [TypeParams] [ Ident "for" ] Ident [TypeParams] "{" { FnDecl } "}" ;
FnDecl = "fn" Ident [TypeParams] "(" [ParamList] ")" "->" Type Block ;
TypeParams = "<" TypeParam { "," TypeParam } ">" ;
TypeParam = Ident ;
ParamList = Param { "," Param } [ "," VariadicParam ] | VariadicParam ;
Param = Ident ":" Type ;
VariadicParam = Ident ":" "..." Type ;
StructDecl = "struct" Ident [TypeParams] "{" { Field [ ";" | "," ] } "}" ;
Field = Ident ":" Type ;
Block = "{" { Stmt } "}" ;
Stmt = LetDecl | IfStmt | WhileStmt | ForStmt | ReturnStmt | MatchStmt | WaitMatch
| BreakStmt | ContinueStmt | DeferStmt | Block | AssignStmt | ExprStmt ;
DeferStmt = "defer" ( Block | AssignStmt | ExprStmt ) ;
BreakStmt = "break" ";" ;
ContinueStmt = "continue" ";" ;
AssignStmt = AssignTarget AssignOp Expr ";" | AssignTarget ( "++" | "--" ) ";" ;
AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=" ;
AssignTarget = Ident | PostfixExpr Index | PostfixExpr Member ;
LetDecl = ("let" | "const") Ident [ ":" Type ] "=" Expr ";" ;
IfStmt = "if" Expr Block [ "else" (Block | IfStmt) ] ;
WhileStmt = "while" Expr Block ;
ForStmt = "for" [ Ident "," ] ( Ident | TupleNames ) "in" ( Expr | Range ) Block ;
Range = OrExpr ( ".." | "..=" ) OrExpr ;
LetTuple = ( "let" | "const" ) TupleNames "=" Expr ";" ;
TupleNames = "(" Ident "," Ident { "," Ident } ")" ; (* "_" skips a value *)
ReturnStmt = "return" [ Expr ] ";" ;
MatchStmt = "match" Expr "{" { MatchCase } "}" ;
MatchCase = ( "case" Pattern { "|" Pattern } [ "if" Expr ] | "default" ) ":" ( Block | Statement ) ;
(* one statement is the block of it: case Err(e): return Err(e); *)
MatchExpr = MatchStmt ; (* as a primary expression, a case is Block, Expr, or return, break, continue;
an Expr may end with "," and the last with nothing *)
WaitMatch = "wait" "match" "{" { WaitCase } "}" ; (* also a value, as MatchExpr *)
WaitCase = "case" ( "let" Ident "=" Expr | PostfixExpr [ "is" Pattern { "|" Pattern } ] ) ":" ( Block | Expr ";" ) ;
Pattern = Literal | [ "-" ] Literal | Range | Type | VariantPat | TuplePat ;
TuplePat = "(" SubPattern "," SubPattern { "," SubPattern } ")" ;
VariantPat = Ident [TypeArgs] { "." Ident } [ "(" [ SubPattern { "," SubPattern } ] ")" ] ;
SubPattern = Ident | "_" | Literal | [ "-" ] Literal | VariantPat | TuplePat ;
ExprStmt = Expr ";" ;
Expr = OrExpr ;
OrExpr = AndExpr { "||" AndExpr } ;
AndExpr = EqExpr { "&&" EqExpr } ;
EqExpr = RelExpr { ("==" | "!=") RelExpr } ;
RelExpr = PipeExpr { ("<" | "<=" | ">" | ">=") PipeExpr } ;
PipeExpr = NullCoalesceExpr { "|>" UnaryExpr } ;
NullCoalesceExpr = BitOrExpr { "??" BitOrExpr } ;
BitOrExpr = BitXorExpr { "|" BitXorExpr } ;
BitXorExpr = BitAndExpr { "^" BitAndExpr } ;
BitAndExpr = ShiftExpr { "&" ShiftExpr } ;
ShiftExpr = AddExpr { ("<<" | ">>") AddExpr } ; (* what a pipe gives can be added to: n |> f + 1 *)
AddExpr = MulExpr { ("+" | "-") MulExpr } ;
MulExpr = UnaryExpr { ("*" | "/" | "%") UnaryExpr } ;
UnaryExpr = ( "-" | "!" | "~" ) UnaryExpr | CastExpr ;
CastExpr = WaitExpr { "as" OptType | "is" ( OptType | VariantPat | TuplePat ) } ;
WaitExpr = "wait" ( PostfixExpr | WaitMatch ) | PostfixExpr ;
IfExpr = "if" Expr ValueBlock "else" ( IfExpr | ValueBlock ) ; (* a Primary *)
ValueBlock = "{" { Stmt } [ Expr ] "}" ;
PostfixExpr = Primary { Call | Index | Member | SafeAccess | "?" } ;
SafeAccess = "?." ( Ident [TypeArgs] | IntLit ) { Call | Index | Member } ;
Call = "(" [ ArgList ] ")" ;
ArgList = Expr { "," Expr } ;
Index = "[" Expr "]" ;
Member = "." ( Ident [TypeArgs] | IntLit ) ; (* a number: a value of a tuple *)
Primary = Literal | Ident [TypeArgs] | "(" Expr ")" | TupleLit
| ArrayLit | StructLit | FnExpr ;
TupleLit = "(" Expr "," Expr { "," Expr } [ "," ] ")" ;
ArrayLit = "[" [ ArgList ] "]" ;
StructLit = Ident [TypeArgs] "{" [ FieldInit { "," FieldInit } [ "," ] ] "}" ;
FieldInit = Ident ":" Expr ;
FnExpr = "fn" "(" [ParamList] ")" [ "->" Type ] Block | ShortFn ;
ShortFn = ( Ident | "(" [ Ident { "," Ident } ] ")" ) "->" ( Block | Expr | Assignment ) ;
TypeArgs = "<" Type { "," Type } ">" ;
Type = OptType { "|" OptType } ;
OptType = SimpleType [ "?" ] ;
SimpleType = BuiltinType | Ident [TypeArgs] | "[" Type "]" | "(" Type ")" | TupleType | FunctionType ;
TupleType = "(" Type "," Type { "," Type } ")" ;
BuiltinType = "Int" | "Float" | "Bool" | "String" | "Any" | "Void" | "Self" ;
FunctionType = "fn" [TypeParams] "(" [TypeList] ")" "->" Type ;
TypeList = Type { "," Type } [ "," "..." Type ] | "..." Type ;
Ident = Letter { Letter | Digit | "_" } ;
Literal = IntLit | FloatLit | StringLit | "true" | "false" | "null" ;