From 084e7cae7eb1518c9cd511b7ccc520770746dfa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Wed, 15 Jan 2025 21:01:23 +0100 Subject: [PATCH 01/15] Update, correct, and expand documentation --- FEATURES.md | 143 +++++---- GUIDE.md | 131 ++++---- docs/builtins.md | 524 ++++++++++++++++++-------------- docs/cli-arguments.md | 12 +- docs/compiler-options.md | 11 + docs/defining-data-types.md | 56 ++-- docs/dups-and-sups.md | 4 +- docs/ffi.md | 10 +- docs/imports.md | 2 + docs/lazy-definitions.md | 21 +- docs/native-numbers.md | 13 +- docs/pattern-matching.md | 44 +-- docs/syntax.md | 16 +- docs/using-scopeless-lambdas.md | 11 +- 14 files changed, 564 insertions(+), 434 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 8f72a2bd4..9cffffb5f 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -10,7 +10,7 @@ To see some more complex examples programs, check out the [examples](examples/) 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 +23,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,7 +44,7 @@ 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: Any) -> Any: # This destructures the tuple into the two values it holds. # '*' means that the value is discarded and not bound to any variable. (fst, *) = x @@ -53,22 +53,22 @@ def tuple_fst(x): # With an object (similar to what other languages call a struct, a class or a record) object Pair { fst, snd } -def Pair/fst(x): +def Pair/fst(x: Pair) -> Any: 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: Paul) -> Any: open Pair: x return x.fst # This is how we can create new objects. -def Pair/with_one(x): +def Pair/with_one(x: Pair) -> Pair: 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) -> Pair: open Pair: x # We can also call the constructor like any normal function. return Pair(x.snd, x.fst) @@ -92,11 +92,11 @@ You can read how this is done internally by the compiler in [Defining data types 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,7 +110,7 @@ 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): +def MyTree.sum(x: MyTree) -> u24: # Sum all the values in the tree. fold x: # The fold is implicitly called for fields marked with '~' in their definition. @@ -119,7 +119,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. @@ -134,20 +134,20 @@ def main: 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 +159,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 +178,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: @@ -201,13 +201,13 @@ To use a variable twice without duplicating it, you can use a `use` statement. It inlines clones of some value in the statements that follow it. ```py -def foo(x): - use result = bar(1, x) +def foo(x: Any) -> (Any, Any): + use result = (1, x) return (result, result) # Is equivalent to -def foo(x): - return (bar(1, x), bar(1, x)) +def foo(x: Any) -> (Any, Any): + return ((1, x), (1, x)) ``` Note that any variable in the `use` will end up being duplicated. @@ -215,7 +215,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: @@ -227,15 +227,12 @@ If your recursive function is not based on pattern matching syntax (like `if`, ` ```py # 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 - ) +def scott_list.add(xs: List, add: u24) -> List: + 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( +def scott_list.add(xs: List, add: u24) -> List: + return xs( λxs.head xs.tail: λadd: λc n: (c (xs.head + add) scott_list.sum(xs.tail, add)), λadd: λc λn: n, add @@ -250,7 +247,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,28 +263,35 @@ 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: +Bend now has a way to convert between the different number types! +Here's some of the builtin functions you can use to cast any native number into the corresponding type: ```py -switch x = 4: - # From '0' to n, ending with the default case '_'. - case 0: "zero" - case 1: "one" - case 2: "two" - # The default case binds the name - - # where 'arg' is the name of the argument and 'n' is the next number. - # In this case, it's 'x-3', which will have value (4 - 3) = 1 - case _: String.concat("other: ", (String.from_num x-3)) -``` +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. +# These are the definitions of the builtin types. ```py -def main: +type String: + Cons { head, ~tail } + Nil +type List: + Cons { head, ~tail } + Nil +``` + +```py +# Here's an example of a List of Strings +def main() -> List(String): return ["You: Hello, 🌎", "🌎: Hello, user"] ``` @@ -296,34 +300,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 +335,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"} -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,7 +381,7 @@ type Bool: True False -def is_odd(x): +def is_odd(x: u24) -> Bool: switch x: case 0: return Bool/False @@ -394,7 +389,7 @@ def is_odd(x): return is_even(x-1) (is_even n) = switch n { - 0: return Bool/True + 0: Bool/True _: (is_odd n-1) } diff --git a/GUIDE.md b/GUIDE.md index ea89b25c2..8508c40bd 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) ``` -bend run-rs main.bend +To run the program above, type: + +``` +bend run 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` 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,7 +304,7 @@ 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" @@ -299,13 +314,13 @@ def parity(x): ... because that would mutate 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) ``` @@ -411,8 +426,9 @@ def enum(tree): return ![tree.left(idx * 2 + 0), tree.right(idx * 2 + 1)] case Tree/Leaf: return !(idx, tree.value) +@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@VER PRA TIPAR ESSA FUNÇÃO QUE NÃO TA FUNCIONANDO@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -def main: +def main() -> Tree: tree = ![![!1, !2],![!3, !4]] return enum(tree) ``` @@ -434,11 +450,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) -> List: # exercise ? -def main: +def main() -> List: return reverse([1,2,3]) ``` @@ -450,7 +466,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,14 +591,15 @@ 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) else: sum = i return sum -``` +``` @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +@@@@@@@@@@@@@@@@@@@@@@@@@ VER DEPOIS PORQUE ISSO ENTRA NUM LOOP INFINITO@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ And that's the parallel "Hello, world"! Now, let's finally run it. But first, let's measure its single-core performance. Also, remember that, for now, Bend @@ -655,14 +672,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: Bend's type system does not support functions such as the one, but you can still write it. -def sum(d, t): +```python +def sum(d: u24, t: u24) -> u24: switch d: case 0: return t @@ -670,14 +690,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 +708,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 +716,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 +724,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 +732,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 +768,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 +779,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 +788,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 +818,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..9ede498f5 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 +```python +#{ Splits a list into two lists at the first occurrence of a value. - +#} +def List/split_once(xs: List(T), cond: T -> u24) +``` +Example: ```python -List/split_once(xs: List(T), val: T) -> (Result(List(T), List(T))) + # Split list at first even number + list = [1,3,4,5,6] + result = List/split_once(list, λx: x % 2 == 0) + return result + # Result: ([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,12 @@ 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`. +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 +231,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 +255,22 @@ Here, `map` must be the name of the `Map` variable, and the keys inside `[]` can ### Map/empty -Initializes an empty map. - ```python +#{ + Initializes an empty map. +#} Map/empty = Map/Leaf ``` ### 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. +#} +def Map/get (map: Map(T), key: u24) -> (T, Map(T)) ``` #### Syntax @@ -279,22 +296,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 +336,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 +355,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 as a `u24` number and the `map` unchanged. +#} +def Map/contains (map: Map(T), key: u24) -> (u24, Map(T)) #### Syntax @@ -394,20 +376,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 +394,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 +440,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 +491,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 +575,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 +590,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,11 +602,11 @@ 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. +#} +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. @@ -616,11 +615,11 @@ Calls a function of a previously opened library. #### IO/DyLib/close ```py -def IO/DyLib/close(dl: u24) -> None +#{ + Closes a previously open library. +#} +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 (`*`). @@ -629,73 +628,107 @@ Closes a previously open library. ### to_f24 ```py -def to_f24(x: any number) -> f24 -``` - -Casts any native number to an f24. +#{ + Casts an u24 number to an f24. +#} +hvm u24/to_f24 -> (u24 -> f24) +#{ + Casts an i24 number to an f24. +#} +hvm 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. +#} +hvm f24/to_u24 -> (f24 -> u24) +#{ + Casts an i24 number to an u24. +#} +hvm i24/to_u24 -> (i24 -> u24) +``` ### to_i24 ```py -def to_i24(x: any number) -> i24 +#{ + Casts an u24 number to an i24. +#} +hvm u24/to_i24 -> (u24 -> i24): +#{ + Casts a f24 number to an i24. +#} +hvm 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 +736,185 @@ 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`. +#} +hvm Math/log -> (f24 -> f24 -> f24): + (x ($([|] $(x ret)) ret)) ``` -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. +#} +hvm Math/atan2 -> (f24 -> f24 -> f24): + ($([&] $(y ret)) (y ret)) ``` -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: + return 3.1415926535 ``` ### Math/E -Euler's number ```py -def Math/E: f24 = 2.718281828 +#{ +Euler's number +#} +def Math/E() -> f24: + return 2.718281828 ``` ### 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. +#} +hvm 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. +#} +hvm 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. +#} +hvm 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..d2712b871 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -35,11 +35,13 @@ def main(x, y): > bend run +5 +3 {+2 -2} -# Calling with three argument -# 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 -# it wouldn't be well-typed. +#{ + Calling with three argument + 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 + it wouldn't be well-typed. +}# > bend run +5 +3 +1 {+2 -2} ``` diff --git a/docs/compiler-options.md b/docs/compiler-options.md index 71d817b6d..06f9d3634 100644 --- a/docs/compiler-options.md +++ b/docs/compiler-options.md @@ -12,6 +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 | Checks if the types are coherent to bend's type system | ## Eta-reduction @@ -259,3 +260,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 that the types are all declared correctly. + +```py + def main() -> Bool: + return 3 +``` +If type checking is 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..0b5dcb4ef 100644 --- a/docs/defining-data-types.md +++ b/docs/defining-data-types.md @@ -3,48 +3,64 @@ 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 +#{ +An option either contains some value, or None +}# +type Option: + Some { value } + None ``` -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) -let (Box value) = boxed; 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. They work like match expressions with explicit bindings: ```py -(Option.map (Some value) f) = (Some (f value)) -(Option.map None f) = None +def Option/map(opt: Option(T), f: T -> U) -> Option(U): + match opt: + case Option/Some: + return f(Option/Some(opt.value)) + case Option/None: + return Option/None ``` 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 +def Option/is_both_some(lft: Option(T), rgt: Option(T)) -> Boolean: + match lft: + case Option/Some: + match rgt: + case Option/Some: + return True + match rgt: + case Option/None: + return False ``` You can read more about pattern matching rules in [Pattern matching](/docs/pattern-matching.md). 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..94b84362e 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -13,7 +13,7 @@ def main(): # The second argument is '0' if we want to load all functions immediately. # Otherwise it should be '1' when we want to load functions as we use them. # 'dl' is the unique id of the dynamic library. - dl <- IO/DyLib/open("./libbend_dirs.so", 0) + dl <- IO/DyLib/open("./libbend_dirs.so", 0) #IO(Result) # We can now call functions from the dynamic library. # We need to know what functions are available in the dynamic library. @@ -27,8 +27,8 @@ 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) + files_bytes <- IO/DyLib/call(Result/unwrap(dl), "ls", "./") #IO(Result) + 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 +40,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(Result/unwrap(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(Result/unwrap(dl)) return wrap(status) ``` diff --git a/docs/imports.md b/docs/imports.md index ada25cd6f..9c57793e6 100644 --- a/docs/imports.md +++ b/docs/imports.md @@ -11,6 +11,8 @@ from path import name # or import path/name ``` +## Placement +Imports should be placed at the top of a file. ## Project Structure Let's assume we have a bend project with the following structure: diff --git a/docs/lazy-definitions.md b/docs/lazy-definitions.md index 1a9a83838..3095c976e 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 and hangs on strict-mode: ```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 never gets reduced. 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. +To handle recursion properly, recursive calls should be wrapped in top-level combinators. This ensures lazy unrolling of recursive terms, preventing infinite expansion during reduction. While [supercombinators](https://en.wikipedia.org/wiki/Supercombinator) are commonly used for this purpose, 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..83d7d354c 100644 --- a/docs/native-numbers.md +++ b/docs/native-numbers.md @@ -50,13 +50,22 @@ 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. +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. We recently introduced 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 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. -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..b0f9c6bf5 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -88,28 +88,37 @@ 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 + +unchecked Foo__C0: _ +(Foo__C0) = λ* λa λb λc (A c a b) # Foo.case_0.case_false.case_cons.case_cons +unchecked Foo__C1: _ +(Foo__C1) = λa switch a { 0: λ* B; _: Foo__C0; } # Part of cons pattern matching +unchecked Foo__C2: _ +(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false.case_cons +unchecked Foo__C3: _ +(Foo__C3) = λa switch a { 0: B; _: Foo__C2; } # Part of cons pattern matching +unchecked Foo__C4: _ +(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false +unchecked Foo__C5: _ +(Foo__C5) = λa switch a { 0: λ* B; _: Foo__C4; } # Foo.case_0 +unchecked Foo__C6: _ +(Foo__C6) = λ* λa (+ a 0) # Foo.case_+.case_false +unchecked Foo__C7: _ +(Foo__C7) = λa switch a { 0: λ* 0; _: Foo__C6; } # Part of bool pattern matching +unchecked Foo__C8: _ +(Foo__C8) = λa λ* (a Foo__C5) # Part of main pattern matching +unchecked Foo__C9: _ +(Foo__C9) = λa λb λ* (b Foo__C7 a) # Foo.case_+ ``` -Pattern matching equations also support matching on non-consecutive numbers: +Pattern matching equations support non-consecutive numeric or character values. For example, this parser matches specific characters with corresponding tokens, with a catch-all case for any other character: ```rust Parse '(' = Token.LParenthesis 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 +132,19 @@ 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. +A key difference from direct switch expressions is how variable binding works. In pattern matching equations, you can't directly access predecessor values. 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`. +This example shows how pattern order matters, 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 ``` -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 List||String/cons and List||String/nil ```py Hi "hi" = 1 @@ -145,7 +155,7 @@ 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 diff --git a/docs/syntax.md b/docs/syntax.md index 76e80c8ec..6c566c149 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) ``` @@ -187,7 +187,7 @@ Returns the expression that follows. The last statement of each branch of a func ```py # Allowed, all branches return -def max(a, b): +def max(a: T, b: T) -> T: if a > b: return a else: @@ -207,7 +207,7 @@ def Foo(x): ```py # Not allowed, one of the branches doesn't return -def Foo(a, b): +def Foo(a: T, b: T) -> T: if a < b: return a else: @@ -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: 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, the 3 number types cannot be mixed, but can be converted in order to correspond to each other. | 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: T, y: T) -> T: 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) From be6662da380e98506e315d2500a8cc5f27adf8f6 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: Fri, 17 Jan 2025 16:20:58 +0100 Subject: [PATCH 02/15] Apply suggestions from code review Co-authored-by: Nicolas Abril --- FEATURES.md | 12 ++++++------ docs/lazy-definitions.md | 2 +- docs/syntax.md | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 9cffffb5f..09c294dde 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -44,31 +44,31 @@ You can bundle multiple values into a single value using a tuple or a struct. ```py # With a tuple -def tuple_fst(x: Any) -> Any: +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: Pair) -> Any: +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: Paul) -> Any: +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: Pair) -> Pair: +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: Pair) -> Pair: +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) diff --git a/docs/lazy-definitions.md b/docs/lazy-definitions.md index 3095c976e..551485771 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 and 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) diff --git a/docs/syntax.md b/docs/syntax.md index 6c566c149..45cf30a5d 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -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: Tree) -> u24: +def fold(x: Tree(u24)) -> u24: match x: case Tree/Node: return x.value + fold(x.left) + fold(x.right) @@ -1357,7 +1357,7 @@ Documentation for functions is meant to be written as a multiline comment right This function always returns the second value that was used as argument. #} -def second(x: T, y: T) -> T: +def second(x: A, y: B) -> B: return y ``` From a962036eac30fbac709e7f32eb4c696b6464b7b9 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: Fri, 17 Jan 2025 19:32:59 +0100 Subject: [PATCH 03/15] Apply suggestions from code review Co-authored-by: Nicolas Abril --- GUIDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GUIDE.md b/GUIDE.md index 8508c40bd..5139820bc 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -450,11 +450,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: List) -> List: +def reverse(list: List(T)) -> List(T): # exercise ? -def main() -> List: +def main() -> List(u24): return reverse([1,2,3]) ``` @@ -679,7 +679,7 @@ def gen(d: u24, x: u24) -> Any: case _: return (gen(d-1, x * 2 + 1), gen(d-1, x * 2)) ``` -> Note: Bend's type system does not support functions such as the one, but you can still write it. +> Note: The type of this function can't be expressed with Bend's type system, but we can still write it using `Any`. ```python def sum(d: u24, t: u24) -> u24: From a61b5e4bbde959fecd7e65cb618fd66de4b196f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Fri, 17 Jan 2025 19:46:37 +0100 Subject: [PATCH 04/15] Add changes asked by PR Review --- FEATURES.md | 28 ++++++++++--------- GUIDE.md | 12 ++++----- docs/builtins.md | 54 +++++++++++++++++-------------------- docs/cli-arguments.md | 17 ++++++------ docs/compiler-options.md | 4 +-- docs/defining-data-types.md | 38 ++++++++++++-------------- docs/ffi.md | 8 +++--- docs/imports.md | 3 +-- docs/lazy-definitions.md | 4 +-- docs/native-numbers.md | 6 ++++- docs/pattern-matching.md | 42 +++++++++++------------------ docs/syntax.md | 6 ++--- 12 files changed, 104 insertions(+), 118 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 9cffffb5f..67f23d017 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -6,8 +6,7 @@ You can read the full reference for both of them [here](docs/syntax.md), but the To see some more complex examples programs, check out the [examples](examples/) folder. ### Basic features - -We can start with a basic program that adds the numbers 3 and 2. +Types in Bend are completely optional - you can write programs without any type annotations, but we'll be typing every function for clarity. We can start with a basic program that adds the numbers 3 and 2. Though ```py def main() -> u24: @@ -110,8 +109,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 +#{ + Sum all the values in the tree. +#} def MyTree.sum(x: MyTree) -> u24: - # Sum all the values in the tree. fold x: # The fold is implicitly called for fields marked with '~' in their definition. case MyTree/Node: @@ -130,6 +131,8 @@ def main() -> u24: return MyTree.sum(x) ``` +It should be noted that in this case, 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. @@ -201,12 +204,12 @@ To use a variable twice without duplicating it, you can use a `use` statement. It inlines clones of some value in the statements that follow it. ```py -def foo(x: Any) -> (Any, Any): +def foo(x): use result = (1, x) return (result, result) # Is equivalent to -def foo(x: Any) -> (Any, Any): +def foo(x): return ((1, x), (1, x)) ``` @@ -225,18 +228,19 @@ def native_num_to_adt(n: u24) -> Nat: 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: List, add: u24) -> List: +def scott_list.add(xs, add): 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: List, add: u24) -> List: +def scott_list.add(xs, add): 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. @@ -282,11 +286,11 @@ Bend has Lists and Strings, which support Unicode characters. # These are the definitions of the builtin types. ```py type String: - Cons { head, ~tail } Nil -type List: - Cons { head, ~tail } + Cons { head: u24, ~tail: String } +type List(T): Nil + Cons { head: T, ~tail: List(T) } ``` ```py @@ -388,7 +392,7 @@ def is_odd(x: u24) -> Bool: case _: return is_even(x-1) -(is_even n) = switch n { +is_even(n: u24): u24 = switch n { 0: Bool/True _: (is_odd n-1) } diff --git a/GUIDE.md b/GUIDE.md index 8508c40bd..44e2dedc7 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -95,11 +95,11 @@ def main() -> IO(u24): To run the program above, type: ``` -bend run main.bend +bend run-c main.bend ``` 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` 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 +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 @@ -311,7 +311,7 @@ def parity(x: u24) -> String: 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: u24) -> String: @@ -426,9 +426,8 @@ def enum(tree): return ![tree.left(idx * 2 + 0), tree.right(idx * 2 + 1)] case Tree/Leaf: return !(idx, tree.value) -@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@VER PRA TIPAR ESSA FUNÇÃO QUE NÃO TA FUNCIONANDO@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -def main() -> Tree: +def main() -> Tree(u24): tree = ![![!1, !2],![!3, !4]] return enum(tree) ``` @@ -598,8 +597,7 @@ def main() -> u24: else: sum = i return sum -``` @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ -@@@@@@@@@@@@@@@@@@@@@@@@@ VER DEPOIS PORQUE ISSO ENTRA NUM LOOP INFINITO@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ +``` And that's the parallel "Hello, world"! Now, let's finally run it. But first, let's measure its single-core performance. Also, remember that, for now, Bend diff --git a/docs/builtins.md b/docs/builtins.md index 9ede498f5..2fb60a4de 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -130,7 +130,7 @@ List/filter(xs: List(T), pred: T -> Bool) -> 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) +def List/split_once(xs: List(T), cond: T -> u24) -> (Result((List(T), List(T)), List(T))): ``` Example: ```python @@ -138,7 +138,7 @@ Example: list = [1,3,4,5,6] result = List/split_once(list, λx: x % 2 == 0) return result - # Result: ([1,3], [5,6]) + # Result: Result/Ok/tag ([1, 3], [5, 6]) ``` ## Result @@ -205,7 +205,6 @@ maybe = Maybe/Some(Nat/Succ(Nat/Zero)) ## Maybe functions ### Maybe/unwrap -Returns the value inside the `Maybe` if it is `Some`, and returns `unreachable()` if it is `None`. ```python #{ Returns the value inside the `Maybe` if it is `Some`, and returns `unreachable()` if it is `None`. @@ -268,8 +267,7 @@ Map/empty = Map/Leaf ```rust #{ - Retrieves a `value` from the `map` based on the `key` and returns a tuple with the value and the `map` unchanged. -#} + 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)) ``` @@ -358,7 +356,7 @@ x[0] @= lambda y: String/concat(y, " and mapped") ```python #{ - Checks if a `map` contains a given `key` and returns 0 or 1 as a `u24` number and the `map` unchanged. + 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)) @@ -604,24 +602,26 @@ def IO/DyLib/open(path: String, lazy: u24) -> IO(Result(u24, String)) ```py #{ 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)) ``` -- `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 #{ 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)) ``` -- `dl` is the id of the library object. -- Returns nothing (`*`). + ## Native number casting @@ -631,12 +631,12 @@ def IO/DyLib/close(dl: u24) -> IO(Result(None, String)) #{ Casts an u24 number to an f24. #} -hvm u24/to_f24 -> (u24 -> f24) +def u24/to_f24 -> (u24 -> f24) #{ Casts an i24 number to an f24. #} -hvm i24/to_f24 -> (i24 -> f24) +def i24/to_f24 -> (i24 -> f24) ``` ### to_u24 @@ -644,12 +644,12 @@ hvm i24/to_f24 -> (i24 -> f24) #{ Casts a f24 number to an u24. #} -hvm f24/to_u24 -> (f24 -> u24) +def f24/to_u24 -> (f24 -> u24) #{ Casts an i24 number to an u24. #} -hvm i24/to_u24 -> (i24 -> u24) +def i24/to_u24 -> (i24 -> u24) ``` ### to_i24 @@ -657,11 +657,11 @@ hvm i24/to_u24 -> (i24 -> u24) #{ Casts an u24 number to an i24. #} -hvm u24/to_i24 -> (u24 -> i24): +def u24/to_i24 -> (u24 -> i24): #{ Casts a f24 number to an i24. #} -hvm f24/to_i24 -> (f24 -> i24): +def f24/to_i24 -> (f24 -> i24): ``` ### to_string @@ -739,8 +739,7 @@ Utf8/REPLACEMENT_CHARACTER : u24 = '\u{FFFD}' #{ Computes the logarithm of `x` with the specified `base`. #} -hvm Math/log -> (f24 -> f24 -> f24): - (x ($([|] $(x ret)) ret)) +def Math/log -> (f24 -> f24 -> f24) ``` @@ -751,8 +750,7 @@ hvm Math/log -> (f24 -> 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)) +def Math/atan2 -> (f24 -> f24 -> f24) ``` @@ -763,8 +761,7 @@ hvm Math/atan2 -> (f24 -> f24 -> f24): #{ Defines the Pi constant. #} -def Math/PI() -> f24: - return 3.1415926535 +def Math/PI() -> f24 ``` ### Math/E @@ -774,8 +771,7 @@ def Math/PI() -> f24: #{ Euler's number #} -def Math/E() -> f24: - return 2.718281828 +def Math/E() -> f24 ``` ### Math/sin @@ -785,7 +781,7 @@ def Math/E() -> f24: #{ Computes the sine of the given angle in radians. #} -hvm Math/sin -> (f24 -> f24) +def Math/sin -> (f24 -> f24) ``` ### Math/cos @@ -795,7 +791,7 @@ hvm Math/sin -> (f24 -> f24) #{ Computes the cosine of the given angle in radians. #} -hvm Math/cos -> (f24 -> f24) +def Math/cos -> (f24 -> f24) ``` ### Math/tan @@ -805,7 +801,7 @@ hvm Math/cos -> (f24 -> f24) #{ Computes the tangent of the given angle in radians. #} -hvm Math/tan -> (f24 -> f24) +def Math/tan -> (f24 -> f24) ``` ### Math/cot diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index d2712b871..ea4ec7f0e 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -33,15 +33,14 @@ def main(x, y): # Calling with two argument > bend run +5 +3 -{+2 -2} - -#{ - Calling with three argument - 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 - it wouldn't be well-typed. -}# +{+2 -2}# +# 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 +# it wouldn't be well-typed. > bend run +5 +3 +1 {+2 -2} ``` + + diff --git a/docs/compiler-options.md b/docs/compiler-options.md index 06f9d3634..a5e4a7e7d 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 | Checks if the types are coherent to bend's type system | +| `-Otype-check` `-Ono-type-check` | type-check | Checks the type compatibility | ## Eta-reduction @@ -263,7 +263,7 @@ Note: IO is **only** available with `-Oadt-num-scott`. ## Type Checking -Type checking is enabled by default and verifies that the types are all declared correctly. +Type checking is enabled by default and verifies and enforces the constraints of types. ```py def main() -> Bool: diff --git a/docs/defining-data-types.md b/docs/defining-data-types.md index 0b5dcb4ef..2fb0109f9 100644 --- a/docs/defining-data-types.md +++ b/docs/defining-data-types.md @@ -3,9 +3,7 @@ It is possible to easily define complex data types using the `type` keyword. ```py -#{ - A Boolean is either True or False -}# +# A Boolean is either True or False type Bool: True False @@ -13,14 +11,23 @@ type Bool: If a constructor has any arguments, parentheses are necessary around it: ```py -#{ -An option either contains some value, or None -}# +# An option either contains some value, or None type Option: Some { value } None ``` +If the data type has a single constructor, it can be destructured using `open`: +```py +# A Box is a wrapper around a value. +type Boxed: + Box { 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. @@ -37,12 +44,8 @@ Rules can also have patterns. They work like match expressions with explicit bindings: ```py -def Option/map(opt: Option(T), f: T -> U) -> Option(U): - match opt: - case Option/Some: - return f(Option/Some(opt.value)) - case Option/None: - return Option/None +(Option.map (Some value) f) = (Some (f value)) +(Option.map None f) = None ``` However, they also allow matching on multiple values at once, which is something that regular `match` can't do: @@ -52,15 +55,8 @@ type Boolean: True False -def Option/is_both_some(lft: Option(T), rgt: Option(T)) -> Boolean: - match lft: - case Option/Some: - match rgt: - case Option/Some: - return True - match rgt: - case Option/None: - return False +(Option.is_both_some (Some lft_val) (Some rgt_val)) = True +(Option.is_both_some lft rgt) = False ``` You can read more about pattern matching rules in [Pattern matching](/docs/pattern-matching.md). diff --git a/docs/ffi.md b/docs/ffi.md index 94b84362e..6b67d19af 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -13,7 +13,7 @@ def main(): # The second argument is '0' if we want to load all functions immediately. # Otherwise it should be '1' when we want to load functions as we use them. # 'dl' is the unique id of the dynamic library. - dl <- IO/DyLib/open("./libbend_dirs.so", 0) #IO(Result) + dl <- Result/unwrap(IO/DyLib/open("./libbend_dirs.so", 0)) # We can now call functions from the dynamic library. # We need to know what functions are available in the dynamic library. @@ -27,7 +27,7 @@ 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(Result/unwrap(dl), "ls", "./") #IO(Result) + files_bytes <- IO/DyLib/call(dl, "ls", "./") files_str = String/decode_utf8(Result/unwrap(files_bytes)) files = String/split(files_str, '\n') @@ -40,14 +40,14 @@ def main(): status = wrap(-1) case List/Nil: # The directory doesn't exist, create it. - * <- IO/DyLib/call(Result/unwrap(dl), "mkdir", "./my_dir") + * <- IO/DyLib/call(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(Result/unwrap(dl)) + * <- IO/DyLib/close(dl) return wrap(status) ``` diff --git a/docs/imports.md b/docs/imports.md index 9c57793e6..d3a849bbc 100644 --- a/docs/imports.md +++ b/docs/imports.md @@ -10,9 +10,8 @@ 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. ``` -## Placement -Imports should be placed at the top of a file. ## Project Structure Let's assume we have a bend project with the following structure: diff --git a/docs/lazy-definitions.md b/docs/lazy-definitions.md index 3095c976e..b9b73735f 100644 --- a/docs/lazy-definitions.md +++ b/docs/lazy-definitions.md @@ -16,7 +16,7 @@ Map = λf λlist Main = (Map λx (+ x 1) (Cons 1 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 never gets reduced. +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. @@ -40,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. -To handle recursion properly, recursive calls should be wrapped in top-level combinators. This ensures lazy unrolling of recursive terms, preventing infinite expansion during reduction. While [supercombinators](https://en.wikipedia.org/wiki/Supercombinator) are commonly used for this purpose, other combinator patterns can work as well, as long as they're lifted to the top level. +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 83d7d354c..e06ac323e 100644 --- a/docs/native-numbers.md +++ b/docs/native-numbers.md @@ -50,7 +50,11 @@ 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. Instead, you should make sure that all numbers are of the same type. We recently introduced a way to convert between the different number types, and using it is very easy, here's an example: +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 + +We introduced a way to convert between the different number types, and using it is very easy, here's an example: ```py def main() -> _: diff --git a/docs/pattern-matching.md b/docs/pattern-matching.md index b0f9c6bf5..1f7623910 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -89,29 +89,21 @@ To ensure that recursive pattern matching functions don't loop in strict mode, i # This is what the Foo function actually compiles to. # With -Olinearize-matches and -Ofloat-combinators (default on strict mode) -unchecked Foo__C0: _ -(Foo__C0) = λ* λa λb λc (A c a b) # Foo.case_0.case_false.case_cons.case_cons -unchecked Foo__C1: _ -(Foo__C1) = λa switch a { 0: λ* B; _: Foo__C0; } # Part of cons pattern matching -unchecked Foo__C2: _ -(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false.case_cons -unchecked Foo__C3: _ -(Foo__C3) = λa switch a { 0: B; _: Foo__C2; } # Part of cons pattern matching -unchecked Foo__C4: _ -(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false -unchecked Foo__C5: _ -(Foo__C5) = λa switch a { 0: λ* B; _: Foo__C4; } # Foo.case_0 -unchecked Foo__C6: _ -(Foo__C6) = λ* λa (+ a 0) # Foo.case_+.case_false -unchecked Foo__C7: _ -(Foo__C7) = λa switch a { 0: λ* 0; _: Foo__C6; } # Part of bool pattern matching -unchecked Foo__C8: _ -(Foo__C8) = λa λ* (a Foo__C5) # Part of main pattern matching -unchecked Foo__C9: _ -(Foo__C9) = λa λb λ* (b Foo__C7 a) # Foo.case_+ +(Foo__C5) = λa switch a { 0: λ* B; _: Foo__C4; } # Foo.case_0 +(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false +(Foo__C3) = λa switch a { 0: B; _: Foo__C2; } # Part of cons pattern matching +(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false.case_cons +(Foo__C1) = λa switch a { 0: λ* B; _: Foo__C0; } # Part of cons pattern matching +(Foo__C0) = λ* λa λb λc (A c a b) # Foo.case_0.case_false.case_cons.case_cons + +(Foo__C6) = λ* λa (+ a 0) # Foo.case_+.case_false +(Foo__C7) = λa switch a { 0: λ* 0; _: Foo__C6; } # Part of bool pattern matching +(Foo__C9) = λa λb λ* (b Foo__C7 a) # Foo.case_+ +(Foo__C8) = λa λ* (a Foo__C5) # Part of main pattern matching +# 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 support non-consecutive numeric or character values. For example, this parser matches specific characters with corresponding tokens, with a catch-all case for any other character: +Pattern matching equations also support matching on non-consecutive numbers: ```rust Parse '(' = Token.LParenthesis Parse ')' = Token.RParenthesis @@ -132,19 +124,17 @@ Parse = λarg0 switch matched = (- arg0 '(') { } } ``` -A key difference from direct switch expressions is how variable binding works. In pattern matching equations, you can't directly access predecessor values. Instead, variables (like n above) are bound to computed expressions based on the matched value. +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`. - -This example shows how pattern order matters, with wildcards covering multiple specific cases: +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 ``` -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 diff --git a/docs/syntax.md b/docs/syntax.md index 6c566c149..ac83b6571 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -187,7 +187,7 @@ Returns the expression that follows. The last statement of each branch of a func ```py # Allowed, all branches return -def max(a: T, b: T) -> T: +def max(a, b): if a > b: return a else: @@ -207,7 +207,7 @@ def Foo(x): ```py # Not allowed, one of the branches doesn't return -def Foo(a: T, b: T) -> T: +def Foo(a, b): if a < b: return a else: @@ -581,7 +581,7 @@ i24 = -42 u24 = 42 ``` -Currently, the 3 number types cannot be mixed, but can be converted in order to correspond to each other. +Currently, We can't write operations that mix two types of number but we can explicitly convert between them. | Operation | Syntax | Supported Types | | --------------------- | -------- | ---------------- | From 1f6e6039697ab4e12894bd74e4666f02082ea6c3 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, 20 Jan 2025 21:43:34 +0100 Subject: [PATCH 05/15] Apply suggestions from code review Co-authored-by: Nicolas Abril --- FEATURES.md | 2 +- GUIDE.md | 2 +- docs/cli-arguments.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index fa51df5c7..06055adfe 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -131,7 +131,7 @@ def main() -> u24: return MyTree.sum(x) ``` -It should be noted that in this case, 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. +> 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. diff --git a/GUIDE.md b/GUIDE.md index db53b1ffd..f048b9b40 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -368,7 +368,7 @@ Could be represented as: ``` tree = Tree/Node{ - left: Tree/Node{left: Tree/Leaf {value: 1}, right: Tree/Leaf {value: 2}}, + 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}}, } ``` diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index ea4ec7f0e..a6a58798d 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -33,7 +33,7 @@ def main(x, y): # Calling with two argument > bend run +5 +3 -{+2 -2}# +{+2 -2} # Calling with three arguments # In this case, the third argument doesn't change anything # due to the underlying interaction rules. From ad3f09c32837e12e7701003b536e7b53eab2e9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Mon, 20 Jan 2025 21:44:53 +0100 Subject: [PATCH 06/15] Added changes and corrections required by the review --- FEATURES.md | 33 ++- docs/native-numbers.md | 2 +- docs/pattern-matching.md | 19 +- src/fun/builtins.bend | 296 +++++++++++++---------- tests/golden_tests/prelude/doctests.bend | 13 + 5 files changed, 219 insertions(+), 144 deletions(-) create mode 100644 tests/golden_tests/prelude/doctests.bend diff --git a/FEATURES.md b/FEATURES.md index fa51df5c7..c05cc7491 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -6,8 +6,8 @@ You can read the full reference for both of them [here](docs/syntax.md), but the To see some more complex examples programs, check out the [examples](examples/) folder. ### Basic features -Types in Bend are completely optional - you can write programs without any type annotations, but we'll be typing every function for clarity. We can start with a basic program that adds the numbers 3 and 2. Though +We can start with a basic program that adds the numbers 3 and 2. ```py def main() -> u24: return 2 + 3 @@ -86,6 +86,16 @@ 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. @@ -267,7 +277,21 @@ 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. -Bend now has a way to convert between the different number types! +You can use `switch` to pattern match on unsigned native numbers: + +```py +switch x = 4: + # From '0' to n, ending with the default case '_'. + case 0: "zero" + case 1: "one" + case 2: "two" + # The default case binds the name - + # where 'arg' is the name of the argument and 'n' is the next number. + # In this case, it's 'x-3', which will have value (4 - 3) = 1 + 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 @@ -282,8 +306,7 @@ You can find the other casting functions and their declarations at [builtins.md] ### Other builtin types Bend has Lists and Strings, which support Unicode characters. - -# These are the definitions of the builtin types. +This is how they are defined: ```py type String: Nil @@ -343,7 +366,7 @@ def empty_map() -> Map(T): return {} def init_map() -> Map(String): - return { 1: "one", 2: "two"} + return { 1: "one", 2: "two", `blue`: "0x0000FF" } def main() -> String: map = init_map diff --git a/docs/native-numbers.md b/docs/native-numbers.md index e06ac323e..00191d4f3 100644 --- a/docs/native-numbers.md +++ b/docs/native-numbers.md @@ -54,7 +54,7 @@ If you mix two numbers of different types, HVM will interpret the binary represe #### Casting numbers -We introduced a way to convert between the different number types, and using it is very easy, here's an example: +There is a way to convert between the different number types, and using it is very easy, here's an example: ```py def main() -> _: diff --git a/docs/pattern-matching.md b/docs/pattern-matching.md index 1f7623910..a1fa12417 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -60,20 +60,20 @@ 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 0 false (Cons h1 (Cons h2 t))) = (Bar h1 h2 t) +(Foo 0 * *) = Baz (Foo n false *) = n (Foo * true *) = 0 Foo = λarg1 λarg2 λarg3 (switch arg1 { 0: λarg2 λarg3 match arg2 { - true: λarg3 B + true: λarg3 Baz false: λarg3 match arg3 { Cons: (match arg3.tail { Cons: λarg3.head (A arg3.head arg3.tail.head arg3.tail.tail) - Nil: λarg3.head B + Nil: λarg3.head Baz } arg3.head) - Nil: B + Nil: Baz } } _: λarg2 λarg3 (match arg2 { @@ -88,13 +88,14 @@ 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__C0) = λ* λa λb λc (Bar c a b) -(Foo__C5) = λa switch a { 0: λ* B; _: Foo__C4; } # Foo.case_0 +(Foo__C5) = λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0 (Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false -(Foo__C3) = λa switch a { 0: B; _: Foo__C2; } # Part of cons pattern matching +(Foo__C3) = λa switch a { 0: Bar; _: Foo__C2; } # Part of cons pattern matching (Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false.case_cons -(Foo__C1) = λa switch a { 0: λ* B; _: Foo__C0; } # Part of cons pattern matching -(Foo__C0) = λ* λa λb λc (A c a b) # Foo.case_0.case_false.case_cons.case_cons +(Foo__C1) = λa switch a { 0: λ* Baz; _: Foo__C0; } # Part of cons pattern matching +(Foo__C0) = λ* λa λb λc (Bar c a b) # Foo.case_0.case_false.case_cons.case_cons (Foo__C6) = λ* λa (+ a 0) # Foo.case_+.case_false (Foo__C7) = λa switch a { 0: λ* 0; _: Foo__C6; } # Part of bool pattern matching diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index 8b188bef2..ef73cd4ff 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 the length of a list 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 a list. #} def List/reverse(xs: List(T)) -> List(T): fold xs with acc=[]: case List/Nil: @@ -22,18 +22,20 @@ def List/reverse(xs: List(T)) -> List(T): case List/Cons: return xs.tail(List/Cons(xs.head, acc)) -# Flattens a list of lists. +#{ Flattens 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. +#{ Concatenates two lists. #} 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 value that passes a condition. + Returns the original list if the value is not found +#} def List/split_once( xs: List(T), cond: T -> u24 @@ -54,7 +56,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 +66,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 +77,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 list into two lists at the first occurrence of a value. #} String/split (s: String) (delimiter: u24) : (List String) String/split s delim = (String/split.go s delim [""]) @@ -83,41 +85,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 +#{ Convert a difference list to a list #} def DiffList/to_list(diff: List(T) -> List(T)) -> (List(T)): return diff(List/Nil) @@ -132,8 +134,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 +145,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 +157,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 +166,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 +174,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 +#{ Removes the value on a Maybe #} def Maybe/unwrap(m: Maybe(T)) -> T: match m: case Maybe/Some: @@ -184,17 +188,17 @@ def Maybe/unwrap(m: Maybe(T)) -> T: case Maybe/None: return unreachable() -# MAP Impl +#{ MAP Impl #} type Map(T): Node { value: Maybe(T), ~left: Map(T), ~right: Map(T) } Leaf -# Creates an empty Map +#{ Creates an empty Map #} def Map/empty() -> Map(T): return Map/Leaf -# Gets a value on a Map +#{ Gets a value on a Map #} def Map/get (map: Map(T), key: u24) -> (T, Map(T)): match map: case Map/Leaf: @@ -210,7 +214,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 +229,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 #} def Map/set (map: Map(T), key: u24, value: T) -> Map(T): match map: case Map/Node: @@ -244,7 +248,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 #} def Map/contains (map: Map(T), key: u24) -> (u24, Map(T)): match map: case Map/Leaf: @@ -263,7 +267,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 funtion to a value on a Map #} def Map/map (map: Map(T), key: u24, f: T -> T) -> Map(T): match map: case Map/Leaf: @@ -276,7 +280,7 @@ def Map/map (map: Map(T), key: u24, f: T -> T) -> Map(T): else: return Map/Node(map.value, map.left, Map/map(map.right, (key / 2), f)) -# IO Impl +#{ IO Impl #} type IO(T): Done { magic: (u24, u24), expr: T } @@ -309,29 +313,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 +351,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) @@ -390,16 +400,16 @@ 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, returning a list of bytes. #} def IO/FS/read_file(path: String) -> IO(Result(List(u24), u24)): with IO: res_fd <- IO/FS/open(path, "r") @@ -412,7 +422,7 @@ 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 the remaining contents of a file, returning a list of read bytes. #} def IO/FS/read_to_end(fd: u24) -> IO(Result(List(u24), u24)): return IO/FS/read_to_end.read_chunks(fd, []) @@ -431,7 +441,7 @@ 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 single line from a file, returning a list of bytes. #} def IO/FS/read_line(fd: u24) -> IO(Result(List(u24), u24)): return IO/FS/read_line.read_chunks(fd, []) @@ -470,7 +480,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 a list of bytes to a file given by a 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 +495,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 a string to stdout, encoding it 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 + Read characters from stdin until a newline is found. + Returns the read input decoded as utf-8. +#} def IO/input() -> IO(Result(String, u24)): return IO/input.go(DiffList/new) @@ -521,37 +533,45 @@ 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. +#{ + 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. +#} 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. +#{ + 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. +#} 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. + Returns nothing. + 'dl' is the id of the library object. +#} 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) +#{ 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) +#} def defer(val: T) -> (T -> T) -> T: return lambda x: x(val) @@ -561,35 +581,37 @@ 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 +#{ 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) @@ -604,12 +626,14 @@ 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. + Invalid utf-8 sequences are replaced with Utf8/REPLACEMENT_CHARACTER. +#} String/decode_utf8 (bytes: (List u24)) : String String/decode_utf8 [] = String/Nil String/decode_utf8 bytes = @@ -619,8 +643,10 @@ 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 one utf-8 character from the start of a sequence of bytes. + Returns the decoded character and the remaining bytes. +#} 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 +708,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 +743,107 @@ 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 #} -# Math/PI() -> f24 -# The Pi (π) constant. +#{ + Math/PI() -> f24 + The Pi (π) constant. +#} def Math/PI() -> f24: return 3.1415926535 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 + Has the same behaviour as `atan2f` in the C math lib. + Computes the arctangent of the quotient of its two arguments. +#} 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 +851,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/tests/golden_tests/prelude/doctests.bend b/tests/golden_tests/prelude/doctests.bend new file mode 100644 index 000000000..49844f7a9 --- /dev/null +++ b/tests/golden_tests/prelude/doctests.bend @@ -0,0 +1,13 @@ +(Foo 0 false (List/Cons h1 (List/Cons h2 t))) = (A h1 h2 t) # Most specific +(Foo 0 * *) = B # General 0 case +(Foo n false *) = n # General false case +(Foo n true *) = 0 + +def A(): + return 0 + +def B(): + return 0 + +def main(): + return 0 From 5321ad702b573cae075ff3f855bcae844a0529cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Mon, 20 Jan 2025 22:05:45 +0100 Subject: [PATCH 07/15] Make builtins.bend function descriptions match those in builtins.md --- docs/builtins.md | 3 +- src/fun/builtins.bend | 109 +++++++++++++++-------- tests/golden_tests/prelude/doctests.bend | 13 --- 3 files changed, 72 insertions(+), 53 deletions(-) delete mode 100644 tests/golden_tests/prelude/doctests.bend diff --git a/docs/builtins.md b/docs/builtins.md index 2fb60a4de..995bd1e47 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -128,7 +128,7 @@ List/filter(xs: List(T), pred: T -> Bool) -> List(T) ```python #{ -Splits a list into two lists at the first occurrence of a value. + 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))): ``` @@ -268,6 +268,7 @@ Map/empty = Map/Leaf ```rust #{ 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)) ``` diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index ef73cd4ff..c16193661 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,19 +22,18 @@ 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), @@ -77,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 [""]) @@ -119,7 +118,7 @@ def DiffList/concat( 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) @@ -127,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: @@ -180,7 +184,7 @@ 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: @@ -194,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: @@ -229,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: @@ -248,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: @@ -267,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: @@ -376,21 +384,37 @@ 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: #} 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)) @@ -409,7 +433,10 @@ 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") @@ -422,7 +449,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, []) @@ -441,7 +471,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, []) @@ -480,7 +513,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") @@ -495,7 +528,7 @@ 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)) @@ -503,8 +536,8 @@ def IO/print(text: String) -> IO(None): #{ IO/input() -> IO String - Read characters from stdin until a newline is found. - Returns the read input decoded as utf-8. + 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) @@ -534,27 +567,30 @@ 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))) +#{ + - `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. - 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. + - `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. + - `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)) @@ -615,6 +651,7 @@ hvm u24/to_f24 -> (u24 -> 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 @@ -630,10 +667,7 @@ def u24/to_string(n: u24) -> String: 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 = @@ -643,10 +677,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, []) } @@ -774,8 +805,8 @@ hvm Math/log -> (f24 -> f24 -> f24): #{ Math/atan2(x: f24, y: f24) -> f24 + Computes the arctangent of `y / x`. Has the same behaviour as `atan2f` in the C math lib. - Computes the arctangent of the quotient of its two arguments. #} hvm Math/atan2 -> (f24 -> f24 -> f24): ($([&] $(y ret)) (y ret)) diff --git a/tests/golden_tests/prelude/doctests.bend b/tests/golden_tests/prelude/doctests.bend deleted file mode 100644 index 49844f7a9..000000000 --- a/tests/golden_tests/prelude/doctests.bend +++ /dev/null @@ -1,13 +0,0 @@ -(Foo 0 false (List/Cons h1 (List/Cons h2 t))) = (A h1 h2 t) # Most specific -(Foo 0 * *) = B # General 0 case -(Foo n false *) = n # General false case -(Foo n true *) = 0 - -def A(): - return 0 - -def B(): - return 0 - -def main(): - return 0 From 8f108781dbbbcd4abad4d4d500c9a8e3b2e54a44 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: Tue, 21 Jan 2025 10:53:13 +0100 Subject: [PATCH 08/15] Update docs/builtins.md Co-authored-by: Nicolas Abril --- docs/builtins.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/builtins.md b/docs/builtins.md index 995bd1e47..4b648e818 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -267,7 +267,9 @@ Map/empty = Map/Leaf ```rust #{ - 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`. + 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)) ``` From df3b45ecdcf2182853171f778bb9e5882b8c254c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Tue, 21 Jan 2025 10:53:48 +0100 Subject: [PATCH 09/15] Add changes required by PR review --- docs/builtins.md | 2 +- docs/compiler-options.md | 7 +++---- docs/doctests.bend | 17 +++++++++++++++++ docs/ffi.md | 9 +++++---- docs/native-numbers.md | 9 +++------ docs/pattern-matching.md | 26 +++++++++++++------------- src/fun/builtins.bend | 12 ++++++------ 7 files changed, 48 insertions(+), 34 deletions(-) create mode 100644 docs/doctests.bend diff --git a/docs/builtins.md b/docs/builtins.md index 995bd1e47..d3f49e493 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -258,7 +258,7 @@ Here, `map` must be the name of the `Map` variable, and the keys inside `[]` can #{ Initializes an empty map. #} -Map/empty = Map/Leaf +def Map/empty() -> Map(T) ``` ### Map/get diff --git a/docs/compiler-options.md b/docs/compiler-options.md index a5e4a7e7d..2c4b42c7c 100644 --- a/docs/compiler-options.md +++ b/docs/compiler-options.md @@ -12,8 +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 | Checks the type compatibility | - +| `-Otype-check` `-Ono-type-check` | type-check | [type-checking](#type-checking) | ## Eta-reduction Enables or disables Eta Reduction for defined functions. @@ -263,10 +262,10 @@ 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. +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 ``` -If type checking is 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`. +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/doctests.bend b/docs/doctests.bend new file mode 100644 index 000000000..f2369235d --- /dev/null +++ b/docs/doctests.bend @@ -0,0 +1,17 @@ +def Bar: + return 0 + +def Baz: + return 0 + +type Bool: + True + False +(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 + + +def main() -> _: + return 0 diff --git a/docs/ffi.md b/docs/ffi.md index 6b67d19af..e69e4b144 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -13,7 +13,7 @@ def main(): # The second argument is '0' if we want to load all functions immediately. # Otherwise it should be '1' when we want to load functions as we use them. # 'dl' is the unique id of the dynamic library. - dl <- Result/unwrap(IO/DyLib/open("./libbend_dirs.so", 0)) + dl <- IO/DyLib/open("./libbend_dirs.so", 0) # We can now call functions from the dynamic library. # We need to know what functions are available in the dynamic library. @@ -27,7 +27,8 @@ 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", "./") + 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') @@ -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/native-numbers.md b/docs/native-numbers.md index 00191d4f3..7838816b7 100644 --- a/docs/native-numbers.md +++ b/docs/native-numbers.md @@ -49,8 +49,9 @@ 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. Instead, you should make sure that all numbers are of the same type. +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 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 @@ -66,10 +67,6 @@ def main() -> _: ``` You can find more number casting functions and their declarations at [builtins.md](docs/builtins.md). -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. - ### Operations diff --git a/docs/pattern-matching.md b/docs/pattern-matching.md index a1fa12417..d3ddaddc1 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -88,19 +88,19 @@ 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__C0) = λ* λa λb λc (Bar c a b) - -(Foo__C5) = λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0 -(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false -(Foo__C3) = λa switch a { 0: Bar; _: Foo__C2; } # Part of cons pattern matching -(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false.case_cons -(Foo__C1) = λa switch a { 0: λ* Baz; _: Foo__C0; } # Part of cons pattern matching -(Foo__C0) = λ* λa λb λc (Bar c a b) # Foo.case_0.case_false.case_cons.case_cons - -(Foo__C6) = λ* λa (+ a 0) # Foo.case_+.case_false -(Foo__C7) = λa switch a { 0: λ* 0; _: Foo__C6; } # Part of bool pattern matching -(Foo__C9) = λa λb λ* (b Foo__C7 a) # Foo.case_+ -(Foo__C8) = λa λ* (a Foo__C5) # Part of main pattern matching +(Foo) = λa λb λc (switch a { 0: Foo__C8; _: Foo__C9; } b c) + +(Foo__C5) = λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0 +(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false +(Foo__C3) = λa switch a { 0: Baz; _: Foo__C2; } # Part of cons pattern matching +(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false.case_cons +(Foo__C1) = λa switch a { 0: λ* Baz; _: Foo__C0; } # Part of cons pattern matching +(Foo__C0) = λ* λa λb λc (Bar c a b) # Foo.case_0.case_false.case_cons.case_cons + +(Foo__C6) = λ* λ* λa (+ a 1) # Foo.case_+.case_false +(Foo__C7) = λa switch a { 0: λ* λ* 0; _: Foo__C6; } # Part of bool pattern matching +(Foo__C9) = λa λb λc (b Foo__C7 c a) # Foo.case_+ +(Foo__C8) = λa λb (a Foo__C5 b) # Part of main pattern matching # As an user, you can't write a function with __ on its name, that sequence is reserved for things generated by the compiler. ``` diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index c16193661..3a6aac453 100644 --- a/src/fun/builtins.bend +++ b/src/fun/builtins.bend @@ -178,7 +178,7 @@ 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 } @@ -192,7 +192,7 @@ def Maybe/unwrap(m: Maybe(T)) -> T: case Maybe/None: return unreachable() -#{ MAP Impl #} +# MAP Impl type Map(T): Node { value: Maybe(T), ~left: Map(T), ~right: Map(T) } @@ -288,7 +288,7 @@ def Map/map (map: Map(T), key: u24, f: T -> T) -> Map(T): else: return Map/Node(map.value, map.left, Map/map(map.right, (key / 2), f)) -#{ IO Impl #} +# IO Impl type IO(T): Done { magic: (u24, u24), expr: T } @@ -595,7 +595,7 @@ def IO/DyLib/call(dl: u24, fn: String, args: Any) -> IO(Result(Any, String)): def IO/DyLib/close(dl: u24) -> IO(Result(None, String)): return IO/unwrap_inner(IO/call("DL_CLOSE", dl)) -#{ Lazy thunks #} +# Lazy thunks #{ We can defer the evaluation of a function by wrapping it in a thunk. @@ -625,7 +625,7 @@ def undefer(defered: (Id -> Id) -> T) -> T: def unreachable() -> Any: return * -#{ Native number casts #} +# Native number casts #{ Casts a f24 number to a u24. #} hvm f24/to_u24 -> (f24 -> u24): @@ -784,7 +784,7 @@ 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 #{ Math/PI() -> f24 From e11943eb5f825c4459c75a9deb265aa51acbef57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Tue, 21 Jan 2025 10:56:00 +0100 Subject: [PATCH 10/15] Merge and complete PR Review requests --- docs/doctests.bend | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 docs/doctests.bend diff --git a/docs/doctests.bend b/docs/doctests.bend deleted file mode 100644 index f2369235d..000000000 --- a/docs/doctests.bend +++ /dev/null @@ -1,17 +0,0 @@ -def Bar: - return 0 - -def Baz: - return 0 - -type Bool: - True - False -(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 - - -def main() -> _: - return 0 From c09402562063754ff51b48174dd0da706d41c8a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Thu, 23 Jan 2025 20:18:07 +0100 Subject: [PATCH 11/15] Add changes required by PR Review --- docs/pattern-matching.md | 22 +++++++++++++--------- src/fun/builtins.bend | 23 +++++++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/pattern-matching.md b/docs/pattern-matching.md index d3ddaddc1..41b9e1e48 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -88,19 +88,23 @@ 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) +# Main function (Foo) = λa λb λc (switch a { 0: Foo__C8; _: Foo__C9; } b c) -(Foo__C5) = λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0 -(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.case_false -(Foo__C3) = λa switch a { 0: Baz; _: Foo__C2; } # Part of cons pattern matching -(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.case_false.case_cons -(Foo__C1) = λa switch a { 0: λ* Baz; _: Foo__C0; } # Part of cons pattern matching -(Foo__C0) = λ* λa λb λc (Bar c a b) # Foo.case_0.case_false.case_cons.case_cons +# Case 0 branch +(Foo__C8) = λa λb (a Foo__C5 b) # Foo.case_0.route +(Foo__C5) = λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0.bool_true +(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.bool_false +(Foo__C3) = λa switch a { 0: Baz; _: Foo__C2; } # Foo.case_0.bool_false.case_cons1 +(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.bool_false.case_cons1.handle +(Foo__C1) = λa switch a { 0: λ* Baz; _: Foo__C0; } # Foo.case_0.bool_false.case_cons2 +(Foo__C0) = λ* λa λb λc (Bar c a b) # Foo.case_0.bool_false.case_cons2.case_nil -(Foo__C6) = λ* λ* λa (+ a 1) # Foo.case_+.case_false -(Foo__C7) = λa switch a { 0: λ* λ* 0; _: Foo__C6; } # Part of bool pattern matching +# Case non-zero branch (Foo__C9) = λa λb λc (b Foo__C7 c a) # Foo.case_+ -(Foo__C8) = λa λb (a Foo__C5 b) # Part of main pattern matching +(Foo__C7) = λa switch a { 0: λ* λ* 0; _: Foo__C6; } # Foo.case_+.case_check +(Foo__C6) = λ* λ* λa (+ a 1) # Foo.case_+.case_false + # As an user, you can't write a function with __ on its name, that sequence is reserved for things generated by the compiler. ``` diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index 3a6aac453..2d708f302 100644 --- a/src/fun/builtins.bend +++ b/src/fun/builtins.bend @@ -84,13 +84,13 @@ 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: [] } } @@ -385,7 +385,16 @@ def IO/sleep(seconds: f24) -> IO(None): ### 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: #} +#{ + 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))) @@ -786,13 +795,11 @@ 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 From 5ccff335fc71cd202453f77741516d3d8d665713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Thu, 30 Jan 2025 19:59:31 +0100 Subject: [PATCH 12/15] Add PR Review changes, and Update compilation-and-readback.md --- docs/compilation-and-readback.md | 47 +++++++++++++++++++++- docs/pattern-matching.md | 68 ++++++++++++++++---------------- 2 files changed, 80 insertions(+), 35 deletions(-) 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/pattern-matching.md b/docs/pattern-matching.md index 41b9e1e48..bd5145c8d 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 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))) = (Bar h1 h2 t) +(Foo 0 Bool/False (List/Cons h1 (List/Cons h2 t))) = (Bar h1 h2 t) (Foo 0 * *) = Baz -(Foo n false *) = n -(Foo * true *) = 0 +(Foo n Bool/False *) = n +(Foo n Bool/True *) = 0 Foo = λarg1 λarg2 λarg3 (switch arg1 { 0: λarg2 λarg3 match arg2 { - true: λarg3 Baz - false: λarg3 match arg3 { - Cons: (match arg3.tail { - Cons: λarg3.head (A arg3.head arg3.tail.head arg3.tail.tail) - Nil: λarg3.head Baz + 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: Baz + 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. @@ -92,18 +92,18 @@ To ensure that recursive pattern matching functions don't loop in strict mode, i (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.route -(Foo__C5) = λa switch a { 0: λ* Baz; _: Foo__C4; } # Foo.case_0.bool_true -(Foo__C4) = λ* λa (a Foo__C3) # Foo.case_0.bool_false -(Foo__C3) = λa switch a { 0: Baz; _: Foo__C2; } # Foo.case_0.bool_false.case_cons1 -(Foo__C2) = λ* λa λb (b Foo__C1 a) # Foo.case_0.bool_false.case_cons1.handle -(Foo__C1) = λa switch a { 0: λ* Baz; _: Foo__C0; } # Foo.case_0.bool_false.case_cons2 -(Foo__C0) = λ* λa λb λc (Bar c a b) # Foo.case_0.bool_false.case_cons2.case_nil +(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_check -(Foo__C6) = λ* λ* λa (+ a 1) # Foo.case_+.case_false +(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. ``` @@ -134,9 +134,9 @@ 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`.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 Cons and Nil @@ -153,7 +153,7 @@ Foo _ = 3 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 ``` From 15b31490ae1c1e60c76058e93f9ff5725f2ef838 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: Fri, 31 Jan 2025 18:13:41 +0100 Subject: [PATCH 13/15] Update docs/pattern-matching.md Co-authored-by: Nicolas Abril --- docs/pattern-matching.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/pattern-matching.md b/docs/pattern-matching.md index bd5145c8d..0ec552ce0 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -153,7 +153,7 @@ Foo _ = 3 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 ``` From ea2f3c1ffd7a0eabf86c95f0f69286e0ddfaba74 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: Fri, 31 Jan 2025 18:13:50 +0100 Subject: [PATCH 14/15] Update docs/pattern-matching.md Co-authored-by: Nicolas Abril --- docs/pattern-matching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pattern-matching.md b/docs/pattern-matching.md index 0ec552ce0..148ed2068 100644 --- a/docs/pattern-matching.md +++ b/docs/pattern-matching.md @@ -41,7 +41,7 @@ UnwrapOrZero x = (x λtag switch tag { # Otherwise, if the current encoding is 'adt-scott' it becomes: Maybe/Some = λval λMaybe/Some λMaybe/None (Maybe/Some val) -Maybe/None = λMaybe/Some λMaybe/None None +Maybe/None = λMaybe/Some λMaybe/None Maybe/None UnwrapOrZero x = (x λx.val x.val 0) ``` From ea10ae6edda454ecc72fbaba98c65c16d454497e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20V=C3=A9rit=C3=A9?= Date: Mon, 3 Feb 2025 20:33:41 +0100 Subject: [PATCH 15/15] Add changes to parser.rs conforming to new clippy version --- src/fun/parser.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 {