Skip to content

Commit

Permalink
Merge pull request #197 from koto-lang/function-objects
Browse files Browse the repository at this point in the history
  • Loading branch information
irh authored Jan 5, 2023
2 parents 137d8cb + ac3a15c commit 24e7dfe
Show file tree
Hide file tree
Showing 13 changed files with 163 additions and 17 deletions.
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

0 comments on commit 24e7dfe

Please sign in to comment.