RFn

As we discussed earlier, one of the possible types for a GameLisp value is rfn. This type stores a Rust function which can be called from GameLisp; rfn stands for "Rust function".

In the Rust API, rfn is represented by the Val::RFn enum variant and the RFn struct. You'll normally access it through a pointer: Root<RFn>.

To construct a new RFn, simply call glsp::rfn, passing in a reference to an appropriate Rust function.


#![allow(unused_variables)]
fn main() {
let swap_bytes: Root<RFn> = glsp::rfn(&i32::swap_bytes);
glsp::bind_global("swap-bytes", swap_bytes)?;

//the standard Rust function i32::swap_bytes can now be called from 
//GameLisp code, by invoking the global rfn (swap-bytes)
}
(prn (swap-bytes 32768)) ; prints 8388608
(prn (swap-bytes 8388608)) ; prints 32768

Because constructing an RFn and binding it to a global variable is such a common operation, we provide glsp::bind_rfn, which performs both steps at once.


#![allow(unused_variables)]
fn main() {
glsp::bind_rfn("swap-bytes", &i32::swap_bytes)?;
}

glsp::rfn and glsp::bind_rfn don't just accept function pointers - they will also accept Rust closures, even closures which capture Rust variables.

However, closures passed to glsp::rfn must be 'static and immutable. This means that if the closure captures any variables, it must take ownership of those variables using the move keyword.


#![allow(unused_variables)]
fn main() {
//a non-capturing closure
glsp::rfn(&|a: i32, b: i32| a.saturating_mul(b + 1));

//a capturing closure which uses interior mutability 
//to update its captured variable
let captured = Cell::new(0_i32);
let print_and_inc = move || {
    println!("{}", captured.get());
    captured.set(captured.get() + 1);
};

glsp::bind_rfn("print-and-inc", Box::new(print_and_inc))?;
}
(print-and-inc) ; prints 0
(print-and-inc) ; prints 1
(print-and-inc) ; prints 2

Even generic Rust functions can be bound to GameLisp - you just need to explicitly select a single concrete type signature. For example, to bind the generic fs::rename function, you might specify that both parameters are Strings:


#![allow(unused_variables)]
fn main() {
glsp::bind_rfn("rename", &fs::rename::<String, String>)?;
}

As demonstrated above, glsp::rfn will silently convert GameLisp values into Rust arguments, as well as converting Rust return values into GameLisp values. This conversion is automatic and invisible; the conversion code is automatically generated by Rust's trait system.

My intent is that it should be possible to write at least 90% of your scriptable functions and methods in a language-agnostic way, so that they can be called from either Rust or GameLisp without any modification. This should also make it easy for you to bind third-party functions, like i32::swap_bytes and fs::rename.

Return Value Conversions

For an rfn's return value to be automatically converted into a GameLisp value, it must implement the IntoVal trait.

IntoVal is implemented for most of Rust's primitive types, many of the types in GameLisp's prelude (such as Root<Arr>), and a number of Rust standard library types. See the rustdoc for the full run-down.

When a function returns Option<T>, GameLisp will convert the Rust value None into the GameLisp value #n.


#![allow(unused_variables)]
fn main() {
//u8::checked_add consumes two u8 and 
//returns an Option<u8>
glsp::bind_rfn("checked-add", &u8::checked_add)?;
}
(prn (checked-add 150 50)) ; prints 200
(prn (checked-add 250 50)) ; prints #n

Functions which return Result<T> will correctly propagate errors to the caller, converting non-GameLisp errors into GameLisp errors when necessary.


