From 0ac443cd2f7df0b970d3bd98ea1b1d5aaf1347c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= <71729558+In-Veritas@users.noreply.github.com> Date: Mon, 3 Feb 2025 20:58:40 +0100 Subject: [PATCH] Update, correct, and expand documentation (#747) Co-authored-by: Nicolas Abril Co-authored-by: Nicolas Abril --- FEATURES.md | 150 +++++---- GUIDE.md | 129 ++++---- docs/builtins.md | 535 +++++++++++++++++-------------- docs/cli-arguments.md | 5 +- docs/compilation-and-readback.md | 47 ++- docs/compiler-options.md | 12 +- docs/defining-data-types.md | 36 ++- docs/dups-and-sups.md | 4 +- docs/ffi.md | 9 +- docs/imports.md | 1 + docs/lazy-definitions.md | 21 +- docs/native-numbers.md | 22 +- docs/pattern-matching.md | 93 +++--- docs/syntax.md | 12 +- docs/using-scopeless-lambdas.md | 11 +- src/fun/builtins.bend | 324 ++++++++++++------- src/fun/parser.rs | 12 +- 17 files changed, 844 insertions(+), 579 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 8f72a2bd4..ce60841f5 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -8,9 +8,8 @@ To see some more complex examples programs, check out the [examples](examples/) ### Basic features We can start with a basic program that adds the numbers 3 and 2. - ```py -def main: +def main() -> u24: return 2 + 3 ``` @@ -23,17 +22,17 @@ Functions can receive arguments both directly and using a lambda abstraction. ```py # These two are equivalent -def add(x, y): +def add(x: u24, y: u24) -> u24: return x + y -def add2: +def add2() -> (u24 -> u24 -> u24): return lambda x, y: x + y ``` You can then call this function like this: ```py -def main: +def main() -> u24: sum = add(2, 3) return sum ``` @@ -44,31 +43,31 @@ You can bundle multiple values into a single value using a tuple or a struct. ```py # With a tuple -def tuple_fst(x): +def tuple_fst(x: (a, b)) -> a: # This destructures the tuple into the two values it holds. # '*' means that the value is discarded and not bound to any variable. (fst, *) = x return fst # With an object (similar to what other languages call a struct, a class or a record) -object Pair { fst, snd } +object Pair(a, b) { fst: a, snd: b } -def Pair/fst(x): +def Pair/fst(x: Pair(a, b)) -> a: match x: case Pair: return x.fst # We can also access the fields of an object after we `open` it. -def Pair/fst_2(x): +def Pair/fst_2(x: Pair(a, b)) -> a: open Pair: x return x.fst # This is how we can create new objects. -def Pair/with_one(x): +def Pair/with_one(x: a) -> Pair(a, u24): return Pair{ fst: x, snd: 1 } # The function can be named anything, but by convention we use Type/function_name. -def Pair/swap(x): +def Pair/swap(x: Pair(a, b)) -> Pair(b, a): open Pair: x # We can also call the constructor like any normal function. return Pair(x.snd, x.fst) @@ -87,16 +86,26 @@ This defines a constructor function for each variant of the type, with names `My Like most things in bend (except tuples and numbers), types defined with `type` and `object` become lambda encoded functions. You can read how this is done internally by the compiler in [Defining data types](docs/defining-data-types.md) and [Pattern matching](docs/pattern-matching.md). +### Optional typing + +Types in Bend are completely optional - you can write programs without any type annotations, but we'll be typing every function for clarity. For instace: +```py +def main(): + sum = add(2, 3) + return sum +``` +Here, this program will run just fine and return the exact same result as the example shown in [Basic features](#basic-features) + ### Pattern matching We can pattern match on values of a data type to perform different actions depending on the variant of the value. ```py -def Maybe/or_default(x, default): +def Maybe/or_default(x: Maybe(T), default: T) -> T: match x: case Maybe/Some: # We can access the fields of the variant using 'matched.field' - return x.val + return x.value case Maybe/None: return default ``` @@ -110,8 +119,10 @@ This allows us to easily create and consume these recursive data structures with `bend` is a pure recursive loop that is very useful for generating data structures. ```py -def MyTree.sum(x): - # Sum all the values in the tree. +#{ + Sum all the values in the tree. +#} +def MyTree.sum(x: MyTree) -> u24: fold x: # The fold is implicitly called for fields marked with '~' in their definition. case MyTree/Node: @@ -119,7 +130,7 @@ def MyTree.sum(x): case MyTree/Leaf: return 0 -def main: +def main() -> u24: bend val = 0: when val < 10: # 'fork' calls the bend recursively with the provided values. @@ -130,24 +141,26 @@ def main: return MyTree.sum(x) ``` +> Note: since MyTree has no type annotations, its fields will be considered of type `Any`, which partially disables the type checker for these values. Thus the fact that `x` is holding a tree of u24 and not a tree of anything else won't be checked and it's up to the user to make sure it's correct. + These are equivalent to inline recursive functions that create a tree and consume it. ```py -def MyTree.sum(x): +def MyTree.sum(x: MyTree) -> u24: match x: case MyTree/Node: return x.val + MyTree.sum(x.left) + MyTree.sum(x.right) case MyTree/Leaf: return 0 -def main_bend(val): +def main_bend(val: u24) -> MyTree: if val < 10: return MyTree/Node(val, main_bend(val + 1), main_bend(val + 1)) else: return MyTree/Leaf -def main: +def main() -> u24: x = main_bend(0) return MyTree.sum(x) ``` @@ -159,7 +172,7 @@ If you give a `fold` some state, then you necessarily need to pass it by calling ```py # This function substitutes each value in the tree with the sum of all the values before it. -def MyTree.map_sum(x): +def MyTree.map_sum(x: MyTree) -> MyTree: acc = 0 fold x with acc: case MyTree/Node: @@ -178,7 +191,7 @@ _Attention_: Note that despite the ADT syntax sugars, Bend is an _untyped_ langu For example, the following program will compile just fine even though `!=` is only defined for native numbers: ```py -def main: +def main(): bend val = [0, 1, 2, 3]: when val != []: match val: @@ -202,12 +215,12 @@ It inlines clones of some value in the statements that follow it. ```py def foo(x): - use result = bar(1, x) + use result = (1, x) return (result, result) # Is equivalent to def foo(x): - return (bar(1, x), bar(1, x)) + return ((1, x), (1, x)) ``` Note that any variable in the `use` will end up being duplicated. @@ -215,7 +228,7 @@ Note that any variable in the `use` will end up being duplicated. Bend supports recursive functions of unrestricted depth: ```py -def native_num_to_adt(n): +def native_num_to_adt(n: u24) -> Nat: if n == 0: return Nat/Zero else: @@ -225,21 +238,19 @@ def native_num_to_adt(n): If your recursive function is not based on pattern matching syntax (like `if`, `match`, `fold`, etc) you have to be careful to avoid an infinite loop. ```py -# A scott-encoded list folding function +# A scott-encoded list folding function. # Writing it like this will cause an infinite loop. def scott_list.add(xs, add): - xs( - λxs.head xs.tail: λc n: (c (xs.head + add) scott_list.sum(xs.tail, add)), - λc λn: n - ) + return xs( λxs.head xs.tail: λc n: (c (xs.head + add), scott_list.add(xs.tail, add))) # Instead we want to write it like this; def scott_list.add(xs, add): - xs( + return xs( λxs.head xs.tail: λadd: λc n: (c (xs.head + add) scott_list.sum(xs.tail, add)), λadd: λc λn: n, add ) +# These functions can't be typed with bend's type system. ``` Since Bend is eagerly executed, some situations will cause function applications to always be expanded, which can lead to looping situations. @@ -250,7 +261,7 @@ You can read how to avoid this in [Lazy definitions](docs/lazy-definitions.md). Bend has native numbers and operations. ```py -def main: +def main() -> (u24, i24, f24): a = 1 # A 24 bit unsigned integer. b = +2 # A 24 bit signed integer. c = -3 # Another signed integer, but with negative value. @@ -266,8 +277,6 @@ Floating point numbers must have the decimal point `.` and can optionally take a The three number types are fundamentally different. If you mix two numbers of different types HVM will interpret the binary representation of one of them incorrectly, leading to incorrect results. Which number is interpreted incorrectly depends on the situation and shouldn't be relied on for now. -At the moment Bend doesn't have a way to convert between the different number types, but it will be added in the future. - You can use `switch` to pattern match on unsigned native numbers: ```py @@ -282,12 +291,34 @@ switch x = 4: case _: String.concat("other: ", (String.from_num x-3)) ``` +You can also convert between the number types using the builtin casting functions. +Here's some of the builtin functions you can use to cast any native number into the corresponding type: + +```py +def main() -> _: + x = f24/to_i24(1.0) + y = u24/to_f24(2) + z = i24/to_u24(-3) + + return (x, y, z) +``` +You can find the other casting functions and their declarations at [builtins.md](docs/builtins.md). ### Other builtin types Bend has Lists and Strings, which support Unicode characters. +This is how they are defined: +```py +type String: + Nil + Cons { head: u24, ~tail: String } +type List(T): + Nil + Cons { head: T, ~tail: List(T) } +``` ```py -def main: +# Here's an example of a List of Strings +def main() -> List(String): return ["You: Hello, 🌎", "🌎: Hello, user"] ``` @@ -296,34 +327,26 @@ List also becomes a type with two constructors, `List/Cons` and `List/Nil`. ```py # When you write this -def StrEx: +def StrEx() -> String: return "Hello" -def ids: +def ids() -> List(u24): return [1, 2, 3] # The compiler converts it to this -def StrEx: - String/Cons('H', String/Cons('e', String/Cons('l', String/Cons('l', String/Cons('o', String/Nil))))) -def ids: - List/Cons(1, List/Cons(2, List/Cons(3, List/Nil))) - -# These are the definitions of the builtin types. -type String: - Cons { head, ~tail } - Nil -type List: - Cons { head, ~tail } - Nil +def StrEx() -> String: + return String/Cons('H', String/Cons('e', String/Cons('l', String/Cons('l', String/Cons('o', String/Nil))))) +def ids() -> List(u24): + return List/Cons(1, List/Cons(2, List/Cons(3, List/Nil))) ``` Characters are delimited by `'` `'` and support Unicode escape sequences. They are encoded as a U24 with the unicode codepoint as their value. ```py # These two are equivalent -def chars: +def chars() -> List(u24): return ['A', '\u{4242}', '🌎'] -def chars2: +def chars2() -> List(u24): return [65, 0x4242, 0x1F30E] ``` @@ -339,38 +362,37 @@ A Map is desugared to a Map data type containing two constructors `Map/Leaf` and ```py # When you write this -def empty_map: +def empty_map() -> Map(T): return {} -def init_map: - return { 1: "one", 2: "two", `blue`: 0x0000FF } +def init_map() -> Map(String): + return { 1: "one", 2: "two", `blue`: "0x0000FF" } -def main: +def main() -> String: map = init_map one = map[1] # map getter syntax map[0] = "zero" # map setter syntax return one # The compiler converts it to this -def empty_map(): +def empty_map() -> Map(T): return Map/Leaf -def init_map(): +def init_map() -> Map(String): map = Map/set(Map/Leaf, 1, "one") map = Map/set(map, 2, "two") - map = Map/set(map, `blue`, 0x0000FF) return map -def main(): +def main() -> String: map = init_map (one, map) = Map/get(map, 1) map = Map/set(map, 0, "zero") return one # The builtin Map type definition -type Map: - Node { value, ~left, ~right } - Leaf +type Map(T): + Node { value: Maybe(T), ~left: Map(T), ~right: Map(T) } + Leaf ``` Notice that the getter and setter syntax induces an order on things using the map, since every get or set operation depends on the value of the previous map. @@ -386,15 +408,15 @@ type Bool: True False -def is_odd(x): +def is_odd(x: u24) -> Bool: switch x: case 0: return Bool/False case _: return is_even(x-1) -(is_even n) = switch n { - 0: return Bool/True +is_even(n: u24): u24 = switch n { + 0: Bool/True _: (is_odd n-1) } diff --git a/GUIDE.md b/GUIDE.md index ea89b25c2..f048b9b40 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -78,19 +78,28 @@ and recursion play an important role. This is how its `"Hello, world!"` looks: def main(): return "Hello, world!" ``` - +To run the program above, type: +``` +bend run-rs main.bend +``` Wait - there is something strange there. Why `return`, not `print`? Well, _for -now_ (you'll read these words a lot), Bend doesn't have IO. We plan on -introducing it very soon! So, _for now_, all you can do is perform computations, -and see results. To run the program above, type: +now_ (you'll read these words a lot), Bend's IO is in an experimental stage. We plan on +fully introducing it very soon! Nevertheless, here's an example on how you can use IO on bend to print `"Hello, world!"`: + +```python +def main() -> IO(u24): + with IO: + * <- IO/print("Hello, world!\n") + return wrap(0) +``` +To run the program above, type: ``` -bend run-rs main.bend +bend run-c main.bend ``` -If all goes well, you should see `"Hello, world!"`. The `bend run-rs` command uses -the reference interpreter, which is slow. In a few moments, we'll teach you how -to run your code in parallel, on both CPUs and GPUs. For now, let's learn some +If all goes well, you should see `"Hello, world!"` in both cases. The `bend run-rs` command uses +the reference interpreter, which is slow, whereas the `bend run-c` command uses the much faster C interpreter, but bend can run even faster! In a few moments, we'll teach you how to run your code in parallel, on both CPUs and GPUs. For now, let's learn some fundamentals! ## Basic Functions and Datatypes @@ -99,40 +108,40 @@ In Bend, functions are pure: they receive something, and they return something. That's all. Here is a function that tells you how old you are: ```python -def am_i_old(age): +def am_i_old(age: u24) -> String: if age < 18: return "you're a kid" else: return "you're an adult" -def main(): +def main() -> String: return am_i_old(32) ``` -That is simple enough, isn't it? Here is one that returns the distance between +That is simple enough, isn't it? Here is one that returns the euclidean distance between two points: ```python -def distance(ax, ay, bx, by): +def distance(ax: f24, ay: f24, bx: f24, by: f24) -> f24: dx = bx - ax dy = by - ay return (dx * dx + dy * dy) ** 0.5 -def main(): +def main() -> f24: return distance(10.0, 10.0, 20.0, 20.0) ``` This isn't so pretty. Could we use tuples instead? Yes: ```python -def distance(a, b): +def distance(a: (f24, f24), b: (f24, f24)) -> f24: (ax, ay) = a (bx, by) = b dx = bx - ax dy = by - ay return (dx * dx + dy * dy) ** 0.5 -def main(): +def main() -> f24: return distance((10.0, 10.0), (20.0, 20.0)) ``` @@ -143,14 +152,14 @@ objects themselves. This is how we create a 2D vector: ```python object V2 { x, y } -def distance(a, b): +def distance(a: V2, b: V2) -> f24: open V2: a open V2: b dx = b.x - a.x dy = b.y - a.y return (dx * dx + dy * dy) ** 0.5 -def main(): +def main() -> f24: return distance(V2 { x: 10.0, y: 10.0 }, V2 { x: 20.0, y: 20.0 }) ``` @@ -175,14 +184,14 @@ type Shape: Circle { radius } Rectangle { width, height } -def area(shape): +def area(shape: Shape) -> f24: match shape: case Shape/Circle: return 3.14 * shape.radius ** 2.0 case Shape/Rectangle: return shape.width * shape.height -def main: +def main() -> f24: return area(Shape/Circle { radius: 10.0 }) ``` @@ -207,15 +216,15 @@ represents a concatenation between an element (`head`) and another list (`tail`). That way, the `[1,2,3]` list could be written as: ```python -def main: - my_list = List/Cons { head: 1, tail: List/Cons { head: 2, tail: List/Cons { head: 3, tail: List/Nil }}} +def main() -> List(u24): + my_list = List/Cons{head: 1, tail: List/Cons{head: 2, tail: List/Cons{head: 3, tail: List/Nil}}} return my_list ``` Obviously - that's terrible. So, you can write just instead: ```python -def main: +def main() -> List(u24): my_list = [1, 2, 3] return my_list ``` @@ -225,7 +234,7 @@ to understand it is just the `List` datatype, which means we can operate on it using the `match` notation. For example: ```python -def main: +def main() -> u24: my_list = [1, 2, 3] match my_list: case List/Cons: @@ -246,7 +255,7 @@ characters (UTF-16 encoded). The `"Hello, world!"` type we've seen used it! Bend also has inline functions, which work just like Python: ```python -def main: +def main() -> u24: mul_2 = lambda x: x * 2 return mul_2(7) ``` @@ -257,27 +266,33 @@ if you can somehow type that. You can also match on native numbers (`u24`) using the `switch` statement: ```python -def slow_mul2(n): +def slow_mul2(n: u24) -> u24: switch n: case 0: return 0 case _: return 2 + slow_mul2(n-1) + +def main() -> u24: + return slow_mul2(7) ``` The `if-else` syntax is a third option to branch, other than `match` and `switch`. It expects a `u24` (`1` for `true` and `0` for `false`): ```python -def is_even(n): +def is_even(n: u24) -> u24: if n % 2 == 0: return 1 else: return 0 + +def main() -> u24: + return is_even(7) ``` _note - some types, like tuples, aren't being pretty-printed correctly after -computation. this will be fixed in the next days (TM)_ +computation. This will be fixed in the future (TM)_ ## The Dreaded Immutability @@ -289,23 +304,23 @@ Haskell: **variables are immutable**. Not "by default". They just **are**. For example, in Bend, we're not allowed to write: ```python -def parity(x): +def parity(x: u24) -> String: result = "odd" if x % 2 == 0: result = "even" return result ``` -... because that would mutate the `result` variable. Instead, we should write: +... because that would require mutating the `result` variable. Instead, we should write: ```python -def is_even(x): +def is_even(x: u24) -> String: if x % 2 == 0: return "even" else: return "odd" -def main: +def main() -> String: return is_even(7) ``` @@ -316,7 +331,7 @@ live with it. But, wait... if variables are immutable... how do we even do loops? For example: ```python -def sum(x): +def sum(x: u24) -> u24: total = 0 for i in range(10) total += i @@ -352,9 +367,9 @@ _recursive_. For example, the tree: Could be represented as: ``` -tree = Tree/Node { - lft: Tree/Node { left: Tree/Leaf { val: 1 }, right: Tree/Leaf { val: 2 } }, - rgt: Tree/Node { left: Tree/Leaf { val: 3 }, right: Tree/Leaf { val: 4 } } +tree = Tree/Node{ + left: Tree/Node{left: Tree/Leaf {value: 1}, right: Tree/Leaf {value: 2}}, + right: Tree/Node{left: Tree/Leaf {value: 3}, right: Tree/Leaf {value: 4}}, } ``` @@ -375,14 +390,14 @@ another construct we can use: it's called `fold`, and it works like a _search and replace_ for datatypes. For example, consider the code below: ```python -def sum(tree): +def sum(tree: Tree(u24)) -> u24: fold tree: case Tree/Node: return tree.left + tree.right case Tree/Leaf: return tree.value -def main: +def main() -> u24: tree = ![![!1, !2],![!3, !4]] return sum(tree) ``` @@ -412,7 +427,7 @@ def enum(tree): case Tree/Leaf: return !(idx, tree.value) -def main: +def main() -> Tree(u24): tree = ![![!1, !2],![!3, !4]] return enum(tree) ``` @@ -434,11 +449,11 @@ it is really liberating, and will let you write better algorithms. As an exercise, use `fold` to implement a "reverse" algorithm for lists: ```python -def reverse(list): +def reverse(list: List(T)) -> List(T): # exercise ? -def main: +def main() -> List(u24): return reverse([1,2,3]) ``` @@ -450,7 +465,7 @@ can "grow" a recursive structure, layer by layer, until the condition is met. For example, consider the code below: ```python -def main(): +def main() -> Tree(u24): bend x = 0: when x < 3: tree = ![fork(x + 1), fork(x + 1)] @@ -575,7 +590,7 @@ unlike the former one, they will run in parallel. And that's why `bend` and example, to add numbers in parallel, we can write: ```python -def main(): +def main() -> u24: bend d = 0, i = 0: when d < 28: sum = fork(d+1, i*2+0) + fork(d+1, i*2+1) @@ -655,14 +670,17 @@ implemented as a series of _immutable tree rotations_, with pattern-matching and recursion. Don't bother trying to understand it, but, here's the code: ```python -def gen(d, x): +def gen(d: u24, x: u24) -> Any: switch d: case 0: return x case _: return (gen(d-1, x * 2 + 1), gen(d-1, x * 2)) +``` +> Note: The type of this function can't be expressed with Bend's type system, but we can still write it using `Any`. -def sum(d, t): +```python +def sum(d: u24, t: u24) -> u24: switch d: case 0: return t @@ -670,14 +688,14 @@ def sum(d, t): (t.a, t.b) = t return sum(d-1, t.a) + sum(d-1, t.b) -def swap(s, a, b): +def swap(s: u24, a: Any, b: Any) -> (Any, Any): switch s: case 0: return (a,b) case _: return (b,a) -def warp(d, s, a, b): +def warp(d: u24, s: u24, a: Any, b: Any) -> (Any, Any): switch d: case 0: return swap(s ^ (a > b), a, b) @@ -688,7 +706,7 @@ def warp(d, s, a, b): (B.a,B.b) = warp(d-1, s, a.b, b.b) return ((A.a,B.a),(A.b,B.b)) -def flow(d, s, t): +def flow(d: u24, s: u24, t: Any) -> Any: switch d: case 0: return t @@ -696,7 +714,7 @@ def flow(d, s, t): (t.a, t.b) = t return down(d, s, warp(d-1, s, t.a, t.b)) -def down(d,s,t): +def down(d: u24, s: u24, t: Any) -> Any: switch d: case 0: return t @@ -704,7 +722,7 @@ def down(d,s,t): (t.a, t.b) = t return (flow(d-1, s, t.a), flow(d-1, s, t.b)) -def sort(d, s, t): +def sort(d: u24, s: u24, t: Any) -> Any: switch d: case 0: return t @@ -712,7 +730,7 @@ def sort(d, s, t): (t.a, t.b) = t return flow(d, s, (sort(d-1, 0, t.a), sort(d-1, 1, t.b))) -def main: +def main() -> u24: return sum(18, sort(18, 0, gen(18, 0))) ``` @@ -748,7 +766,7 @@ compute-heavy, but less memory-hungry, computations. For example, consider: ```python # given a shader, returns a square image -def render(depth): +def render(depth: u24) -> Any: bend d = 0, i = 0: when d < depth: color = (fork(d+1, i*2+0), fork(d+1, i*2+1)) @@ -759,7 +777,7 @@ def render(depth): # given a position, returns a color # for this demo, it just busy loops -def demo_shader(x, y): +def demo_shader(x: Any, y: Any) -> Any: bend i = 0: when i < 100000: color = fork(i + 1) @@ -768,7 +786,7 @@ def demo_shader(x, y): return color # renders a 256x256 image using demo_shader -def main: +def main() -> Any: return render(16, demo_shader) ``` @@ -798,10 +816,7 @@ reach 1000+ MIPS. ## To be continued... This guide isn't extensive, and there's a lot uncovered. For example, Bend also -has an entire "secret" Haskell-like syntax that is compatible with old HVM1. -[Here](https://gist.github.com/VictorTaelin/9cbb43e2b1f39006bae01238f99ff224) is -an implementation of the Bitonic Sort with Haskell-like equations. We'll -document its syntax here soon! +has an entire Haskell-like functional syntax that is compatible with old HVM1, you can find it documented [here](https://github.com/HigherOrderCO/Bend/blob/main/docs/syntax.md#fun-syntax). You can also check [this](https://gist.github.com/VictorTaelin/9cbb43e2b1f39006bae01238f99ff224) out, it's an implementation of the Bitonic Sort with Haskell-like equations. ## Community diff --git a/docs/builtins.md b/docs/builtins.md index ee6f4c170..4ab66dccc 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -7,7 +7,9 @@ ## String ```python -type String = (Cons head ~tail) | (Nil) +type String: + Nil + Cons { head: u24, ~tail: String } ``` - **Nil**: Represents an empty string. @@ -25,28 +27,33 @@ A String literal is surrounded with `"`. Accepts the same values as characters l #### String/equals -Checks if two strings are equal. - ```python -def String/equals(s1: String, s2: String) -> u24 +#{ + Checks if two strings are equal. +#} +def String/equals (s1: String) (s2: String) : u24 ``` #### String/split -Splits a string into a list of strings based on the given delimiter. - ```python -def String/split(s: String, delimiter: u24) -> [String] +#{ + Splits a string into a list of strings based on the given delimiter. +#} +String/split (s: String) (delimiter: u24) : (List String) ``` ## List ```python -type List = (Cons head ~tail) | (Nil) +type List(T): + Nil + Cons { head: T, ~tail: List(T) } ``` - **Nil**: Represents an empty list. - **Cons head ~tail**: Represents a list with a `head` element and a `tail` list. +- **T**: Represents the type of the elements in the list. ### Syntax @@ -61,27 +68,32 @@ A List of values can be written using `[ ]`, it can have multiple values inside, #### List/length ```python -def List/length(list: [a]) -> (length: u24, list: [a]) +#{ + Returns a tuple containing the length and the list itself. +#} +def List/length(xs: List(T)) -> (u24, List(T)): ``` -Returns a tuple containing the length and the list itself. + #### List/reverse ```python -def List/reverse(list: [a]) -> [a] +#{ + Reverses the elements of a list. +#} +def List/reverse(xs: List(T)) -> List(T): ``` -Reverses the elements of a list. - #### List/flatten ```python -def List/flatten(list: [[a]]) -> [a] +#{ + Returns a flattened list from a list of lists. +#} +List/flatten (xs: (List (List T))) : (List T) ``` - -Returns a flattened list from a list of lists. Example: - +Example: ```python List/flatten([[1], [2, 3], [4]]) @@ -91,11 +103,12 @@ List/flatten([[1], [2, 3], [4]]) #### List/concat ```python -def List/concat(xs: [a], ys: [a]) -> [a] +#{ + Appends two lists together. +#} +def List/concat(xs: (List T)) (ys: (List T)) : (List T) ``` - -Appends two lists together. Example: - +Example: ```python List/concat([1, 2], [4, 5]) @@ -104,26 +117,34 @@ List/concat([1, 2], [4, 5]) #### List/filter -Filters a list based on a predicate function. - ```python +#{ + Filters a list based on a predicate function. +#} List/filter(xs: List(T), pred: T -> Bool) -> List(T) ``` #### List/split_once -Splits a list into two lists at the first occurrence of a value. - ```python -List/split_once(xs: List(T), val: T) -> (Result(List(T), List(T))) +#{ + Splits a list into two lists at the first occurrence of a value. +#} +def List/split_once(xs: List(T), cond: T -> u24) -> (Result((List(T), List(T)), List(T))): +``` +Example: +```python + # Split list at first even number + list = [1,3,4,5,6] + result = List/split_once(list, λx: x % 2 == 0) + return result + # Result: Result/Ok/tag ([1, 3], [5, 6]) ``` ## Result ```python -type Result: - Ok { val: A } - Err { val: B } +type (Result o e) = (Ok (val: o)) | (Err (val: e)) ``` ### Result/unwrap @@ -133,15 +154,20 @@ Returns the inner value of `Result/Ok` or `Result/Err`. If the types `A` and `B` are different, should only be used in type unsafe programs or when only one variant is guaranteed to happen. ```python -def Result/unwrap(result: Result): A || B +#{ +Returns the inner value of `Result/Ok` or `Result/Err`. + +If the types `A` and `B` are different, should only be used in type unsafe programs or when only one variant is guaranteed to happen. +#} +def Result/unwrap(res: Result(T, E)) -> Any: ``` ## Tree ```python -type Tree: - Node { ~left, ~right } - Leaf { value } +type Tree(T): + Node { ~left: Tree(T), ~right: Tree(T) } + Leaf { value: T } ``` **`Tree`** represents a tree with values stored in the leaves. @@ -149,6 +175,7 @@ Trees are a structure that naturally lends itself to parallel recursion, so writ - **Node { ~left ~right }**: Represents a tree node with `left` and `right` subtrees. - **Leaf { value }**: Represents one of the ends of the tree, storing `value`. +- **T**: Represents the type of the elements in the tree. #### Syntax @@ -178,14 +205,11 @@ maybe = Maybe/Some(Nat/Succ(Nat/Zero)) ## Maybe functions ### Maybe/unwrap -Maybe has a builtin function that returns the value inside the `Maybe` if it is `Some`, and returns `unreachable()` if it is `None`. ```python -def Maybe/unwrap(m: Maybe(T)) -> T: - match m: - case Maybe/Some: - return m.val - case Maybe/None: - return unreachable() +#{ +Returns the value inside the `Maybe` if it is `Some`, and returns `unreachable()` if it is `None`. +#} +def Maybe/unwrap(m: Maybe(T)) -> T ``` ## Map @@ -206,12 +230,13 @@ It is meant to be used as an efficient map data structure with integer keys and Here's how you create a new `Map` with some initial values.: ```python -{ 0: 4, `hi`: "bye", 'c': 2 + 3 } +def main(): + return { 0: 4, `hi`: "bye", 'c': 2 + 3 } ``` The keys must be `U24` numbers, and can be given as literals or any other expression that evaluates to a `U24`. -The values can be anything, but storing data of different types in a `Map` will make it harder for you to reason about it. +As long as your function isn't typed, like the one in the example, the values can be anything. But storing data of different types in a `Map` will make it harder for you to reason about it. You can read and write a value of a map with the `[]` operator: @@ -229,31 +254,24 @@ Here, `map` must be the name of the `Map` variable, and the keys inside `[]` can ### Map/empty -Initializes an empty map. - ```python -Map/empty = Map/Leaf +#{ + Initializes an empty map. +#} +def Map/empty() -> Map(T) ``` ### Map/get -Retrieves a `value` from the `map` based on the `key`. -Returns a tuple with the value and the `map` unchanged. + ```rust -def Map/get (map: Map(T), key: u24) -> (T, Map(T)): - match map: - case Map/Leaf: - return (unreachable(), map) - case Map/Node: - if (0 == key): - return (Maybe/unwrap(map.value), map) - elif (key % 2 == 0): - (got, rest) = Map/get(map.left, (key / 2)) - return(got, Map/Node(map.value, rest, map.right)) - else: - (got, rest) = Map/get(map.right, (key / 2)) - return(got, Map/Node(map.value, map.left, rest)) +#{ + Retrieves a `value` from the `map` based on the `key` and returns a tuple with the value and the `map` unchanged. + + The logic for checking whether a value is or not contained in a `map` is not done in the `get` function, so if we try to get a key that is not in the map, the program will return `unreachable`. +#} +def Map/get (map: Map(T), key: u24) -> (T, Map(T)) ``` #### Syntax @@ -279,22 +297,10 @@ And the value resultant from the get function would be: ### Map/set ```rust -def Map/set (map: Map(T), key: u24, value: T) -> Map(T): - match map: - case Map/Node: - if (0 == key): - return Map/Node(Maybe/Some(value), map.left, map.right) - elif ((key % 2) == 0): - return Map/Node(map.value, Map/set(map.left, (key / 2), value), map.right) - else: - return Map/Node(map.value, map.left, Map/set(map.right, (key / 2), value)) - case Map/Leaf: - if (0 == key): - return Map/Node(Maybe/Some(value), Map/Leaf, Map/Leaf) - elif ((key % 2) == 0): - return Map/Node(Maybe/None, Map/set(Map/Leaf, (key / 2), value), Map/Leaf) - else: - return Map/Node(Maybe/None, Map/Leaf, Map/set(Map/Leaf, (key / 2),value)) +#{ + Sets a value on a Map, returning the map with the value mapped. +#} +def Map/set (map: Map(T), key: u24, value: T) -> Map(T) ``` #### Syntax @@ -331,21 +337,12 @@ The new tree ### Map/map -Applies a function to a value in the map. -Returns the map with the value mapped. ```rust -def Map/map (map: Map(T), key: u24, f: T -> T) -> Map(T): - match map: - case Map/Leaf: - return Map/Leaf - case Map/Node: - if (0 == key): - return Map/Node(Maybe/Some(f(Maybe/unwrap(map.value))), map.left, map.right) - elif ((key % 2) == 0): - return Map/Node(map.value, Map/map(map.left, (key / 2), f), map.right) - else: - return Map/Node(map.value, map.left, Map/map(map.right, (key / 2), f)) +#{ + Applies a function to a value in the map and returns the map with the value mapped. +#} +def Map/map (map: Map(T), key: u24, f: T -> T) -> Map(T) ``` #### Syntax @@ -359,26 +356,12 @@ x[0] @= lambda y: String/concat(y, " and mapped") ### Map/contains -Checks if a `map` contains a given `key` and returns 0 or 1 as a `u24` number and the `map` unchanged. -```python -def Map/contains (map: Map(T), key: u24) -> (u24, Map(T)): - match map: - case Map/Leaf: - return (0, map) - case Map/Node: - if (0 == key): - match map.value: - case Maybe/Some: - return (1, map) - case Maybe/None: - return (0, map) - elif ((key % 2) == 0): - (new_value, new_map) = Map/contains(map.left, (key / 2)) - return (new_value, Map/Node(map.value, new_map, map.right)) - else: - (new_value, new_map) = Map/contains(map.right, (key / 2)) - return (new_value, Map/Node(map.value, map.left, new_map)) -``` + +```python +#{ + Checks if a `map` contains a given `key` and returns 0 or 1 along with and `map` unchanged. +#} +def Map/contains (map: Map(T), key: u24) -> (u24, Map(T)) #### Syntax @@ -394,20 +377,12 @@ Whilst the `num` variable will contain 0 or 1 depending on if the key is in the ## Nat ```python -type Nat = (Succ ~pred) | (Zero) +type Nat = (Succ ~(pred: Nat)) | (Zero) ``` - **Succ ~pred**: Represents a natural number successor. - **Zero**: Represents the natural number zero. -### Syntax - -A Natural Number can be written with literals with a `#` before the literal number. - -``` -#1337 -``` - ## DiffList DiffList is a list that has constant time prepends (cons), appends and concatenation, but can't be pattern matched. @@ -420,33 +395,40 @@ For example, the list `List/Cons(1, List/Cons(2, List/Nil))` can be written as t #### DiffList/new -Creates a new difference list. ```python +#{ +Creates a new difference list. +#} def DiffList/new() -> (List(T) -> List(T)) ``` #### DiffList/append -Appends a value to the end of the difference list. ```python +#{ + Appends a value to the end of the difference list. +#} def DiffList/append(diff: List(T) -> List(T), val: T) -> (List(T) -> List(T)) ``` #### DiffList/cons -Appends a value to the beginning of the difference list. - ```python +#{ + Appends a value to the beginning of the difference list. +#} def DiffList/cons(diff: List(T) -> List(T), val: T) -> (List(T) -> List(T)) ``` #### DiffList/to_list -Converts a difference list to a regular cons list. ```python +#{ + Converts a difference list to a regular cons list. +#} def DiffList/to_list(diff: List(T) -> List(T)) -> (List(T)) ``` @@ -459,30 +441,36 @@ Here is the current list of functions, but be aware that they may change in the ### Printing ```python -def IO/print(text) +#{ + Prints the string `text` to the standard output, encoded with utf-8. +#} +def IO/print(text: String) -> IO(None) ``` -Prints the string `text` to the standard output, encoded with utf-8. ### Input ```python -def IO/input() -> String +#{ + Reads characters from the standard input until a newline is found. + Returns the read input as a String decoded with utf-8. +#} +def IO/input() -> IO(Result(String, u24)) ``` -Reads characters from the standard input until a newline is found. -Returns the read input as a String decoded with utf-8. ### File IO #### File open ```python -def IO/FS/open(path, mode) +#{ + Opens a file with with `path` being given as a string and `mode` being a string with the mode to open the file in. The mode should be one of the following: +#} +def IO/FS/open(path: String, mode: String) -> IO(Result(u24, u24)) ``` -Opens a file with with `path` being given as a string and `mode` being a string with the mode to open the file in. The mode should be one of the following: - `"r"`: Read mode - `"w"`: Write mode (write at the beginning of the file, overwriting any existing content) @@ -504,67 +492,79 @@ The standard input/output files are always open and assigned the following file #### File close ```python -def IO/FS/close(file) +#{ + Closes the file with the given `file` descriptor. +#} +def IO/FS/close(file: u24) -> IO(Result(None, u24)) ``` -Closes the file with the given `file` descriptor. #### File read ```python -def IO/FS/read(file, num_bytes) +#{ +Reads `num_bytes` bytes from the file with the given `file` descriptor. +Returns a list of U24 with each element representing a byte read from the file. +#} +def IO/FS/read(file: u24, num_bytes: u24) -> IO(Result(List(u24), u24)) ``` -Reads `num_bytes` bytes from the file with the given `file` descriptor. -Returns a list of U24 with each element representing a byte read from the file. ```python -def IO/FS/read_line(file) +#{ + Reads a line from the file with the given `file` descriptor. + Returns a list of U24 with each element representing a byte read from the file. +#} +def IO/FS/read_line(fd: u24) -> IO(Result(List(u24), u24)) ``` -Reads a line from the file with the given `file` descriptor. -Returns a list of U24 with each element representing a byte read from the file. ```python -def IO/FS/read_until_end(file) +#{ + Reads until the end of the file with the given `file` descriptor. + Returns a list of U24 with each element representing a byte read from the file. +#} +def IO/FS/read_to_end(fd: u24) -> IO(Result(List(u24), u24)) ``` -Reads until the end of the file with the given `file` descriptor. -Returns a list of U24 with each element representing a byte read from the file. ```python -def IO/FS/read_file(path) +#{ + Reads an entire file with the given `path` and returns a list of U24 with each element representing a byte read from the file. +#} +def IO/FS/read_file(path: String) -> IO(Result(List(u24), u24)) ``` -Reads an entire file with the given `path` and returns a list of U24 with each element representing a byte read from the file. #### File write ```python -def IO/FS/write(file, bytes) +#{ + Writes `bytes`, a list of U24 with each element representing a byte, to the file with the given `file` descriptor. + Returns nothing (`*`). +#} +def IO/FS/write(file: u24, bytes: List(u24)) -> IO(Result(None, u24)) ``` -Writes `bytes`, a list of U24 with each element representing a byte, to the file with the given `file` descriptor. - -Returns nothing (`*`). - ```python -def IO/FS/write_file(path, bytes) +#{ + Writes `bytes`, a list of U24 with each element representing a byte, as the entire content of the file with the given `path`. +#} +def IO/FS/write_file(path: String, bytes: List(u24)) -> IO(Result(None, u24)) ``` -Writes `bytes`, a list of U24 with each element representing a byte, as the entire content of the file with the given `path`. - #### File seek ```python -def IO/FS/seek(file, offset, mode) +#{ + Moves the current position of the file with the given `file` descriptor to the given `offset`, an I24 or U24 number, in bytes. +#} +def IO/FS/seek(file: u24, offset: i24, mode: i24) -> IO(Result(None, u24)) ``` -Moves the current position of the file with the given `file` descriptor to the given `offset`, an I24 or U24 number, in bytes. - `mode` can be one of the following: - `IO/FS/SEEK_SET = 0`: Seek from start of file @@ -576,13 +576,13 @@ Returns nothing (`*`). #### File flush ```python -def IO/FS/flush(file) +#{ + Flushes the file with the given `file` descriptor. + Returns nothing (`*`). +#} +def IO/FS/flush(file: u24) -> IO(Result(None, u24)) ``` -Flushes the file with the given `file` descriptor. - -Returns nothing (`*`). - ### Dinamically linked libraries It's possible to dynamically load shared objects (libraries) with functions that implement the Bend IO interface. @@ -591,11 +591,11 @@ You can read more on how to implement these libraries in the [Dynamically linked #### IO/DyLib/open ```py -def IO/DyLib/open(path: String, lazy: u24) -> u24 +#{ + Loads a dynamic library file. +#} +def IO/DyLib/open(path: String, lazy: u24) -> IO(Result(u24, String)) ``` - -Loads a dynamic library file. - - `path` is the path to the library file. - `lazy` is a boolean encoded as a `u24` that determines if all functions are loaded lazily (`1`) or upfront (`0`). - Returns an unique id to the library object encoded as a `u24`. @@ -603,99 +603,135 @@ Loads a dynamic library file. #### IO/DyLib/call ```py -def IO/DyLib/call(dl: u24, fn: String, args: Any) -> Any +#{ + Calls a function of a previously opened library. + - `dl` is the id of the library object. + - `fn` is the name of the function in the library. + - `args` are the arguments to the function. The expected values depend on the called function. + - The returned value is determined by the called function. +#} +def IO/DyLib/call(dl: u24, fn: String, args: Any) -> IO(Result(Any, String)) ``` -Calls a function of a previously opened library. - -- `dl` is the id of the library object. -- `fn` is the name of the function in the library. -- `args` are the arguments to the function. The expected values depend on the called function. -- The returned value is determined by the called function. #### IO/DyLib/close ```py -def IO/DyLib/close(dl: u24) -> None +#{ + Closes a previously open library. + - `dl` is the id of the library object. + - Returns nothing (`*`). +#} +def IO/DyLib/close(dl: u24) -> IO(Result(None, String)) ``` -Closes a previously open library. - -- `dl` is the id of the library object. -- Returns nothing (`*`). ## Native number casting ### to_f24 ```py -def to_f24(x: any number) -> f24 -``` - -Casts any native number to an f24. +#{ + Casts an u24 number to an f24. +#} +def u24/to_f24 -> (u24 -> f24) +#{ + Casts an i24 number to an f24. +#} +def i24/to_f24 -> (i24 -> f24) +``` ### to_u24 ```py -def to_u24(x: any number) -> u24 -``` - -Casts any native number to a u24. +#{ + Casts a f24 number to an u24. +#} +def f24/to_u24 -> (f24 -> u24) +#{ + Casts an i24 number to an u24. +#} +def i24/to_u24 -> (i24 -> u24) +``` ### to_i24 ```py -def to_i24(x: any number) -> i24 +#{ + Casts an u24 number to an i24. +#} +def u24/to_i24 -> (u24 -> i24): +#{ + Casts a f24 number to an i24. +#} +def f24/to_i24 -> (f24 -> i24): ``` -Casts any native number to an i24. +### to_string + +```py +#{ + Casts an u24 native number to a string. +#} +def u24/to_string(n: u24) -> String: +``` ## String encoding / decoding ### String/decode_utf8 ```py -def String/decode_utf8(bytes: [u24]) -> String +#{ + Decodes a sequence of bytes to a String using utf-8 encoding. +#} +String/decode_utf8 (bytes: (List u24)) : String ``` -Decodes a sequence of bytes to a String using utf-8 encoding. ### String/decode_ascii ```py -def String/decode_ascii(bytes: [u24]) -> String +#{ + Decodes a sequence of bytes to a String using ascii encoding. +#} +String/decode_ascii (bytes: (List u24)) : String ``` -Decodes a sequence of bytes to a String using ascii encoding. ### String/encode_utf8 ```py -def String/encode_utf8(s: String) -> [u24] +#{ + Encodes a String to a sequence of bytes using utf-8 encoding. +#} +String/encode_utf8 (str: String) : (List u24) ``` -Encodes a String to a sequence of bytes using utf-8 encoding. ### String/encode_ascii ```py -def String/encode_ascii(s: String) -> [u24] +#{ + Encodes a String to a sequence of bytes using ascii encoding. +#} +String/encode_ascii (str: String) : (List u24) ``` -Encodes a String to a sequence of bytes using ascii encoding. ### Utf8/decode_character ```py -def Utf8/decode_character(bytes: [u24]) -> (rune: u24, rest: [u24]) +#{ + Decodes a utf-8 character, returns a tuple containing the rune and the rest of the byte sequence. +#} +Utf8/decode_character (bytes: (List u24)) : (u24, (List u24)) ``` -Decodes a utf-8 character, returns a tuple containing the rune and the rest of the byte sequence. ### Utf8/REPLACEMENT_CHARACTER ```py -def Utf8/REPLACEMENT_CHARACTER: u24 = '\u{FFFD}' +Utf8/REPLACEMENT_CHARACTER : u24 = '\u{FFFD}' ``` ## Math @@ -703,146 +739,181 @@ def Utf8/REPLACEMENT_CHARACTER: u24 = '\u{FFFD}' ### Math/log ```py -def Math/log(x: f24, base: f24) -> f24 +#{ + Computes the logarithm of `x` with the specified `base`. +#} +def Math/log -> (f24 -> f24 -> f24) ``` -Computes the logarithm of `x` with the specified `base`. ### Math/atan2 ```py -def Math/atan2(x: f24, y: f24) -> f24 +#{ + Computes the arctangent of `y / x`. + Has the same behaviour as `atan2f` in the C math lib. +#} +def Math/atan2 -> (f24 -> f24 -> f24) ``` -Computes the arctangent of `y / x`. - -Has the same behaviour as `atan2f` in the C math lib. ### Math/PI -Defines the Pi constant. ```py -def Math/PI: f24 = 3.1415926535 +#{ + Defines the Pi constant. +#} +def Math/PI() -> f24 ``` ### Math/E -Euler's number ```py -def Math/E: f24 = 2.718281828 +#{ +Euler's number +#} +def Math/E() -> f24 ``` ### Math/sin -Computes the sine of the given angle in radians. ```py -def Math/sin(a: f24) -> f24 +#{ + Computes the sine of the given angle in radians. +#} +def Math/sin -> (f24 -> f24) ``` ### Math/cos -Computes the cosine of the given angle in radians. ```py -def Math/cos(a: f24) -> f24 +#{ + Computes the cosine of the given angle in radians. +#} +def Math/cos -> (f24 -> f24) ``` ### Math/tan -Computes the tangent of the given angle in radians. ```py -def Math/tan(a: f24) -> f24 +#{ + Computes the tangent of the given angle in radians. +#} +def Math/tan -> (f24 -> f24) ``` ### Math/cot -Computes the cotangent of the given angle in radians. ```py -def Math/cot(a: f24) -> f24 +#{ + Computes the cotangent of the given angle in radians. +#} +Math/cot (a: f24) : f24 ``` ### Math/sec -Computes the secant of the given angle in radians. ```py -def Math/sec(a: f24) -> f24 +#{ + Computes the secant of the given angle in radians. +#} +Math/sec (a: f24) : f24 ``` ### Math/csc -Computes the cosecant of the given angle in radians. ```py -def Math/csc(a: f24) -> f24 +#{ + Computes the cosecant of the given angle in radians. +#} +Math/csc (a: f24) : f24 ``` ### Math/atan -Computes the arctangent of the given angle. + ```py -def Math/atan(a: f24) -> f24 +#{ + Computes the arctangent of the given angle. +#} +Math/atan (a: f24) : f24 ``` ### Math/asin -Computes the arcsine of the given angle. ```py -def Math/asin(a: f24) -> f24 +#{ + Computes the arcsine of the given angle. +#} +Math/asin (a: f24) : f24 ``` ### Math/acos -Computes the arccosine of the given angle. ```py -def Math/acos(a: f24) -> f24 -``` +#{ + Computes the arccosine of the given angle. +#} +Math/acos (a: f24) : f24 ### Math/radians -Converts degrees to radians. ```py -def Math/radians(a: f24) -> f24 +#{ + Converts degrees to radians. +#} +Math/radians (a: f24) : f24 ``` ### Math/sqrt -Computes the square root of the given number. ```py -def Math/sqrt(n: f24) -> f24 +#{ + Computes the square root of the given number. +#} +Math/sqrt (n: f24) : f24 ``` ### Math/ceil -Round float up to the nearest integer. ```py +#{ + Round float up to the nearest integer. +#} def Math/ceil(n: f24) -> f24 ``` ### Math/floor -Round float down to the nearest integer. ```py +#{ + Round float down to the nearest integer. +#} def Math/floor(n: f24) -> f24 ``` ### Math/round -Round float to the nearest integer. ```py +#{ + Round float to the nearest integer. +#} def Math/round(n: f24) -> f24 ``` diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index c72f64e23..a6a58798d 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -34,8 +34,7 @@ def main(x, y): # Calling with two argument > bend run +5 +3 {+2 -2} - -# Calling with three argument +# Calling with three arguments # In this case, the third argument doesn't change anything # due to the underlying interaction rules. # If this were a variant of simply-typed lambda-calculus @@ -43,3 +42,5 @@ def main(x, y): > bend run +5 +3 +1 {+2 -2} ``` + + diff --git a/docs/compilation-and-readback.md b/docs/compilation-and-readback.md index 94088ef37..0d27bf5f8 100644 --- a/docs/compilation-and-readback.md +++ b/docs/compilation-and-readback.md @@ -37,5 +37,50 @@ A superposition `{a b}` compiles to a Duplicator node too. The difference here c | | Points to the first value Points to the first binding ``` +Bend core terms directly compile to HVM nodes. +- Application -> CON node with --+ polarization. +- Lambda -> CON node with ++- polarization. +- Duplication -> DUP node with -++ polarization. +- Superposition -> DUP node with +-- polarization +- Pairs -> CON node with +-- polarization. +- Pair elimination -> CON node with -++ polarization. +- Erasure values (as in λx *) -> ERA node with + polarization. +- Erased variables (as in λ* x) -> ERA node with + polarization. +- Numbers -> NUM node (always + polarization). +- Switches -> MAT node (--+) + CON node (+--) on port 1 that links to the if 0 and if >= 1 cases. +- Numeric operations -> an OPR node (--+) plus a NUM that holds the kind of operation as per the HVM2 paper. +- References to top level functions -> REF node (+). + +Matches get compiled to the above core constructs according to the adt-encoding option. +Check out [HVM2](https://github.com/HigherOrderCO/HVM), one of the Higher Order Company's projects, to know more about this. + +### Bend compiler passes: + +**encode_adt**: Create functions for constructors. +**desugar_open**: Convert open terms into match terms. +**encode_builtins**: Convert sugars for builtin types (e.g., list, string) into function calls. +**desugar_match_def**: Convert equational-style pattern matching functions into trees of match and switch terms. +**fix_match_terms**: Normalize all match and switch terms. +**lift_local_defs**: Convert `def` terms into top-level functions. +**desugar_bend**: Convert Bend terms into top-level functions. +**desugar_fold**: Convert `fold` terms into top-level functions. +**desugar_with_blocks**: Convert `with` terms and ask (`<-`) terms into monadic bind and unit (wrap). +**make_var_names_unique**: Give a unique name to each variable in each function. +**desugar_use**: Resolve alias terms (`use`) by substituting their occurrences with the aliased term (syntactic duplication). +**linearize_matches**: Linearize the variables in match and switch terms according to the linearize-matches option. +**linearize_match_with**: Linearize the variables specified in `with` clauses of match and switch if they haven't already been linearized by `linearize_matches`. +**type_check_book**: Run the type checker (no elaboration, only inference/checking). +**encode_matches**: Transform match terms into their lambda-calculus forms as specified by the adt-encoding option. +**linearize_vars**: Linearize the occurrences of variables by placing duplicates when variables are used more than once, erasing unused variables, and inlining `let` terms whose variables only occur once. +**float_combinators**: Convert combinator terms into top-level functions according to the size heuristic described in the source code. +**prune**: Remove unused functions according to the prune option. +**merge_definitions**: Merge identical top-level functions. +**expand_main**: Expand the term of the `main` function by dereferencing so that it includes computation and isn't just lazy refs or data containing lazy refs. +**book_to_hvm**: Lower to HVM (as described in the compilation-and-readback file). +**eta**: Perform eta-reduction at the inet level without reducing nodes with `ERA` or `NUM` at both ports (logically equivalent but looks incorrect to users). +**check_cycles**: Heuristic check for mutually recursive cycles of function calls that could cause loops in HVM. +**inline_hvm_book**: Inline REFs to networks that are nullary nodes. +**prune_hvm_book**: Additional layer of pruning after eta-reducing at the inet level. +**check_net_sizes**: Ensure no generated definition will be too large to run on the CUDA runtime. +**add_recursive_priority**: Mark some binary recursive calls with a flag at the inet level so that the GPU runtime can properly distribute work. -Check out [HVM-Core](https://github.com/HigherOrderCO/hvm-core/tree/main#language), one of the Higher Order Company's projects, to know more about this. diff --git a/docs/compiler-options.md b/docs/compiler-options.md index 71d817b6d..2c4b42c7c 100644 --- a/docs/compiler-options.md +++ b/docs/compiler-options.md @@ -12,7 +12,7 @@ | `-Oinline` `-Ono-inline` | Disabled | [inline](#inline) | | `-Ocheck-net-size` `-Ono-check-net-size` | Disabled | [check-net-size](#check-net-size) | | `-Oadt-scott` `-Oadt-num-scott` | adt-num-scott | [adt-encoding](#adt-encoding) | - +| `-Otype-check` `-Ono-type-check` | type-check | [type-checking](#type-checking) | ## Eta-reduction Enables or disables Eta Reduction for defined functions. @@ -259,3 +259,13 @@ Option/None/tag = 1 Pattern-matching with `match` and `fold` is generated according to the encoding. Note: IO is **only** available with `-Oadt-num-scott`. + +## Type Checking + +Type checking is enabled by default and verifies and enforces the constraints of types. When enabled, verifies the type safety of the program based on the source code. If it passes the check, then the program is guaranteed to satisfy type constraints for all possible inputs. + +```py + def main() -> Bool: + return 3 +``` +With type checking enabled, The following program will throw a type error `Expected function type 'Bool' but found 'u24'`, whereas if it is disabled, it will compile successfully and return `3`. diff --git a/docs/defining-data-types.md b/docs/defining-data-types.md index c2bce26f9..2fb0109f9 100644 --- a/docs/defining-data-types.md +++ b/docs/defining-data-types.md @@ -3,31 +3,41 @@ It is possible to easily define complex data types using the `type` keyword. ```py -# A Boolean is either True or False -type Bool = True | False +# A Boolean is either True or False +type Bool: + True + False ``` If a constructor has any arguments, parentheses are necessary around it: ```py # An option either contains some value, or None -type Option = (Some val) | None +type Option: + Some { value } + None ``` +If the data type has a single constructor, it can be destructured using `open`: -If the data type has a single constructor, it can be destructured using `let`: ```py # A Box is a wrapper around a value. -type Boxed = (Box val) +type Boxed: + Box { value } -let (Box value) = boxed; value +def main() -> _: + b = Boxed/Box(1) + open Boxed: b + return b.value ``` + The fields of the constructor that is being destructured with the `match` are bound to the matched variable plus `.` and the field names. ```py -Option.map = λoption λf - match option { - Some: (Some (f option.val)) - None: None - } +opt = Option/Some(1) +match opt: + case Option/Some: + return opt.value + case Option/None: + return 0 ``` Rules can also have patterns. @@ -41,7 +51,9 @@ They work like match expressions with explicit bindings: However, they also allow matching on multiple values at once, which is something that regular `match` can't do: ```py -type Boolean = True | False +type Boolean: + True + False (Option.is_both_some (Some lft_val) (Some rgt_val)) = True (Option.is_both_some lft rgt) = False diff --git a/docs/dups-and-sups.md b/docs/dups-and-sups.md index 9d3819d48..672694dea 100644 --- a/docs/dups-and-sups.md +++ b/docs/dups-and-sups.md @@ -37,7 +37,7 @@ That imposes a strong restriction on correct Bend programs: a variable should no The program below is an example where this can go wrong when using higher-order functions. ```py -def List/map(xs, f): +def List/map(xs: List(A), f: A -> B) -> List(B): fold xs: case List/Nil: return List/Nil @@ -48,7 +48,7 @@ def List/map(xs, f): # {f1 f2} = f # return List/Cons(f1(xs.head), List/map(xs.tail, f2)) -def main: +def main() -> _: # This lambda duplicates `x` and is itself duplicated by the map function. # This will result in wrong behavior. # In this specific case, the runtime will catch it and generate an error, diff --git a/docs/ffi.md b/docs/ffi.md index 0c770b6ad..e69e4b144 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -27,8 +27,9 @@ def main(): # In our example, 'ls' receives a path as a String and # returns a String with the result of the 'ls' command. - files_bytes <- IO/DyLib/call(dl, "ls", "./") - files_str = String/decode_utf8(files_bytes) + unwrapped_dl = Result/unwrap(dl) + files_bytes <- IO/DyLib/call(unwrapped_dl, "ls", "./") + files_str = String/decode_utf8(Result/unwrap(files_bytes)) files = String/split(files_str, '\n') # We want to create a directory for a new user "my_user" if it doesn't exist. @@ -40,14 +41,14 @@ def main(): status = wrap(-1) case List/Nil: # The directory doesn't exist, create it. - * <- IO/DyLib/call(dl, "mkdir", "./my_dir") + * <- IO/DyLib/call(unwrapped_dl, "mkdir", "./my_dir") * <- IO/print("Directory created.\n") status = wrap(+0) status <- status # Here the program ends so we didn't need to close the dynamic library, # but it's good practice to do so once we know we won't need it anymore. - * <- IO/DyLib/close(dl) + * <- IO/DyLib/close(unwrapped_dl) return wrap(status) ``` diff --git a/docs/imports.md b/docs/imports.md index ada25cd6f..d3a849bbc 100644 --- a/docs/imports.md +++ b/docs/imports.md @@ -10,6 +10,7 @@ Imports can be declared two ways: from path import name # or import path/name +# We recommend always placing the imports at the top of the file. ``` ## Project Structure diff --git a/docs/lazy-definitions.md b/docs/lazy-definitions.md index 1a9a83838..db491d146 100644 --- a/docs/lazy-definitions.md +++ b/docs/lazy-definitions.md @@ -2,7 +2,7 @@ In strict-mode, some types of recursive terms will unroll indefinitely. -This is a simple piece of code that works on many other functional programming languages, including hvm's lazy-mode, but hangs on strict-mode: +This is a simple piece of code that works on many other functional programming languages but hangs on Bend due to the strict evaluation of HVM2. ```rust Cons = λx λxs λcons λnil (cons x xs) @@ -16,20 +16,7 @@ Map = λf λlist Main = (Map λx (+ x 1) (Cons 1 Nil)) ``` -The recursive `Map` definition never gets reduced. -Using the debug mode `-d` we can see the steps: - -``` -(Map λa (+ a 1) (Cons 1 Nil)) ---------------------------------------- -(Map λa (+ a 1) λb λ* (b 1 Nil)) ---------------------------------------- -(Cons (λa (+ a 1) 1) (Map λa (+ a 1) Nil)) ---------------------------------------- -(Cons (λa (+ a 1) 1) (Nil λb λc (Cons (λa (+ a 1) b) (Map λa (+ a 1) c)) Nil)) ---------------------------------------- -... -``` +The recursive `Map` creates an infinite reduction sequence because each recursive call expands into another call to Map, never reaching a base case. Which means that functionally, it will reduce it infinitely, never reaching a normal form. For similar reasons, if we try using Y combinator it also won't work. @@ -42,7 +29,7 @@ Map = (Y λrec λf λlist (list cons nil f)) ``` -By linearizing `f`, the `Map` function "fully reduces" first and then applies `f`. +By linearizing `f`, the `Map` function only expands after applying the argument `f`, because the `cons` function will be lifted to a separate top-level function by the compiler (when this option is enabled). ```rust Map = λf λlist @@ -53,7 +40,7 @@ Map = λf λlist This code will work as expected, since `cons` and `nil` are lambdas without free variables, they will be automatically floated to new definitions if the [float-combinators](compiler-options.md#float-combinators) option is active, allowing them to be unrolled lazily by hvm. -It's recommended to use a [supercombinator](https://en.wikipedia.org/wiki/Supercombinator) formulation to make terms be unrolled lazily, preventing infinite expansion in recursive function bodies. +The recursive part of the function should be part of a combinator that is not in an active position. That way it can be lifted into a top-level function which is compiled into a lazy reference thus preventing the infinite expansion. [Supercombinators](https://en.wikipedia.org/wiki/Supercombinator) can be used in order to ensure said lazy unrolling of recursive terms. Other combinator patterns can work as well, as long as they're lifted to the top level. If you have a set of mutually recursive functions, you only need to make one of the steps lazy. This might be useful when doing micro-optimizations, since it's possible to avoid part of the small performance cost of linearizing lambdas. diff --git a/docs/native-numbers.md b/docs/native-numbers.md index 8bdc95e2a..7838816b7 100644 --- a/docs/native-numbers.md +++ b/docs/native-numbers.md @@ -49,14 +49,24 @@ minus_zero = -0.0 ### Mixing number types -The three number types are fundamentally different. -If you mix two numbers of different types, HVM will interpret the binary representation of one of them incorrectly, leading to incorrect results. Which number is interpreted incorrectly depends on the situation and shouldn't be relied on for now. - -At the HVM level, both type and the operation are stored inside the number nodes as tags. One number stores the type, the other the operation. +The three number types are fundamentally different. At the HVM level, both type and the operation are stored inside the number nodes as tags. One number stores the type, the other the operation. That means that we lose the type information of one of the numbers, which causes this behavior. -During runtime, the executed numeric function depends on both the type tag and the operation tag. For example, the same tag is used for unsigned bitwise and floating point atan2, so mixing number types can give you very unexpected results. +During runtime, the executed numeric function depends on both the type tag and the operation tag. For example, the same tag is used for unsigned bitwise and floating point atan2, so if you mix two numbers of different types, HVM will interpret the binary representation of one of them incorrectly, leading to incorrect results. Which number is interpreted incorrectly depends on the situation and shouldn't be relied on for now. Instead, you should make sure that all numbers are of the same type. + +#### Casting numbers + +There is a way to convert between the different number types, and using it is very easy, here's an example: + +```py +def main() -> _: + x = f24/to_i24(1.0) + y = u24/to_f24(2) + z = i24/to_u24(-3) + + return (x, y, z) +``` +You can find more number casting functions and their declarations at [builtins.md](docs/builtins.md). -At the moment Bend doesn't have a way to convert between the different number types, but it will be added in the future. ### Operations diff --git a/docs/pattern-matching.md b/docs/pattern-matching.md index 69068e17d..148ed2068 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -27,21 +27,21 @@ Matches on ADT constructors are compiled to different expressions depending on t type Maybe = (Some val) | None UnwrapOrZero x = match x { - Some: x.val - None: 0 + Maybe/Some: x.val + Maybe/None: 0 } # If the current encoding is 'adt-num-scott' it becomes: -Some = λval λx (x 0 val) -None = λx (x 1) +Maybe/Some = λval λx (x 0 val) +Maybe/None = λx (x 1) UnwrapOrZero x = (x λtag switch tag { 0: λx.val x.val _: λ* 0 }) # Otherwise, if the current encoding is 'adt-scott' it becomes: -Some = λval λSome λNone (Some val) -None = λSome λNone None +Maybe/Some = λval λMaybe/Some λMaybe/None (Maybe/Some val) +Maybe/None = λMaybe/Some λMaybe/None Maybe/None UnwrapOrZero x = (x λx.val x.val 0) ``` @@ -50,8 +50,8 @@ UnwrapOrZero x = (x λx.val x.val 0) Besides `match`and `switch` terms, Bend also supports equational-style pattern matching functions. ```py -And True b = b -And False * = False +And Bool/True b = b +And Bool/False * = Bool/False ``` There are advantages and disadvantages to using this syntax. @@ -60,26 +60,26 @@ They offer more advanced pattern matching capabilities and also take care linear Pattern matching equations are transformed into a tree of `match` and `switch` terms from left to right. ```py # These two are equivalent -(Foo 0 false (Cons h1 (Cons h2 t))) = (A h1 h2 t) -(Foo 0 * *) = B -(Foo n false *) = n -(Foo * true *) = 0 +(Foo 0 Bool/False (List/Cons h1 (List/Cons h2 t))) = (Bar h1 h2 t) +(Foo 0 * *) = Baz +(Foo n Bool/False *) = n +(Foo n Bool/True *) = 0 Foo = λarg1 λarg2 λarg3 (switch arg1 { 0: λarg2 λarg3 match arg2 { - true: λarg3 B - false: λarg3 match arg3 { - Cons: (match arg3.tail { - Cons: λarg3.head (A arg3.head arg3.tail.head arg3.tail.tail) - Nil: λarg3.head B + Bool/True: λarg3 Baz + Bool/False: λarg3 match arg3 { + List/Cons: (match arg3.tail { + List/Cons: λarg3.head (Bar arg3.head arg3.tail.head arg3.tail.tail) + List/Nil: λarg3.head Baz } arg3.head) - Nil: B + List/Nil: Baz } } _: λarg2 λarg3 (match arg2 { - true: λarg1-1 0 - false: λarg1-1 (+ arg1-1 0) - } arg1-1) + Bool/True: λarg1 0 + Bool/False: λarg1 arg1 + } arg1) } arg2 arg3) ``` Besides the compilation of complex pattern matching into simple `match` and `switch` expressions, this example also shows how some arguments are pushed inside the match. @@ -88,18 +88,24 @@ To ensure that recursive pattern matching functions don't loop in strict mode, i ```py # This is what the Foo function actually compiles to. # With -Olinearize-matches and -Ofloat-combinators (default on strict mode) -(Foo) = λa λb λc (switch a { 0: Foo$C5; _: Foo$C8 } b c) - -(Foo$C5) = λd λe (d Foo$C0 Foo$C4 e) # Foo.case_0 -(Foo$C0) = λ* B # Foo.case_0.case_true -(Foo$C4) = λg (g Foo$C3 B) # Foo.case_0.case_false -(Foo$C3) = λh λi (i Foo$C1 Foo$C2 h) # Foo.case_0.case_false.case_cons -(Foo$C1) = λj λk λl (A l j k) # Foo.case_0.case_false.case_cons.case_cons -(Foo$C2) = λ* B # Foo.case_0.case_false.case_cons.case_nil - -(Foo$C8) = λn λo λ* (o Foo$C6 Foo$C7 n) # Foo.case_+ -(Foo$C6) = λ* 0 # Foo.case_+.case_true -(Foo$C7) = λr (+ r 1) # Foo.case_+.case_false +# Main function +(Foo) = λa λb λc (switch a { 0: Foo__C8; _: Foo__C9; } b c) + +# Case 0 branch +(Foo__C8) = λa λb (a Foo__C5 b) # Foo.case_0 +(Foo__C5) = λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0.case_true +(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false +(Foo__C3) = λa switch a { 0: Baz; _: Foo__C2; } # Foo.case_0.case_false_cons +(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false_cons_cons +(Foo__C1) = λa switch a { 0: λ* Baz; _: Foo__C0; } # Foo.case_0.case_false_cons_nil +(Foo__C0) = λ* λa λb λc (Bar c a b) # Foo.case_0.case_false_nil + +# Case non-zero branch +(Foo__C9) = λa λb λc (b Foo__C7 c a) # Foo.case_+ +(Foo__C7) = λa switch a { 0: λ* λ* 0; _: Foo__C6; } # Foo.case_+.case_false +(Foo__C6) = λ* λ* λa (+ a 1) # Foo.case_+.case_true + +# As an user, you can't write a function with __ on its name, that sequence is reserved for things generated by the compiler. ``` Pattern matching equations also support matching on non-consecutive numbers: @@ -109,7 +115,7 @@ Parse ')' = Token.RParenthesis Parse 'λ' = Token.Lambda Parse n = (Token.Name n) ``` -This is compiled to a cascade of `switch` expressions, from smallest value to largest. +The compiler transforms this into an optimized cascade of switch expressions. Each switch computes the distance from the smallest character to efficiently test each case: ```py Parse = λarg0 switch matched = (- arg0 '(') { 0: Token.LParenthesis @@ -123,18 +129,17 @@ Parse = λarg0 switch matched = (- arg0 '(') { } } ``` -Unlike with `switch`, with pattern matching equations you can't access the value of the predecessor of the matched value directly, but instead you can match on a variable. +Unlike with `switch`, with pattern matching equations you can't access the value of the predecessor of the matched value directly, but instead you can match on a variable. Instead, variables (like n above) are bound to computed expressions based on the matched value. Notice how in the example above, `n` is bound to `(+ 1 matched-1)`. -Notice that this definition is valid, since `*` will cover both `p` and `0` cases when the first argument is `False`. - +Notice that this definition is valid, since `*` will cover both `p` and `0` cases when the first argument is `False`.This example shows how patterns are considered from top to bottom, with wildcards covering multiple specific cases: ```rust -pred_if False * if_false = if_false -pred_if True p * = (- p 1) -pred_if True 0 * = 0 +pred_if Bool/False * if_false = if_false +pred_if Bool/True p * = (- p 1) +pred_if Bool/True 0 * = 0 ``` -Pattern matching on strings and lists desugars to a list of matches on List/String.cons and List/String.nil +Pattern matching on strings and lists desugars to a list of matches on Cons and Nil ```py Hi "hi" = 1 @@ -145,10 +150,10 @@ Foo [x] = x Foo _ = 3 # Becomes: -Hi (String.cons 'h' (String.cons 'i' String.nil)) = 2 +Hi (String/Cons 'h' (String/Cons 'i' String/Nil)) = 2 Hi _ = 0 -Foo List.nil = 0 -Foo (List.cons x List.nil) = x +Foo List/Nil = 0 +Foo (List/Cons x List/Nil) = x Foo _ = 3 ``` diff --git a/docs/syntax.md b/docs/syntax.md index 76e80c8ec..299741f67 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -55,7 +55,7 @@ def add(x: u24, y: u24) -> u24: def unchecked two() -> u24: return 2 -def main: +def main() -> u24: return add(40, two) ``` @@ -307,7 +307,7 @@ For fields notated with `~` in the type definition, the fold function is called It is equivalent to the inline recursive function: ```python -def fold(x): +def fold(x: Tree(u24)) -> u24: match x: case Tree/Node: return x.value + fold(x.left) + fold(x.right) @@ -448,7 +448,7 @@ with Result: Creates a local function visible in the current block capturing variables: ```python -def main: +def main() -> _: y = 41 x = 1 def aux_add(x): @@ -581,7 +581,7 @@ i24 = -42 u24 = 42 ``` -Currently, the 3 number types cannot be mixed. +Currently, We can't write operations that mix two types of number but we can explicitly convert between them. | Operation | Syntax | Supported Types | | --------------------- | -------- | ---------------- | @@ -1350,14 +1350,14 @@ def main(): Use `#{ ... #}` to indicate a multi-line comment. Multi-line commenting should also be used to document code. - +Documentation for functions is meant to be written as a multiline comment right above the function. ```py #{ Expects two arguments to be passed. This function always returns the second value that was used as argument. #} -def second(x, y): +def second(x: A, y: B) -> B: return y ``` diff --git a/docs/using-scopeless-lambdas.md b/docs/using-scopeless-lambdas.md index 5353bcbea..1cc99afbd 100644 --- a/docs/using-scopeless-lambdas.md +++ b/docs/using-scopeless-lambdas.md @@ -15,6 +15,14 @@ main = (((λ$x 1) 2), $x) # $x gets replaced by 2 and the application ((λ$x 1) 2) gets replaced by 1 # Outputs (1, 2) ``` +In the imp syntax, scopeless lambdas can be written in the following way: +```py +def main() -> _: + # This is the equivalent code to the above example + # Notice that in the imp syntax, you scopeless lambdas are written as `lambda $x: 1` instead of `λ$x 1`. + f = lambda $x: 1 + return (f(2), $x) +``` Take some time to think about the program above. It is valid, despite `$x` being used outside the lambda's body. @@ -52,13 +60,14 @@ main = let f = λ$x 1 # Assign the lambda to a variable ((f 2), ((f 3), $x)) # Return a tuple of (f 2) and another tuple. -# Outputs (1, (1, {#0 3 2})) +# Outputs (1, (1, {2 3})) ``` What? This is even more confusing. The first two values are `1`, as expected. But what about the last term? The last term in the tuple is a **superposition** of two values. A [superposition](dups-and-sups.md) is the "other side" of a duplication. It is created here because we implicitly duplicated `f` when we used it twice, and duplicating lambdas creates superpositions. +When implicitly duplicating a lambda, the order of the arguments is left to the compiler's discretion. So it's possible that depending on the context of your program, the order of the arguments on the superposition might be different than expected. If you want to make sure that your duplications come out in a specific order, you need to explicitly duplicate the lambda. ## Usage Now that we know how scopeless lambdas work, we can make programs using them. An example of a function that is usually thought as "primitive", but can be implemented using scopeless lambdas is [call/cc](http://www.madore.org/~david/computers/callcc.html) diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index 8b188bef2..2d708f302 100644 --- a/src/fun/builtins.bend +++ b/src/fun/builtins.bend @@ -6,7 +6,7 @@ type List(T): Nil Cons { head: T, ~tail: List(T) } -# Returns the length of a list and the list itself. +#{ Returns a tuple containing the length and the list itself. #} def List/length(xs: List(T)) -> (u24, List(T)): fold xs with len=0, acc=DiffList/new: case List/Nil: @@ -14,7 +14,7 @@ def List/length(xs: List(T)) -> (u24, List(T)): case List/Cons: return xs.tail(len + 1, DiffList/append(acc, xs.head)) -# Reverses a list. +#{ Reverses the elements of a list. #} def List/reverse(xs: List(T)) -> List(T): fold xs with acc=[]: case List/Nil: @@ -22,18 +22,19 @@ def List/reverse(xs: List(T)) -> List(T): case List/Cons: return xs.tail(List/Cons(xs.head, acc)) -# Flattens a list of lists. +#{ Returns a flattened list from a list of lists. #} List/flatten (xs: (List (List T))) : (List T) List/flatten (List/Cons x xs) = (List/concat x (List/flatten xs)) List/flatten (List/Nil) = (List/Nil) -# Concatenates two lists. +#{ Appends two lists together. #} List/concat(xs: (List T)) (ys: (List T)) : (List T) List/concat (List/Cons x xs) ys = (List/Cons x (List/concat xs ys)) List/concat (List/Nil) ys = ys -# Splits a list into two lists at the first value that passes a condition. -# Returns the original list if the value is not found +#{ + Splits a list into two lists at the first occurrence of a value. +#} def List/split_once( xs: List(T), cond: T -> u24 @@ -54,7 +55,7 @@ def List/split_once.go( else: return List/split_once.go(xs.tail, cond, DiffList/append(acc, xs.head)) -# Filters a list based on a predicate function. +#{ Filters a list based on a predicate function. #} List/filter (xs: (List T)) (pred: T -> u24) : (List T) List/filter (List/Nil) _ = List/Nil List/filter (List/Cons x xs) pred = @@ -64,7 +65,7 @@ List/filter (List/Cons x xs) pred = (List/filter xs pred) } -# Checks if two strings are equal. +#{ Checks if two strings are equal. #} String/equals (s1: String) (s2: String) : u24 String/equals (String/Nil) (String/Nil) = 1 String/equals (String/Cons x xs) (String/Cons y ys) = @@ -75,7 +76,7 @@ String/equals (String/Cons x xs) (String/Cons y ys) = } String/equals * * = 0 -# Splits a list into two lists at the first occurrence of a value. +#{ Splits a string into a list of strings based on the given delimiter. #} String/split (s: String) (delimiter: u24) : (List String) String/split s delim = (String/split.go s delim [""]) @@ -83,41 +84,41 @@ String/split.go (cs: String) (delim: u24) (acc: (List String)) : (List String) String/split.go (String/Nil) _ acc = (List/reverse acc) String/split.go (String/Cons c cs) delim acc = if (== c delim) { - # Start a new split string + # Start a new split string. (String/split.go cs delim (List/Cons String/Nil acc)) } else { match acc { - # Add the current character to the current split string + # Add the current character to the current split string. List/Cons: (String/split.go cs delim (List/Cons (String/Cons c acc.head) acc.tail)) - # Should be unreachable + # Should be unreachable. List/Nil: [] } } -# Create a new difference list +#{ Create a new difference list #} def DiffList/new() -> (List(T) -> List(T)): return lambda x: x -# Creates a new difference list with just the given value. +#{ Creates a new difference list with just the given value. #} def DiffList/wrap(head: T) -> (List(T) -> List(T)): return lambda tail: List/Cons(head, tail) -# Append a value to the end of the difference list +#{ Append a value to the end of the difference list #} def DiffList/append(diff: List(T) -> List(T), val: T) -> (List(T) -> List(T)): return lambda x: diff(List/Cons(val, x)) -# Concatenates two difference lists. +#{ Concatenates two difference lists. #} def DiffList/concat( left: List(T) -> List(T), right: List(T) -> List(T) ) -> (List(T) -> List(T)): return lambda x: left(right(x)) -# Append a value to the beginning of the difference list +#{ Append a value to the beginning of the difference list #} def DiffList/cons(diff: List(T) -> List(T), val: T) -> (List(T) -> List(T)): return lambda x: List/Cons(val, diff(x)) -# Convert a difference list to a list +#{ Converts a difference list to a regular cons list. #} def DiffList/to_list(diff: List(T) -> List(T)) -> (List(T)): return diff(List/Nil) @@ -125,6 +126,11 @@ type Nat = (Succ ~(pred: Nat)) | (Zero) type (Result o e) = (Ok (val: o)) | (Err (val: e)) +#{ +Returns the inner value of `Result/Ok` or `Result/Err`. + +If the types `A` and `B` are different, should only be used in type unsafe programs or when only one variant is guaranteed to happen. +#} def Result/unwrap(res: Result(T, E)) -> Any: match res: case Result/Ok: @@ -132,8 +138,10 @@ def Result/unwrap(res: Result(T, E)) -> Any: case Result/Err: return res.val -# Returns the second result if the first one is `Ok`. -# Otherwise, returns the `Err` of the first result. +#{ + Returns the second result if the first one is `Ok`. + Otherwise, returns the `Err` of the first result. +#} def Result/and(fst: Result(A, E), snd: Result(B, E)) -> Result(B, E): match fst: case Result/Ok: @@ -141,7 +149,7 @@ def Result/and(fst: Result(A, E), snd: Result(B, E)) -> Result(B, E): case Result/Err: return fst -# Maps the error value of a result. +#{ Maps the error value of a result. #} def Result/map_err(res: Result(T, E), f: E -> F) -> Result(T, F): match res: case Result/Ok: @@ -153,7 +161,7 @@ type Tree(T): Node { ~left: Tree(T), ~right: Tree(T) } Leaf { value: T } -# Returns a List converted from a Tree. +#{ Returns a List converted from a Tree. #} def Tree/to_list(tree: Tree(T)) -> List(T): fold tree: case Tree/Leaf: @@ -162,7 +170,7 @@ def Tree/to_list(tree: Tree(T)) -> List(T): list = DiffList/concat(tree.left, tree.right) return DiffList/to_list(list) -# Reverses a tree swapping right and left leaves. +#{ Reverses a tree swapping right and left leaves. #} def Tree/reverse(tree: Tree(T)) -> Tree(T): fold tree: case Tree/Leaf: @@ -170,13 +178,13 @@ def Tree/reverse(tree: Tree(T)) -> Tree(T): case Tree/Node: return ![tree.right, tree.left] -# MAYBE Impl +# MAYBE Impl type Maybe(T): Some { value: T } None -# Removes the value on a Maybe +#{ Returns the value inside the `Maybe` if it is `Some`, and returns `unreachable()` if it is `None`. #} def Maybe/unwrap(m: Maybe(T)) -> T: match m: case Maybe/Some: @@ -190,11 +198,15 @@ type Map(T): Node { value: Maybe(T), ~left: Map(T), ~right: Map(T) } Leaf -# Creates an empty Map +#{ Initializes an empty map. #} def Map/empty() -> Map(T): return Map/Leaf -# Gets a value on a Map +#{ + Retrieves a `value` from the `map` based on the `key` and returns a tuple with the value and the `map` unchanged. + The logic for checking whether a value is or not contained in a `map` is not done in the `get` function, so if + we try to get a key that is not in the map, the program will return `unreachable`. +#} def Map/get (map: Map(T), key: u24) -> (T, Map(T)): match map: case Map/Leaf: @@ -210,7 +222,7 @@ def Map/get (map: Map(T), key: u24) -> (T, Map(T)): return(got, Map/Node(map.value, map.left, rest)) -# Checks if a node has a value on a given key, returning Maybe/Some if it does, Maybe/None otherwise +#{ Checks if a node has a value on a given key, returning Maybe/Some if it does, Maybe/None otherwise #} def Map/get_check (map: Map(T), key: u24) -> (Maybe(T), Map(T)): match map: case Map/Leaf: @@ -225,7 +237,7 @@ def Map/get_check (map: Map(T), key: u24) -> (Maybe(T), Map(T)): (new_value, new_map) = Map/get_check(map.right, (key / 2)) return (new_value, Map/Node(map.value, map.left, new_map)) -# Sets a value on a Map +#{ Sets a value on a Map, returning the map with the value mapped. #} def Map/set (map: Map(T), key: u24, value: T) -> Map(T): match map: case Map/Node: @@ -244,7 +256,7 @@ def Map/set (map: Map(T), key: u24, value: T) -> Map(T): return Map/Node(Maybe/None, Map/Leaf, Map/set(Map/Leaf, (key / 2),value)) -# Checks if a Map contains a given key +#{ Checks if a `map` contains a given `key` and returns 0 or 1 along with and `map` unchanged. #} def Map/contains (map: Map(T), key: u24) -> (u24, Map(T)): match map: case Map/Leaf: @@ -263,7 +275,7 @@ def Map/contains (map: Map(T), key: u24) -> (u24, Map(T)): (new_value, new_map) = Map/contains(map.right, (key / 2)) return (new_value, Map/Node(map.value, map.left, new_map)) -# Applies a funtion to a value on a Map +#{ Applies a function to a value in the map and returns the map with the value mapped. #} def Map/map (map: Map(T), key: u24, f: T -> T) -> Map(T): match map: case Map/Leaf: @@ -309,29 +321,33 @@ def IO/bind(a: IO(A), b: ((Id -> Id) -> A -> IO(B))) -> IO(B): case IO/Call: return IO/Call(a.magic, a.func, a.argm, lambda x: IO/bind(a.cont(x), b)) -# Calls an IO by its name with the given arguments. -# -# The arguments are untyped and not checked for type correctness. -# If type safety is desired, this function should be wrapped with -# another that checks the types of the arguments and of the return. -# -# Always returns a `Result` where the error is an `IOError`, a type -# that either contains an internal error of the IO function, like an -# `errno` in the case of FS functions, or a general Bend IO error, -# like a type error if the arguments are invalid or a name error if -# the called IO is not found. +#{ + Calls an IO by its name with the given arguments. + + The arguments are untyped and not checked for type correctness. + If type safety is desired, this function should be wrapped with + another that checks the types of the arguments and of the return. + + Always returns a `Result` where the error is an `IOError`, a type + that either contains an internal error of the IO function, like an + `errno` in the case of FS functions, or a general Bend IO error, + like a type error if the arguments are invalid or a name error if + the called IO is not found. +#} def IO/call(func: String, argm: Any) -> IO(Result(Any, IOError(Any))): return IO/Call(IO/MAGIC, func, argm, lambda x: IO/Done(IO/MAGIC, x)) -# Maps the result of an IO. +#{ Maps the result of an IO. #} def IO/map(io: IO(A), f: A -> B) -> IO(B): with IO: a <- io return wrap(f(a)) -# Unwraps the `IOError` of the result of an IO, returning the `Inner` variant. -# -# Should only be called if the other `IOError` variants are unreachable. +#{ + Unwraps the `IOError` of the result of an IO, returning the `Inner` variant. + + Should only be called if the other `IOError` variants are unreachable. +#} def IO/unwrap_inner(io: IO(Result(A, IOError(B)))) -> IO(Result(A, B)): with IO: res <- io @@ -343,20 +359,22 @@ def IO/unwrap_inner(io: IO(Result(A, IOError(B)))) -> IO(Result(A, B)): ## Time and sleep -# Returns a monotonically increasing nanosecond timestamp as an u48 -# encoded as a pair of u24s. +#{ + Returns a monotonically increasing nanosecond timestamp as an u48 + encoded as a pair of u24s. +#} def IO/get_time() -> IO((u24, u24)): with IO: res <- IO/call("GET_TIME", *) return wrap(Result/unwrap(res)) -# Sleeps for the given number of nanoseconds, given by an u48 encoded as a pair of u24s. +#{ Sleeps for the given number of nanoseconds, given by an u48 encoded as a pair of u24s. #} def IO/nanosleep(hi_lo: (u24, u24)) -> IO(None): with IO: res <- IO/call("SLEEP", hi_lo) return wrap(Result/unwrap(res)) -# Sleeps for a given amount of seconds as a float. +#{ Sleeps for a given amount of seconds as a float. #} def IO/sleep(seconds: f24) -> IO(None): nanos = seconds * 1_000_000_000.0 lo = f24/to_u24(nanos % 0x1_000_000.0) @@ -366,21 +384,46 @@ def IO/sleep(seconds: f24) -> IO(None): ## File IO ### File IO primitives + +#{ + Opens a file with with `path` being given as a string and `mode` being a string with the mode to open the file in. + The mode should be one of the following: + "r": Read mode + "w": Write mode (write at the beginning of the file, overwriting any existing content) + "a": Append mode (write at the end of the file) + "r+": Read and write mode + "w+": Read and write mode + "a+": Read and append mode +#} def IO/FS/open(path: String, mode: String) -> IO(Result(u24, u24)): return IO/unwrap_inner(IO/call("OPEN", (path, mode))) +#{ Closes the file with the given `file` descriptor. #} def IO/FS/close(file: u24) -> IO(Result(None, u24)): return IO/unwrap_inner(IO/call("CLOSE", file)) +#{ +Reads `num_bytes` bytes from the file with the given `file` descriptor. +Returns a list of U24 with each element representing a byte read from the file. +#} def IO/FS/read(file: u24, num_bytes: u24) -> IO(Result(List(u24), u24)): return IO/unwrap_inner(IO/call("READ", (file, num_bytes))) +#{ + Writes `bytes`, a list of U24 with each element representing a byte, to the file with the given `file` descriptor. + Returns nothing (`*`). +#} def IO/FS/write(file: u24, bytes: List(u24)) -> IO(Result(None, u24)): return IO/unwrap_inner(IO/call("WRITE", (file, bytes))) +#{ Moves the current position of the file with the given `file` descriptor to the given `offset`, an I24 or U24 number, in bytes. #} def IO/FS/seek(file: u24, offset: i24, mode: i24) -> IO(Result(None, u24)): return IO/unwrap_inner(IO/call("SEEK", (file, (offset, mode)))) +#{ + Flushes the file with the given `file` descriptor. + Returns nothing (`*`). +#} def IO/FS/flush(file: u24) -> IO(Result(None, u24)): return IO/unwrap_inner(IO/call("FLUSH", file)) @@ -390,16 +433,19 @@ IO/FS/STDOUT : u24 = 1 IO/FS/STDERR : u24 = 2 ### Seek modes -# Seek from start of file. +#{ Seek from start of file. #} IO/FS/SEEK_SET : i24 = +0 -# Seek from current position. +#{ Seek from current position. #} IO/FS/SEEK_CUR : i24 = +1 -# Seek from end of file. +#{ Seek from end of file. #} IO/FS/SEEK_END : i24 = +2 ### File utilities -# Reads an entire file, returning a list of bytes. +#{ + Reads an entire file with the given `path` and returns a list of U24 with each + element representing a byte read from the file. +#} def IO/FS/read_file(path: String) -> IO(Result(List(u24), u24)): with IO: res_fd <- IO/FS/open(path, "r") @@ -412,7 +458,10 @@ def IO/FS/read_file(path: String) -> IO(Result(List(u24), u24)): case Result/Err: return wrap(Result/Err(res_fd.val)) -# Reads the remaining contents of a file, returning a list of read bytes. +#{ + Reads until the end of the file with the given `file` descriptor. + Returns a list of U24 with each element representing a byte read from the file. +#} def IO/FS/read_to_end(fd: u24) -> IO(Result(List(u24), u24)): return IO/FS/read_to_end.read_chunks(fd, []) @@ -431,7 +480,10 @@ def IO/FS/read_to_end.read_chunks(fd: u24, chunks: List(List(u24))) -> IO(Result case Result/Err: return wrap(Result/Err(res_chunk.val)) -# Reads a single line from a file, returning a list of bytes. +#{ + Reads a line from the file with the given `file` descriptor. + Returns a list of U24 with each element representing a byte read from the file. +#} def IO/FS/read_line(fd: u24) -> IO(Result(List(u24), u24)): return IO/FS/read_line.read_chunks(fd, []) @@ -470,7 +522,7 @@ def IO/FS/read_line.read_chunks(fd: u24, chunks: List(List(u24))) -> IO(Result(L case Result/Err: return wrap(Result/Err(res_chunk.val)) -# Writes a list of bytes to a file given by a path. +#{ Writes `bytes`, a list of U24 with each element representing a byte, as the entire content of the file with the given `path`. #} def IO/FS/write_file(path: String, bytes: List(u24)) -> IO(Result(None, u24)): with IO: res_f <- IO/FS/open(path, "w") @@ -485,15 +537,17 @@ def IO/FS/write_file(path: String, bytes: List(u24)) -> IO(Result(None, u24)): ### Standard input and output utilities -# Prints a string to stdout, encoding it with utf-8. +#{ Prints the string `text` to the standard output, encoded with utf-8. #} def IO/print(text: String) -> IO(None): with IO: res <- IO/FS/write(IO/FS/STDOUT, String/encode_utf8(text)) return wrap(Result/unwrap(res)) -# IO/input() -> IO String -# Read characters from stdin until a newline is found. -# Returns the read input decoded as utf-8. +#{ + IO/input() -> IO String + Reads characters from the standard input until a newline is found. + Returns the read input as a String decoded with utf-8. +#} def IO/input() -> IO(Result(String, u24)): return IO/input.go(DiffList/new) @@ -521,37 +575,48 @@ def IO/input.go(acc: List(u24) -> List(u24)) -> IO(Result(String, u24)): ### Dynamically linked libraries -# Returns an unique id to the library object encoded as a u24 -# 'path' is the path to the library file. -# 'lazy' is a boolean encoded as a u24 that determines if all functions are loaded lazily or upfront. +#{ + Loads a dynamic library file. +#} def IO/DyLib/open(path: String, lazy: u24) -> IO(Result(u24, String)): return IO/unwrap_inner(IO/call("DL_OPEN", (path, lazy))) - -# Calls a function of a previously opened library. -# The returned value is determined by the called function. -# 'dl' is the id of the library object. -# 'fn' is the name of the function in the library. -# 'args' are the arguments to the function. The expected values depend on the called function. +#{ + - `path` is the path to the library file. + - `lazy` is a boolean encoded as a `u24` that determines if all functions are loaded lazily (`1`) or upfront (`0`). + - Returns an unique id to the library object encoded as a `u24`. +#} + +#{ + Calls a function of a previously opened library. + - `dl` is the id of the library object. + - `fn` is the name of the function in the library. + - `args` are the arguments to the function. The expected values depend on the called function. + - The returned value is determined by the called function. +#} def IO/DyLib/call(dl: u24, fn: String, args: Any) -> IO(Result(Any, String)): return IO/unwrap_inner(IO/call("DL_CALL", (dl, (fn, args)))) -# Closes a previously open library. -# Returns nothing. -# 'dl' is the id of the library object. +#{ + Closes a previously open library. + - `dl` is the id of the library object. + - Returns nothing (`*`). +#} def IO/DyLib/close(dl: u24) -> IO(Result(None, String)): return IO/unwrap_inner(IO/call("DL_CLOSE", dl)) # Lazy thunks -# We can defer the evaluation of a function by wrapping it in a thunk. -# Ex: @x (x @arg1 @arg2 @arg3 (f arg1 arg2 arg3) arg1 arg2 arg3) -# -# This is only evaluated when we call it with `(undefer my_thunk)`. -# We can build a defered call directly or by by using `defer` and `defer_arg`. -# -# The example above can be written as: -# -# (defer_arg (defer_arg (defer_arg (defer @arg1 @arg2 @arg3 (f arg1 arg2 arg3)) arg1) arg2) arg3) +#{ + We can defer the evaluation of a function by wrapping it in a thunk. + Ex: @x (x @arg1 @arg2 @arg3 (f arg1 arg2 arg3) arg1 arg2 arg3) + + This is only evaluated when we call it with `(undefer my_thunk)`. + We can build a defered call directly or by by using `defer` and `defer_arg`. + + The example above can be written as: + + (defer_arg (defer_arg (defer_arg (defer @arg1 @arg2 @arg3 (f arg1 arg2 arg3)) arg1) arg2) arg3) +#} def defer(val: T) -> (T -> T) -> T: return lambda x: x(val) @@ -561,38 +626,41 @@ def defer_arg(defered: (Id -> Id) -> A -> B, arg: A) -> ((Id -> Id) -> B): def undefer(defered: (Id -> Id) -> T) -> T: return defered(lambda x: x) -# A function that can be used in unreachable code. -# -# Is not type safe and if used in code that is actually reachable, will corrupt the program. +#{ + A function that can be used in unreachable code. + + Is not type safe and if used in code that is actually reachable, will corrupt the program. +#} def unreachable() -> Any: return * # Native number casts -# Casts a f24 number to a u24. +#{ Casts a f24 number to a u24. #} hvm f24/to_u24 -> (f24 -> u24): ($([u24] ret) ret) -# Casts an i24 number to a u24. +#{ Casts an i24 number to a u24. #} hvm i24/to_u24 -> (i24 -> u24): ($([u24] ret) ret) -# Casts a u24 number to an i24. +#{ Casts a u24 number to an i24. #} hvm u24/to_i24 -> (u24 -> i24): ($([i24] ret) ret) -# Casts a f24 number to an i24. +#{ Casts a f24 number to an i24. #} hvm f24/to_i24 -> (f24 -> i24): ($([i24] ret) ret) -# Casts a u24 number to a f24. +#{ Casts a u24 number to a f24. #} hvm u24/to_f24 -> (u24 -> f24): ($([f24] ret) ret) -# Casts an i24 number to a f24. +#{ Casts an i24 number to a f24. #} hvm i24/to_f24 -> (i24 -> f24): ($([f24] ret) ret) +#{ Casts an u24 native number to a string. #} def u24/to_string(n: u24) -> String: def go(n: u24) -> String -> String: r = n % 10 @@ -604,12 +672,11 @@ def u24/to_string(n: u24) -> String: return lambda t: go(d, String/Cons(c, t)) return go(n, String/Nil) -# String Encoding and Decoding +#{ String Encoding and Decoding #} Utf8/REPLACEMENT_CHARACTER : u24 = '\u{FFFD}' -# Decodes a sequence of bytes to a String using utf-8 encoding. -# Invalid utf-8 sequences are replaced with Utf8/REPLACEMENT_CHARACTER. +#{ Decodes a sequence of bytes to a String using utf-8 encoding. #} String/decode_utf8 (bytes: (List u24)) : String String/decode_utf8 [] = String/Nil String/decode_utf8 bytes = @@ -619,8 +686,7 @@ String/decode_utf8 bytes = List/Cons: (String/Cons got (String/decode_utf8 rest)) } -# Decodes one utf-8 character from the start of a sequence of bytes. -# Returns the decoded character and the remaining bytes. +#{ Decodes a utf-8 character, returns a tuple containing the rune and the rest of the byte sequence. #} Utf8/decode_character (bytes: (List u24)) : (u24, (List u24)) Utf8/decode_character [] = (0, []) Utf8/decode_character [a] = if (<= a 0x7F) { (a, []) } else { (Utf8/REPLACEMENT_CHARACTER, []) } @@ -682,7 +748,7 @@ Utf8/decode_character (List/Cons a (List/Cons b (List/Cons c (List/Cons d rest)) } } -# Encodes a string to a sequence of bytes using utf-8 encoding. +#{ Encodes a string to a sequence of bytes using utf-8 encoding. #} String/encode_utf8 (str: String) : (List u24) String/encode_utf8 (String/Nil) = (List/Nil) String/encode_utf8 (String/Cons x xs) = @@ -717,95 +783,105 @@ String/encode_utf8 (String/Cons x xs) = } } -# Decodes a sequence of bytes to a String using ascii encoding. +#{ Decodes a sequence of bytes to a String using ascii encoding. #} String/decode_ascii (bytes: (List u24)) : String String/decode_ascii (List/Cons x xs) = (String/Cons x (String/decode_ascii xs)) String/decode_ascii (List/Nil) = (String/Nil) -# Encodes a string to a sequence of bytes using ascii encoding. +#{ Encodes a string to a sequence of bytes using ascii encoding. #} String/encode_ascii (str: String) : (List u24) String/encode_ascii (String/Cons x xs) = (List/Cons x (String/encode_ascii xs)) String/encode_ascii (String/Nil) = (List/Nil) # Math -# Math/PI() -> f24 -# The Pi (π) constant. +#{ The Pi (π) constant.#} def Math/PI() -> f24: return 3.1415926535 +#{ Euler's number #} def Math/E() -> f24: return 2.718281828 -# Math/log(x: f24, base: f24) -> f24 -# Computes the logarithm of `x` with the specified `base`. +#{ + Math/log(x: f24, base: f24) -> f24 + Computes the logarithm of `x` with the specified `base`. +#} hvm Math/log -> (f24 -> f24 -> f24): (x ($([|] $(x ret)) ret)) -# Math/atan2(x: f24, y: f24) -> f24 -# Has the same behaviour as `atan2f` in the C math lib. -# Computes the arctangent of the quotient of its two arguments. +#{ + Math/atan2(x: f24, y: f24) -> f24 + Computes the arctangent of `y / x`. + Has the same behaviour as `atan2f` in the C math lib. +#} hvm Math/atan2 -> (f24 -> f24 -> f24): ($([&] $(y ret)) (y ret)) -# Math/sin(a: f24) -> f24 -# Computes the sine of the given angle in radians. +#{ + Math/sin(a: f24) -> f24 + Computes the sine of the given angle in radians. +#} hvm Math/sin -> (f24 -> f24): ($([<<0x0] a) a) -# Math/cos(a: f24) -> f24 -# Computes the cosine of the given angle in radians. +#{ + Math/cos(a: f24) -> f24 + Computes the cosine of the given angle in radians. +#} hvm Math/cos -> (f24 -> f24): (a b) & @Math/PI ~ $([:/2.0] $([-] $(a $([<<0x0] b)))) -# Math/tan(a: f24) -> f24 -# Computes the tangent of the given angle in radians. +#{ + Math/tan(a: f24) -> f24 + Computes the tangent of the given angle in radians. +#} hvm Math/tan -> (f24 -> f24): ($([>>0x0] a) a) -# Computes the cotangent of the given angle in radians. +#{ Computes the cotangent of the given angle in radians. #} Math/cot (a: f24) : f24 = (/ 1.0 (Math/tan a)) -# Computes the secant of the given angle in radians. +#{ Computes the secant of the given angle in radians. #} Math/sec (a: f24) : f24 = (/ 1.0 (Math/cos a)) -# Computes the cosecant of the given angle in radians. +#{ Computes the cosecant of the given angle in radians. #} Math/csc (a: f24) : f24 = (/ 1.0 (Math/sin a)) -# Computes the arctangent of the given angle. +#{ Computes the arctangent of the given angle. #} Math/atan (a: f24) : f24 = (Math/atan2 a 1.0) -# Computes the arcsine of the given angle. +#{ Computes the arcsine of the given angle. #} Math/asin (a: f24) : f24 = (Math/atan2 a (Math/sqrt (- 1.0 (* a a)))) -# Computes the arccosine of the given angle. +#{ Computes the arccosine of the given angle. #} Math/acos (a: f24) : f24 = (Math/atan2 (Math/sqrt (- 1.0 (* a a))) a) -# Converts degrees to radians. +#{ Converts degrees to radians. #} Math/radians (a: f24) : f24 = (* a (/ Math/PI 180.0)) -# Computes the square root of the given number. +#{ Computes the square root of the given number. #} Math/sqrt (n: f24) : f24 = (** n 0.5) -# Round float up to the nearest integer. +#{ Round float up to the nearest integer. #} def Math/ceil(n: f24) -> f24: i_n = i24/to_f24(f24/to_i24(n)) if n <= i_n: return i_n else: return i_n + 1.0 - -# Round float down to the nearest integer. + +#{ Round float down to the nearest integer. #} def Math/floor(n: f24) -> f24: i_n = i24/to_f24(f24/to_i24(n)) if n < i_n: @@ -813,7 +889,7 @@ def Math/floor(n: f24) -> f24: else: return i_n -# Round float to the nearest integer. +#{ Round float to the nearest integer. #} def Math/round(n: f24) -> f24: i_n = i24/to_f24(f24/to_i24(n)) if (n - i_n) < 0.5: diff --git a/src/fun/parser.rs b/src/fun/parser.rs index b498c7ade..928910b62 100644 --- a/src/fun/parser.rs +++ b/src/fun/parser.rs @@ -537,7 +537,7 @@ impl<'a> FunParser<'a> { } // Number - if self.peek_one().map_or(false, |c| c.is_ascii_digit()) { + if self.peek_one().is_some_and(|c| c.is_ascii_digit()) { unexpected_tag(self)?; let num = self.parse_u32()?; return Ok(Pattern::Num(num)); @@ -719,7 +719,7 @@ impl<'a> FunParser<'a> { } // Native Number - if self.peek_one().map_or(false, is_num_char) { + if self.peek_one().is_some_and(is_num_char) { unexpected_tag(self)?; let num = self.parse_number()?; return Ok(Term::Num { val: num }); @@ -1499,7 +1499,7 @@ pub trait ParserCommons<'a>: Parser<'a> { self.consume_exactly(keyword)?; let end_idx = *self.index(); let input = &self.input()[*self.index()..]; - let next_is_name = input.chars().next().map_or(false, is_name_char); + let next_is_name = input.chars().next().is_some_and(is_name_char); if !next_is_name { Ok(()) } else { @@ -1510,7 +1510,7 @@ pub trait ParserCommons<'a>: Parser<'a> { fn starts_with_keyword(&mut self, keyword: &str) -> bool { if self.starts_with(keyword) { let input = &self.input()[*self.index() + keyword.len()..]; - let next_is_name = input.chars().next().map_or(false, is_name_char); + let next_is_name = input.chars().next().is_some_and(is_name_char); !next_is_name } else { false @@ -1660,7 +1660,7 @@ pub trait ParserCommons<'a>: Parser<'a> { let num_str = self.take_while(move |c| c.is_digit(radix as u32) || c == '_'); let num_str = num_str.chars().filter(|c| *c != '_').collect::(); - let next_is_hex = self.peek_one().map_or(false, |c| "0123456789abcdefABCDEF".contains(c)); + let next_is_hex = self.peek_one().is_some_and(|c| "0123456789abcdefABCDEF".contains(c)); if next_is_hex || num_str.is_empty() { self.expected(format!("valid {radix} digit").as_str()) } else { @@ -1672,7 +1672,7 @@ pub trait ParserCommons<'a>: Parser<'a> { fn u32_with_radix(&mut self, radix: Radix) -> ParseResult { let num_str = self.take_while(move |c| c.is_digit(radix as u32) || c == '_'); let num_str = num_str.chars().filter(|c| *c != '_').collect::(); - let next_is_hex = self.peek_one().map_or(false, |c| "0123456789abcdefABCDEF".contains(c)); + let next_is_hex = self.peek_one().is_some_and(|c| "0123456789abcdefABCDEF".contains(c)); if next_is_hex || num_str.is_empty() { self.expected(format!("valid {radix} digit").as_str()) } else {