Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for function objects #197

Merged
merged 4 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: cargo fmt --all -- --check

- name: Clippy
run: cargo clippy --all-targets --all-features
run: cargo clippy --all-targets --all-features -- -D warnings

- name: Docs
run: cargo doc --workspace --exclude koto_cli
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ The Koto project adheres to
f (10, 100, 1, 2, 3)
# 1006
```
- Arithmetic-assignment operators (`@+=`, `@*=`, etc.) can now be implemented in meta maps and external values.
- Meta map improvements
- Arithmetic-assignment operators (`@+=`, `@*=`, etc.) can now be implemented in meta maps and external values.
- The function call operator (`@||`) can be implemented to values that behave like functions.

#### Internals

Expand Down
19 changes: 19 additions & 0 deletions docs/language/meta_maps.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ print! (foo 10)[7]
check! 17
```

### `@||`

The `@||` meta key defines how the value should behave when called as a
function.

```koto
foo = |n|
data: n
@||: |self|
self.data *= 2
self.data

x = foo 2
print! x()
check! 4
print! x()
check! 8
```

### `@iterator`

The `@iterator` meta key defines how iterators should be made when the value is
Expand Down
19 changes: 9 additions & 10 deletions koto/tests/libs/random.koto
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ from test import assert, assert_eq, assert_ne, assert_near
random.seed 1

@test bool: ||
rng_bool = import random.bool
assert rng_bool()
assert not rng_bool()
assert random.bool()
assert not random.bool()

@test number: ||
assert_near random.number(), 0.402, 0.001
Expand All @@ -20,7 +19,7 @@ from test import assert, assert_eq, assert_ne, assert_near

@test pick_list: ||
x = ["foo", "bar", "baz"]
assert (x.contains (random.pick x))
assert x.contains random.pick x

@test pick_map: ||
x = {foo: 123, bar: 456, baz: 789}
Expand All @@ -29,15 +28,15 @@ from test import assert, assert_eq, assert_ne, assert_near

@test pick_range: ||
x = 0..10
assert (x.contains (random.pick x))
assert x.contains random.pick x

@test pick_tuple: ||
x = ("foo", "bar", "baz")
assert (x.contains (random.pick x))
assert x.contains random.pick x

@test generator: ||
get_rng_output = |rng|
(0..10)
0..10
.each |_| rng.pick 0..5
.to_tuple()

Expand All @@ -47,9 +46,9 @@ from test import assert, assert_eq, assert_ne, assert_near

output1 = get_rng_output rng1

assert_eq output1, (get_rng_output rng2)
assert_ne output1, (get_rng_output rng3)
assert_eq output1, get_rng_output rng2
assert_ne output1, get_rng_output rng3

# seed can be used to reseed the unique generator
rng3.seed 0
assert_eq output1, (get_rng_output rng3)
assert_eq output1, get_rng_output rng3
11 changes: 9 additions & 2 deletions koto/tests/meta_maps.koto
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@ globals.foo_meta =
@==: |self, other| self.x == other.x
@!=: |self, other| not self == other

# Negation
# Negation (e.g. -foo)
@negate: |self| foo -self.x

# Not
# Not (e.g. !foo)
@not: |self| if self.x == 0 then true else false

# Function call
@||: |self| self.x

# Indexing
@[]: |self, index| self.x + index

# Overloaded iteration behaviour
@iterator: |self| 0..self.x

# Formatting
Expand Down Expand Up @@ -136,6 +140,9 @@ globals.foo_meta =
assert_eq foo(10)[5], 15
assert_eq foo(100)[-1], 99

@test call: ||
assert_eq foo(99)(), 99

