diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 00000000..53c718b1 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,270 @@ +## Features + +Bend offers two flavors of syntax, a user-friendly python-like syntax (the default) and the core ML/Haskell-like syntax that's used internally by the compiler. +You can read the full reference for both of them [here](docs/syntax.md), but these examples will use the first one. + +To see some more complex examples programs, check out the [examples](examples/) folder. + +We can start with a basic program that adds the numbers 3 and 2. + +```py +def main: + return 2 + 3 +``` + +Normalizing this program will show the number 5. +Be careful with `run` and `norm`, since they will not show any warnings by default. Before running a new program it's useful to first `check` it. + +Bend programs consist of a series of function definitions, always starting with a function called `main` or `Main`. + +Functions can receive arguments both directly and using a lambda abstraction. + +```py +# These two are equivalent +def add(x, y): + return x + y + +def add2: + return lambda x, y: x + y +``` + +You can then call this function like this: + +```py +def main: + sum = add(2, 3) + return sum +``` + +You can bundle multiple values into a single value using a tuple or a struct. + +```py +# With a tuple +def Tuple.fst(x): + # 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 a struct +struct Pair(fst, snd): +def Pair.fst(x): + match x: + Pair: + return x.fst + +# We can also directly access the fields of a struct. +# This requires that we tell the compiler the type of the variable where it is defined. +def Pair.fst_2(x: Pair): + return x.fst +``` + +For more complicated data structures, we can use `enum` to define a algebraic data types. + +```py +enum MyTree: + Node(val, ~left, ~right) + Leaf +``` + +We can then pattern match on the enum to perform different actions depending on the variant of the value. + +```py +def Maybe.or_default(x, default): + match x: + Maybe/some: + # We can access the fields of the variant using 'matched.field' + return x.val + Maybe/none: + return default +``` + +We use `~` to indicate that a field is recursive. +This allows us to easily create and consume these recursive data structures with `bend` and `fold`: + +```py +def MyTree.sum(x): + # Sum all the values in the tree. + fold x: + # The fold is implicitly called for fields marked with '~' in their definition. + Node: + return val + x.left + x.right + Leaf: + return 0 + +def main: + bend val = 0 while val < 0: + # 'go' calls the bend recursively with the provided values. + x = Node(val=val, left=go(val + 1), right=go(val + 1)) + then: + # 'then' is the base case, when the condition fails. + x = Leaf + + return MyTree.sum(x) +``` + +These are equivalent to inline recursive functions that create a tree and consume it. + +```py +def MyTree.sum(x): + match x: + Node: + return x.val + MyTree.sum(x.left) + MyTree.sum(x.right) + Leaf: + return 0 + +def main_bend(val): + if val < 0: + return Node(val, main_bend(val + 1), main_bend(val + 1)) + else: + return Leaf + +def main: + return main_bend(0) +``` + +Making your program around trees is a very good way of making it parallelizable, since each core can be dispatched to work on a different branch of the tree. + +_Attention_: Note that despite the ADT syntax sugars, Bend is an _untyped_ language and the compiler will not stop you from using values incorrectly, which can lead to very unexpected results. +For example, the following program will compile just fine even though `!=` is only defined for native numbers: + +```py +def main: + bend val = [0, 1, 2, 3] while val != []: + match val: + List.cons: + x = val.head + go(val.tail) + List.nil: + x = 0 + then: + x = 0 + return x +``` + +Normalizing this program will show `λ* *` and not the expected `6`. + +It's also important to note that Bend is linear (technically affine), meaning that every variable is only used once. When a variable is used more than once, the compiler will automatically insert a duplication. +Duplications efficiently share the same value between two locations, only cloning a value when it's actually needed, but their exact behaviour is slightly more complicated than that and escapes normal lambda-calculus rules. +You can read more about it in [Dups and sups](docs/dups-and-sups.md) and learn how pattern matching avoids this problem in [Pattern matching](docs/pattern-matching.md). + +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) + return (result, result) + +# Is equivalent to +def foo(x): + return (bar(1, x), bar(1, x)) +``` + +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): + if n == 0: + return Nat.zero + else: + return Nat.succ(native_num_to_adt(n - 1)) +``` + +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. +Since Bend is eagerly executed, some situations will cause function applications to always be expanded, which can lead to looping situations. +You can read how to avoid this in [Lazy definitions](docs/lazy-definitions.md). + +Bend has native numbers and operations. + +```py +def main: + a = 1 # A 24 bit unsigned integer. + b = +2 # A 24 bit signed integer. + c = -3 # Another signed integer, but with negative value. + d = 1.0 # A 24 bit floating point number. + e = +0.001 # Also a float. + return (a * 2, b - c, d / e) +``` + +`switch` pattern matches on unsigned native numbers: + +```py +switch x = 4: + # From '0' to n, ending with the default case '_'. + 0: "zero" + 1: "one" + 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 + _: String.concat("other: ", (String.from_num x-3)) +``` + +Bend has Lists and Strings, which support Unicode characters. + +```rs +def main: + return ["You: Hello, 🌎", "🌎: Hello, user"] +``` + +A string is desugared to a String data type containing two constructors, `String.cons` and `String.nil`. +List also becomes a type with two constructors, `List.cons` and `List.nil`. + +```rs +# These two are equivalent +def StrEx: + "Hello" + +def ids: + [1, 2, 3] + +# These types are builtin. +enum String: + String.cons(head, tail) + String.nil +enum List: + List.cons(head, tail) + List.nil +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))) +``` + +Characters are delimited by `'` `'` and support Unicode escape sequences. They are encoded as a U24 with the unicode codepoint as their value. + +``` +# These two are equivalent +def chars: + ['A', '\u{4242}', '🌎'] + +def chars2: + [65, 0x4242, 0x1F30E] +``` + +### More features + +Key: + +- 📗: Basic resources +- 📙: Intermediate resources +- 📕: Advanced resources + +Other features are described in the following documentation files: + +- 📗 Lazy definitions: [Making recursive definitions lazy](docs/lazy-definitions.md) +- 📗 Data types: [Defining data types](docs/defining-data-types.md) +- 📗 Pattern matching: [Pattern matching](docs/pattern-matching.md) +- 📗 Native numbers and operations: [Native numbers](docs/native-numbers.md) +- 📗 Builtin definitions: [Builtin definitions](docs/builtin-defs.md) +- 📗 CLI arguments: [CLI arguments](docs/cli-arguments.md) +- 📙 Duplications and superpositions: [Dups and sups](docs/dups-and-sups.md) +- 📙 Scopeless lambdas: [Using scopeless lambdas](docs/using-scopeless-lambdas.md) +- 📕: Fusing functions: [Writing fusing functions](docs/writing-fusing-functions.md) + +## Further reading + +- 📙 [Compilation and readback](docs/compilation-and-readback.md) +- 📙 [Old HVM wiki learning material](https://github.com/HigherOrderCO/HVM/wiki/HVM-Wiki). It is outdated, but it can still teach you some of the basics. diff --git a/GUIDE.md b/GUIDE.md index aaab04ad..5eeeedd1 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -28,9 +28,10 @@ To use Bend, first, install [Rust](https://rust-lang.org/): curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` -Then, install Bend itself with: +Then, install HVM2 and Bend tself with: ``` +cargo install hvm cargo install bend-lang ``` diff --git a/README.md b/README.md index 957e2d52..6e21b2a0 100644 --- a/README.md +++ b/README.md @@ -1,320 +1,86 @@ # Bend -Bend is a programming language that can run massively parallel programs on the GPU or the CPU using the power of interaction nets and the [HVM](https://github.com/HigherOrderCO/hvm). -With Bend, you can write programs for the GPU as easily as you'd write a normal program in your favorite language. +Bend is a massively parallel, high-level programming language. Unlike existing +alternatives like CUDA, OpenCL and Metal, which are low-level and limited, Bend +has the feel and features of a modern language like Python and Haskell. Yet, it +runs with 1000's of cores, on CPUs and GPUs, powered by the +[HVM2](https://github.com/HigherOrderCO/hvm2). -It is based on the [Interaction-Calculus](https://github.com/VictorTaelin/Interaction-Calculus#interaction-calculus), a variation of the untyped lambda calculus that compiles efficiently to interaction nets. +## Using Bend -Currently Bend only supports strict/eager evaluation. If you need lazy, optimal evaluation, we recommend using [HVM1](https://github.com/HigherOrderCO/HVM1) for now. +First, install [Rust nightly](https://www.oreilly.com/library/view/rust-programming-by/9781788390637/e07dc768-de29-482e-804b-0274b4bef418.xhtml). Then, install both HVM2 and Bend with: -## Installation - -With the nightly version of rust installed, install with cargo: - -```bash +```sh +cargo install hvm cargo install bend-lang ``` -Or install the development version from the github repository: -```bash -git clone https://github.com/HigherOrderCO/bend.git -cd bend -cargo install --path . --locked +Then, just write a Bend file, and run it with: + +```sh +bend run # uses the Rust interpreter (sequential) +bend run-c # uses the C interpreter (parallel) +bend run-cu # uses the CUDA interpreter (massively parallel) ``` -If you want to run programs directly from Bend, you also need to have [HVM](https://github.com/HigherOrderCO/hvm) installed. +You can also compile `Bend` to standalone C/CUDA files with `gen-c` and +`gen-cu`, for maximum possible performance. -## Usage +## Parallel Programming in Bend -| Command | Usage | Description | -| ------- | --------------------- | ----------------------------------------------------------------- | -| Check | `bend check ` | Checks if a program is valid | -| GenHvm | `bend gen-hvm ` | Compiles a program to HVM and outputs it to stdout | -| Run | `bend run ` | Compiles and then runs a program with the Rust HVM implementation | -| Run-C | `bend run-c ` | Compiles and then runs a program with the C HVM implementation | -| Run-Cu | `bend run-cu ` | Compiles and then runs a program with the Cuda HVM implementation | -| Gen-C | `bend gen-c ` | Compiles the program to standalone C | -| Gen-Cu | `bend gen-cu ` | Compiles the program to standalone Cuda | -| Desugar | `bend desugar ` | Desugars a program to the core syntax and outputs it to stdout | +To write parallel programs in Bend, all you have to do is... **nothing**. Other +than not making it *inherently sequential*! For example, the expression: -If your program uses IO, it should return an IO value and you need to run it with --io. See [using io](docs/using-io.md), for more details. -If your program doesn't return an IO value, then you should not run it with --io - -If you want to compile a file to a file, just redirect the output with `>`: - -```bash -bend compile > +```python +(((1 + 2) + 3) + 4) ``` -There are many compiler options that can be passed through the CLI. You can see the list of options [here](docs/compiler-options.md). +Can **not** run in parallel, inherently so, because `+4` depends on `+3` which +depends on `(1+2)`. But the following expression: -## Examples +```python +((1 + 2) + (3 + 4)) +``` -Bend offers two flavors of syntax, a user-friendly python-like syntax (the default) and the core ML/Haskell-like syntax that's used internally by the compiler. -You can read the full reference for both of them [here](docs/syntax.md), but these examples will use the first one. +Can run in parallel, and will, due to Bend's fundamental pledge: -To see some more complex examples programs, check out the [examples](examples/) folder. +> Everything that **can** run in parallel, **will** run in parallel. -We can start with a basic program that adds the numbers 3 and 2. +For a more complete example, consider: -```py +```python +def sum(depth, x): + switch depth: + case 0: + return x + case _: + fst = sum(depth-1, x*2+0) # adds the fst half + snd = sum(depth-1, x*2+1) # adds the snd half + return fst + snd + def main: - return 2 + 3 + return sum(30, 0) ``` -Normalizing this program will show the number 5. -Be careful with `run` and `norm`, since they will not show any warnings by default. Before running a new program it's useful to first `check` it. +This code adds all numbers from 0 to 2^30, but, instead of a loop, we use a +recursive divide-and-conquer approach. Since this approach is *inherently +parallel*, the Bend executable will run in many cores. Here are some benchmarks: -Bend programs consist of a series of function definitions, always starting with a function called `main` or `Main`. +- CPU, Apple M3 Max, 1 thread: **3.5 minutes** -Functions can receive arguments both directly and using a lambda abstraction. +- CPU, Apple M3 Max, 16 threads: **10.26 seconds** -```py -# These two are equivalent -def add(x, y): - return x + y +- GPU, NVIDIA RTX 4090, 32k threads: **1.88 seconds** -def add2: - return lambda x, y: x + y -``` +That's a **111x speedup** by doing nothing. No thread spawning, no explicit +management of locks, mutexes. From shaders, to transformers, to Erlang-like +actor-based systems, every concurrent setup can be implemented on Bend with no +explicit annotations. Long-distance communication is performed by global +beta-reduction, and handled correctly and efficiently by the +[HVM2](https://github.com/HigherOrderCO/HVM2) runtime. -You can then call this function like this: +- For more in-depth information, check HVM's [paper](https://github.com/HigherOrderCO/HVM/raw/main/PAPER.pdf). -```py -def main: - sum = add(2, 3) - return sum -``` +- To jump straight into action, check Bend's [GUIDE.md](https://github.com/HigherOrderCO/bend/blob/main/GUIDE.md). -You can bundle multiple values into a single value using a tuple or a struct. - -```py -# With a tuple -def Tuple.fst(x): - # 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 a struct -struct Pair(fst, snd): -def Pair.fst(x): - match x: - Pair: - return x.fst - -# We can also directly access the fields of a struct. -# This requires that we tell the compiler the type of the variable where it is defined. -def Pair.fst_2(x: Pair): - return x.fst -``` - -For more complicated data structures, we can use `enum` to define a algebraic data types. - -```py -enum MyTree: - Node(val, ~left, ~right) - Leaf -``` - -We can then pattern match on the enum to perform different actions depending on the variant of the value. - -```py -def Maybe.or_default(x, default): - match x: - Maybe/some: - # We can access the fields of the variant using 'matched.field' - return x.val - Maybe/none: - return default -``` - -We use `~` to indicate that a field is recursive. -This allows us to easily create and consume these recursive data structures with `bend` and `fold`: - -```py -def MyTree.sum(x): - # Sum all the values in the tree. - fold x: - # The fold is implicitly called for fields marked with '~' in their definition. - Node: - return val + x.left + x.right - Leaf: - return 0 - -def main: - bend val = 0 while val < 0: - # 'go' calls the bend recursively with the provided values. - x = Node(val=val, left=go(val + 1), right=go(val + 1)) - then: - # 'then' is the base case, when the condition fails. - x = Leaf - - return MyTree.sum(x) -``` - -These are equivalent to inline recursive functions that create a tree and consume it. - -```py -def MyTree.sum(x): - match x: - Node: - return x.val + MyTree.sum(x.left) + MyTree.sum(x.right) - Leaf: - return 0 - -def main_bend(val): - if val < 0: - return Node(val, main_bend(val + 1), main_bend(val + 1)) - else: - return Leaf - -def main: - return main_bend(0) -``` - -Making your program around trees is a very good way of making it parallelizable, since each core can be dispatched to work on a different branch of the tree. - -_Attention_: Note that despite the ADT syntax sugars, Bend is an _untyped_ language and the compiler will not stop you from using values incorrectly, which can lead to very unexpected results. -For example, the following program will compile just fine even though `!=` is only defined for native numbers: - -```py -def main: - bend val = [0, 1, 2, 3] while val != []: - match val: - List.cons: - x = val.head + go(val.tail) - List.nil: - x = 0 - then: - x = 0 - return x -``` - -Normalizing this program will show `λ* *` and not the expected `6`. - -It's also important to note that Bend is linear (technically affine), meaning that every variable is only used once. When a variable is used more than once, the compiler will automatically insert a duplication. -Duplications efficiently share the same value between two locations, only cloning a value when it's actually needed, but their exact behaviour is slightly more complicated than that and escapes normal lambda-calculus rules. -You can read more about it in [Dups and sups](docs/dups-and-sups.md) and learn how pattern matching avoids this problem in [Pattern matching](docs/pattern-matching.md). - -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) - return (result, result) - -# Is equivalent to -def foo(x): - return (bar(1, x), bar(1, x)) -``` - -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): - if n == 0: - return Nat.zero - else: - return Nat.succ(native_num_to_adt(n - 1)) -``` - -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. -Since Bend is eagerly executed, some situations will cause function applications to always be expanded, which can lead to looping situations. -You can read how to avoid this in [Lazy definitions](docs/lazy-definitions.md). - -Bend has native numbers and operations. - -```py -def main: - a = 1 # A 24 bit unsigned integer. - b = +2 # A 24 bit signed integer. - c = -3 # Another signed integer, but with negative value. - d = 1.0 # A 24 bit floating point number. - e = +0.001 # Also a float. - return (a * 2, b - c, d / e) -``` - -`switch` pattern matches on unsigned native numbers: - -```py -switch x = 4: - # From '0' to n, ending with the default case '_'. - 0: "zero" - 1: "one" - 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 - _: String.concat("other: ", (String.from_num x-3)) -``` - -Bend has Lists and Strings, which support Unicode characters. - -```rs -def main: - return ["You: Hello, 🌎", "🌎: Hello, user"] -``` - -A string is desugared to a String data type containing two constructors, `String.cons` and `String.nil`. -List also becomes a type with two constructors, `List.cons` and `List.nil`. - -```rs -# These two are equivalent -def StrEx: - "Hello" - -def ids: - [1, 2, 3] - -# These types are builtin. -enum String: - String.cons(head, tail) - String.nil -enum List: - List.cons(head, tail) - List.nil -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))) -``` - -Characters are delimited by `'` `'` and support Unicode escape sequences. They are encoded as a U24 with the unicode codepoint as their value. - -``` -# These two are equivalent -def chars: - ['A', '\u{4242}', '🌎'] - -def chars2: - [65, 0x4242, 0x1F30E] -``` - -### More features - -Key: - -- 📗: Basic resources -- 📙: Intermediate resources -- 📕: Advanced resources - -Other features are described in the following documentation files: - -- 📗 Lazy definitions: [Making recursive definitions lazy](docs/lazy-definitions.md) -- 📗 Data types: [Defining data types](docs/defining-data-types.md) -- 📗 Pattern matching: [Pattern matching](docs/pattern-matching.md) -- 📗 Native numbers and operations: [Native numbers](docs/native-numbers.md) -- 📗 Builtin definitions: [Builtin definitions](docs/builtin-defs.md) -- 📗 CLI arguments: [CLI arguments](docs/cli-arguments.md) -- 📙 Duplications and superpositions: [Dups and sups](docs/dups-and-sups.md) -- 📙 Scopeless lambdas: [Using scopeless lambdas](docs/using-scopeless-lambdas.md) -- 📕: Fusing functions: [Writing fusing functions](docs/writing-fusing-functions.md) - -## Further reading - -- 📙 [Compilation and readback](docs/compilation-and-readback.md) -- 📙 [Old HVM wiki learning material](https://github.com/HigherOrderCO/HVM/wiki/HVM-Wiki). It is outdated, but it can still teach you some of the basics. +- For an extensive list of features, check [FEATURES.md](https://github.com/HigherOrderCO/bend/blob/main/FEATURES.md).