From d8d728a31d8563930d1366a06e451c9a45e349b7 Mon Sep 17 00:00:00 2001 From: Nicolas Abril Date: Mon, 8 Jul 2024 18:02:53 +0200 Subject: [PATCH 1/8] Add dylib functions, improve the builtins file a bit --- docs/ffi.md | 212 ++++++++++++++++++ examples/fusing_not.bend | 4 +- examples/hello_world.bend | 4 + ...lel_hello_world.bend => parallel_sum.bend} | 0 src/fun/builtins.bend | 185 ++++++++++----- src/fun/builtins.rs | 6 +- tests/golden_tests.rs | 34 ++- .../golden_tests/run_file/basic_num_ops.bend | 4 +- .../run_file/encode_decode_utf8.bend | 2 +- .../snapshots/examples__hello_world.bend.snap | 5 + .../examples__parallel_sum.bend.snap | 5 + 11 files changed, 382 insertions(+), 79 deletions(-) create mode 100644 docs/ffi.md create mode 100644 examples/hello_world.bend rename examples/{parallel_hello_world.bend => parallel_sum.bend} (100%) create mode 100644 tests/snapshots/examples__hello_world.bend.snap create mode 100644 tests/snapshots/examples__parallel_sum.bend.snap diff --git a/docs/ffi.md b/docs/ffi.md new file mode 100644 index 000000000..773b070a4 --- /dev/null +++ b/docs/ffi.md @@ -0,0 +1,212 @@ +# Dynamically linked libraries and foreign functions + +We can add new IO functions to Bend during runtime by loading dynamic libraries. + +## Using IO dynamic libraries in Bend + +Here is an example of how we could load a Bend library that contains functions for working with directories. + +```py +import IO/DyLib +from List import (Cons, Nil, filter) +from Result import (Err, Ok) +import String + +def main(): + with IO: + # Open the dynamic library file + # 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 <- 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. + # If you're writing a library for Bend that uses a dynamically linked library + # you should wrap the IO calls so that users don't need to know what's in the dynamic library. + + # The first argument is the dynamic library id. + # The second argument is the name of the function we want to call as a String. + # The third argument are the arguments to the function. + # You need to know the types of the arguments and the return type of the function. + + # In our example, 'ls' receives a path as a String and + # returns a String with the result of the 'ls' command. + files_bytes <- DyLib/call(dl, "ls", "/home") + files_str = String/decode_utf8(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. + my_dir = filter(files, String/equals("my_user")) + match my_dir: + case Cons: + # The directory already exists, do nothing. + * <- print("Directory already exists.") + status = -1 + case Nil: + # The directory doesn't exist, create it. + * <- DyLib/call(dl, "mkdir", "/home/my_user") + * <- print("Directory created.") + status = +0 + + # 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. + * <- DyLib/close(dl) + return wrap(status) +``` + +## Writing IO dynamic libraries for Bend + +Bend IO libraries need to be implemented in C or Cuda (depending on the backend you're targeting) using the HVM API. + +### Writing libraries for the C runtime + +The functions you call from Bend using `IO/DyLib/call` must have the following signature: + +```c +Port function_name(Net* net, Book* book, Port arg); +``` + +Where: + +- `net` is a pointer to the current network state. +- `book` is a pointer to the book of function definitions. +- `arg` is a pointer to the arguments of the function. + +The return value must be a `Port` that points to the return value of the function. + +HVM provides some util functions to do the conversions from HVM to C and vice versa, +so that you don't need to understand the details of the HVM runtime. + +We can implement the example library from earlier for the C runtime with the following C code: + +```c +// This is a header file that contains the HVM API. +#include + +// The headers we need to open and read directories. +#include +#include +#include + +// IO functions must have this exact signature. +// The first argument is a pointer to the graph with the current state of the program. +// The second argument is a pointer to the book of function definitions. +// The third argument points to the arguments of the function. +// The return value must be a port that points to the return value of the function. +Port ls(Net* net, Book* book, Port arg) { + // The arguments first need to be converted from HVM to C. + // For the 'ls' function, this is just a single string. + Str ls_path = readback_str(net, book, tup.elem_buf[0]); + + // Now we can do the actual IO operations. + // In this case, we list the contents of the directory + // by calling the 'ls' program as a subprocess. + char* cmd = malloc(strlen(ls_path) + strlen("ls ") + 1); + sprintf(cmd, "ls %s", ls_path); + free(ls_path.buf); + + FILE* pipe = popen(cmd, "r"); + free(cmd); + if (pipe == NULL) { + fprintf(stderr, "failed to run command '%s': %s\n", cmd, strerror(errno)); + + // It'd be best practice to return a Result type instead of a null value (ERA). + // If this command fails and the calling Bend program tries to use the result, + // it will get corrupted and spit out garbage. + return new_port(ERA, 0); + } + char buffer[512]; + char *output; + size_t output_len = 0; + while (fgets(buffer, sizeof(buffer), pipe) != NULL) { + size_t len = strlen(buffer); + char* new_result = realloc(output, output_len + len + 1); + if (new_result == NULL) { + free(output); + pclose(pipe); + fprintf(stderr, "failed to allocate space for output of '%s': %s\n", cmd, strerror(errno)); + return new_port(ERA, 0); + } + output = new_result; + strcpy(output + output_len, buffer); + output_len += len; + } + + // After we're done with the operation, we convert it to HVM format. + // In this case, the output is the output of the 'ls' command as a list of bytes. + // We need to process it in Bend later to convert it to a list of file names. + Bytes output_bytes = Bytes { .buf = output, .len = output_len }; + Port output_port = inject_bytes(net, output_bytes); + + // Remember to free all the allocated memory. + free(output); + pclose(pipe); + return output_port; +} + +Port mkdir(Net* net, Book* book, Port arg) { + // We do the same thing here as in the 'ls' function, + // except we call 'mkdir' which doesn't output anything. + Str ls_path = readback_str(net, book, tup.elem_buf[0]); + + char* cmd = malloc(strlen(ls_path) + strlen("mkdir ") + 1); + sprintf(cmd, "mkdir %s", ls_path); + int res = system(cmd); + + free(ls_path.buf); + free(cmd); + return new_port(ERA, 0); +} +``` + +To compile this code into a library, we can use the `gcc` compiler and include the HVM header files. + +Assuming that it's saved in a file called `libbend_dirs.c`, we can compile it with the following command: + +```sh +gcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.c +``` + +Now we can use the dynamic library in our Bend program, we just need to pass the path to the library to `IO/DyLib/open`. + +### Writing libraries for the Cuda runtime + +Writing libraries for the Cuda runtime is very similar to writing libraries for the C runtime. + +The main difference is the function signature: + +```c++ +Port function_name(GNet* gnet, Port argm) +``` + +Where: + +- `gnet` is a pointer to the current network state. +- `argm` is the argument to the function. + +The return value must be a `Port` that points to the return value of the function. + +To compile libraries of the Cuda runtime, we can use the `nvcc` compiler and include the HVM header files. + +Assuming that it's saved in a file called `libbend_dirs.cu`, we can compile it with the following command: + +```sh +nvcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.cu +``` + +### Compiling Bend programs that use dynamic libraries + +To compile the C or Cuda program generated from a Bend program that uses dynamic libraries, we need to use the `-rdynamic` flag to allow the dynamic library to use symbols from the main program. + +For example, if we have a Bend program called `main.bend` that uses the dynamic library `libbend_dirs.so`, we need compile to it with the following commands: + +```sh +# Compiling for C +bend gen-c my_app.bend > my_app.c +gcc -rdynamic -lm my_app.c -o my_app + +# Compiling for Cuda +bend gen-cu my_app.bend > my_app.cu +nvcc --compiler-options=-rdynamic my_app.cu -o my_app +``` diff --git a/examples/fusing_not.bend b/examples/fusing_not.bend index a83bea059..b6fd4909b 100644 --- a/examples/fusing_not.bend +++ b/examples/fusing_not.bend @@ -12,5 +12,5 @@ to_church n = switch n { _: λf λx (f (to_church n-1 f x)) } main = - # Self-composes `not` 2^24-1 times and prints the result. - ((to_church 0xFFFFFF) fusing_not) # try replacing 'fusing_not' by 'not'. Will it still work? + # Self-composes `not` 2^23-1 times and prints the result. + ((to_church 0x7FFFFF) fusing_not) # try replacing 'fusing_not' by 'not'. Will it still work? diff --git a/examples/hello_world.bend b/examples/hello_world.bend new file mode 100644 index 000000000..a1e3ce631 --- /dev/null +++ b/examples/hello_world.bend @@ -0,0 +1,4 @@ +def main(): + with IO: + * <- IO/print("Hello, world!") + return wrap(0) \ No newline at end of file diff --git a/examples/parallel_hello_world.bend b/examples/parallel_sum.bend similarity index 100% rename from examples/parallel_hello_world.bend rename to examples/parallel_sum.bend diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index cbae7c78a..ec977e73d 100644 --- a/src/fun/builtins.bend +++ b/src/fun/builtins.bend @@ -2,22 +2,57 @@ type String = (Nil) | (Cons head ~tail) type List = (Nil) | (Cons head ~tail) -List/length xs = fold xs with len=0, acc=[] { - List/Nil: (len, (List/reverse acc)) - List/Cons: (xs.tail (+ len 1) (List/Cons xs.head acc)) +# List/length(xs: List(T)) -> (u24, List(T)) +# Returns the length of a list and the list itself. +List/length xs = fold xs with len=0, acc=DiffList/new { + List/Nil: (len, (DiffList/to_list acc)) + List/Cons: (xs.tail (+ len 1) (DiffList/append acc xs.head)) } +# List/reverse(xs: List(T)) -> List(T) +# Reverses a list. List/reverse xs = fold xs with acc=[] { List/Nil: acc List/Cons: (xs.tail (List/Cons xs.head acc)) } +# List/flatten(xs: List(List(T))) -> List(T) +# Flattens a list of lists. List/flatten (List/Cons x xs) = (List/concat x (List/flatten xs)) List/flatten (List/Nil) = (List/Nil) +# List/concat(xs: List(T), ys: List(T)) -> List(T) +# Concatenates two lists. List/concat (List/Cons x xs) ys = (List/Cons x (List/concat xs ys)) List/concat (List/Nil) ys = ys +# List/split_once(xs: List(T), val: T) -> (Result(List(T), List(T))) +# Splits a list into two lists at the first occurrence of a value. +List/split_once xs val = (List/split_once.go xs val @x x) + List/split_once.go List/Nil val acc = (Result/Err (acc List/Nil)) + List/split_once.go (List/Cons x xs) val acc = + if (== val x) { + (Result/Ok ((acc List/Nil), xs)) + } else { + (List/split_once.go xs val @y (acc (List/Cons x y))) + } + +# DiffList/new() -> (List(T) -> List(T)) +# Create a new difference list +DiffList/new = λx x + +# DiffList/append(diff: List(T) -> List(T), val: T) -> (List(T) -> List(T)) +# Append a value to the end of the difference list +DiffList/append diff val = λx (diff (List/Cons val x)) + +# DiffList/cons(diff: List(T) -> List(T), val: T) -> (List(T) -> List(T)) +# Append a value to the beginning of the difference list +DiffList/cons diff val = λx (List/Cons val (diff x)) + +# DiffList/to_list(diff: List(T) -> List(T)) -> (List(T)) +# Convert a difference list to a list +DiffList/to_list diff = (diff List/Nil) + type Nat = (Succ ~pred) | (Zero) type Result = (Ok val) | (Err val) @@ -123,12 +158,23 @@ def IO/sleep(seconds): ## File IO ### File IO primitives -IO/FS/open path mode = (IO/Call IO/MAGIC "OPEN" (path, mode) @x (IO/Done IO/MAGIC x)) -IO/FS/close file = (IO/Call IO/MAGIC "CLOSE" file @x (IO/Done IO/MAGIC x)) -IO/FS/read file num_bytes = (IO/Call IO/MAGIC "READ" (file, num_bytes) @x (IO/Done IO/MAGIC x)) -IO/FS/write file bytes = (IO/Call IO/MAGIC "WRITE" (file, bytes) @x (IO/Done IO/MAGIC x)) -IO/FS/seek file offset mode = (IO/Call IO/MAGIC "SEEK" (file, (offset, mode)) @x (IO/Done IO/MAGIC x)) -IO/FS/flush file = (IO/Call IO/MAGIC "FLUSH" file @x (IO/Done IO/MAGIC x)) +# IO/FS/open(path: String, mode: u24) -> (IO u24) +IO/FS/open path mode = (IO/Call IO/MAGIC "OPEN" (path, mode) IO/wrap) + +# IO/FS/close(file: u24) -> (IO None) +IO/FS/close file = (IO/Call IO/MAGIC "CLOSE" file IO/wrap) + +# IO/FS/read(file: u24, num_bytes: u24) -> (IO (List u24)) +IO/FS/read file num_bytes = (IO/Call IO/MAGIC "READ" (file, num_bytes) IO/wrap) + +# IO/FS/write(file: u24, bytes: (List u24)) -> (IO None) +IO/FS/write file bytes = (IO/Call IO/MAGIC "WRITE" (file, bytes) IO/wrap) + +# IO/FS/seek(file: u24, offset: i24, mode: u24) -> (IO None) +IO/FS/seek file offset mode = (IO/Call IO/MAGIC "SEEK" (file, (offset, mode)) IO/wrap) + +# IO/FS/flush(file: u24) -> (IO None) +IO/FS/flush file = (IO/Call IO/MAGIC "FLUSH" file IO/wrap) ### Always available files IO/FS/STDIN = 0 @@ -144,8 +190,9 @@ IO/FS/SEEK_CUR = +1 IO/FS/SEEK_END = +2 ### File utilities + +# IO/FS/read_file(path: String) -> (IO (List u24)) # Reads an entire file, returning a list of bytes. -# def IO/FS/read_file(path: String) -> IO [u24] def IO/FS/read_file(path): with IO: fd <- IO/FS/open(path, "r") @@ -153,8 +200,8 @@ def IO/FS/read_file(path): * <- IO/FS/close(fd) return wrap(bytes) +# IO/FS/read_to_end(fd: u24) -> (IO (List u24)) # Reads the remaining contents of a file, returning a list of read bytes. -# def IO/FS/read_to_end(fd: u24) -> IO [u24] def IO/FS/read_to_end(fd): return IO/FS/read_to_end.read_chunks(fd, []) @@ -168,8 +215,8 @@ def IO/FS/read_to_end.read_chunks(fd, chunks): case List/Cons: return IO/FS/read_to_end.read_chunks(fd, List/Cons(chunk, chunks)) +# IO/FS/read_line(fd: u24) -> (IO (List u24)) # Reads a single line from a file, returning a list of bytes. -# def IO/FS/read_line(fd: u24) -> IO [u24] def IO/FS/read_line(fd): return IO/FS/read_line.read_chunks(fd, []) @@ -177,7 +224,7 @@ def IO/FS/read_line.read_chunks(fd, chunks): with IO: # Read line in 1kB chunks chunk <- IO/FS/read(fd, 1024) - match res = Bytes/split_once(chunk, '\n'): + match res = List/split_once(chunk, '\n'): case Result/Ok: (line, rest) = res.val (length, *) = List/length(rest) @@ -190,8 +237,8 @@ def IO/FS/read_line.read_chunks(fd, chunks): chunks = List/Cons(line, chunks) return IO/FS/read_line.read_chunks(fd, chunks) +# IO/FS/write_file(path: String, bytes: (List u24)) -> (IO None) # Writes a list of bytes to a file given by a path. -# def IO/FS/write_file(path: String, bytes: [u24]) -> IO * def IO/FS/write_file(path, bytes): with IO: f <- IO/FS/open(path, "w") @@ -199,25 +246,16 @@ def IO/FS/write_file(path, bytes): * <- IO/FS/close(f) return wrap(bytes) -Bytes/split_once xs cond = (Bytes/split_once.go xs cond @x x) - Bytes/split_once.go List/Nil cond acc = (Result/Err (acc List/Nil)) - Bytes/split_once.go (List/Cons x xs) cond acc = - if (== cond x) { - (Result/Ok ((acc List/Nil), xs)) - } else { - (Bytes/split_once.go xs cond @y (acc (List/Cons x y))) - } - ### Standard input and output utilities +# IO/print(text: String) -> (IO *) # Prints a string to stdout, encoding it with utf-8. -# def IO/print(text: String) -> IO * IO/print text = (IO/FS/write IO/FS/STDOUT (String/encode_utf8 text)) +# 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 String -IO/input = (IO/input.go @x x) +IO/input = (IO/input.go DiffList/new) def IO/input.go(acc): # TODO: This is slow and inefficient, should be done in hvm using fgets. with IO: @@ -228,14 +266,40 @@ def IO/input.go(acc): return IO/input.go(acc) case List/Cons: if byte.head == '\n': - bytes = acc(List/Nil) - text = Bytes/decode_utf8(bytes) + bytes = DiffList/to_list(acc) + text = String/decode_utf8(bytes) return wrap(text) else: - acc = lambda x: acc(List/Cons(byte.head, x)) + acc = DiffList/append(acc, byte.head) return IO/input.go(acc) +### Dynamically linked libraries + +# IO/DyLib/open(path: String, lazy: u24) -> u24 +# 'path' is the path to the dl 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 dl object encoded as a u24 +def IO/DyLib/open(path, lazy): + return IO/Call(IO/MAGIC, "DL_OPEN", (path, lazy), IO/wrap) + +# IO/DyLib/call(dl: u24, fn: String, args: Any) -> Any +# Calls a function of a previously opened dylib. +# 'dl' is the id of the dl object +# 'fn' is the name of the function in the dylib +# '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, fn, args): + return IO/Call(IO/MAGIC, "DL_CALL", (dl, fn, args), IO/wrap) + +# IO/DyLib/close(dl: u24) -> None +# Closes a previously open dylib. +# 'dl' is the id of the dylib. +# Returns nothing. +def IO/DyLib/close(dl): + return IO/Call(IO/MAGIC, "DL_CLOSE", (dl), IO/wrap) + # Lazy thunks +# defer(val: T) -> ((T -> T) -> T) # 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' (undefer my_thunk) @@ -243,22 +307,15 @@ def IO/input.go(acc): # The example above can be written as: # (defer_arg (defer_arg (defer_arg (defer @arg1 @arg2 @arg3 (f arg1 arg2 arg3)) arg1) arg2) arg3) defer val = @x (x val) + +# defer_arg(defered: A -> B -> C, arg: B) -> (A -> C) defer_arg defered arg = @x (defered x arg) -undefer defered = (defered @x x) +# undefer(defered: (A -> A) -> B) -> B +undefer defered = (defered @x x) -# Native numeric operations -# log(x: f24, base: f24) -> f24 -# Computes the logarithm of `x` with the specified `base`. -hvm log: - (x ($([|] $(x ret)) ret)) - -# 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 atan2: - ($([&] $(y ret)) (y ret)) +# Native number casts # to_f24(x: native number) -> f24 # Casts any native number to an f24. @@ -279,13 +336,18 @@ hvm to_i24: Utf8/REPLACEMENT_CHARACTER = '\u{FFFD}' -Bytes/decode_utf8 bytes = +# String/decode_utf8(List u24) -> String +# Decodes a sequence of bytes to a String using utf-8 encoding. +String/decode_utf8 bytes = let (got, rest) = (Utf8/decode_character bytes) match rest { List/Nil: (String/Cons got String/Nil) - List/Cons: (String/Cons got (Bytes/decode_utf8 rest)) + List/Cons: (String/Cons got (String/decode_utf8 rest)) } +# Utf8/decode_character(List u24) -> (u24, List u24) +# Decodes one utf-8 character from the start of a sequence of bytes. +# Returns the decoded character and the remaining bytes. Utf8/decode_character [] = (0, []) Utf8/decode_character [a] = if (<= a 0x7F) { (a, []) } else { (Utf8/REPLACEMENT_CHARACTER, []) } Utf8/decode_character [a, b] = @@ -346,6 +408,8 @@ Utf8/decode_character (List/Cons a (List/Cons b (List/Cons c (List/Cons d rest)) } } +# String/encode_utf8(String) -> (List u24) +# Encodes a string to a sequence of bytes using utf-8 encoding. String/encode_utf8 (String/Nil) = (List/Nil) String/encode_utf8 (String/Cons x xs) = use Utf8/rune1max = 0b01111111 @@ -379,16 +443,37 @@ String/encode_utf8 (String/Cons x xs) = } } -Bytes/decode_ascii (List/Cons x xs) = (String/Cons x (Bytes/decode_ascii xs)) -Bytes/decode_ascii (List/Nil) = (String/Nil) +# String/decode_ascii(List u24) -> String +# Decodes a sequence of bytes to a String using ascii encoding. +String/decode_ascii (List/Cons x xs) = (String/Cons x (String/decode_ascii xs)) +String/decode_ascii (List/Nil) = (String/Nil) +# String/encode_ascii(String) -> (List u24) +# Encodes a string to a sequence of bytes using ascii encoding. String/encode_ascii (String/Cons x xs) = (List/Cons x (String/encode_ascii xs)) String/encode_ascii (String/Nil) = (List/Nil) # Math -# The Pi constant. -Math/PI = 3.1415926535 +# Math/PI() -> f24 +# The Pi (π) constant. +def Math/PI(): + return 3.1415926535 + +def Math/E(): + return 2.718281828 + +# Math/log(x: f24, base: f24) -> f24 +# Computes the logarithm of `x` with the specified `base`. +hvm Math/log: + (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. +hvm Math/atan2: + ($([&] $(y ret)) (y ret)) + # Math/sin(a: f24) -> f24 # Computes the sine of the given angle in radians. @@ -420,15 +505,15 @@ Math/csc a = (/ 1.0 (Math/sin a)) # Math/atan(a: f24) -> f24 # Computes the arctangent of the given angle. -Math/atan a = (atan2 a 1.0) +Math/atan a = (Math/atan2 a 1.0) # Math/asin(a: f24) -> f24 # Computes the arcsine of the given angle. -Math/asin a = (atan2 a (Math/sqrt (- 1.0 (* a a)))) +Math/asin a = (Math/atan2 a (Math/sqrt (- 1.0 (* a a)))) # Math/acos(a: f24) -> f24 # Computes the arccosine of the given angle. -Math/acos a = (atan2 (Math/sqrt (- 1.0 (* a a))) a) +Math/acos a = (Math/atan2 (Math/sqrt (- 1.0 (* a a))) a) # Math/radians(a: f24) -> f24 # Converts degrees to radians. diff --git a/src/fun/builtins.rs b/src/fun/builtins.rs index 882f750c3..500b02b63 100644 --- a/src/fun/builtins.rs +++ b/src/fun/builtins.rs @@ -47,9 +47,9 @@ pub const BUILTIN_TYPES: &[&str] = &[LIST, STRING, NAT, TREE, MAP, IO]; impl ParseBook { pub fn builtins() -> Self { - TermParser::new(BUILTINS) - .parse_book(Self::default(), true) - .expect("Error parsing builtin file, this should not happen") + let book = TermParser::new(BUILTINS) + .parse_book(Self::default(), true); + book.unwrap_or_else(|e| panic!("Error parsing builtin file, this should not happen:\n{e}")) } } diff --git a/tests/golden_tests.rs b/tests/golden_tests.rs index d1334e916..a377c24ff 100644 --- a/tests/golden_tests.rs +++ b/tests/golden_tests.rs @@ -1,9 +1,7 @@ use bend::{ compile_book, desugar_book, diagnostics::{Diagnostics, DiagnosticsConfig, Severity}, - fun::{ - load_book::do_parse_book_default, net_to_term::net_to_term, term_to_net::Labels, Book, Ctx, Name, Term, - }, + fun::{load_book::do_parse_book_default, net_to_term::net_to_term, term_to_net::Labels, Book, Ctx, Name}, hvm::hvm_book_show_pretty, imports::DefaultLoader, load_to_book, @@ -86,16 +84,6 @@ fn run_golden_test_dir_multiple(test_name: &str, run: &[&RunFn]) { } } -pub fn run_book_simple( - book: Book, - run_opts: RunOpts, - compile_opts: CompileOpts, - diagnostics_cfg: DiagnosticsConfig, - args: Option>, -) -> Result<(Term, String, Diagnostics), Diagnostics> { - run_book(book, run_opts, compile_opts, diagnostics_cfg, args, "run").map(Option::unwrap) -} - /* Snapshot/regression/golden tests Each tests runs all the files in tests/golden_tests/. @@ -150,13 +138,15 @@ fn linear_readback() { let book = do_parse_book_default(code, path)?; let compile_opts = CompileOpts::default().set_all(); let diagnostics_cfg = DiagnosticsConfig::default(); - let (term, _, diags) = run_book_simple( + let (term, _, diags) = run_book( book, RunOpts { linear_readback: true, ..Default::default() }, compile_opts, diagnostics_cfg, None, - )?; + "run", + )? + .unwrap(); let res = format!("{diags}{term}"); Ok(res) }); @@ -180,7 +170,7 @@ fn run_file() { for adt_encoding in [AdtEncoding::NumScott, AdtEncoding::Scott] { let compile_opts = CompileOpts { adt_encoding, ..CompileOpts::default() }; let (term, _, diags) = - run_book_simple(book.clone(), run_opts.clone(), compile_opts, diagnostics_cfg, None)?; + run_book(book.clone(), run_opts.clone(), compile_opts, diagnostics_cfg, None, "run")?.unwrap(); res.push_str(&format!("{adt_encoding}:\n{diags}{term}\n\n")); } Ok(res) @@ -205,7 +195,7 @@ fn import_system() { let mut res = String::new(); let compile_opts = CompileOpts::default(); - let (term, _, diags) = run_book_simple(book, run_opts, compile_opts, diagnostics_cfg, None)?; + let (term, _, diags) = run_book(book, run_opts, compile_opts, diagnostics_cfg, None, "run")?.unwrap(); res.push_str(&format!("{diags}{term}\n\n")); Ok(res) })], @@ -363,7 +353,7 @@ fn hangs() { let diagnostics_cfg = DiagnosticsConfig::new(Severity::Allow, false); let thread = std::thread::spawn(move || { - run_book_simple(book, RunOpts::default(), compile_opts, diagnostics_cfg, None) + run_book(book, RunOpts::default(), compile_opts, diagnostics_cfg, None, "run") }); std::thread::sleep(std::time::Duration::from_secs(expected_normalization_time)); @@ -397,7 +387,8 @@ fn run_entrypoint() { book.entrypoint = Some(Name::new("foo")); let compile_opts = CompileOpts::default().set_all(); let diagnostics_cfg = DiagnosticsConfig { ..DiagnosticsConfig::new(Severity::Error, true) }; - let (term, _, diags) = run_book_simple(book, RunOpts::default(), compile_opts, diagnostics_cfg, None)?; + let (term, _, diags) = + run_book(book, RunOpts::default(), compile_opts, diagnostics_cfg, None, "run")?.unwrap(); let res = format!("{diags}{term}"); Ok(res) }) @@ -459,7 +450,7 @@ fn io() { let compile_opts = CompileOpts::default(); let diagnostics_cfg = DiagnosticsConfig::default(); let (term, _, diags) = - run_book_simple(book, RunOpts::default(), compile_opts, diagnostics_cfg, None)?; + run_book(book, RunOpts::default(), compile_opts, diagnostics_cfg, None, "run-c")?.unwrap(); let res = format!("{diags}{term}"); Ok(format!("Strict mode:\n{res}")) }), @@ -485,7 +476,8 @@ fn examples() -> Result<(), Diagnostics> { let book = do_parse_book_default(&code, path).unwrap(); let compile_opts = CompileOpts::default(); let diagnostics_cfg = DiagnosticsConfig::default(); - let (term, _, diags) = run_book_simple(book, RunOpts::default(), compile_opts, diagnostics_cfg, None)?; + let (term, _, diags) = + run_book(book, RunOpts::default(), compile_opts, diagnostics_cfg, None, "run-c")?.unwrap(); let res = format!("{diags}{term}"); let mut settings = insta::Settings::clone_current(); diff --git a/tests/golden_tests/run_file/basic_num_ops.bend b/tests/golden_tests/run_file/basic_num_ops.bend index 3264972e5..7d74b544b 100644 --- a/tests/golden_tests/run_file/basic_num_ops.bend +++ b/tests/golden_tests/run_file/basic_num_ops.bend @@ -140,7 +140,7 @@ main = (List/expand 0xFFFF - (log 2.0 3.0) - (atan2 3.0 4.0) + (Math/log 2.0 3.0) + (Math/atan2 3.0 4.0) ] ) diff --git a/tests/golden_tests/run_file/encode_decode_utf8.bend b/tests/golden_tests/run_file/encode_decode_utf8.bend index ff01534a4..24ed09eb0 100644 --- a/tests/golden_tests/run_file/encode_decode_utf8.bend +++ b/tests/golden_tests/run_file/encode_decode_utf8.bend @@ -1,4 +1,4 @@ def main: use bytes = [72, 101, 108, 108, 111, 44, 32, 228, 184, 150, 231, 149, 140, 33] use s = "Hello, 世界!" - return (String/encode_utf8(s), Bytes/decode_utf8(bytes)) + return (String/encode_utf8(s), String/decode_utf8(bytes)) diff --git a/tests/snapshots/examples__hello_world.bend.snap b/tests/snapshots/examples__hello_world.bend.snap new file mode 100644 index 000000000..f1ae49b32 --- /dev/null +++ b/tests/snapshots/examples__hello_world.bend.snap @@ -0,0 +1,5 @@ +--- +source: tests/golden_tests.rs +input_file: examples/hello_world.bend +--- +λa (a IO/Done/tag IO/MAGIC 0) diff --git a/tests/snapshots/examples__parallel_sum.bend.snap b/tests/snapshots/examples__parallel_sum.bend.snap new file mode 100644 index 000000000..fd66b305a --- /dev/null +++ b/tests/snapshots/examples__parallel_sum.bend.snap @@ -0,0 +1,5 @@ +--- +source: tests/golden_tests.rs +input_file: examples/parallel_sum.bend +--- +16744448 From 2c092a0eb6d81df16f7a568de1e78e7e9a282c92 Mon Sep 17 00:00:00 2001 From: Nicolas Abril Date: Mon, 8 Jul 2024 18:22:48 +0200 Subject: [PATCH 2/8] Don't spell check literal blocks --- cspell.json | 2 +- src/fun/builtins.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/cspell.json b/cspell.json index 72e962277..4aa80d973 100644 --- a/cspell.json +++ b/cspell.json @@ -131,6 +131,6 @@ "/`...`/g", "/`....`/g", "/`.....`/g", - "/```.*\n.*\n```/g" + "/```(.*\n)*```/g" ] } diff --git a/src/fun/builtins.rs b/src/fun/builtins.rs index 500b02b63..d9489ffef 100644 --- a/src/fun/builtins.rs +++ b/src/fun/builtins.rs @@ -47,8 +47,7 @@ pub const BUILTIN_TYPES: &[&str] = &[LIST, STRING, NAT, TREE, MAP, IO]; impl ParseBook { pub fn builtins() -> Self { - let book = TermParser::new(BUILTINS) - .parse_book(Self::default(), true); + let book = TermParser::new(BUILTINS).parse_book(Self::default(), true); book.unwrap_or_else(|e| panic!("Error parsing builtin file, this should not happen:\n{e}")) } } From 8574cea19d4bdbabd574c52bca823fe8b8833181 Mon Sep 17 00:00:00 2001 From: Eduardo Sandalo Porto Date: Mon, 8 Jul 2024 14:54:35 -0300 Subject: [PATCH 3/8] Fix bugs in FFI documentation code --- docs/ffi.md | 36 ++++++++++++++++-------------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/docs/ffi.md b/docs/ffi.md index 773b070a4..fdcb764e9 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -7,18 +7,13 @@ We can add new IO functions to Bend during runtime by loading dynamic libraries. Here is an example of how we could load a Bend library that contains functions for working with directories. ```py -import IO/DyLib -from List import (Cons, Nil, filter) -from Result import (Err, Ok) -import String - def main(): with IO: # Open the dynamic library file # 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 <- 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. @@ -32,26 +27,26 @@ 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 <- DyLib/call(dl, "ls", "/home") + files_bytes <- IO/DyLib/call(dl, "ls", "/home") files_str = String/decode_utf8(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. - my_dir = filter(files, String/equals("my_user")) + my_dir = List/filter(files, String/equals("my_user")) match my_dir: case Cons: # The directory already exists, do nothing. - * <- print("Directory already exists.") + * <- IO/print("Directory already exists.") status = -1 case Nil: # The directory doesn't exist, create it. - * <- DyLib/call(dl, "mkdir", "/home/my_user") - * <- print("Directory created.") + * <- IO/DyLib/call(dl, "mkdir", "/home/my_user") + * <- IO/print("Directory created.") status = +0 # 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. - * <- DyLib/close(dl) + * <- IO/DyLib/close(dl) return wrap(status) ``` @@ -87,6 +82,7 @@ We can implement the example library from earlier for the C runtime with the fol // The headers we need to open and read directories. #include #include +#include #include // IO functions must have this exact signature. @@ -97,13 +93,13 @@ We can implement the example library from earlier for the C runtime with the fol Port ls(Net* net, Book* book, Port arg) { // The arguments first need to be converted from HVM to C. // For the 'ls' function, this is just a single string. - Str ls_path = readback_str(net, book, tup.elem_buf[0]); + Str ls_path = readback_str(net, book, arg); // Now we can do the actual IO operations. // In this case, we list the contents of the directory // by calling the 'ls' program as a subprocess. - char* cmd = malloc(strlen(ls_path) + strlen("ls ") + 1); - sprintf(cmd, "ls %s", ls_path); + char* cmd = malloc(strlen(ls_path.buf) + strlen("ls ") + 1); + sprintf(cmd, "ls %s", ls_path.buf); free(ls_path.buf); FILE* pipe = popen(cmd, "r"); @@ -136,8 +132,8 @@ Port ls(Net* net, Book* book, Port arg) { // After we're done with the operation, we convert it to HVM format. // In this case, the output is the output of the 'ls' command as a list of bytes. // We need to process it in Bend later to convert it to a list of file names. - Bytes output_bytes = Bytes { .buf = output, .len = output_len }; - Port output_port = inject_bytes(net, output_bytes); + Bytes output_bytes = { .buf = output, .len = output_len }; + Port output_port = inject_bytes(net, &output_bytes); // Remember to free all the allocated memory. free(output); @@ -148,10 +144,10 @@ Port ls(Net* net, Book* book, Port arg) { Port mkdir(Net* net, Book* book, Port arg) { // We do the same thing here as in the 'ls' function, // except we call 'mkdir' which doesn't output anything. - Str ls_path = readback_str(net, book, tup.elem_buf[0]); + Str ls_path = readback_str(net, book, arg); - char* cmd = malloc(strlen(ls_path) + strlen("mkdir ") + 1); - sprintf(cmd, "mkdir %s", ls_path); + char* cmd = malloc(strlen(ls_path.buf) + strlen("mkdir ") + 1); + sprintf(cmd, "mkdir %s", ls_path.buf); int res = system(cmd); free(ls_path.buf); From 5306e2694ea5478f308b3ad7ac20cdcb1fd68665 Mon Sep 17 00:00:00 2001 From: imaqtkatt Date: Wed, 10 Jul 2024 10:51:30 -0300 Subject: [PATCH 4/8] Add String and List builtins, update docs --- docs/builtins.md | 26 ++++++++++++++++++++++++++ docs/ffi.md | 9 +++++++-- src/fun/builtins.bend | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/docs/builtins.md b/docs/builtins.md index f15291dbf..05f165e42 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -21,6 +21,24 @@ A String literal is surrounded with `"`. Accepts the same values as characters l "Hello, World!" ``` +### Functions + +#### String/equals + +Checks if two strings are equal. + +```python +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] +``` + ## List ```python @@ -84,6 +102,14 @@ List/concat([1, 2], [4, 5]) # Result: [1, 2, 4, 5] ``` +#### List/filter + +Filters a list based on a predicate function. + +```python +List/filter(xs: List(T), pred: T -> Bool) -> List(T) +``` + ## Tree ```python diff --git a/docs/ffi.md b/docs/ffi.md index fdcb764e9..9b6198b37 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -161,12 +161,17 @@ To compile this code into a library, we can use the `gcc` compiler and include t Assuming that it's saved in a file called `libbend_dirs.c`, we can compile it with the following command: ```sh -gcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.c +Compile the shared library with unresolved symbols: +For macOS: +gcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.c -undefined dynamic_lookup + +For Linux: +gcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.c -Wl,--unresolved-symbols=ignore-all ``` Now we can use the dynamic library in our Bend program, we just need to pass the path to the library to `IO/DyLib/open`. -### Writing libraries for the Cuda runtime +### Writing libraries for the Cuda runtime Writing libraries for the Cuda runtime is very similar to writing libraries for the C runtime. diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index ec977e73d..27dc70a45 100644 --- a/src/fun/builtins.bend +++ b/src/fun/builtins.bend @@ -37,6 +37,41 @@ List/split_once xs val = (List/split_once.go xs val @x x) (List/split_once.go xs val @y (acc (List/Cons x y))) } +# List/filter(xs: List(T), pred: T -> Bool) -> List(T) +# Filters a list based on a predicate function. +List/filter (List/Nil) _ = List/Nil +List/filter (List/Cons x xs) pred = + if (pred x) { + (List/Cons x (List/filter xs pred)) + } else { + (List/filter xs pred) + } + +# String/equals(s1: String, s2: String) -> u24 +# Checks if two strings are equal. +String/equals (String/Nil) (String/Nil) = 1 +String/equals (String/Cons x xs) (String/Cons y ys) = + if (== x y) { + (String/equals xs ys) + } else { + 0 + } +String/equals * * = 0 + +# String/split(s: String, delimiter: u24) -> List(String) +# Splits a list into two lists at the first occurrence of a value. +String/split s delim = (String/split.go s delim (List/Cons String/Nil List/Nil)) + String/split.go (String/Nil) _ acc = (List/reverse acc) + String/split.go (String/Cons c cs) delim acc = + if (== c delim) { + (String/split.go cs delim (List/Cons String/Nil acc)) + } else { + match acc { + List/Cons: (String/split.go cs delim (List/Cons (String/Cons c acc.head) acc.tail)) + List/Nil: [] + } + } + # DiffList/new() -> (List(T) -> List(T)) # Create a new difference list DiffList/new = λx x @@ -275,7 +310,7 @@ def IO/input.go(acc): ### Dynamically linked libraries -# IO/DyLib/open(path: String, lazy: u24) -> u24 +# IO/DyLib/open(path: String, lazy: u24) -> u24 # 'path' is the path to the dl 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 dl object encoded as a u24 @@ -299,7 +334,7 @@ def IO/DyLib/close(dl): return IO/Call(IO/MAGIC, "DL_CLOSE", (dl), IO/wrap) # Lazy thunks -# defer(val: T) -> ((T -> T) -> T) +# defer(val: T) -> ((T -> T) -> T) # 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' (undefer my_thunk) From 45ca7bf70f728974aba1b6789053bf94cc23c22a Mon Sep 17 00:00:00 2001 From: Nicolas Abril Date: Thu, 11 Jul 2024 18:21:31 +0200 Subject: [PATCH 5/8] Fix stuff for the ffi documentation example --- docs/ffi.md | 68 +++++++++++++++++++++---------------------- src/fun/builtins.bend | 14 ++++----- src/lib.rs | 5 +++- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/docs/ffi.md b/docs/ffi.md index 9b6198b37..2c2a0d614 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -27,22 +27,23 @@ 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", "/home") + files_bytes <- IO/DyLib/call(dl, "ls", "./") files_str = String/decode_utf8(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. - my_dir = List/filter(files, String/equals("my_user")) + my_dir = List/filter(files, String/equals("my_dir")) match my_dir: - case Cons: + case List/Cons: # The directory already exists, do nothing. - * <- IO/print("Directory already exists.") - status = -1 - case Nil: + * <- IO/print("Directory already exists.\n") + status = wrap(-1) + case List/Nil: # The directory doesn't exist, create it. - * <- IO/DyLib/call(dl, "mkdir", "/home/my_user") - * <- IO/print("Directory created.") - status = +0 + * <- 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. @@ -77,7 +78,7 @@ We can implement the example library from earlier for the C runtime with the fol ```c // This is a header file that contains the HVM API. -#include +#include "hvm.h" // The headers we need to open and read directories. #include @@ -93,50 +94,49 @@ We can implement the example library from earlier for the C runtime with the fol Port ls(Net* net, Book* book, Port arg) { // The arguments first need to be converted from HVM to C. // For the 'ls' function, this is just a single string. - Str ls_path = readback_str(net, book, arg); + Str path = readback_str(net, book, arg); + // Now we can do the actual IO operations. // In this case, we list the contents of the directory // by calling the 'ls' program as a subprocess. - char* cmd = malloc(strlen(ls_path.buf) + strlen("ls ") + 1); - sprintf(cmd, "ls %s", ls_path.buf); - free(ls_path.buf); + char* cmd = malloc(path.len + strlen("ls ") + 1); + sprintf(cmd, "ls %s", path.buf); + free(path.buf); FILE* pipe = popen(cmd, "r"); - free(cmd); if (pipe == NULL) { - fprintf(stderr, "failed to run command '%s': %s\n", cmd, strerror(errno)); - // It'd be best practice to return a Result type instead of a null value (ERA). // If this command fails and the calling Bend program tries to use the result, // it will get corrupted and spit out garbage. + fprintf(stderr, "failed to run command '%s': %s\n", cmd, strerror(errno)); return new_port(ERA, 0); } char buffer[512]; - char *output; - size_t output_len = 0; + Bytes output = { .buf = NULL, .len = 0 }; while (fgets(buffer, sizeof(buffer), pipe) != NULL) { size_t len = strlen(buffer); - char* new_result = realloc(output, output_len + len + 1); + char* new_result = realloc(output.buf, output.len + len + 1); if (new_result == NULL) { - free(output); - pclose(pipe); fprintf(stderr, "failed to allocate space for output of '%s': %s\n", cmd, strerror(errno)); + free(cmd); + free(output.buf); + pclose(pipe); return new_port(ERA, 0); } - output = new_result; - strcpy(output + output_len, buffer); - output_len += len; + output.buf = new_result; + strcpy(output.buf + output.len, buffer); + output.len += len; } // After we're done with the operation, we convert it to HVM format. // In this case, the output is the output of the 'ls' command as a list of bytes. // We need to process it in Bend later to convert it to a list of file names. - Bytes output_bytes = { .buf = output, .len = output_len }; - Port output_port = inject_bytes(net, &output_bytes); + Port output_port = inject_bytes(net, &output); // Remember to free all the allocated memory. - free(output); + free(cmd); + free(output.buf); pclose(pipe); return output_port; } @@ -144,13 +144,13 @@ Port ls(Net* net, Book* book, Port arg) { Port mkdir(Net* net, Book* book, Port arg) { // We do the same thing here as in the 'ls' function, // except we call 'mkdir' which doesn't output anything. - Str ls_path = readback_str(net, book, arg); + Str path = readback_str(net, book, arg); - char* cmd = malloc(strlen(ls_path.buf) + strlen("mkdir ") + 1); - sprintf(cmd, "mkdir %s", ls_path.buf); + char* cmd = malloc(path.len + strlen("mkdir ") + 1); + sprintf(cmd, "mkdir %s", path.buf); int res = system(cmd); - free(ls_path.buf); + free(path.buf); free(cmd); return new_port(ERA, 0); } @@ -163,10 +163,10 @@ Assuming that it's saved in a file called `libbend_dirs.c`, we can compile it wi ```sh Compile the shared library with unresolved symbols: For macOS: -gcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.c -undefined dynamic_lookup +gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -undefined dynamic_lookup -fPIC For Linux: -gcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.c -Wl,--unresolved-symbols=ignore-all +gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -Wl,--unresolved-symbols=ignore-all -fPIC ``` Now we can use the dynamic library in our Bend program, we just need to pass the path to the library to `IO/DyLib/open`. diff --git a/src/fun/builtins.bend b/src/fun/builtins.bend index 27dc70a45..89c04fd15 100644 --- a/src/fun/builtins.bend +++ b/src/fun/builtins.bend @@ -311,24 +311,24 @@ def IO/input.go(acc): ### Dynamically linked libraries # IO/DyLib/open(path: String, lazy: u24) -> u24 -# 'path' is the path to the dl 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 or upfront. -# Returns an unique id to the dl object encoded as a u24 +# Returns an unique id to the library object encoded as a u24 def IO/DyLib/open(path, lazy): return IO/Call(IO/MAGIC, "DL_OPEN", (path, lazy), IO/wrap) # IO/DyLib/call(dl: u24, fn: String, args: Any) -> Any -# Calls a function of a previously opened dylib. -# 'dl' is the id of the dl object -# 'fn' is the name of the function in the dylib +# 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, fn, args): return IO/Call(IO/MAGIC, "DL_CALL", (dl, fn, args), IO/wrap) # IO/DyLib/close(dl: u24) -> None -# Closes a previously open dylib. -# 'dl' is the id of the dylib. +# Closes a previously open library. +# 'dl' is the id of the library object. # Returns nothing. def IO/DyLib/close(dl): return IO/Call(IO/MAGIC, "DL_CLOSE", (dl), IO/wrap) diff --git a/src/lib.rs b/src/lib.rs index 0789d3f9e..1f1435791 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,7 +248,9 @@ fn run_hvm(book: &::hvm::ast::Book, cmd: &str, run_opts: &RunOpts) -> Result Result Date: Thu, 11 Jul 2024 23:39:14 +0200 Subject: [PATCH 6/8] Fix sh block with instruction to compile dylibs --- docs/ffi.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ffi.md b/docs/ffi.md index 2c2a0d614..0c770b6ad 100644 --- a/docs/ffi.md +++ b/docs/ffi.md @@ -161,11 +161,11 @@ To compile this code into a library, we can use the `gcc` compiler and include t Assuming that it's saved in a file called `libbend_dirs.c`, we can compile it with the following command: ```sh -Compile the shared library with unresolved symbols: -For macOS: +# Needs to be compiled as a shared library with unresolved symbols. +# For macOS: gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -undefined dynamic_lookup -fPIC -For Linux: +# For Linux: gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -Wl,--unresolved-symbols=ignore-all -fPIC ``` From 0f685fc7c40f5dbccf60efddf9e4cd63f725e11e Mon Sep 17 00:00:00 2001 From: imaqtkatt Date: Fri, 12 Jul 2024 12:39:05 -0300 Subject: [PATCH 7/8] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66dae9232..ac34a5cee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project does not currently adhere to a particular versioning scheme. - Add import system. ([#544][gh-544]) - Add multi line comment `#{ ... #}` syntax. ([#595][gh-595]) - Add error message when input file is not found. ([#513][gh-513]) +- Add DyLib IO functions. ([#621][gh-621]) +- Add `List/filter` and `String/{equals, filter}` builtins. ## [0.2.36] - 2024-07-04 @@ -395,5 +397,6 @@ and this project does not currently adhere to a particular versioning scheme. [gh-598]: https://github.com/HigherOrderCO/Bend/issues/598 [gh-618]: https://github.com/HigherOrderCO/Bend/issues/618 [gh-620]: https://github.com/HigherOrderCO/Bend/issues/620 +[gh-621]: https://github.com/HigherOrderCO/Bend/issues/621 [gh-623]: https://github.com/HigherOrderCO/Bend/issues/623 [Unreleased]: https://github.com/HigherOrderCO/Bend/compare/0.2.36...HEAD From 3c042d4b0cc4cf20c28e68668b195dd2dd693975 Mon Sep 17 00:00:00 2001 From: Nicolas Abril Date: Fri, 12 Jul 2024 18:18:43 +0200 Subject: [PATCH 8/8] Update changelog and builtins doc with FFI functions --- CHANGELOG.md | 4 +- docs/builtins.md | 130 ++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 120 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac34a5cee..dee675c07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,8 @@ and this project does not currently adhere to a particular versioning scheme. - Add import system. ([#544][gh-544]) - Add multi line comment `#{ ... #}` syntax. ([#595][gh-595]) - Add error message when input file is not found. ([#513][gh-513]) -- Add DyLib IO functions. ([#621][gh-621]) - Add `List/filter` and `String/{equals, filter}` builtins. +- Add IO functions for loading dynamically linked libraries (`IO/DyLib/open`, `IO/DyLib/call`, `IO/DyLib/close`). ([#621][gh-621]) ## [0.2.36] - 2024-07-04 @@ -29,6 +29,8 @@ and this project does not currently adhere to a particular versioning scheme. - Don't allow tabs to be used for indentation or spacing. ([#463][gh-463]) - Rename builtin function `sleep` to `IO/nanosleep`. ([#581][gh-581]) - Equational number pattern compilation to use the predecessor variable when possible. ([#470][gh-470]) +- Rename `Bytes/decode_utf8` to `String/decode_utf8` and `Bytes/decode_ascii` to `String/decode_ascii`. +- Rename `log` to `Math/log` and `atan2` to `Math/atan2`. ### Fixed diff --git a/docs/builtins.md b/docs/builtins.md index 05f165e42..b5be28ce2 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -110,6 +110,14 @@ Filters a list based on a predicate function. List/filter(xs: List(T), pred: T -> Bool) -> List(T) ``` +#### List/split_once + +Splits a list into two lists at the first occurrence of a value. + +```python +List/split_once(xs: List(T), val: T) -> (Result(List(T), List(T))) +``` + ## Tree ```python @@ -332,6 +340,48 @@ A Natural Number can be written with literals with a `#` before the literal numb #1337 ``` +## DiffList + +DiffList is a list that has constant time prepends (cons), appends and concatenation, but can't be pattern matched. + +It is implemented as a function that receives a list to be appended to the last element of the DiffList. + +For example, the list `List/Cons(1, List/Cons(2, List/Nil))` can be written as the difference list `lambda x: List/Cons(1, List/Cons(2, x))`. + +### Functions + +#### DiffList/new + +Creates a new difference list. + +```python +def DiffList/new() -> (List(T) -> List(T)) +``` + +#### DiffList/append + +Appends a value to the end of the difference list. + +```python +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 +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 +def DiffList/to_list(diff: List(T) -> List(T)) -> (List(T)) +``` + ## IO The basic builtin IO functions are under development and will be stable in the next milestone. @@ -465,25 +515,45 @@ Flushes the file with the given `file` descriptor. Returns nothing (`*`). -## Numeric operations +### Dinamically linked libraries + +It's possible to dynamically load shared objects (libraries) with functions that implement the Bend IO interface. +You can read more on how to implement these libraries in the [Dynamically linked libraries and foreign functions](docs/ffi.md) documentation. -### log +#### IO/DyLib/open ```py -def log(x: f24, base: f24) -> f24 +def IO/DyLib/open(path: String, lazy: u24) -> u24 ``` -Computes the logarithm of `x` with the specified `base`. +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`. + +#### IO/DyLib/call + +``` py +def IO/DyLib/call(dl: u24, fn: String, args: Any) -> Any +``` -### atan2 +Calls a function of a previously opened library. +* `dl` is the id of the library object. +* `fn` is the name of the function in the library. +* `args` are the arguments to the function. The expected values depend on the called function. +* The returned value is determined by the called function. + +#### IO/DyLib/close ```py -def atan2(x: f24, y: f24) -> f24 +def IO/DyLib/close(dl: u24) -> None ``` -Computes the arctangent of `y / x`. +Closes a previously open library. +* `dl` is the id of the library object. +* Returns nothing (`*`). -Has the same behaviour as `atan2f` in the C math lib. +## Native number casting ### to_f24 @@ -511,18 +581,18 @@ Casts any native number to an i24. ## String encoding / decoding -### Bytes/decode_utf8 +### String/decode_utf8 ```py -def Bytes/decode_utf8(bytes: [u24]) -> String -``` +def String/decode_utf8(bytes: [u24]) -> String +``` Decodes a sequence of bytes to a String using utf-8 encoding. -### Bytes/decode_ascii +### String/decode_ascii ```py -def Bytes/decode_ascii(bytes: [u24]) -> String +def String/decode_ascii(bytes: [u24]) -> String ``` Decodes a sequence of bytes to a String using ascii encoding. @@ -559,6 +629,25 @@ def Utf8/REPLACEMENT_CHARACTER: u24 = '\u{FFFD}' ## Math +### Math/log + +```py +def Math/log(x: f24, base: f24) -> f24 +``` + +Computes the logarithm of `x` with the specified `base`. + +### Math/atan2 + +```py +def Math/atan2(x: f24, y: f24) -> f24 +``` + +Computes the arctangent of `y / x`. + +Has the same behaviour as `atan2f` in the C math lib. + + ### Math/PI Defines the Pi constant. @@ -567,6 +656,14 @@ Defines the Pi constant. def Math/PI: f24 = 3.1415926535 ``` +### Math/E + +Euler's number + +```py +def Math/E: f24 = 2.718281828 +``` + ### Math/sin Computes the sine of the given angle in radians. @@ -678,3 +775,10 @@ Round float to the nearest integer. ```py def Math/round(n: f24) -> f24 ``` + +## Lazy thunks + +You can force a function call to be evaluated lazily by wrapping it in a lazy thunk. +In Bend, this can be expressed as `lambda x: x(my_function, arg1, arg2, ...)`. + +To evaluate the thunk, you can use the `undefer` function or apply `lambda x: x` to it. \ No newline at end of file