diff --git a/CHANGELOG.md b/CHANGELOG.md index e563921e3..6584d881c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ The Koto project adheres to - `export` can be used with multi-assignment expressions. - e.g. expressions like `export a, b, c = foo()` are now allowed. - Maps now support `[]` indexing, returning the Nth entry as a tuple. +- Objects that implement `KotoObject::call` can now be used in operations that + expect functions. + - `KotoObject::is_callable` has been added to support this, and needs to be + implemented for the runtime to accept the object as a function. #### Core Library diff --git a/crates/cli/docs/language_guide.md b/crates/cli/docs/language_guide.md index 1e100973f..a4eddae21 100644 --- a/crates/cli/docs/language_guide.md +++ b/crates/cli/docs/language_guide.md @@ -1444,6 +1444,17 @@ print! let x: Any = 'hello' check! hello ``` +#### `Callable` + +The `Callable` type hint will accept functions, or any value that can behave +like a function. + +```koto +let say_hello: Callable = || 'hello' +print! say_hello() +check! hello +``` + #### `Indexable` The `Indexable` type hint will accept any value that supports `[]` indexing. diff --git a/crates/runtime/src/types/object.rs b/crates/runtime/src/types/object.rs index 2cf13f087..8222add62 100644 --- a/crates/runtime/src/types/object.rs +++ b/crates/runtime/src/types/object.rs @@ -144,7 +144,17 @@ pub trait KotoObject: KotoType + KotoCopy + KotoEntries + KotoSend + KotoSync + None } + /// Declares to the runtime whether or not the object is callable + /// + /// The `Callable` type hint defers to the function, expecting `true` to be returned for objects + /// that implement [KotoObject::call]. + fn is_callable(&self) -> bool { + false + } + /// Allows the object to behave as a function + /// + /// Objects that implement `call` should return `true` from [KotoObject::call]. fn call(&mut self, _ctx: &mut CallContext) -> Result { unimplemented_error("@||", self.type_string()) } diff --git a/crates/runtime/src/types/value.rs b/crates/runtime/src/types/value.rs index 67d0336ea..4bec2de57 100644 --- a/crates/runtime/src/types/value.rs +++ b/crates/runtime/src/types/value.rs @@ -101,6 +101,7 @@ impl KValue { CaptureFunction(f) if f.info.generator => false, Function(_) | CaptureFunction(_) | NativeFunction(_) => true, Map(m) => m.contains_meta_key(&MetaKey::Call), + Object(o) => o.try_borrow().map_or(false, |o| o.is_callable()), _ => false, } } diff --git a/crates/runtime/src/vm.rs b/crates/runtime/src/vm.rs index 452a56a82..c0ef74e82 100644 --- a/crates/runtime/src/vm.rs +++ b/crates/runtime/src/vm.rs @@ -2834,6 +2834,7 @@ impl KotoVm { let value = self.get_register(value_register); match self.get_constant_str(type_index) { "Any" => true, + "Callable" => value.is_callable(), "Indexable" => value.is_indexable(), "Iterable" => value.is_iterable(), expected_type => { diff --git a/crates/runtime/tests/object_tests.rs b/crates/runtime/tests/object_tests.rs index edd9bb81b..e6a99b89b 100644 --- a/crates/runtime/tests/object_tests.rs +++ b/crates/runtime/tests/object_tests.rs @@ -144,6 +144,10 @@ mod objects { Some(self.x.unsigned_abs() as usize) } + fn is_callable(&self) -> bool { + true + } + fn call(&mut self, _ctx: &mut CallContext) -> Result { Ok(self.x.into()) } @@ -592,6 +596,15 @@ x.as_number() test_object_script(script, 256); } + #[test] + fn callable() { + let script = " +let x: Callable = make_object 256 +x() +"; + test_object_script(script, 256); + } + #[test] fn iterable() { let script = " diff --git a/crates/runtime/tests/runtime_failures.rs b/crates/runtime/tests/runtime_failures.rs index 70cbb78f6..4ad64d5e6 100644 --- a/crates/runtime/tests/runtime_failures.rs +++ b/crates/runtime/tests/runtime_failures.rs @@ -113,6 +113,21 @@ let x: String, y: Bool = 'abc', 123 ); } + #[test] + fn expected_callable() { + let script = "\ +let foo: Callable = 99 +# ^^^ +"; + check_script_fails_with_span( + script, + Span { + start: Position { line: 0, column: 4 }, + end: Position { line: 0, column: 7 }, + }, + ); + } + #[test] fn expected_indexable() { let script = "\ diff --git a/crates/runtime/tests/vm_tests.rs b/crates/runtime/tests/vm_tests.rs index a8e46238a..cf1a7e837 100644 --- a/crates/runtime/tests/vm_tests.rs +++ b/crates/runtime/tests/vm_tests.rs @@ -555,6 +555,15 @@ true check_script_output(script, true); } + #[test] + fn callable_matches_functions() { + let script = " +let x: Callable = || true +x() +"; + check_script_output(script, true); + } + #[test] fn indexable_matches_indexable_values() { let script = "