Repository navigation
fix(vm): check a function from Any against the function type it's used as 🔒 #329
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
Changes from all commits
22c7999
abb6d79
2483548
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,6 +22,29 @@ pub enum Function { | |
| cache: Rc<RefCell<HashMap<u64, Value>>>, | ||
| function: Rc<Self>, | ||
| }, | ||
|
|
||
| /// A function passed as a function type it may not have: a value of | ||
| /// type `Any` used as a `Fn(Int) -> Int` that holds a function whose | ||
| /// return type isn't known. Each call checks what it returns. | ||
| Checked { | ||
| function: Rc<Self>, | ||
| /// The function type it's used as, boxed so that it doesn't make | ||
| /// every function value bigger. | ||
| typ: Box<StaticType>, | ||
| }, | ||
| } | ||
|
|
||
| /// How a function fits a function type, from the function's own type, | ||
| /// which leaves out what isn't known as the program runs. | ||
| #[derive(Debug, PartialEq, Eq)] | ||
| pub enum FunctionFit { | ||
| /// It takes what the type passes and returns what it returns. | ||
| Proven, | ||
| /// It takes what the type passes, but what it returns has to be checked | ||
| /// at each call. | ||
| IfChecked, | ||
| /// It can't be a function of that type. | ||
| No, | ||
| } | ||
|
|
||
| pub struct NativeFunction { | ||
|
|
@@ -182,15 +205,19 @@ impl Function { | |
| match self { | ||
| Self::Closure(c) => Some(&c.prototype), | ||
| Self::Compiled(f) => Some(f), | ||
| Self::Memoized { function, .. } => function.prototype(), | ||
| Self::Memoized { function, .. } | Self::Checked { function, .. } => { | ||
| function.prototype() | ||
| } | ||
| Self::Native(_) => None, | ||
| } | ||
| } | ||
|
|
||
| pub fn documentation(&self) -> Option<&str> { | ||
| match self { | ||
| Self::Native(f) => f.documentation.as_deref(), | ||
| Self::Memoized { function, .. } => function.documentation(), | ||
| Self::Memoized { function, .. } | Self::Checked { function, .. } => { | ||
| function.documentation() | ||
| } | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
@@ -200,30 +227,95 @@ impl Function { | |
| Self::Compiled(f) => f.name.as_deref(), | ||
| Self::Native(f) => Some(&f.name), | ||
| Self::Closure(c) => c.prototype.name.as_deref(), | ||
| Self::Memoized { function, .. } => function.name(), | ||
| Self::Memoized { function, .. } | Self::Checked { function, .. } => function.name(), | ||
| } | ||
| } | ||
|
|
||
| pub fn static_type(&self) -> StaticType { | ||
| self.signature().clone() | ||
| } | ||
|
|
||
| /// The function's type: what it takes and returns, as far as that's | ||
| /// known as the program runs. | ||
| fn signature(&self) -> &StaticType { | ||
| match self { | ||
| Self::Compiled(f) => f.static_type.clone(), | ||
| Self::Native(f) => f.static_type.clone(), | ||
| Self::Closure(c) => c.prototype.static_type.clone(), | ||
| Self::Memoized { function, .. } => function.static_type(), | ||
| Self::Compiled(f) => &f.static_type, | ||
| Self::Native(f) => &f.static_type, | ||
| Self::Closure(c) => &c.prototype.static_type, | ||
| Self::Memoized { function, .. } => function.signature(), | ||
| Self::Checked { typ, .. } => typ, | ||
| } | ||
| } | ||
|
|
||
| /// The function a [`Self::Checked`] checks, or this one: checking its | ||
| /// calls doesn't make it another function, as far as `==` goes. | ||
| pub fn unchecked(&self) -> &Self { | ||
| match self { | ||
| Self::Checked { function, .. } => function.unchecked(), | ||
| function => function, | ||
| } | ||
| } | ||
|
|
||
| /// The types of the parameters, when it takes a known number of them. | ||
| pub fn parameters(&self) -> Option<&[StaticType]> { | ||
| match self.signature() { | ||
| StaticType::Function { | ||
| parameters: Some(parameters), | ||
| .. | ||
| } => Some(parameters), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
||
| /// How this function fits `expected`, a function type. A parameter or | ||
| /// return type that its own type leaves unknown, as `Any`, fits | ||
| /// anything, but an unknown return type has to be checked at each call: | ||
| /// a lambda that returns a String doesn't fit `Fn(Int) -> Int`, and one | ||
| /// whose return type isn't known fits it if checked. | ||
| pub fn fit(&self, expected: &StaticType) -> FunctionFit { | ||
| let StaticType::Function { | ||
| parameters: expected_parameters, | ||
| return_type: expected_return, | ||
| } = expected | ||
| else { | ||
| return FunctionFit::No; | ||
| }; | ||
| let StaticType::Function { | ||
| parameters, | ||
| return_type, | ||
| } = self.signature() | ||
| else { | ||
| return FunctionFit::No; | ||
| }; | ||
| let (takes, takes_proven) = match (expected_parameters, parameters) { | ||
| (Some(expected), Some(parameters)) => ( | ||
| expected.len() == parameters.len() | ||
| && expected | ||
| .iter() | ||
| .zip(parameters) | ||
| .all(|(expected, parameter)| could_be(expected, parameter)), | ||
| expected | ||
| .iter() | ||
| .zip(parameters) | ||
| .all(|(expected, parameter)| proves(expected, parameter)), | ||
| ), | ||
| (_, None) => (true, true), | ||
| (None, Some(_)) => (true, false), | ||
| }; | ||
| if !takes || !could_be(return_type, expected_return) { | ||
| FunctionFit::No | ||
|
Comment on lines
+305
to
+306
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For a function annotated to return Useful? React with 👍 / 👎. |
||
| } else if takes_proven && proves(return_type, expected_return) { | ||
| FunctionFit::Proven | ||
| } else { | ||
| FunctionFit::IfChecked | ||
| } | ||
| } | ||
|
|
||
| /// Whether this function accepts the given arguments, checking every | ||
| /// element of a container argument against a parameter like | ||
| /// `List<Int>`, which [`Self::matches_value_args`] doesn't. | ||
| pub fn conforms_value_args(&self, args: &[Value]) -> bool { | ||
| let st = match self { | ||
| Self::Native(f) => &f.static_type, | ||
| Self::Compiled(f) => &f.static_type, | ||
| Self::Closure(c) => &c.prototype.static_type, | ||
| Self::Memoized { function, .. } => return function.conforms_value_args(args), | ||
| }; | ||
| match st { | ||
| match self.signature() { | ||
| StaticType::Function { | ||
| parameters: Some(params), | ||
| .. | ||
|
|
@@ -244,13 +336,7 @@ impl Function { | |
| /// Returns true if this function accepts the given arguments, without | ||
| /// allocating a `StaticType` for each of them. | ||
| pub fn matches_value_args(&self, args: &[Value]) -> bool { | ||
| let st = match self { | ||
| Self::Native(f) => &f.static_type, | ||
| Self::Compiled(f) => &f.static_type, | ||
| Self::Closure(c) => &c.prototype.static_type, | ||
| Self::Memoized { function, .. } => return function.matches_value_args(args), | ||
| }; | ||
| let StaticType::Function { parameters, .. } = st else { | ||
| let StaticType::Function { parameters, .. } = self.signature() else { | ||
| return false; | ||
| }; | ||
| match parameters { | ||
|
|
@@ -273,6 +359,7 @@ impl fmt::Debug for Function { | |
| Self::Native(native) => write!(f, "{native:?}"), | ||
| Self::Closure(closure) => write!(f, "<closure over {:?}>", closure.prototype.name), | ||
| Self::Memoized { function, .. } => write!(f, "<memoized {:?}>", function), | ||
| Self::Checked { function, typ } => write!(f, "<{function:?} checked as {typ}>"), | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -287,6 +374,7 @@ impl fmt::Display for Function { | |
| Self::Native(native) => write!(f, "<fn {}>", native.name), | ||
| Self::Closure(closure) => write!(f, "<closure over {:?}>", closure.prototype.name), | ||
| Self::Memoized { function, .. } => write!(f, "<memoized {function}>"), | ||
| Self::Checked { function, .. } => write!(f, "{function}"), | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -295,3 +383,76 @@ impl fmt::Debug for NativeFunction { | |
| write!(f, "<native fn {} {:?}>", self.name, self.static_type) | ||
| } | ||
| } | ||
|
|
||
| /// Whether a value of type `found` could be an `expected`, where what isn't | ||
| /// known in either, `Any` or a type parameter, could be anything. | ||
| fn could_be(found: &StaticType, expected: &StaticType) -> bool { | ||
| let unknown = |typ: &StaticType| { | ||
| matches!( | ||
| typ, | ||
| StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) | ||
| ) | ||
| }; | ||
| // A function that never returns returns whatever is expected. | ||
| if unknown(found) || unknown(expected) || *found == StaticType::Never { | ||
| return true; | ||
| } | ||
| match (found, expected) { | ||
| (StaticType::List(a), StaticType::List(b)) | ||
| | (StaticType::Deque(a), StaticType::Deque(b)) | ||
| | (StaticType::Option(a), StaticType::Option(b)) | ||
| | (StaticType::Iterator(a), StaticType::Iterator(b)) | ||
| | (StaticType::Iterable(a), StaticType::Iterable(b)) | ||
| | (StaticType::MinHeap(a), StaticType::MinHeap(b)) | ||
| | (StaticType::MaxHeap(a), StaticType::MaxHeap(b)) => could_be(a, b), | ||
| (found, StaticType::Iterable(element)) => found | ||
| .sequence_element_type() | ||
| .is_some_and(|found| could_be(&found, element)), | ||
| (StaticType::Tuple(a), StaticType::Tuple(b)) => { | ||
| a.len() == b.len() && a.iter().zip(b).all(|(a, b)| could_be(a, b)) | ||
| } | ||
| ( | ||
| StaticType::Map { key, value }, | ||
| StaticType::Map { | ||
| key: expected_key, | ||
| value: expected_value, | ||
| }, | ||
| ) => could_be(key, expected_key) && could_be(value, expected_value), | ||
| ( | ||
| StaticType::Function { | ||
| parameters, | ||
| return_type, | ||
| }, | ||
| StaticType::Function { | ||
| parameters: expected_parameters, | ||
| return_type: expected_return, | ||
| }, | ||
| ) => { | ||
| let takes = match (parameters, expected_parameters) { | ||
| (Some(a), Some(b)) => { | ||
| a.len() == b.len() && a.iter().zip(b).all(|(a, b)| could_be(b, a)) | ||
| } | ||
| _ => true, | ||
| }; | ||
| takes && could_be(return_type, expected_return) | ||
| } | ||
| (found, expected) => found == expected, | ||
| } | ||
| } | ||
|
|
||
| /// Whether every value of type `found` is an `expected`: a function that | ||
| /// takes `Any` takes anything, and one that returns a `List<Int>` returns an | ||
| /// `Iterable<Int>`. | ||
| fn proves(found: &StaticType, expected: &StaticType) -> bool { | ||
| *expected == StaticType::Any || (!has_unknowns(found) && found.fits(expected)) | ||
| } | ||
|
|
||
| /// Whether `typ` leaves something unknown: `Any` or a type parameter. | ||
| fn has_unknowns(typ: &StaticType) -> bool { | ||
| typ.contains(&|part| { | ||
| matches!( | ||
| part, | ||
| StaticType::Any | StaticType::Generic(_) | StaticType::Var(_) | ||
| ) | ||
| }) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a named function with an
Iterable<T>parameter, promoting the sole dynamic candidate to a direct call drops the runtime check of that annotation. For example,fn consume(xs: Iterable<Int>) => 0; let a: Any = true; print(consume(a));now accepts the non-sequence and prints0, whereas dynamic dispatch previously rejected it.lower_parameter_annotationrepresents this parameter as a type variable plus an iteration constraint, socommit_callbinds its instantiated parameter toAny;expected_of_anyconsequently produces no effective guard. The copied iteration constraint also checks with no expression node and cannot emit a runtime guard. Preserve the callable'sIterable<Int>shape when generating argument checks before bypassing dynamic dispatch.Useful? React with 👍 / 👎.