@test iterator: ||
assert_eq foo(5).to_tuple(), (0, 1, 2, 3, 4)
assert_eq foo(-4).to_list(), [0, -1, -2, -3]
Expand Down
1 change: 0 additions & 1 deletion src/bytecode/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,6 @@ impl Compiler {
result
}
Node::SmallInt(n) => {
dbg!(n);
let result = self.get_result_register(result_register)?;
if let Some(result) = result {
match *n {
Expand Down
3 changes: 3 additions & 0 deletions src/parser/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -573,6 +573,9 @@ pub enum MetaKeyId {
/// @type
Type,

/// @||
Call,

/// @tests
Tests,
/// @test test_name
Expand Down
4 changes: 4 additions & 0 deletions src/parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2042,6 +2042,10 @@ impl<'source> Parser<'source> {
Some(Token::SquareClose) => MetaKeyId::Index,
_ => return self.error(SyntaxError::UnexpectedMetaKey),
},
Some(Token::Function) => match self.consume_token() {
Some(Token::Function) => MetaKeyId::Call,
_ => return self.error(SyntaxError::UnexpectedMetaKey),
},
_ => return self.error(SyntaxError::UnexpectedMetaKey),
};

Expand Down
8 changes: 8 additions & 0 deletions src/runtime/src/meta_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ pub enum MetaKey {
///
/// e.g. `@not`
UnaryOp(UnaryOp),
/// Function call - `@||`
///
/// Defines the behaviour when performing a function call on the value.
Call,
/// A named key
///
/// e.g. `@meta my_named_key`
Expand Down Expand Up @@ -116,6 +120,7 @@ impl MetaKey {
match self {
MetaKey::BinaryOp(op) => MetaKeyRef::BinaryOp(*op),
MetaKey::UnaryOp(op) => MetaKeyRef::UnaryOp(*op),
MetaKey::Call => MetaKeyRef::Call,
MetaKey::Named(name) => MetaKeyRef::Named(name),
MetaKey::Test(name) => MetaKeyRef::Test(name),
MetaKey::Tests => MetaKeyRef::Tests,
Expand All @@ -132,6 +137,7 @@ impl fmt::Display for MetaKey {
match self {
MetaKey::BinaryOp(op) => write!(f, "@{op}"),
MetaKey::UnaryOp(op) => write!(f, "@{op}"),
MetaKey::Call => f.write_str("@||"),
MetaKey::Named(name) => write!(f, "{name}"),
MetaKey::Test(test) => write!(f, "test({test})"),
MetaKey::Tests => f.write_str("@tests"),
Expand Down Expand Up @@ -296,6 +302,7 @@ pub fn meta_id_to_key(id: MetaKeyId, name: Option<ValueString>) -> Result<MetaKe
MetaKeyId::Negate => MetaKey::UnaryOp(Negate),
MetaKeyId::Not => MetaKey::UnaryOp(Not),
MetaKeyId::Display => MetaKey::UnaryOp(Display),
MetaKeyId::Call => MetaKey::Call,
MetaKeyId::Named => {
MetaKey::Named(name.ok_or_else(|| "Missing name for named meta entry".to_string())?)
}
Expand All @@ -316,6 +323,7 @@ pub fn meta_id_to_key(id: MetaKeyId, name: Option<ValueString>) -> Result<MetaKe
enum MetaKeyRef<'a> {
BinaryOp(BinaryOp),
UnaryOp(UnaryOp),
Call,
Named(&'a str),
Test(&'a str),
Tests,
Expand Down
7 changes: 6 additions & 1 deletion src/runtime/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ impl Value {
/// Returns true if the value has function-like callable behaviour
pub fn is_callable(&self) -> bool {
use Value::*;
matches!(self, SimpleFunction(_) | Function(_) | ExternalFunction(_))
match self {
SimpleFunction(_) | Function(_) | ExternalFunction(_) => true,
Map(m) => m.contains_meta_key(&MetaKey::Call),
ExternalValue(v) => v.contains_meta_key(&MetaKey::Call),
_ => false,
}
}

/// Returns true if the value doesn't have internal mutability
Expand Down
26 changes: 26 additions & 0 deletions src/runtime/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3045,6 +3045,32 @@ impl Vm {
call_arg_count,
instance_register,
),
Map(ref map) if map.contains_meta_key(&MetaKey::Call) => {
// Set the map as the instance by placing it in the frame base,
// and then passing it into call_callable
let instance = function.clone();
self.set_register(frame_base, instance);
self.call_callable(
result_register,
map.get_meta_value(&MetaKey::Call).unwrap(),
frame_base,
call_arg_count,
Some(frame_base),
temp_tuple_values,
)
}
ExternalValue(ref v) if v.contains_meta_key(&MetaKey::Call) => {
let instance = function.clone();
self.set_register(frame_base, instance);
self.call_callable(
result_register,
v.get_meta_value(&MetaKey::Call).unwrap(),
frame_base,
call_arg_count,
Some(frame_base),
temp_tuple_values,
)
}
unexpected => type_error("callable function", &unexpected),
}
}
Expand Down
43 changes: 42 additions & 1 deletion src/runtime/tests/external_value_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ mod external_values {
}
unexpected => type_error_with_slice("Number", unexpected),
})
.data_fn(MetaKey::Call, |data| Ok(Number(data.x.into())))
.data_fn("to_number", |data| Ok(Number(data.x.into())))
.data_fn_mut("invert", |data| {
data.x *= -1.0;
Expand Down Expand Up @@ -275,6 +276,17 @@ x.to_number()
test_script_with_external_value(script, 66);
}

#[test]
fn subtract_assign() {
let script = "
x = make_external 42
x -= make_external 20
x -= 2
x.to_number()
";
test_script_with_external_value(script, 20);
}

#[test]
fn multiply_assign() {
let script = "
Expand All @@ -286,7 +298,27 @@ x.to_number()
test_script_with_external_value(script, 99);
}

// TODO missing tests
#[test]
fn divide_assign() {
let script = "
x = make_external 99
x /= make_external 3
x /= 3
x.to_number()
";
test_script_with_external_value(script, 11);
}

#[test]
fn remainder_assign() {
let script = "
x = make_external 99
x %= make_external 90
x %= 5
x.to_number()
";
test_script_with_external_value(script, 4);
}

#[test]
fn less() {
Expand Down Expand Up @@ -320,6 +352,15 @@ x[23]
";
test_script_with_external_value(script, 123);
}

#[test]
fn call() {
let script = "
x = make_external 256
x()
";
test_script_with_external_value(script, 256);
}
}

mod temporaries {
Expand Down
33 changes: 33 additions & 0 deletions src/runtime/tests/vm_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2964,6 +2964,39 @@ foos[0] == foos[1]
}
}

mod overloaded_call {
use super::*;

#[test]
fn basic_call() {
let script = "
x = { @||: || 42 }
x()
";
test_script(script, 42);
}

#[test]
fn with_args() {
let script = "
x = { @||: |a, b| a + b }
x 12, 34
";
test_script(script, 46);
}

#[test]
fn instance() {
let script = "
x =
data: 99
@||: |self, z| self.data * z
x 10
";
test_script(script, 990);
}
}

mod named_meta_entries {
use super::*;

Expand Down