-
Notifications
You must be signed in to change notification settings - Fork 13
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
Cover compile time errors in tests #86
Cover compile time errors in tests #86
Conversation
3690ebf
to
e34f5ae
Compare
edee4c5
to
8b25c55
Compare
8b25c55
to
a8f7bd9
Compare
There is an optimized implementation for a literally indicated number but that wasn't being used. It turns out it was because we hadn't declared this in the Syntax Spec grammar, without which, it was annotating the number with `#%host-expression` (which could have been another way to fix this, by matching #%host-expression in the compiler).
This has been a rule in the expander for some time.
This is ready for review. We are now almost at 100% coverage! There are just a few lines missing in deforest.rkt. For some of them, it looks like they aren't being hit right now when we try expressions like In increasing coverage, it turned out that a lot of lines that weren't covered were indicative of a real problem, so this PR includes some of those fixes as well, and it also uncovered a potential problem: It looks like we aren't deforesting nested positions, so something like You can get a coverage report locally by running Any input appreciated! |
Both `fix` and `find-and-map` apply the same type of function (i.e. a compiler rewrite rule) to syntax, but they have incompatible expectations about the return value. Specifically, `fix` terminates on a false return value, while `find-and-map` continues. This reconciles them so that they both terminate upon receiving false, and both continue if the transformed syntax is identical to the original.
So, I ran into the issue that @dzoep had a little while ago suspected might be present, which is, premature termination of compiler passes upon receiving, or not receiving, a false return value. The commit message fixing the issue explains it well:
Unfortunately, there is now a weird failing test, and I don't understand what is going on with it. I added some logs to
... somewhere in the course of traversing the expression using
This doesn't look right since it isn't a true syntax node in the input expression but more of a "sublist." In any case, once it receives this syntax, it matches this normalization rule:
... which results in the containing expression becoming:
... resulting in the error:
The debug logs I added show this sequence of syntax objects passed to
For comparison, when I manually evaluate all the necessary functions and then invoke them like so:
... the output looks sensible. In particular, it never passes One unusual thing in the traced output is that the offending In case you're back from vacay @michaelballantyne , would love your thoughts! ⛷️ 😼 |
I seem to recall a |
Related: isn’t |
Good idea. Do you mean this one? That is happening at the code generation stage (i.e. I'm also suspicious of this part of
Yeah I didn't recognize it at first either 😆 . But then I remembered that we'd added support for |
This should help with testing the issue where compiling the expression `(thread tee collect)` attempts to normalize sublists instead of just subexpressions. This new test passes (but the surface-level test fails).
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'm concerned about prettify/partition/try and keeping all the passes straight, as well as a couple minor refactoring questions.
A few comments are just some comments trying to explain to myself where things have gone :)
#`(partition [e1-prettified e2-prettified])] | ||
#`(partition e1-prettified e2-prettified)] | ||
[(try expr | ||
[e1 e2] ...) | ||
#:with expr-prettified (prettify-flow-syntax #'expr) | ||
#:with e1-prettified (map prettify-flow-syntax (attribute e1)) | ||
#:with e2-prettified (map prettify-flow-syntax (attribute e2)) | ||
#`(try expr-prettified [e1-prettified e2-prettified])] | ||
#`(try expr-prettified e1-prettified e2-prettified)] |
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 wouldn't mind if commit 9956d74 explained why we stop wrapping the partition
and try
sub-forms, esp. if this function is ever used for output (like in the contract machinery for error reporting?).
Indeed, I see the later comment about jumbling… I would have expected the template to be (partition [e1-pretty e2-pretty] ...)
and similar for try
.
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 e1-pretty
syntax is already a syntax list, so it comes with its own parens 😄 . The wrapping in the template was causing it to have an extra set of parens.
This ✨ de-expander ✨ , although a top of the line chrome plated model 🏎️ , is super hacky and we're hoping to eliminate it pretty soon as Michael is working on adding syntax tracking to Syntax Spec that will give us access to source syntax "the right way."
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 still seems wrong, like it jumbles the patterns... wouldn't it appear extra bizarre with 3 try clauses? I think the version I suggested with ellipses might work fine, modulo any finagling to make list of syntax work I suppose.
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.
Ah yes, you're right.
(partition ((esc (#%host-expression a)) (esc (#%host-expression b))) ((esc (#%host-expression b)) (esc (#%host-expression c)))) | ||
(try (esc (#%host-expression q)) | ||
((esc (#%host-expression a)) (esc (#%host-expression b))) | ||
((esc (#%host-expression a)) (esc (#%host-expression b)))) |
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.
Likewise, I'd expect these tests to reflect structure with more variation, like (partition [a b] [c d])
and (try [a b] [c d])
.
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.
ditto, this is just for basic test coverage of the de-expander. Prettify/etc. is definitely a temporary solution to be able to provide source-syntax-level error messages and we'll hopefully have a proper solution soon.
(define-syntax define-qi-syntax | ||
(syntax-parser | ||
[(_ name transformer) | ||
#`(define-syntax #,((make-interned-syntax-introducer 'qi) #'name) | ||
transformer)])) | ||
|
||
;; TODO: get this to work | ||
;; (define-syntax define-qi-alias | ||
;; (syntax-parser | ||
;; [(_ alias:id name:id) #'(define-qi-syntax alias (make-rename-transformer #'name))])) |
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 can't figure out why these moved to qi-lib/space.rkt. They're not the only things that use the make-interned-syntax-introducer
(which should maybe be bound once as qi-introducer
?).
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 thinking was that binding and referencing identifiers in a certain space isn't specifically about macros, but just about bindings. So I moved those core interfaces, that bind and refer, to space.rkt
. Like we could technically have (define-qi-syntax abc 5)
and it wouldn't be macro. But everything else in the module is specifically about defining and using syntax transformers so I retained them in macro.rkt
.
And good idea! I've bound (make-interned-syntax-introducer 'qi)
to introduce-qi-syntax
.
qi-lib/flow/space.rkt
Outdated
;; reference bindings in qi space | ||
(define-syntax-parser reference-qi | ||
[(_ name) | ||
#:with spaced-name ((make-interned-syntax-introducer 'qi) #'name) | ||
#'spaced-name]) |
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 looks like it's used only for testing. Would it make sense to export it only in a private submodule?
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 seems reasonable. Not too familiar with proper submodule use, but I tried a few variations of using module+
, module*
and module
and none of them worked. E.g.:
space.rkt
:
(module+ refer
(provide reference-qi)
(define-syntax-parser reference-qi
...))
and then in the tests/space.rkt
module:
(require qi/flow/space/refer)
... says "collection not found."
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.
You probably need module+
, but might not. The require is (submod qi/flow/space refer)
(or some other variant using relative module paths to refer to the supermodule, if you prefer).
(define (all? . args) | ||
(and (for/and ([v (in-list args)]) v) #t)) | ||
|
||
(define (any? . args) | ||
(and (for/or ([v (in-list args)]) v) #t)) | ||
|
||
(define (none? . args) | ||
(not (for/or ([v (in-list args)]) v))) |
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.
Commit 268a5fd says these were removed because they are in qi-lib/flow/extended/impl.rkt, but those implementations are different (they omit the boolean casting; I think we prefer without it).
I believe what actually happened is that commit 8e4338f removed their use in favor of folds; later, commit c4244bc turned those forms into macros. I think then commit 22d2cee re-introduced ~all?
as a Qi function (so it's now a Qi function instead of a macro, and it does double-duty serving AND
). Similar for commit 8156eb0 and ~any?
, and commit c0531e7 and ~none?
.
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.
Truth is stranger than commit messages 🌠
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 would be nice if this was more traceable, though. Spelunking through commits to answer logs is helped by cross-references and detailed commit messages :)
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.
True, I'll try to write better commit messages and think more "archeologically" about this :)
(define (literal-parser stx) | ||
(syntax-parse stx | ||
[val:literal #'(qi0->racket (gen 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.
Commit 0fb1afa says this has been in the expander for a while; I think it was effectively introduced (and then several times refactored) by commit 5e1e06e as part of the flow macro (this was Oct. 2021 and is in main
!).
This does make me wonder what machinery besides coverage reports we have to make sure that all passes, parsers, etc., are tied together. For example, with the compiler->expander pipeline, I think there are several stages to trace through to follow an input flow syntax to an output syntax: how do we make the sure each "pass" or "stage" handles all (and only) the outputs of its previous stage?
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 definitely agree the implementation has reached a level of complexity where having many different and complementary ways to test things would be valuable and would save us a lot of time. I'll summarize what we have today and what is planned -- lmk if this addresses your concern adequately or if you feel more is needed:
- we have regular unit tests validating the semantics of the language (e.g.
qi-test/tests/flow.rkt
). In addition to local tests of individual forms, this includes some nonlocal flows like the counterexamples you found, and in general we'd want these to tell us if we change the meaning of the language (including unsound rules). - there are starter "expander" tests (
qi-test/tests/expander.rkt
) that verify that surface expressions are correctly expanded to core expressions (but it can only compare the result as datums for now) - there are compiler "rules" tests (
qi-test/tests/compiler/rules.rkt
) that check individual rewrite rules, and also check transformations effected by overall individual compiler passes like normalization (which may repeatedly apply sets of rules to a fixed point). These verify that when given correct input they would produce the expected optimized output. - compiler "semantics" tests (
qi-test/tests/compiler/semantics.rkt
) that check various combinations of patterns that are matched by optimization rules, to verify that they produce the same output (including raising errors) as the original expression. These patterns are specific to the compiler implementation and that's why they aren't language level (regular) unit tests.
Planned:
The above tests would validate that different components behave as expected when they are invoked, but we still don't have a way to know whether they are invoked for a particular expression. Some folks on discourse suggested "trace" testing, where we add logging to each pass of the compiler and then verify that a given expression goes through the correct sequence of compiler passes by capturing logs from invoking the compiler. E.g. we would expect (~> (~> f))
to be normalized, (~>> (filter odd?) (map sqr))
to be deforested, and (~>> (~>> (filter odd?) (map sqr))
to be both normalized as well as deforested (FTR every expression today does go through both normalize -> deforest, but for any given expression, either or both of these passes may leave it unchanged).
Wdyt? Anything else that would be useful?
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.
All sounds reasonable.
One thing I was considering was: how do I check what the syntax (or other input) to each "stage" is? Such as compiler, expander, optimizer, etc. What is the output?
I would call them languages, but HtDP might call them data definitions 😅 basically a "this is what we can expect to see coming in and out, and code should handle all these cases." That would pave way to follow the code through each pass, for example, to trace a specific form through the pipeline.
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'll give this some more thought. It would be nice to be able to trace an expression like this on-demand in a testing/REPL context. The macro stepper should be one way to gain visibility of this kind but currently doesn't give us the most support due to some APIs being private and not supported IIUC.
That's almost definitely the problem, but I don't see an easy fix. Does everything still work if the |
Thanks for the detailed review! Looking at What's weird is that this test for normalization of the core language passes (normalization is mostly all datum rather than literal matching), and adding a debug log at the top of That could imply that |
I'll continue looking into (1) private submodule, (2) pattern jumbling, (3) find-and-map, (4) testing ideas. But I'll go ahead and merge this PR now in case @dzoep wants to experiment with things. Thanks! |
efccd92
into
drym-org:lets-write-a-qi-compiler
Summary of Changes
Qi has never quite managed to reach 100% test coverage because there are some parts of the code that are only hit at compile time (e.g. syntax errors), which I didn't know how to write unit tests for. Michael recently mentioned
convert-compile-time-error
which converts a compile-time error into a runtime error, allowing us to write unit tests for it. Now with this available, we should be able to get to 100% test coverage.Public Domain Dedication
(Why: The freely released, copyright-free work in this repository represents an investment in a better way of doing things called attribution-based economics. Attribution-based economics is based on the simple idea that we gain more by giving more, not by holding on to things that, truly, we could only create because we, in our turn, received from others. As it turns out, an economic system based on attribution -- where those who give more are more empowered -- is significantly more efficient than capitalism while also being stable and fair (unlike capitalism, on both counts), giving it transformative power to elevate the human condition and address the problems that face us today along with a host of others that have been intractable since the beginning. You can help make this a reality by releasing your work in the same way -- freely into the public domain in the simple hope of providing value. Learn more about attribution-based economics at drym.org, tell your friends, do your part.)