-
Notifications
You must be signed in to change notification settings - Fork 54
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
Split function
into constructor
/relation
/(custom)function
; Remove default
; Disallow function
lookup in the RHS of a rule
#461
base: main
Are you sure you want to change the base?
Conversation
CodSpeed Performance ReportMerging #461 will not alter performanceComparing Summary
Benchmarks breakdown
|
src/gj.rs
Outdated
// for the later atoms, we consider everything | ||
let mut timestamp_ranges = | ||
vec![0..u32::MAX; cq.query.funcs().collect::<Vec<_>>().len()]; | ||
if do_seminaive { |
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 believe we still want to keep the -naive
flag as well as the code here, so the user can still do naive evaluation (useful for debugging, also have a different semantics than semi-naive for "unsafe" egglog).
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.
Yeah, I agree that we should probably keep naive evaluation
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 think we should keep it. It is unhelpful and adds complexity to the later passes as they need to support the naive semantics correctly. I am also against keeping it as a use-at-your-own-risk feature.
For Egglog users, if you don't use delete, semi-naive and naive are indistinguishable, so it is unhelpful for debugging. If you use delete, then you care much about performance, and there's no point in using naive. Even when you debug with unsafe features, you should probably debug the semi-naive case instead because that's what you want.
For Egglog developers, I see some value in being a sanity check for ensuring semi-naive is implemented correctly. But we are not doing this now, and it can also be done through stronger end-to-end test cases.
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.
That was convincing to me. I've never personally used it before
@yihozhang what do you think?
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.
What complexity does this add to later passes? I thought the only difference between seminaive and naive is that in the seminaive case, we split the original query into many small queries depending on the timestamps (i.e., what this code snippet does).
I strongly recommend that we keep the naive evaluation. We can view semi-naive as an optimization of the naive evaluation, and this optimization is not always semantic-preserving, when given bizarre programs that violate certain assumptions. Examples include
- rules that use
extract
/ user-defined primitives - rules where the merge function is not associative or idempotent
I'm also not confident that our semi-naive is implemented correctly- do we really update timestamp every time we update the table? I just looked at table.rs and it seems we don't update the timestamp for at least get_mut
. The naive evaluation serves as a ground truth for this purpose. Personally, when I am debugging a primitive I wrote, the first thing I do is to disable semi-naive evaluation.
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.
If we keep the naive flag, either we need to split the latter passes into two, which is unlikely, or each piece of downstream code must support both naive and semi-naive. I am skeptical about the claim that semi-naive code would just work for naive. For one thing, I don't see how semi-naive can be implemented as pure syntactic rewrites. As you pointed out, something more needs to happen to the timestamps in the semi-naive case. And yet, the naive flag is not used anywhere else in the codebase.
There are cases where the two give different semantics. However, the naive semantics is not more helpful to the users in those cases because they still need semi-naive to work in the end.
For your last point: Firstly, you still need to debug your new primitive for semi-naive. Secondly, I will only trust naive evaluation as a ground truth if it is well supported with a clear separation between the two semantics. Relying on your program to be tested to produce the test output is a terrible idea to me.
However, I do think this discussion raised a significant concern about the correctness of Egglog. We should investigate the issue.
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.
Conclusion: Keep
- Not comprising the comfort of
-naive
for a smaller core - Too much effort to actually implement
-naive
, we settle for the timestamp hack - Reconsider when merging the new backend
I'm a little worried about the time improvements, especially for lambda... That one is so dramatic I worry that maybe the semantics of the example changed? Seeing all the changes, I also worry about the degradation for UX, it seems just more unwieldy with this change. I know you said that automated desuguring had some issues, but I am wondering if that could be used to at least addressost of these cases? Where there particular issues with it for some cases or just in general? |
src/typechecking.rs
Outdated
//Disallowing Let/Set actions to look up non-constructor functions in rules | ||
for action in head.iter() { | ||
match action { | ||
GenericAction::Let(_, _, Expr::Call(_, symbol, _)) => { |
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.
Don't you need to check if this is a function vs a constructor call here?
tests/eggcc-extraction.egg
Outdated
((set (ival lhs) (IntI n n)))) | ||
(rule ((= lhs (Node (PureOp (Const (IntT) (const) (Num n))))) | ||
(= nval (IntI n n))) | ||
((set (ival lhs) nval))) |
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.
IntI
is a constructor, not a function
So this isn't a lookup and doesn't need to be changed
function
into constructor
/relation
/(custom)function
; Remove default
; Disallow function
lookup in the RHS of a rule
Bumping up this PR again for review:
|
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.
Nice job! Code is clean, with detailed documentation and good tests.
Let us make a release after this PR is merged.
function.insert(values, value, ts); | ||
value | ||
} else { | ||
return Err(Error::NotFoundError(NotFoundError(format!( |
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.
Nit: this should probably provide a different error message given this PR, since the only case this is possible is when there is a bug in our checker.
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 it can still be triggered by merge functions reading a table, e.g.:
(function foo () i64)
(function bar () i64 :merge (foo))
(set (bar) 0)
(fail (set (bar) 1))
/// Now `MathVec` can be used as an input or output sort. | ||
Sort(Span, Symbol, Option<(Symbol, Vec<Expr>)>), | ||
|
||
/// Egglog supports three types of functions |
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.
nice documentation!
src/ast/mod.rs
Outdated
/// A relation models a datalog-style mathematical relation | ||
/// It can only be defined through the `relation` command | ||
/// | ||
/// A custom function is a map |
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 map part of the definition is a bit weird to me, but it's fine for now since we will need a big documentation refactor anyway.
src/ast/mod.rs
Outdated
/// ```text | ||
/// (sort MathVec (Vec Math)) | ||
/// (Constructor Add (i64 i64) Math) |
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.
nit Constructor -> constructor
src/ast/mod.rs
Outdated
/// ``` | ||
/// | ||
/// However, this function is not: | ||
/// Specifically, a custom function can also have an EqSort output type: |
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.
nit: this is not an example where a custom function has an EqSort output
@@ -13,7 +13,7 @@ | |||
(rule ((End a s) | |||
(= s (getString pos))) | |||
((P 1 pos a) | |||
(union (B 1 pos a) (T a s)))) |
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.
Should B be a constructor so that union
would still work?
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.
union
still works for functions whose output is an EqSort. I have added this case to the documentation.
tests/intersection.egg
Outdated
@@ -27,8 +27,8 @@ | |||
(let t2p (f (f b2))) | |||
(union t2 t2p) | |||
|
|||
(union (intersect a1 a2) a3) | |||
(union (intersect b1 b2) b3) | |||
(set (intersect a1 a2) a3) |
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.
this could just be union
?
@@ -62,20 +62,20 @@ | |||
(function evals-to (Term) Value) | |||
|
|||
(rule ((= e (Val val))) | |||
((union (evals-to e) val))) |
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.
Same here. It's an interesting choice to make evals-to a custom function instead of a constructor. This is a new pattern to me, but it seems to work.
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.
This is indeed a new pattern. I'll explain more during the meeting.
This PR fixes Issue #420. Lookup actions in rules will now cause a type error
LookupInRuleDisallowed
.Move specifically, this PR:
Removes
-naive
flag and related desugaring code due to being replaced by this change.Fixes 'fail' failing due to not being identified as global in the
remove_global
rewrite pass.Adds new positive and negative tests for this type error.
Rewrites the existing tests for compatibility with the new type error.