diff --git a/examples/example.psl b/examples/example.psl index b3ab643..a5b2883 100644 --- a/examples/example.psl +++ b/examples/example.psl @@ -28,7 +28,7 @@ func merge(x: u32, y: u32) -> u64 { mut v = vec2(10, 20) length(v) -// can be called using method syntax +// TODO: can be called using method syntax v.length() // function pointers diff --git a/spec.txt b/spec.txt index 5b38a55..742e50e 100644 --- a/spec.txt +++ b/spec.txt @@ -73,14 +73,14 @@ 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). +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 - x | y + x & y // evaluates both arguments + x | y // evaluates both arguments x && y // short-circuit x || y // short-circuit x ^ y @@ -97,8 +97,8 @@ Comparison (all built-in types, all pointer types, all array/struct types, only Bitwise (integer types, only same type): !x - x & y - x | y + x & y // evaluates both arguments + x | y // evaluates both arguments x && y // short-circuit x || y // short-circuit x ^ y @@ -120,13 +120,14 @@ Pointer arithmetic (any pointer type + any integer type): 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 -The only implicit casting allowed is T mut* -> T* (maybe?) +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 // TODO: alignment? UB or safe fallback? Probably UB. +Any pointer types can be cast to each other // Misalignment => UB Ternary if operator: if condition then true_value else false_value @@ -170,16 +171,16 @@ While loop: } } -Iterator interface: +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 -Range interface: +TODO: Range interface: begin(range) returns the begin iterator end(range) returns the end iterator -For loop: +TODO: For loop: Operates only on ranges. for x in range(10) { @@ -216,7 +217,7 @@ Struct types: Creating a struct value: let x = rect(10u, 20u) - let y = rect(width = 10u, height = 20u) // named function arguments in general? + let y = rect(width = 10u, height = 20u) // TODO, also named function arguments in general? Struct field access: let r = rect(1u, 2u) @@ -224,7 +225,54 @@ Struct field access: let p = &r let y = p.height // field access through pointer is the same -// TODO: inner struct functions maybe? to act as namespace/module containers +======== 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 ======== @@ -250,11 +298,62 @@ Function definition: 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? +// TODO: alternative - Rust-like traits, aka parametric polymorphism? Jesus, no, NO, noooo, probably not -======== TEMPLATES ======== +======== 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) ======== -// TODO // Definitely monomorphized. // Parametric (C++ templates) vs ad-hoc (Rust traits, Haskell typeclasses)? // @@ -273,7 +372,23 @@ Function definition: // - 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 -======== TYPE OF TYPES ======== +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. @@ -288,32 +403,29 @@ E.g. do_smth() } -======== CONST EXPRESSIONS ======== +======== CONST EXPRESSIONS (TODO) ======== -// TODO -// Auto-upgrading values to compile-time when a pure function is executed from const-only values? +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. -======== METAPROGRAMMING ======== +Some expressions must be compile-time expressions: + Initializer of a const variable + Array type size -// TODO -// Functions returning functions/structs -// Syntactic sugar for common cases -// Figure out: max(a,b) - how to deduce type parameters? How does it play with overloading? -// func max(t: type) { -// return func(x : t, y : t) { -// if x > y: -// return x -// else: -// return y -// } -// } - -======== PRELUDE ======== +======== 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 { @@ -341,18 +453,39 @@ It contains: for i in range(5u, 10u) { ... } for i in range(1.0, 10.0, 0.5) { ... } -======== MODULES AND IMPORTS ======== +======== PREPROCESSOR-LIKE (TODO) ======== -// TODO -// A build system / package metadata? How to e.g. conditionally add some files based on environment (maybe just forbid that)? -// How to describe platform-dependent behavior? -// * Different files - who decides which files to include? -// * Compile-time built-ins - how flexible are they? -// Can we create a different type based on platform? -// Important for some posix stuff like timespec or threads -// * Special compiler intrinsics/attributes/macros/whatever - need a new concept in the language -// Maybe a good thing - can merge with alignment specification and other stuff +Basic features for conditional code compilation: -======== STANDARD LIBRARY ======== + @if(windows) { + // Code included only on windows + } -// TODO: containers, memory management, strings, io, math, threads, networking(?) + 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(?)