491 lines
12 KiB
Text
491 lines
12 KiB
Text
======== TYPES ========
|
|
|
|
Built-in types:
|
|
unit
|
|
bool
|
|
u8
|
|
u16
|
|
u32
|
|
u64
|
|
i8
|
|
i16
|
|
i32
|
|
i64
|
|
f16
|
|
f32
|
|
f64
|
|
|
|
N.B.: there is no dedicated 'char' type, strings operate on u8 (utf-8) or u32 (utf-32)
|
|
|
|
Pointer types:
|
|
T* (pointer to const T)
|
|
T mut* (pointer to mutable T)
|
|
T** (pointer to const pointer to const T)
|
|
T* mut* (pointer to mutable pointer to const T)
|
|
T mut** (pointer to const pointer to mutable T)
|
|
T mut* mut* (pointer to mutable pointer to mutable T)
|
|
|
|
Array types:
|
|
T[N] array of N elements of type T (N must be a compile-time value)
|
|
|
|
Function types:
|
|
(arg1, arg2, arg3) -> result
|
|
(arg1, arg2) -> unit // no return value
|
|
arg -> result // simplified syntax for single-argument case
|
|
|
|
======== LITERALS ========
|
|
|
|
Literals:
|
|
56b -> i8
|
|
42ub -> u8
|
|
456s -> i16
|
|
456us -> u16
|
|
98765 -> i32
|
|
98765u -> u32
|
|
123l -> i64
|
|
123ul -> u64
|
|
3.14h -> f16
|
|
3.14 -> f32
|
|
3.14d -> f64
|
|
'a' -> u8 (ascii only?)
|
|
'猫'u -> u32
|
|
|
|
// TODO: string literals? fixed-size arrays? built-in spans? Probably built-in spans (defined in prelude.psl)
|
|
"hello, world" -> utf-8 string
|
|
"здарова, братки"u -> utf-32 string
|
|
|
|
======== VARIABLES ========
|
|
|
|
Variable declaration:
|
|
const x = ... compile-time value, type inferred
|
|
const x: T = ... compile-time value of type T
|
|
let x = ... immutable value, type inferred
|
|
let x: T = ... immutable value of type T
|
|
mut x = ... mutable, ...
|
|
mut x: T = ...
|
|
|
|
Array declaration:
|
|
let arr: i32[4] = [12, 15, 65, 42]
|
|
let arr = [2, 5, 6] // size and type inferred as i32[3]
|
|
let arr: i32[0] = [] // need special empty array literal, type cannot be inferred
|
|
|
|
Null pointer literal:
|
|
let p: u32* = null // special like empty array literal, type cannot be inferred
|
|
|
|
Variables must always be initialized. // TODO: really? What about arrays? Maybe need special syntax for zero-initialization or mass-initialization. Alternative: default to zero-initialization
|
|
Const variables must be initialized with a const expression (any expression that doesn't include non-const values & calls only pure functions).
|
|
|
|
======== OPERATORS ========
|
|
|
|
Logical (only bool type):
|
|
!x
|
|
x & y // evaluates both arguments
|
|
x | y // evaluates both arguments
|
|
x && y // short-circuit
|
|
x || y // short-circuit
|
|
x ^ y
|
|
|
|
Equality (all built-in types, all pointer types, all array/struct types, only same type unless integers):
|
|
x == y
|
|
x != y
|
|
|
|
Comparison (all built-in types, all pointer types, all array/struct types, only same type unless integers):
|
|
x < y
|
|
x > y
|
|
x <= y
|
|
x >= y
|
|
|
|
Bitwise (integer types, only same type):
|
|
!x
|
|
x & y // evaluates both arguments
|
|
x | y // evaluates both arguments
|
|
x && y // short-circuit
|
|
x || y // short-circuit
|
|
x ^ y
|
|
|
|
Bitwise shift (any integer + any unsigned integer type):
|
|
x >> y
|
|
x << y
|
|
|
|
Arithmetic (only same integer/floating-point type):
|
|
-x
|
|
x + y
|
|
x * y
|
|
x / y
|
|
x % y
|
|
|
|
Pointer arithmetic (any pointer type + any integer type):
|
|
p + x
|
|
p - x
|
|
p - q // returns i64
|
|
|
|
Pointer arithmetic works element-wise (like C or C++), i.e. p + n advances by n * sizeof(T) when typeof(p) is *T
|
|
Pointer arithmetic is prohibited on empty types
|
|
|
|
Casting:
|
|
x as u32 // always explicit, no implicit casts allowed
|
|
|
|
TODO: The only implicit casting allowed is T mut* -> T* (maybe?)
|
|
Any integer/floating-point types can be cast to each other.
|
|
Any pointer types can be cast to each other // Misalignment => UB
|
|
|
|
Ternary if operator:
|
|
if condition then true_value else false_value
|
|
|
|
Address:
|
|
&x // returns *T, fails if x is a const variable
|
|
&mut x // returns *mut T, fails if x is non-mut variable
|
|
|
|
Assignment:
|
|
x = 15 // requires x to be a mut variable
|
|
*p = 15 // p must be a pointer to mut
|
|
|
|
Special built-ins:
|
|
typeof(value)
|
|
sizeof(type)
|
|
sizeof(value) // same as sizeof(typeof(value))
|
|
alignof(type)
|
|
alignof(value) // same as alignof(typeof(value))
|
|
offsetof(struct type, field)
|
|
offsetof(field access expression)
|
|
|
|
======== FLOW CONTROL ========
|
|
|
|
Conditionals:
|
|
if condition {
|
|
statements
|
|
} else if condition {
|
|
statements
|
|
} else {
|
|
statements
|
|
}
|
|
|
|
While loop:
|
|
while condition {
|
|
statements
|
|
if x {
|
|
break
|
|
}
|
|
if y {
|
|
continue
|
|
}
|
|
}
|
|
|
|
TODO: Iterator interface:
|
|
get(it) returns the currently pointed-to value
|
|
get_ref(it) returns the pointer to the currently pointed-to value
|
|
next(it) returns the next iterator
|
|
|
|
TODO: Range interface:
|
|
begin(range) returns the begin iterator
|
|
end(range) returns the end iterator
|
|
|
|
TODO: For loop:
|
|
Operates only on ranges.
|
|
|
|
for x in range(10) {
|
|
do_something(x)
|
|
}
|
|
|
|
i is immutable within the loop body.
|
|
|
|
Modifiable ranges use special syntax for pointers to elements:
|
|
|
|
for &x in array {
|
|
*x += 1
|
|
}
|
|
|
|
The loop is equivalent to
|
|
|
|
mut it = begin(range)
|
|
let end = end(range)
|
|
while it != end {
|
|
let x = get(it) // or get_ref(x) for pointer loop
|
|
statements
|
|
it = next(it)
|
|
}
|
|
|
|
The prelude contains an implementation of range interface for built-in arrays.
|
|
|
|
======== STRUCTS ========
|
|
|
|
Struct types:
|
|
struct rect {
|
|
width: u32
|
|
height: u32
|
|
}
|
|
|
|
Creating a struct value:
|
|
let x = rect(10u, 20u)
|
|
let y = rect(width = 10u, height = 20u) // TODO, also named function arguments in general?
|
|
|
|
Struct field access:
|
|
let r = rect(1u, 2u)
|
|
let x = r.width
|
|
let p = &r
|
|
let y = p.height // field access through pointer is the same
|
|
|
|
======== ENUMS (TODO) ========
|
|
|
|
Simplified enum syntax:
|
|
enum buffer_usage {
|
|
read
|
|
write
|
|
vertex
|
|
index
|
|
uniform
|
|
}
|
|
|
|
Stored as tag + payload bytes
|
|
Tag inferred as minimal sufficient unsigned integer
|
|
|
|
Explicit tag type:
|
|
enum color: u32 {
|
|
...
|
|
}
|
|
|
|
Full enum with arguments:
|
|
enum node {
|
|
literal: i32,
|
|
variable: string,
|
|
unary_op: unary_op,
|
|
binary_op: binary_op,
|
|
}
|
|
|
|
TODO: match statement
|
|
TODO: support multi-argument enum values, e.g. unary_op(op_type, node_ptr)? Make them named, i.e. treat them like anonymous structs?
|
|
Or allow only one type, and require explicit struct types when multi-arg is necessary?
|
|
|
|
======== STATIC METHODS/VARIABLES (TODO) ========
|
|
|
|
Can be declared for any type:
|
|
in my_type {
|
|
const RESULT: type = f32
|
|
|
|
const DEFAULT_SIZE = 16
|
|
|
|
global mut instance_count = 0
|
|
|
|
func new(size: i32) -> my_type {
|
|
...
|
|
}
|
|
}
|
|
|
|
Can be only accessed explicitly through the type name:
|
|
let x = my_type.new(my_type.DEFAULT_SIZE)
|
|
|
|
======== FUNCTIONS ========
|
|
|
|
Function definition:
|
|
func foo(x: i32, y: i32) -> i32 {
|
|
return x * y
|
|
}
|
|
|
|
func bar(x: f32) { // deduced return type unit
|
|
print(x)
|
|
}
|
|
|
|
Function arguments are immutable by default (as if declared with let) unless declared with mut:
|
|
|
|
func bar(mut x: u32) -> u32 {
|
|
x = (x + 1u)
|
|
x = x * x
|
|
return x
|
|
}
|
|
|
|
// External function: name taken literally as `powf`
|
|
// and C calling convention assumed
|
|
foreign func powf(x: f32, y: f32) -> f32 // no implementation
|
|
|
|
// TODO: function overloading? Probably requires selecting a specific overload using `as` operator to save to a value (but not on call site)
|
|
// TODO: alternative - Rust-like traits, aka parametric polymorphism? Jesus, no, NO, noooo, probably not
|
|
|
|
======== OPERATOR OVERLOADING (TODO) ========
|
|
|
|
Most operators can be overloaded by providing a function with an appropriate name:
|
|
func add(a: vec2i, b: vec2i) -> vec2i {
|
|
return vec2i(a.x + b.x, a.y + b.y)
|
|
}
|
|
|
|
======== RAII/MOVE (TODO) ========
|
|
|
|
Defer statement:
|
|
func foo(size: u64) {
|
|
let temp = malloc(size) as u8 mut*
|
|
defer free(temp)
|
|
}
|
|
|
|
Can be used with any other statement, e.g.
|
|
defer if temp != null {
|
|
free(temp)
|
|
}
|
|
|
|
defer for file in files {
|
|
file.close()
|
|
}
|
|
|
|
A `destroy` function is automatically called (if present) with pointer argument when a value lifetime ends:
|
|
func destroy(x: file*) {
|
|
x.close()
|
|
}
|
|
|
|
Some types can be declared as noncopyable (TODO: how? `move`/`linear` attribute before declataion? Mere presence of `move` function?)
|
|
A struct/enum containing a noncopyable field must itself be marked as noncopyable
|
|
Noncopyable types can be moved by calling the `move` function (it must exist for any noncopyable type):
|
|
move struct file {
|
|
handle: unit*
|
|
}
|
|
|
|
func move(from: file mut*, to: file mut*) {
|
|
to.handle = from.handle
|
|
from.handle = null
|
|
}
|
|
|
|
======== ERROR HANDLING (TODO) ========
|
|
|
|
Error type interface:
|
|
type.from_error(error)
|
|
type.from_value(value)
|
|
|
|
Try expression - immediately returns on error, continues on value:
|
|
let file = try open("file.txt", file.mode.read)
|
|
|
|
TODO: multiple error types? Converting one error to another? `catch` statement?
|
|
|
|
======== TEMPLATES (TODO) ========
|
|
|
|
// Definitely monomorphized.
|
|
// Parametric (C++ templates) vs ad-hoc (Rust traits, Haskell typeclasses)?
|
|
//
|
|
// Ad-hoc:
|
|
// + More powerful
|
|
// + Less concepts in the core language
|
|
// + Simpler to use in basic cases
|
|
// - Bad error messages (can be improved with concepts)
|
|
// - Slow compilation (due to type-checking each instantiation)
|
|
//
|
|
// Parametric:
|
|
// + Cleaner, stricter
|
|
// + Faster compilation (type-checking only once)
|
|
// + Better error messages
|
|
// - Less powerful
|
|
// - Harder to use in basic cases (e.g. have to declare type constraints or create new traits for any desired per-type behavior)
|
|
// - A bunch of new required language concepts (trait, impl, constraint), much more complicated to implement in compiler
|
|
|
|
Basic idea:
|
|
// Can be just <t> - assumed to be a type by default
|
|
struct array_view<t: type> {
|
|
data: t*
|
|
size: u64
|
|
}
|
|
|
|
enum option<t: type> {
|
|
none,
|
|
some: t,
|
|
}
|
|
|
|
func prefix<t: type>(view: array_view<t>, size: u64) -> array_view<t> {
|
|
return array_view<t>(view.data, min(view.size, size))
|
|
}
|
|
|
|
======== TYPE OF TYPES (TODO) ========
|
|
|
|
Types are also considered to be values. The keyword `type` denotes the type of all types.
|
|
I.e. `typeof(16) == i32` and `typeof(i32) == type`. Incidentally, `typeof(type) == type` as well; there are no type kinds or etc.
|
|
`type` can be used in any place where a type is required (variable types, function arguments, function return value, etc).
|
|
E.g.
|
|
func foo(x: type) -> type {
|
|
return x[4] // type of arrays of 4 elements of type x
|
|
}
|
|
|
|
let y: type = u32
|
|
if foo(y) == u32[4] {
|
|
do_smth()
|
|
}
|
|
|
|
======== CONST EXPRESSIONS (TODO) ========
|
|
|
|
A function that doesn't access global variables is called pure (TODO: pointers? Probably all good but need to think a bit more)
|
|
An expression involving only const values (const variables, literals), built-in operators, and pure functions can be promoted
|
|
to be a compile-time expression, it can be evaluated at compile-time.
|
|
|
|
Some expressions must be compile-time expressions:
|
|
Initializer of a const variable
|
|
Array type size
|
|
|
|
======== PRELUDE (TODO) ========
|
|
|
|
Prelude is a special source file implicitly included in any project (unless explicitly requested otherwise).
|
|
|
|
It contains:
|
|
|
|
Numeric types limits:
|
|
|
|
in u8 {
|
|
const min = 0ub
|
|
const max = 255ub
|
|
}
|
|
|
|
An array_view template struct:
|
|
|
|
struct array_view<t: type> {
|
|
size: u64
|
|
data: t*
|
|
}
|
|
|
|
A specialization for strings:
|
|
|
|
const string_view = array_view<u8>
|
|
|
|
(String literals compile into string_view objects.)
|
|
|
|
Range interface for built-in arrays and for array_view.
|
|
|
|
Numeric ranges with signatures
|
|
|
|
range(end) // begin implicitly zero
|
|
range(begin, end) // step implicitly one
|
|
range(begin, end, step)
|
|
|
|
that allow iteration like
|
|
|
|
for i in range(10) { ... }
|
|
for i in range(5u, 10u) { ... }
|
|
for i in range(1.0, 10.0, 0.5) { ... }
|
|
|
|
======== PREPROCESSOR-LIKE (TODO) ========
|
|
|
|
Basic features for conditional code compilation:
|
|
|
|
@if(windows) {
|
|
// Code included only on windows
|
|
}
|
|
|
|
The condition is evaluated in a type const context having just a few constants in scope
|
|
|
|
Can be reused for other annotations later (e.g. @vertex for compiling to shaders, etc)
|
|
|
|
======== MODULES AND IMPORTS (TODO) ========
|
|
|
|
Every file is a separate module
|
|
All files/directories in a directory are considered submodules
|
|
Everything is exported by default
|
|
|
|
Import statement:
|
|
// Imports the whole library
|
|
import math
|
|
|
|
// Imports a sub-library (can be a single file)
|
|
import math.vector
|
|
|
|
let x = vec2i(1, 2)
|
|
|
|
// Can use explicit name resolution
|
|
math.vector.vec2i(1, 2)
|
|
|
|
// Submodule's names are automatically present in the parent module
|
|
math.vec2i(1, 2)
|
|
|
|
======== STANDARD LIBRARY (TODO) ========
|
|
|
|
Containers, memory management, strings, io, math, threads, networking(?)
|