-
Notifications
You must be signed in to change notification settings - Fork 14
feat: validate Terms used as parameter types are appropriate (into #2309) #2345
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
base: main
Are you sure you want to change the base?
Conversation
} | ||
(Term::Tuple(es1), Term::Tuple(es2)) => { | ||
es1.len() == es2.len() && es1.iter().zip(es2).all(|(e1, e2)| e1.is_supertype(e2)) | ||
( |
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.
So we can drop this clause too, it's only there to provide a place for the comment...
@@ -308,7 +333,7 @@ impl Term { | |||
|
|||
check_typevar_decl(var_decls, *idx, cached_decl) | |||
} | |||
Term::RuntimeType { .. } => Ok(()), | |||
Term::RuntimeType(_) => Ok(()), |
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.
driveby. Hmmm, and could do the next line too. I'm surprised the old code is legal...
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## zrho/core-merge-arg-param #2345 +/- ##
==========================================================
Coverage 82.13% 82.14%
==========================================================
Files 241 241
Lines 43936 43949 +13
Branches 39765 39778 +13
==========================================================
+ Hits 36086 36101 +15
+ Misses 5862 5858 -4
- Partials 1988 1990 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
_, | ||
) => { | ||
// This is not a type at all, so it's not a supertype of anything. | ||
false |
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.
I don't see the problem with this relation being reflexive.
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.
More than that it's helpful for it to be reflexive: Say you have a variable v
. If this variable stands for a static type, we should have v ⊆ v
, since whatever static type it stands for, it is a subtype of itself. Since we don't know if v
does stand for a static type, we should have v ⊆ v
for all variables. But we also should have that replacing variables with concrete terms preserves the subtyping relation. Which then forces t ⊆ t
for every term t
.
Why don't we know if v
stands for a static type? Consider
(declare-ctr foo
(param ?t core.static)
(param ?v ?t)
core.static)
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.
Re. the (param ?v ?t)
, see other comment
Say you have a variable v. If this variable stands for a static type, we should have v \subseteq v`
I think we should conclude that v \subseteq v
this is true only if the bound on v
is StaticType...I mean, my gut feeling is that if we find variables here, we should be calling (recursing) is_supertype
using the cached_decl
. Possibly on both sides?? (That is, only if we go with dependent types.)
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.
Here ?v
has type ?t
.
Suppose ?v ⊆ ?v
is false. We then instantiate ?t
with core.static
. Now ?v ⊆ ?v
becomes true. That appears to me to violate the principle of least surprise. So ?v ⊆ ?v
should be true. ⊆
should also be preserved by instantiating. We instantiate ?t
with core.nat
. Then ?v ⊆ ?v
should remain true. Instantiate ?v
with 5
then 5 ⊆ 5
should also hold.
Ultimately this is a huge distraction. Subtyping will go away ideally anyway. If we have reflexivity, we don't need additional information, the relation behaves well with substitution, and nothing breaks.
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.
Arrgh. So.... given your above foo
, I could instantiate ?t
with RuntimeType
and then ?v
with Qubit
....which seems ok. But could I instantiate ?t
with Qubit
? (Or even a classical runtime type, doesn't have to be quantum). Then there would be no valid/possible instantiations of ?v
...right?
If we instantiate ?t
with BoundedNatType
then ?v
would have to be a BoundedNat
. But if we instantiate ?t
with some e.g. BoundedNat(5)
, then ?v
would....following your suggestion of reflexivity...have to be BoundedNat(5)
as well?
So my suggestion that a variable is a type (with "subtypes") only if it's bounded by static type doesn't make sense - rather, I'd need to check that it's bounded such that it cannot be a Runtime. Which would need a different type hierarchy and (I speculate) be unworkable, yeah.
So if reflexivity is the way out of this then...ok (I appreciate as you say that subtyping is gonna go away)...but I worry users (writers of polymorphic functions, say) are gonna shoot themselves in the foot here because they don't understand the shape of the gun. At the moment I can't figure it out so I am looking for my steel-toecapped boots...
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.
Ok, thanks Lukas - that's helpful. I realized about ?t=Qubit
too. I guess I am not too worried about is_supertype
- as you say, it will go as we get rid of subtyping (or rather, it will become constraint implication!). I'm more worried about footgun's like (param ?t (BoundedNat 5))
....
Regarding:
Suppose ?v ⊆ ?v is false. We then instantiate ?t with core.static. Now ?v ⊆ ?v becomes true. That appears to me to violate the principle of least surprise.
I'm less sure of this. I think many things can become true when you instantiate something with something else. Rather, given only the definition of ?t
(and no particular instantiation), we should be able to figure out the things that are true for all instantiations. Additional things may become true for particular instantiations.
So in old hugr-core
, you'd never really have the situation where you had instantiated ?t
but still had ?v
"free" - so you'd never be asking questions about what was true for all ?v
but for a fixed ?t
. This because you provide values for all TypeArgs at once (and they were not allowed to refer to each other). I think that invariant is now disappearing - you can Term::Apply(Term::Apply(t, ...args1...), ...args2...)
right?
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.
Apply can only apply symbols and not other terms. So the invariant still holds.
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.
I think where I'm heading is that while your foo
is ok/good, IIUC, it has the property that for any possible instantiation of ?t
, there is at least some legal ?v
. Whereas in something like this:
(declare-ctr foo3
(param ?t core.static)
(param ?u ?t)
(param ?v ?u)
core.static)
- Are there legal instantiations? Definitely if
?t = core.static
then there are (e.g.?u = RuntimeType
+?v = Qubit
) - But if
?t = RuntimeType
then there are possible values for?u
e.g.Qubit
orRuntimeInt
but no possible values for?v
(whatever?u
is) - Ok, let's pick, oh,
?t = FloatType
. Then?u = Float(5.0)
or any other float. But then there are no sensible values for?v
- unless we allow reflexive ones, but I don't think you are proposingTerm::Float : Term::Float
(it's not in the code), and to me these just seem a bit silly. - Similarly if
?t = ListType(....)
then?u = List(...)
is the only possibility and again there is no possible?v
- So, is there any
?t != core.static
which admits instantiation offoo3
?
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.
Yes, there are instantiations with ?t != core.static
. Say we have
(declare-ctr a core.static)
(declare-ctr b a)
(declare-ctr c b)
Then (foo3 a b c)
is a valid instantiation. Less contrived examples:
(core.order_hint.key 2) : core.meta : core.static
(arithmetic.int.const 6 1337) : (core.const (arithmetic.int.types.int 6)) : core.static
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.
Right - so my foo3
is rather bogus but only given the flat two-level hierarchy we have ATM
hugr-core/src/types/type_param.rs
Outdated
| Term::Float(_) | ||
| Term::List(_) | ||
| Term::Tuple(_) | ||
| Term::Variable(_) => Err(SignatureError::InvalidTypeParam(self.clone())), |
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.
A variable can be a static type!
(declare-ctr pair (param ?t core.static) (param ?first ?t) (param ?second ?t) core.static)
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.
Ok isn't this pair
here basically dependent types? That kind of example is something we deliberately tried to avoid in Hugr "v1". Are you sure you are not opening the door to undecidability and all kinds of trouble here?
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.
It is a very restricted form of dependent types. We don’t have higher order functions in the static language, no recursion, etc. And it’s quite necessary to give types to some things we want (e.g. the load constant op or the call direct op).
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.
(declare-op core.load_const
(param ?t core.type)
(param ?c (core.const ?t))
(core.fn [] [?t]))
(declare-op core.call_direct
(param ?inputs (core.list core.type))
(param ?outputs (core.list core.type))
(param ?f (core.const (core.fn ?inputs ?outputs)))
(core.fn ?inputs ?outputs))
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.
We don't have higher order functions in the static language, no recursion
Ok....I mean, custom constructors look a bit like that, admittedly they are very simple, so are we sure they don't provide enough of that?
quite necessary to give types to some things we want, e.g. the load constant op
Ah, so in Hugr the "wire type" (EdgeKind::Const
) depends on the type argument, but the other type arguments do not, so the "two level" system happens to be just expressive enough. But in hugr-model that "wire" is another type arg. Hmmm.
I admit this does make the checking more complicated. One would need to check each type parameter given only the preceding ones (to avoid cyclic dependencies), say. But it's not a disaster. Hmmmm.
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.
The "two level" system is only expressive enough because we special cased a lot of things (like the constant one), and even for custom ops we use Rust code to compute signatures whenever they're even slightly non-trivial. The dependent types allow us to capture much more without resorting to escape hatches like that.
The custom constructors don't introduce issues since they don't reduce. Aliases could though, and should therefore be restricted to be non-recursive.
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.
Ok, thanks - seems reasonable
b322c2e
to
c401c30
Compare
…*earlier* parameters
e1d39a7
to
1bf35d8
Compare
PR into #2309, of which I think this should be part. Rule out e.g. BoundedNat's as parameters!
Note the converse - is it a valid argument? - is basically done by
validate
as anything is a valid argument (for some parameter).Could do with some tests, but proptest gets us some of the way.