diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d90937ca..6d2905e80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index b4c10dcc5..392159ea9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/language/meta_maps.md b/docs/language/meta_maps.md index 5db308890..af404bc15 100644 --- a/docs/language/meta_maps.md +++ b/docs/language/meta_maps.md @@ -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 diff --git a/koto/tests/libs/random.koto b/koto/tests/libs/random.koto index 5b12089d8..4cbebd48a 100644 --- a/koto/tests/libs/random.koto +++ b/koto/tests/libs/random.koto @@ -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 @@ -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} @@ -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() @@ -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 diff --git a/koto/tests/meta_maps.koto b/koto/tests/meta_maps.koto index d856a22f9..452c3eabc 100644 --- a/koto/tests/meta_maps.koto +++ b/koto/tests/meta_maps.koto @@ -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 @@ -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] diff --git a/src/bytecode/src/compiler.rs b/src/bytecode/src/compiler.rs index dd713a239..963967d7d 100644 --- a/src/bytecode/src/compiler.rs +++ b/src/bytecode/src/compiler.rs @@ -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 { diff --git a/src/parser/src/node.rs b/src/parser/src/node.rs index 3ca9866c6..f3820a0b2 100644 --- a/src/parser/src/node.rs +++ b/src/parser/src/node.rs @@ -573,6 +573,9 @@ pub enum MetaKeyId { /// @type Type, + /// @|| + Call, + /// @tests Tests, /// @test test_name diff --git a/src/parser/src/parser.rs b/src/parser/src/parser.rs index 76a897ca5..1dc6ee9c7 100644 --- a/src/parser/src/parser.rs +++ b/src/parser/src/parser.rs @@ -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), }; diff --git a/src/runtime/src/meta_map.rs b/src/runtime/src/meta_map.rs index 1c88e687e..8d74146c7 100644 --- a/src/runtime/src/meta_map.rs +++ b/src/runtime/src/meta_map.rs @@ -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` @@ -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, @@ -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"), @@ -296,6 +302,7 @@ pub fn meta_id_to_key(id: MetaKeyId, name: Option) -> Result 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())?) } @@ -316,6 +323,7 @@ pub fn meta_id_to_key(id: MetaKeyId, name: Option) -> Result { BinaryOp(BinaryOp), UnaryOp(UnaryOp), + Call, Named(&'a str), Test(&'a str), Tests, diff --git a/src/runtime/src/value.rs b/src/runtime/src/value.rs index 9d47bffd9..ca35d0f97 100644 --- a/src/runtime/src/value.rs +++ b/src/runtime/src/value.rs @@ -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 diff --git a/src/runtime/src/vm.rs b/src/runtime/src/vm.rs index 774834881..b9e7115aa 100644 --- a/src/runtime/src/vm.rs +++ b/src/runtime/src/vm.rs @@ -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), } } diff --git a/src/runtime/tests/external_value_tests.rs b/src/runtime/tests/external_value_tests.rs index eb70136a1..6037ab36b 100644 --- a/src/runtime/tests/external_value_tests.rs +++ b/src/runtime/tests/external_value_tests.rs @@ -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; @@ -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 = " @@ -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() { @@ -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 { diff --git a/src/runtime/tests/vm_tests.rs b/src/runtime/tests/vm_tests.rs index 22fbb4886..b31d3dc54 100644 --- a/src/runtime/tests/vm_tests.rs +++ b/src/runtime/tests/vm_tests.rs @@ -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::*;