Merge pull request #324 from HigherOrderCO/feature/sc-697/update-features-md-to-the-current-syntax

[sc-697] Update documentation to reflect the current syntax
This commit is contained in:
Nicolas Abril 2024-05-17 13:55:46 +02:00 committed by GitHub
commit 7632da2ed3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 443 additions and 105 deletions

12
Cargo.lock generated
View File

@ -178,9 +178,9 @@ dependencies = [
[[package]]
name = "either"
version = "1.11.0"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b"
[[package]]
name = "encode_unicode"
@ -278,9 +278,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.154"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linked-hash-map"
@ -423,9 +423,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.63"
version = "2.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
checksum = "7ad3dee41f36859875573074334c200d1add8e4a87bb37113ebd31d926b7b11f"
dependencies = [
"proc-macro2",
"quote",

View File

@ -1,10 +1,12 @@
## 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.
Bend offers two flavors of syntax, the user-friendly python-like syntax "Imp" (the default) and the core ML/Haskell-like syntax "Fun".
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.
### Basic features
We can start with a basic program that adds the numbers 3 and 2.
```py
@ -12,8 +14,8 @@ 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.
Running this program will show the number 5.
Be careful with `run` since it 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`.
@ -36,69 +38,95 @@ def main:
return sum
```
### Data types
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):
# 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):
# With an object (similar to what other languages call a struct, a class or a record)
object Pair { fst, snd }
def Pair/fst(x):
match x:
Pair:
case 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):
# We can also access the fields of an object after we `open` it.
def Pair/fst_2(x):
open Pair: x
return x.fst
# This is how we can create new objects.
def Pair/with_one(x):
return Pair{ fst: x, snd: 1 }
# The function can be named anything, but by convention we use Type/function_name.
def Pair/swap(x):
open Pair: x
# We can also call the constructor like any normal function.
return Pair(x.snd, x.fst)
```
For more complicated data structures, we can use `enum` to define a algebraic data types.
For more complicated data structures, we can use `type` to define algebraic data types.
```py
enum MyTree:
Node(val, ~left, ~right)
type 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.
This defines a constructor function for each variant of the type, with names `MyTree/Node` and `MyTree/Leaf`.
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).
### Pattern matching
We can pattern match on values of a data type to perform different actions depending on the variant of the value.
```py
def Maybe.or_default(x, default):
def Maybe/or_default(x, default):
match x:
Maybe/some:
case Maybe/Some:
# We can access the fields of the variant using 'matched.field'
return x.val
Maybe/none:
case Maybe/None:
return default
```
### Folding
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`:
This allows us to easily create and consume these recursive data structures with `bend` and `fold`.
`fold` is a recursive `match` that you can use to transform and consume data structures.
`bend` is a pure recursive loop that is very useful for generating data structures.
```py
def MyTree.sum(x):
# Sum all the values in the tree.
fold x:
# The fold is implicitly called for fields marked with '~' in their definition.
Node:
return val + x.left + x.right
Leaf:
case MyTree/Node:
return x.val + x.left + x.right
case MyTree/Leaf:
return 0
def main:
bend val = 0 while val < 0:
# 'fork' calls the bend recursively with the provided values.
x = Node(val=val, left=fork(val + 1), right=fork(val + 1))
then:
# 'then' is the base case, when the condition fails.
x = Leaf
bend val = 0:
when val < 10:
# 'fork' calls the bend recursively with the provided values.
x = MyTree/Node { val:val, left:fork(val + 1), right:fork(val + 1) }
else:
# 'else' is the base case, when the condition fails.
x = MyTree/Leaf
return MyTree.sum(x)
```
@ -108,40 +136,63 @@ These are equivalent to inline recursive functions that create a tree and consum
```py
def MyTree.sum(x):
match x:
Node:
case MyTree/Node:
return x.val + MyTree.sum(x.left) + MyTree.sum(x.right)
Leaf:
case MyTree/Leaf:
return 0
def main_bend(val):
if val < 0:
return Node(val, main_bend(val + 1), main_bend(val + 1))
if val < 10:
return MyTree/Node(val, main_bend(val + 1), main_bend(val + 1))
else:
return Leaf
return MyTree/Leaf
def main:
return main_bend(0)
x = main_bend(0)
return MyTree.sum(x)
```
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.
You can also pass some state variables to `fold` just like the variables used in a `bend`.
If you give a `fold` some state, then you necessarily need to pass it by calling the folded fields of the matched value, like passing an additional argument to the fold call.
```py
# This function substitutes each value in the tree with the sum of all the values before it.
def MyTree.map_sum(x):
acc = 0
fold x with acc:
case MyTree/Node:
# `x.left` and `x.right` are called with the new state value.
# Note that values are copied if you use them more than once, so you don't want to pass something very large.
return MyTree/Node{ val: x.val + acc, left: x.left(x.val + acc), right: x.right(x.val + acc) }
case MyTree/Leaf:
return x
```
This allows `fold` to be a very powerful and generic tool that can be used to implement most pure data transformations.
### Some caveats and limitations
_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 + fork(val.tail)
List.nil:
x = 0
then:
x = 0
bend val = [0, 1, 2, 3]:
when val != []:
match val:
case List/Cons:
x = val.head + fork(val.tail)
case List/Nil:
x = 0
else:
x = 0
return x
```
Normalizing this program will show `λ* *` and not the expected `6`.
Running 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.
@ -167,15 +218,36 @@ Bend supports recursive functions of unrestricted depth:
```py
def native_num_to_adt(n):
if n == 0:
return Nat.zero
return Nat/Zero
else:
return Nat.succ(native_num_to_adt(n - 1))
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.
```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
)
# Instead we want to write it like this;
def scott_list.add(xs, add):
xs(
λxs.head xs.tail: λadd: λc n: (c (xs.head + add) scott_list.sum(xs.tail, add)),
λadd: λc λn: n,
add
)
```
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).
### Numbers
Bend has native numbers and operations.
```py
@ -188,20 +260,31 @@ def main:
return (a * 2, b - c, d / e)
```
`switch` pattern matches on unsigned native numbers:
Unsigned numbers are written as just the number.
Signed numbers are written with a `+` or `-` sign.
Floating point numbers must have the decimal point `.` and can optionally take a sign `+` or `-`.
The three number types are fundamentally different.
If you mix two numbers of different types HVM will interpret the binary representation of one of them incorrectly, leading to incorrect results. Which number is interpreted incorrectly depends on the situation and shouldn't be relied on for now.
At the moment Bend doesn't have a way to convert between the different number types, but it will be added in the future.
You can use `switch` to pattern match on unsigned native numbers:
```py
switch x = 4:
# From '0' to n, ending with the default case '_'.
0: "zero"
1: "one"
2: "two"
case 0: "zero"
case 1: "one"
case 2: "two"
# The default case binds the name <arg>-<n>
# 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))
case _: String.concat("other: ", (String.from_num x-3))
```
### Other builtin types
Bend has Lists and Strings, which support Unicode characters.
```rs
@ -209,28 +292,29 @@ 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`.
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
# When you write this
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
# 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)))))
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)))
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
```
Characters are delimited by `'` `'` and support Unicode escape sequences. They are encoded as a U24 with the unicode codepoint as their value.
@ -244,6 +328,32 @@ def chars2:
[65, 0x4242, 0x1F30E]
```
### Mixing syntaxes
As was said in the beginning, Bend offers two flavors of syntax.
You can mix and match them freely in your program, as long as each function uses only one flavor.
```py
type Bool:
True
False
def is_odd(x):
switch x:
case 0:
return Bool/False
case _:
return is_even(x-1)
(is_even n) = switch n {
0: return Bool/True
_: (is_odd n-1)
}
main = (is_odd 20)
```
### More features
Key:
@ -258,11 +368,11 @@ Other features are described in the following documentation files:
- &#128215; Data types: [Defining data types](docs/defining-data-types.md)
- &#128215; Pattern matching: [Pattern matching](docs/pattern-matching.md)
- &#128215; Native numbers and operations: [Native numbers](docs/native-numbers.md)
- &#128215; Builtin definitions: [Builtin definitions](docs/builtin-defs.md)
- &#128215; Builtin definitions: *Documentation coming soon*
- &#128215; CLI arguments: [CLI arguments](docs/cli-arguments.md)
- &#128217; Duplications and superpositions: [Dups and sups](docs/dups-and-sups.md)
- &#128217; Scopeless lambdas: [Using scopeless lambdas](docs/using-scopeless-lambdas.md)
- &#128213;: Fusing functions: [Writing fusing functions](docs/writing-fusing-functions.md)
- &#128213; Fusing functions: [Writing fusing functions](docs/writing-fusing-functions.md)
## Further reading

