======== 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(?)