#![allow(unused_variables)]
fn main() {
//fs::read_to_string consumes a String by value and
//returns a Result<String, std::io::Error>
glsp::bind_rfn("read-to-string", &fs::read_to_string::<String>)?;
}
(ensure (str? (read-to-string "Cargo.toml")))
(ensure (matches?
  (try (read-to-string "does-not-exist.txt"))
  ('err _)))

Functions which return various Rust collection types - including tuples, slices, arrays, string slices, and paths - will construct a new GameLisp array, string or table.


#![allow(unused_variables)]
fn main() {
fn count_chars(src: &str) -> HashMap<char, usize> {
    let mut char_counts = HashMap::<char, usize>::new();
    for ch in src.chars() {
        *char_counts.entry(ch).or_insert(0) += 1;
    }

    char_counts
}

glsp::bind_rfn("count-chars", &count_chars)?;
}
(let char-counts (count-chars "consonance"))
(ensure (tab? char-counts))
(prn (len char-counts)) ; prints 6

Argument Conversions

Arguments are a little more complicated than return values. The full set of automatic argument conversions is listed in the rustdoc; we'll explore some of them in more detail over the next three chapters.

For now, it's enough for you to know that there's a FromVal trait which is implemented for many Rust and GameLisp types. GameLisp can automatically convert rfn arguments into any type which implements FromVal.


#![allow(unused_variables)]
fn main() {
fn example(integer: u64, string: String, tuple: (i8, i8)) {
    println!("{:?} {:?} {:?}", integer, string, tuple);
}

glsp::bind_rfn("example", &example)?;
}
(example 1 "two" '(3 4)) ; prints 1 "two" (3, 4)

In addition, automatic argument conversions are provided for a handful of reference types, like &Arr, &RData, &str, &Path and &[T].


#![allow(unused_variables)]
fn main() {
fn example(string: &str, array: &[Sym]) {
    println!("{:?} {:?}", string, array);
}

glsp::bind_rfn("example", &example)?;
}
(example "hello" '(game lisp)) ; prints "hello" [game, lisp]

Optional and Rest Parameters

Parameters of type Option<T> are optional. If no value is passed at that position in the argument list, the argument will default to None. It will also be set to None if the caller passes in #n.

If an rfn's final parameter has the type Rest<T>, it will collect any number of trailing arguments.


#![allow(unused_variables)]
fn main() {
fn example(non_opt: u8, opt: Option<u8>, rest: Rest<u8>) {
    prn!("{:?} {:?} {:?}", non_opt, opt, &*rest);
}

glsp::bind_rfn("example", &example)?;
}
(prn (min-args example)) ; prints 1
(prn (max-args example)) ; prints #n

(example)          ; error: too few arguments
(example 1)        ; prints 1 None []
(example 1 2)      ; prints 1 Some(2) []
(example 1 2 3)    ; prints 1 Some(2) [3]
(example 1 2 3 4)  ; prints 1 Some(2) [3, 4]
(example 1 #n 3 4) ; prints 1 None [3, 4]

Custom Conversions

It's possible to implement IntoVal and FromVal for your own Rust types. This will enable your Rust types to participate in automatic conversions when they're used as an argument or return value.

For example, it often makes sense to represent a Rust enum as a GameLisp symbol:


#![allow(unused_variables)]
fn main() {
#[derive(Copy, Clone)]
enum Activity {
    Rest,
    Walk,
    Fight
}

impl IntoVal for Activity {
    fn into_val(self) -> GResult<Val> {
        let sym = match self {
            Activity::Rest => sym!("rest"),
            Activity::Walk => sym!("walk"),
            Activity::Fight => sym!("fight")
        };

        sym.into_val()
    }
}

impl FromVal for Activity {
    fn from_val(val: &Val) -> GResult<Self> {
        Ok(match *val {
            Val::Sym(s) if s == sym!("rest") => Activity::Rest,
            Val::Sym(s) if s == sym!("walk") => Activity::Walk,
            Val::Sym(s) if s == sym!("fight") => Activity::Fight,
            ref val => bail!("expected an Activity, received {}", val)
        })
    }
}

impl Activity {
    fn energy_cost(self) -> i32 {
        match self {
            Activity::Rest => 1,
            Activity::Walk => 5,
            Activity::Fight => 25
        }
    }
}

glsp::bind_rfn("energy-cost", &Activity::energy_cost)?;
}
(prn (energy-cost 'rest)) ; prints 1
(prn (energy-cost 'fight)) ; prints 25
(prn (energy-cost 'sprint)) ; type conversion error

You might also consider representing tuple structs as GameLisp arrays. For example, the tuple struct Rgb(128, 64, 32) could be represented in GameLisp as an array of three integers, (128 64 32).

Errors

To return a GameLisp error from an rfn, you can simply set the function's return type to GResult<T>, which is an alias for Result<T, GError>.

The usual way to trigger a GameLisp error is using the macros bail!() and ensure!(). bail constructs a new GError and returns it. ensure tests a condition and calls bail when the condition is false. (The names of these macros are conventional in Rust error-handling libraries, such as error-chain and failure.)

If you need to create an error manually, you can use the error!() macro, or one of GError's constructors. An arbitrary Error type can be reported as the cause of an error using the with_source method.


#![allow(unused_variables)]
fn main() {
fn file_to_nonempty_string(path: &Path) -> GResult<String> {
    match std::fs::read_to_string(path) {
        Ok(st) => {
            ensure!(st.len() > 0, "empty string in file {}", path);
            Ok(st)
        }
        Err(io_error) => {
            let glsp_error = error!("failed to open the file {}", path);
            Err(glsp_error.with_source(io_error))
        }
    }
}
}

If a panic occurs within an rfn's dynamic scope, the panic will be caught by the innermost rfn call and converted into a GResult. The panic will still print its usual message to stderr. If this is undesirable, you can override the default printing behaviour with a custom panic hook.

RFn Macros

Both Rust functions and GameLisp functions can be used as macros (although GameLisp functions are usually the preferred choice).

Within a Rust function, macro_no_op!() will create and return a special kind of GError which suppresses further expansion of the current macro. (Incidentally, this is also how the macro-no-op built-in function works.)

This means that you can only use macro_no_op!() in a function which returns GResult.

Limitations

GameLisp's automatic function-binding machinery pushes Rust's type system to its limit. To make it work, I've had to explore some obscure corners of the trait system. Unfortunately, this has led to a few tricky limitations.

Due to rustc bug #79207, it's not possible to pass a function pointer or closure to glsp::rfn by value; it will cause a type-inference error. Instead, functions should be passed by reference, and capturing closures should be wrapped in a call to Box::new.

Due to rustc bug #70263, some functions which return non-'static references can't be passed to glsp::rfn, even when the function's return type implements IntoVal. This usually occurs with function signatures which both consume and return a reference, like fn(&str) -> &str.

glsp::rfn won't accept Rust functions with more than eight parameters. If necessary, you can work around this by capturing any number of trailing arguments as a Rest<T>, and unpacking those arguments manually:


#![allow(unused_variables)]
fn main() {
fn process_ten_integers(
    arg0: i32,
    arg1: i32,
    arg2: i32,
    arg3: i32,
    arg4: i32,
    arg5: i32,
    arg6: i32,
    rest: Rest<i32>
) -> GResult<()> {

    ensure!(
        rest.len() == 3,
        "expected exactly 10 arguments, but received {}",
        7 + rest.len()
    );

    let [arg7, arg8, arg9] = [rest[0], rest[1], rest[2]];

    //...
}
}

We use specialization internally. To implement the IntoVal, FromVal or RGlobal traits for your own types, you'll need to enable the nightly feature min_specialization, by placing this attribute towards the top of your main.rs or lib.rs file:


#![allow(unused_variables)]
#![feature(min_specialization)]
fn main() {
}

Finally, many rfn parameter types which are accepted by value can't be accepted by reference. String and i32 are fine, but &String and &mut i32 aren't.