View File

@ -89,7 +89,7 @@ two points:
def distance(ax, ay, bx, by):
dx = bx - ax
dy = by - ay
return (dx * dx + dy * dy) ^ 0.5
return (dx * dx + dy * dy) ** 0.5
def main():
return distance(10.0, 10.0, 20.0, 20.0)
@ -103,7 +103,7 @@ def distance(a, b):
(bx, by) = b
dx = bx - ax
dy = by - ay
return (dx * dx + dy * dy) ^ 0.5
return (dx * dx + dy * dy) ** 0.5
def main():
return distance((10.0, 10.0), (20.0, 20.0))
@ -121,7 +121,7 @@ def distance(a, b):
open V2: b
dx = b.x - a.x
dy = b.y - a.y
return (dx * dx + dy * dy) ^ 0.5
return (dx * dx + dy * dy) ** 0.5
def main():
return distance(V2 { x: 10.0, y: 10.0 }, V2 { x: 20.0, y: 20.0 })
@ -151,7 +151,7 @@ type Shape:
def area(shape):
match shape:
case Shape/Circle:
return 3.14 * shape.radius ^ 2.0
return 3.14 * shape.radius ** 2.0
case Shape/Rectangle:
return shape.width * shape.height

View File

@ -68,9 +68,9 @@ def main:
return sum(30, 0)
```
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*, Bend will run it multi-threaded. Some benchmarks:
This code adds all numbers from 0 up to (but not including) 2^30. But, instead
of a loop, we use a recursive divide-and-conquer approach. Since this approach
is *inherently parallel*, Bend will run it multi-threaded. Some benchmarks:
- CPU, Apple M3 Max, 1 thread: **3.5 minutes**

View File

@ -1,12 +1,31 @@
# Native numbers
HVM is more than a pure functional programming language. HVM also supports native unsigned 60-bits integers to improve performance.
Currently Bend supports 3 types of native numbers for fast numeric operations (compared to lambda-encoded numbers):
- U24: Unsigned integers (24 bits)
- I24: Signed integers (24 bits, two's complement)
- F24: Floating point numbers (single precision IEEE-754 floating point with the last bits of the mantissa implicitly set to zero)
### U24
Unsigned numbers are written as just the number and are represented as a 24 bit unsigned integer.
```rs
two = 2
```
Native integers should be used instead of Scott-encoded natural numbers when performance is needed.
### I24
Signed numbers are written with a `+` or `-` sign and are represented as a 24 bit two's complement integer.
```rs
minus_two = -2
plus_0 = +0
```
Positive numbers _must_ be written with a `+` sign, otherwise they'll be interpreted as unsigned.
Numbers can also be written in binary or hexadecimal form. Underscores can be optionally used as digit separators to make large numbers more readable.
@ -14,25 +33,69 @@ Numbers can also be written in binary or hexadecimal form. Underscores can be op
decimal = 1194684
binary = 0b100_100_011_101_010_111_100
hexadecimal = 0x123_abc
hex_signed = -0xbeef
### F24
Floating point numbers must have the decimal point `.` and can optionally take a sign `+` or `-`.
They are represented as IEEE-754 single precision floating point numbers with the last bits of the mantissa implicitly set to zero.
```py
one = 1.0
pi = +3.1415926535897932384626433 # Will get rounded to 24bit float
a_millionth = 0.000001
zero = 0.0
minus_zero = -0.0
```
There is also support for native operations. They are written in reverse polish notation and take 2 arguments each.
### Mixing number types
The three number types are fundamentally different.
If you mix two numbers of different types HVM will interpret the binary representation of one of them incorrectly, leading to incorrect results. Which number is interpreted incorrectly depends on the situation and shouldn't be relied on for now.
At the HVM level, both type and the operation are stored inside the number nodes as tags. One number stores the type, the other the operation.
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
There is also support for native operations.
In "Imp" syntax they are infix operators and in "Fun" syntax they are written in reverse polish notation (like you'd call a normal function).
Each operation takes two arguments and returns a new number.
```rs
# In Fun syntax
some_val = (+ (+ 7 4) (* 2 3))
```
The current operations include `+, -, *, /, %, ==, !=, <, >, <=, >=, &, |, ^, ~, <<, >>`.
The `~` symbol stands for NOT. It takes two arguments and calculates the 60-bit binary NOT of the first one, but ignores its second one. However, because of implementation details, the second argument must be a number too.
These are the currently available operations:
```rs
main = (~ 42 10)
// Outputs the same thing as (~ 42 50)
// And as (~ 42 1729)
// But not the same thing as (~ 42 *) (!)
```
Operation | Description | Accepted types | Return type
----------|-------------|----------------|------------
+ | Addition | U24, I24, F24 | Same as arguments
- | Subtraction | U24, I24, F24 | Same as arguments
* | Multiplication | U24, I24, F24 | Same as arguments
/ | Division | U24, I24, F24 | Same as arguments
% | Modulo | U24, I24, F24 | Same as arguments
== | Equality | U24, I24, F24 | U24
!= | Inequality | U24, I24, F24 | U24
< | Less than | U24, I24, F24 | U24
\> | Greater than | U24, I24, F24 | U24
& | Bitwise and | U24, I24 | Same as arguments
| | Bitwise or | U24, I24 | Same as arguments
^ | Bitwise xor | U24, I24 | Same as arguments
** | Exponentiation | F24 | F24
### Pattern matching
HVM-lang also includes a `switch` syntax for pattern-matching U24 numbers.
HVM-lang also includes a `switch` syntax for pattern-matching native numbers. The `0` case is chosen when `n` is 0, and the `_` case is chosen when `n` is greater than 0. The previous number, by default, bound to `n-1`.
```rs
Number.to_church = λn λf λx
switch n {
@ -41,6 +104,23 @@ Number.to_church = λn λf λx
}
```
The `0` case matches when `n` is 0, and the `_` case matches when `n` is greater than 0.
In the `_` arm, we can access the predecessor of `n` with the `n-1` variable.
We can also match on more than one value at once.
If we do that, we must necessarily cover the cases in order, starting from 0.
```rs
Number.minus_three = λn λf λx
switch n {
0: 0
1: 0
2: 0
_: n-3
}
```
Using everything we learned, we can write a program that calculates the n-th Fibonacci number using native numbers
```rs
@ -57,3 +137,20 @@ fibonacci = λn // n is the argument
main = (fibonacci 15)
```
### Pattern matching numbers in Fun syntax equations
In Fun syntax, we can also use pattern matching equations to match on native unsigned numbers.
```rs
(fib 1) = 1
(fib 0) = 0
(fib n) = (+ (fib (- n 1)) (fib (- n 2)))
```
Unlike with `switch`, you can match any number and in any order.
The variable pattern is used to match on all other numbers.
Unlike with `switch`, you can't directly access the predecessor of the number.
You can read [Pattern matching](pattern-matching.md) for more information about how pattern matching equations are converted to `switch` and `match` expressions.

View File

@ -69,6 +69,7 @@ pub struct Reader<'a> {
dup_paths: Option<HashMap<u16, Vec<SlotId>>>,
/// Store for floating/unscoped terms, like dups and let tups.
scope: Scope,
// To avoid reinserting things in the scope.
seen_fans: Scope,
seen: HashSet<Port>,
errors: Vec<ReadbackError>,
@ -79,7 +80,7 @@ impl Reader<'_> {
use CtrKind::*;
maybe_grow(|| {
if self.dup_paths.is_none() && !self.seen.insert(next) {
if !self.seen.insert(next) && self.dup_paths.is_none() {
self.error(ReadbackError::Cyclic);
return Term::Var { nam: Name::new("...") };
}
@ -93,14 +94,22 @@ impl Reader<'_> {
}
// If we're visiting a con node...
NodeKind::Ctr(CtrKind::Con(lab)) => match next.slot() {
// If we're visiting a port 0, then it is a lambda.
// If we're visiting a port 0, then it is a tuple or a lambda.
0 => {
let nam = self.namegen.decl_name(self.net, Port(node, 1));
let bod = self.read_term(self.net.enter_port(Port(node, 2)));
Term::Lam {
tag: self.labels.con.to_tag(*lab),
pat: Box::new(Pattern::Var(nam)),
bod: Box::new(bod),
if self.is_tup(node) {
// A tuple
let lft = self.read_term(self.net.enter_port(Port(node, 1)));
let rgt = self.read_term(self.net.enter_port(Port(node, 2)));
Term::Fan { fan: FanKind::Tup, tag: self.labels.con.to_tag(*lab), els: vec![lft, rgt] }
} else {
// A lambda
let nam = self.namegen.decl_name(self.net, Port(node, 1));
let bod = self.read_term(self.net.enter_port(Port(node, 2)));
Term::Lam {
tag: self.labels.con.to_tag(*lab),
pat: Box::new(Pattern::Var(nam)),
bod: Box::new(bod),
}
}
}
// If we're visiting a port 1, then it is a variable.
@ -332,8 +341,8 @@ impl Reader<'_> {
}
}
/// Enters both ports 1 and 2 of a node,
/// Returning a Term if is possible to simplify the net, or the Terms on the two ports of the node.
/// Enters both ports 1 and 2 of a node. Returns a Term if it is
/// possible to simplify the net, or the Terms on the two ports of the node.
/// The two possible outcomes are always equivalent.
///
/// If:
@ -404,6 +413,42 @@ impl Reader<'_> {
diagnostics.add_diagnostic(msg.as_str(), Severity::Warning, DiagnosticOrigin::Readback);
}
}
/// Returns whether the given port represents a tuple or some other
/// term (usually a lambda).
///
/// Used heuristic: a con node is a tuple if port 1 is a closed net and not an ERA.
fn is_tup(&self, node: NodeId) -> bool {
if !matches!(self.net.node(node).kind, NodeKind::Ctr(CtrKind::Con(_))) {
return false;
}
if self.net.node(self.net.enter_port(Port(node, 1)).node()).kind == NodeKind::Era {
return false;
}
let mut wires = HashSet::new();
let mut to_check = vec![self.net.enter_port(Port(node, 1))];
while let Some(port) = to_check.pop() {
match port.slot() {
0 => {
let node = port.node();
let lft = self.net.enter_port(Port(node, 1));
let rgt = self.net.enter_port(Port(node, 2));
to_check.push(lft);
to_check.push(rgt);
}
1 | 2 => {
// Mark as a wire. If already present, mark as visited by removing it.
if !(wires.insert(port) && wires.insert(self.net.enter_port(port))) {
wires.remove(&port);
wires.remove(&self.net.enter_port(port));
}
}
_ => unreachable!(),
}
}
// No hanging wires = a combinator = a tuple
wires.is_empty()
}
}
/// Argument for a Opr node

View File

@ -104,6 +104,13 @@ impl Stmt {
Stmt::Open { typ: _, var: _, nxt } => {
nxt.gen_map_get(id);
}
Stmt::Use { nam: _, val: bod, nxt } => {
nxt.gen_map_get(id);
let substitutions = bod.substitute_map_gets(id);
if !substitutions.is_empty() {
*self = gen_get(self, substitutions);
}
}
Stmt::Err => {}
}
}

View File

@ -167,6 +167,12 @@ pub enum Stmt {
var: Name,
nxt: Box<Stmt>,
},
// "use" {name} "=" {expr} ";"? {nxt}
Use {
nam: Name,
val: Box<Expr>,
nxt: Box<Stmt>,
},
#[default]
Err,
}

View File

@ -84,6 +84,10 @@ impl Stmt {
Stmt::Open { typ: _, var: _, nxt } => {
nxt.order_kwargs(book)?;
}
Stmt::Use { nam: _, val: bod, nxt } => {
bod.order_kwargs(book)?;
nxt.order_kwargs(book)?;
}
Stmt::Return { term } => term.order_kwargs(book)?,
Stmt::Err => {}
}

View File

@ -241,14 +241,16 @@ impl<'a> PyParser<'a> {
fn list_or_comprehension(&mut self) -> ParseResult<Expr> {
self.consume_exactly("[")?;
// Empty list
self.skip_trivia();
if self.starts_with("]") {
if self.try_consume_exactly("]") {
return Ok(Expr::Lst { els: vec![] });
}
let head = self.parse_expr(false)?;
self.skip_trivia();
if self.try_parse_keyword("for") {
// Comprehension
self.skip_trivia();
let bind = self.parse_bend_name()?;
self.skip_trivia();
@ -262,6 +264,7 @@ impl<'a> PyParser<'a> {
self.consume("]")?;
Ok(Expr::Comprehension { term: Box::new(head), bind, iter: Box::new(iter), cond })
} else {
// List
let mut head = vec![head];
self.try_consume(",");
let tail = self.list_like(|p| p.parse_expr(false), "", "]", ",", false, 0)?;
@ -374,6 +377,8 @@ impl<'a> PyParser<'a> {
self.parse_do(indent)
} else if self.try_parse_keyword("open") {
self.parse_open(indent)
} else if self.try_parse_keyword("use") {
self.parse_use(indent)
} else {
self.parse_assign(indent)
}
@ -852,6 +857,22 @@ impl<'a> PyParser<'a> {
Ok((stmt, nxt_indent))
}
fn parse_use(&mut self, indent: &mut Indent) -> ParseResult<(Stmt, Indent)> {
self.skip_trivia_inline();
let nam = self.parse_bend_name()?;
self.skip_trivia_inline();
self.consume_exactly("=")?;
self.skip_trivia_inline();
let bod = self.parse_expr(true)?;
self.skip_trivia_inline();
self.try_consume_exactly(";");
self.consume_new_line()?;
self.consume_indent_exactly(*indent)?;
let (nxt, nxt_indent) = self.parse_statement(indent)?;
let stmt = Stmt::Use { nam, val: Box::new(bod), nxt: Box::new(nxt) };
Ok((stmt, nxt_indent))
}
pub fn parse_def(&mut self, mut indent: Indent) -> ParseResult<(Definition, Indent)> {
if indent != Indent::Val(0) {
let msg = "Indentation error. Functions defined with 'def' must be at the start of the line.";

View File

@ -272,6 +272,14 @@ impl Stmt {
let term = fun::Term::Open { typ, var, bod: Box::new(nxt) };
if let Some(pat) = nxt_pat { StmtToFun::Assign(pat, term) } else { StmtToFun::Return(term) }
}
Stmt::Use { nam, val, nxt } => {
let (nxt_pat, nxt) = match nxt.into_fun()? {
StmtToFun::Return(term) => (None, term),
StmtToFun::Assign(pat, term) => (Some(pat), term),
};
let term = fun::Term::Use { nam: Some(nam), val: Box::new(val.to_fun()), nxt: Box::new(nxt) };
if let Some(pat) = nxt_pat { StmtToFun::Assign(pat, term) } else { StmtToFun::Return(term) }
}
Stmt::Return { term } => StmtToFun::Return(term.to_fun()),
Stmt::Err => unreachable!(),
};

View File

@ -0,0 +1,4 @@
run
tests/golden_tests/cli/tuple_readback.bend
-Ono-float-combinators
-Aall

View File

@ -0,0 +1,9 @@
# We probably will not be able to handle all these
main = (
@a (1, a),
(1, 2),
(*, 2),
($x, @$x *),
((1, @$y $z), (2, @$z $y)),
((@$a $b, @$b $c), (@$c $d, @$d $a))
)

View File

@ -7,6 +7,9 @@ String/from_list (List/Cons x xs) = (String/Cons x (String/from_list xs))
(Join List/Nil) = ""
(Join (List/Cons x xs)) = (Concat x (Join xs))
(Expand xs) = fold xs { String/Nil: xs; String/Cons: xs }
main =
((String/from_list ['\n', '\r', '\t', '\0', '\"', '\'', '\u{AFE}', '\\'])
, (Join ["\n", "\r", "\t", "\0", "\"", "\'", "\u{AFE}", "\\"]))
let a = (String/from_list ['\n', '\r', '\t', '\0', '\"', '\'', '\u{AFE}', '\\'])
let b = (Join ["\n", "\r", "\t", "\0", "\"", "\'", "\u{AFE}", "\\"])
(Expand (Concat a b))

View File

@ -0,0 +1,2 @@
def main:
return []

View File

@ -0,0 +1,6 @@
def bar(x, y):
return {x y}
def main(x):
use result = bar(1, x)
return {result result}

View File

@ -0,0 +1,6 @@
---
source: tests/golden_tests.rs
input_file: tests/golden_tests/cli/tuple_readback.bend
---
Warning: Running in strict mode without enabling the float_combinators pass can lead to some functions expanding infinitely.
Result: (λa (1, a), ((1, 2), (λ* 2, (λb λc *, (λd (2, λe $f), λg λh λi $j)))))

View File

@ -2,4 +2,4 @@
source: tests/golden_tests.rs
input_file: tests/golden_tests/run_file/escape_sequences.bend
---
λa (Join ["\n", "\r", "\t", "\0", "\"", "'", "\u{afe}", "\\"])
"\n\r\t\0\"'\u{afe}\\\n\r\t\0\"'\u{afe}\\"

View File

@ -0,0 +1,5 @@
---
source: tests/golden_tests.rs
input_file: tests/golden_tests/run_file/imp_empty_list.bend
---
λ* λa a

View File

@ -0,0 +1,5 @@
---
source: tests/golden_tests.rs
input_file: tests/golden_tests/run_file/imp_use_statement.bend
---
λa {{1 a} {1 a}}

View File

@ -2,4 +2,4 @@
source: tests/golden_tests.rs
input_file: tests/golden_tests/run_file/nested_str.bend
---
λa λb λc λd λ* (d "ab" λg λ* (g "cd" ""))
((String/Cons "a" ""), (λa λ* (a 97 λb λ* (b "bc" "")), (λe λ* (e "ab" "c"), λi λ* (i "ab" λl λ* (l "cd" "")))))