Nov is a multi-paradigm functional programming language.
Nov typing discipline is static, strong, sound and manifest with bidirectional inference.
Nov has automatic memory management via a minimal tracing GC.
Regular comments start with ;
and end on a newline.
Doc comments start with exactly three semicolon (i.e. ;;;
but not ;;;;
).
Doc comments are used to automatically generate documentation.
Nov functions can be understood as lazy or postponed expressions that takes parameters.
Parameters are immutable by default.
Parenthesis are specific to functions, whenever there is a parenthesis there is a function involved.
; signature: () -> void
let doNothing: () = {}
; here x is passed as a reference and its value is mutable
; signature: (*mut int) -> void
let retNothing: (x: *mut int) = {
x += 1
}
; functions body doesn't need to be a block
; signature: () -> int
let ret2: () -> int = 2
; signature: (int, int) -> int
let add: (a: int, b: int) -> int = a + b
; return a tuple
; see #Struct for more information about tuples and structs
; signature: (int, int) -> struct{ int, int }
let div: (a: int, b: int) -> struct{ int, int } = {
[a / b, a % b]
}
; generic functions
; see #Comptime for more information about that #
; signature: (#type, any, any) -> bool
let eql: (T: #type, x: T, y: T) -> bool = x == y
eql(int, 1, 2) ; returns false
eql(int, true, 0) ; compile error
let add: (T: #type, x: T, y: T) -> T = x + y
; it's easy to create a custom function from a generic one
let addInt: (x: int, y: int) -> int = add(int, x, y)
addInt(1, 2) ; returns 3
add(string, "he", "llo") ; returns "hello"
add(bool, true, false) ; compile error: bool doesn't support `+` operator
Blocks are expression, they are also used for grouping.
let x = {1} ; this is the same as `let x = 1`
let x = {1 + 1} * 3 ; x = 6
; we can do more complex things with blocks
; here x will be equal to "hahaha"
let x = {
let max = 1 + 2
let mut str = ""
loop _ in 0..max {
str += "ha"
}
str
}
Containers are types that can have fields and declarations. Available containers are Enum, Struct and Union.
Arrays are builtin containers.
Similarly to functions, brackets are specific to containers.
Nov enums are just like C enums except that they can have methods and their fields are not global.
Overall enums are a nice way to represent tags or constant numbers.
let Season = enum {
spring, ; 0
summer = 5, ; 5
autumn, ; 6
winter, ; 7
let eql: (self: Season, other: Season) -> bool = {
self == other
}
}
let s1 = Season.spring ; type of s1 is inferred from the initializer
let s2: Season = .spring ; type of the initializer is inferred from the type of s2
s1.eql(s2) |> @println ; prints true
match s1 {
.spring => ... ; do something
_ => {} ; do nothing
}
Struct is Nov's equivalent of Product type.
let MyStruct = struct {
name: string,
x: float = 1.0,
y: float = 1.0,
let max = 100.0
let init: (name: string, x: float, y: float) -> MyStruct = {
if x > max or y > max {
@panic()
}
return [ .name = name, .x = x, .y = y ]
}
}
; create a new MyStruct using its init method
let a = MyStruct.init("", 0, 0)
; create a new MyStruct with the container syntax
; here x and y use the default value specified i.e. 1.0
let b = MyStruct[ .name = "" ]
; same as above except that the type of the initializer is inferred from the type of c
let c: MyStruct = [ .name = "", .x = 0, .y = 0 ]
;;; Tuples
; tuples are simply anonymous structs
; is a tuple of type struct{ int, string }
let x = [ 0, "test" ]
; this is also a tuple
let y = [ .name = "", .x = 0, .y = 0 ]
; this is not a tuple because all values are of the same type, see #Arrays for more information
let z = [ 10, 20, 30 ]
Union is Nov's equivalent of Sum type
since Nov's unions are always tagged unless annotated with @[extern]
.
Thus we can match on an union to find its active field.
See Result and Option unions for an example of generic unions.
let NodeKind = enum { empty, node }
let Tree = union(NodeKind) {
empty,
node: struct {
value: int,
left: *mut Tree,
right: *mut Tree,
},
let sum: (self: *mut Tree) -> int = match self {
.empty => 0
.node => |n| n.value + n.left.sum() + n.right.sum()
}
}
let mut leaf = Tree.empty[]
let a = Tree.node[ .value = 0, .left = &leaf, .right = &leaf ]
let b: Tree = .node[ .value = 0, .left = &leaf, .right = &leaf ]
; we can also create an union without explicit enum
let Number = union {
raw: int = 0, ; provide a default value for Number.raw
text: string,
;;; Converts a Number to an int
let toRaw: (self: Number) -> int = {
match self {
.raw => |raw| return raw
.text => ... ; TODO: parseInt
}
}
@[public]
@[operator(.@"==")]
let eql: (a: Number, b: Number) -> bool = {
a.toRaw() == b.toRaw()
}
}
let x = Number.raw[3]
x.text ; runtime error
; `==` can be used to check the tag of an union and to compare two unions
; overloading `==` in an union only overload the comparison between two unions,
; not between the tag
@println(x == .raw) ; prints true
@println(x == .text) ; prints false
@println(x == .raw[4]) ; prints false
@println(x == .text["3"]) ; prints true, would have been false if we hadn't overloaded the `==` operator
We can generate a type with a function thus creating a generic type.
let Stack: (T: #type) -> type = struct {
list: []T = [],
let Self = @This()
@[public]
let push: (self: *mut Self, value: T) = {
self.list += [value]
}
@[public]
let pop: (self: *mut Self) -> Option(T) = {
if self.list.len == 0 {
return .none
}
let value = self.list[self.list.len - 1]
self.list.len -= 1
return .some[value]
}
@[public]
let isEmpty: (self: Self) -> bool = self.list.len == 0
}
.!
is syntax sugar for Result/Option which is equivalent to unwrapping the
value or returning it if it's .err
/.none
.
@[public]
let Result: (T: type, E: type) -> type = union {
ok: T,
err: E,
let Self = @This() ; see #Builtins
@[public]
let unwrapOr: (self: Self, fallback: T) -> T = match self {
.ok => |value| value ; catch the value and return it
.err => fallback
}
; here unwrapOrElse takes a function and not a value so the fallback is
; only evaluated when needed
@[public]
let unwrapOrElse: (self: Self, fallback: (E) -> T) -> T = match self {
.ok => |value| value
.err => |err| fallback(err)
}
}
let MyResult = Result(int, string)
let my_value = MyResult.ok[0]
let my_err: MyResult = .err["my error string"]
; error handling
let file = match File.open("file.txt") {
.ok => |f| f
.err => |err| match err.kind {
.not_found => {
; do something
}
_ => return err ; return early with the error
}
}
; .! unwrap and returns the err if there is any
let file = File.open("file.txt").!
let Option: (T: type) -> type = union {
some: T,
none,
@[public]
let unwrapOrElse: (self: @This(), fallback: () -> T) -> T = {
match self {
.some => |value| value
.none => fallback()
}
}
}
let x = 5
let y = Option(int).some[5]
let sum = x + y.unwrapOrElse(0)
let MyOption = Option(float)
let a = MyOption.some[1.0]
let b = MyOption.none[]
let prod = a.! * b.! ; will return .none to the calling function since b is .none
let mut my_array = [1, 2, 3]
@TypeOf(my_array) ; returns []int
; arrays have a special field for its len, it is mutable if the string is mutable
my_array.len == 3 ; true
my_array[0] == 1 ; true
my_array[-1] == 3 ; true
; append an element
my_array += [5]
my_array |> @println ; prints [1, 2, 3, 5]
; append multiple elements
my_array += [1, 1, 7]
my_array |> @println ; prints [1, 2, 3, 5, 1, 1, 7]
; remove the last element
my_array.len -= 1
my_array |> @println ; prints [1, 2, 3, 5, 1, 1]
my_array = []
@TypeOf(my_array) ; still return []int
; check if an element is in the array
6 in my_array ; false
let my_array_of_array = [["Hello", "World!"], ["Bonjour", "Monde!"]]
@TypeOf(my_array_of_array) ; returns [][]string
; TODO:
; proposal about copy/ref of variables
; side note, it kinda sucks to hide through type if something is copied or not
; we're trying to solve that btw https://jvns.ca/blog/2024/08/06/go-structs-copied-on-assignment/
; ---
; alias because it's long to type. we specify the type so it's passed by
; reference and create an actual alias instead of copying the data
let arr_arr: *[][]string = my_array_of_array
arr_arr.len == 2 ; true
; this works like a pointer in C because it's mutable, note that the compiler
; will emit an error if it is never mutated like here
let mut ref_arr: *[][]string = my_array_of_array
; this is a copy
let arr_copy = my_array_of_array
; error type mismatch my_array_of_array is immutable
let arr_error: *mut [][]string = my_array_of_array
; imperative way of printing an array
loop arr in arr_arr {
loop w in arr {
@print(w + " ")
}
@println()
}
; functional way, I think
arr_arr.map(|arr| {
arr.map(|word| word + " " |> @print)
@println
}
; with monad bind operator
; type annotation is optional
arr_arr >>= |arr: []string| {
arr >>= |word| word + " " |> @print
@println()
}
TODO
Ranges are values too!
5 in 0..10 ; true
match 5 {
0 | 1 | 2 => ... ; use `|` to specify multiple cases
{0b100 | 0b001} => ... ; wrap your expr in a {} to use `|` bitwise operator
10..20 => ... ; use `x..y` to match over x to y excluded, [x;y[
20..=30 => ... ; use `x..=` to match over x to y included, [x;y]
31 => {} ; {} does nothing, it's a empty block
_ => {} ; _ corresponds to every other possible values
}
let a = 10
let b = 20
; braces are mandatory, else is optional
if a < b {
@println("{a} < {b}")
} else if a > b {
@println("{a} > {b}")
} else {
@println("{a} == {b}")
}
; all ifs are expressions which means that they all return a value
; the previous if returns `void`
; this one returns a bool
let is_even = if 69 % 2 == 0 {true} else {false}
; another example which returns an Option(int)
let x: Option(int) = if is_even {
@println("even")
.some[42]
} else {
@println("not even")
.none[]
}
; items can be an array, a slice, a string or an iterator
; iterator semantic are not defined yet...
; type is always inferred
loop item in items {}
; n takes values [0;10[
; note than the boundary doesn't need to be literal
; loop n in x..y {} is fine as long as x and y are unsigned? integers
loop n in 0..10 {}
; n takes values [0;10]
loop n in 0..=10 {}
; it's possible to loop on multiple values at the same time as long as they
; have the same length
loop item, i in items, 0.. {}
; item is a copy by default, add & to get a ref instead
loop &item in items {}
; use underscore to ignore a value
loop _ in 0..10 {}
; we can also loop on a condition
loop x > 10 {}
; infinite loop
loop {}
; "zig while" loop
let mut i = 0
loop i < 100 : i += 2 {}
; else branch
; the else branch is evaluated when the loop is not exited with a break
let rangeHasNumber: (begin: uint, end: uint, number: uint) -> bool = {
var i = begin;
return loop i < end : i += 1 {
if i == number {
break true;
}
} else {
false;
}
}
TODO: same as zig
TODO: same as zig
Check if an element is in an array
let nums = [1, 2, 3]
@println(1 in nums) ; true
@println(5 in nums) ; false
let mut a = 3 ; @TypeOf(a) == int
let b = 4 ; @TypeOf(b) == int
let x = y = 3 ; parse error
let x = a += b ; parse error
let x = {y = 3} ; should be fine, @TypeOf(x) == void
let z += 3 ; compile error, expected `=`
TODO
- bool
- string
- rune
See also:
TODO
- c_int, ...
- u8, u16, u32, u64, u128, i8, i16, i32, i64, i128, f16, f32, f64, f80, f128
- int = i32 or i64 based on architecture
- uint = u32 or u64 based on architecture
- float = f32 or f64 based on architecture
float
- f32 or f64 depending on architecturef16
- IEEE-754-2008 binary16f32
- IEEE-754-2008 binary32f64
- IEEE-754-2008 binary64f80
- IEEE-754-2008 80-bit extended precisionf128
- IEEE-754-2008 binary128c_longdouble
- matches long double for the target C ABI
Name | Syntax | Types | Remarks |
---|---|---|---|
Assignment | a = b | All types | a is an identifier and b is an expression. |
Addition | a + b a += b |
Integers Floats |
TODO |
Concatenation | a + b a += b |
string Arrays |
TODO |
Substraction | a - b a -= b |
Integers Floats |
TODO |
Negation | -a | Integers Floats |
TODO |
Multiplication | a * b a *= b |
Integers Floats |
TODO |
Division | a / b a /= b |
Integers Floats |
TODO |
Remainder Division | a % b a %= b |
Integers | TODO |
Bit Shift Left | a << b | Integers | TODO |
Bit Shift Right | a >> b | Integers | TODO |
Bitwise And | a & b | Integers | TODO |
Bitwise Or | a | b | Integers | TODO |
Bitwise Xor | a ^ b | Integers | TODO |
Bitwise Not | ~a | Integers | TODO |
Logical And | a and b | bool | TODO |
Logical Or | a or b | bool | TODO |
Boolean Not | !a | bool | TODO |
Equality | a == b | All types | TODO |
Inequality | a != b | All types | TODO |
Greater Than | a > b | Integers Floats |
TODO |
Greater or Equal | a >= b | Integers Floats |
TODO |
Less Than | a < b | Integers Floats |
TODO |
Less or Equal | a <= b | Integers Floats |
TODO |
Bind | a >>= |b| ... | Monads | TODO |
Function Pipe | a |> f | Functions | TODO |
Member Search | a in b | Arrays | TODO |
Access | a[b] | Arrays string |
TODO: b is an Integer |
Field / Method Access | a.b | All types | TODO |
Reference Type | *T *mut T |
All types | Create a reference type from T . Unless mut is specified the wrapped value is constant |
Reference Of | &a | All types | Returns a reference to a . |
Dereference | a.* | Reference | Unwrap a reference type, this is done automatically when using . or [] . |
Unwrap or Rethrow | a.! | Result Option |
Unwrap a value or return if it's wrong (.err , .none ). |
x() x[] x.y x.! x.*
!x -x ~x &x *T
* / %
+ -
<< >>
& ^ | in
== != < > <= >=
and
or
|> >>=
= *= /= %= += -=
Operator overloading is possible on the following operators:
+
: (T, T) -> T-
: (T, T) -> T*
: (T, T) -> T/
: (T, T) -> T%
: (T, T) -> T<
: (T, T) -> bool==
: (T, T) -> bool
Note:
==
is automatically generated for all types by the compiler but can be overridden.!=
,>
,<=
,>=
are automatically generated when==
and<
are defined.+=
,-=
,*=
,/=
,%=
are automatically generated when the corresponding operator is defined.
let Complex = struct {
re: float
im: float
@[operator(.@"+")]
let add: (self: Complex, other: Complex) -> Complex = [
.re = self.re + other.re,
.im = self.im + other.im,
]
; used by print
; signature must be `toString: (T) -> string` where T is the container type
let toString: (self: Complex) -> string = "{self.re} + i{self.im}"
}
let x: Complex = [ .re = 5, .im = 3 ]
let y: Complex = [ .re = 2, .im = 7 ]
x + y ; returns Complex[ .re = 7, .im = 10 ]
"{varname:[fill][alignment][width][.precision]}"
Escape {
and }
with \
.
TODO
@import(path: string)
: Import a nov file. See Visibility for which declarations gets imported.@TypeOf(...)
: Returns the type of a value.@typeInfo(...)
: Returns type information about a value.@This()
: Returns the type of the current container@print(...)
: Output all args separated with a space to stdout. (supports string interpolation) TODO: what about printf? what about print to another file?@println(...)
: Same as print with a newline at the end.@fprint(file: File, ...)
: Same as print but output to a specific file. Returns a Result<>.
@panic(s: string)
: Outputs
and backtrace to stderr, then terminate the program with error code 1.@max(a: T, b: T, ...)
: Returns the maximum value between all the supplied arguments@min(a: T, b: T, ...)
: Returns the minimum value between all the supplied arguments@dump(expr)
: TODO: https://docs.vlang.io/builtin-functions.html#dumping-expressions-at-runtime@embedFile()
: TODO, also allow for compressing the file@sizeOf()
: TODO@bitSizeOf()
: TODO
@swap(a: any, b: @Typeof(a))
@unreachable()
: TODO- atomic stuff
@memcpy()
or@dupe()
/@clone()
?- math func? prob no, put them in std.math
@call()
: TODO@volatile_load()
: TODO@volatile_store()
: TODO
- os type
- architecture
- support i128...
- optimization level
- safety check enabled
- current file name
- current func name
@line
: current line
@[deprecated]
-@[deprecated("message...")]
@[operator(op)]
: Note that op is an enum for easier parsing.@[pure]
: Mainly for extern functions, a pure function can only call pure functions but impure functions can call a pure function without issue.@[extern]
@[packed]
@[export]
@[inline]
@[noinline]
@[cold]
@[noreturn]
: Specify that a function can't return.@[test]
: Decl with it are ignored unless run withnov test
where it behave likezig test
.
@[public]
@[private]
@[memoize]
: (?, related to pure...)@[require(condition..., "optional error message")
: Ensure that the specified conditions are verified for the arguments given to a function or to a container initializer.
Attributes can be set only on top level / container declarations
Visibility is modified via attributes.
- public: visible everywhere
- private: visible only in current file
- no modifier: visible when importing as a file (
@import("std.nov")
) but not when importing as a module (@import("std")
)
Works almost like zig except that we use #
instead of comptime
; comptime expr
let x = #fibonacci(10)
; comptime block
let x = #{}
; comptime variable
let mut x: #int = 0
; comptime parameter
let intList: (len: #uint) -> []int = ...
; comptime assertion
let assert_name = #{
let cond = 1 == 1
if !cond {
@unreachable()
}
}