======== 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 - assumed to be a type by default struct array_view { data: t* size: u64 } enum option { none, some: t, } func prefix(view: array_view, size: u64) -> array_view { return array_view(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 { size: u64 data: t* } A specialization for strings: const string_view = array_view (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(?)