Today's Qi meeting notes

Hi all,
We've been sharing these meeting notes privately among attendees and enthusiasts, but some of us felt it would be fun and useful to share them on here, so we'll start doing that. The Qi compiler project has really picked up steam lately and there are lots of very interesting avenues for exploration. They are open to all -- come on by!

Here are the latest notes (actually from Friday -- but the topic is so that this can be reused weekly).

A Long Stream is Not Easily Exhausted

We try to have fun summary names for each meeting to help us remember what they were about. This one references a Chinese proverb.

Enjoy!

3 Likes

Hey, thanks for placing it here so quickly.

I was (one of) requesting this, because:

I found more (future) issues with fold interaction with left and right threading:

#lang racket/base

(require qi)

(display "Racket foldr: ")
(foldr
 string-append
 "0"
 (map
  number->string
  (filter odd? '(1 2 3 4 5))))

(display "Qi foldr under right threading: ")
((flow
  (~> (filter odd?)
      (map number->string)
      (foldr string-append "0")))
 '(1 2 3 4 5))

(display "Qi foldr under left threading: ")
((flow
  (~>> (filter odd?)
      (map number->string)
      (foldr string-append "0")))
 '(1 2 3 4 5))

(display "Racket foldl: ")
(foldl
 string-append
 "0"
 (map
  number->string
  (filter odd? '(1 2 3 4 5))))

(display "Qi foldl under left threading: ")
((flow
  (~>> (filter odd?)
      (map number->string)
      (foldl string-append "0")))
 '(1 2 3 4 5))

(display "Qi foldl under right threading: ")
(with-handlers ((exn? (λ (ex)
                        (newline)
                        (raise ex))))
  ((flow
    (~> (filter odd?)
        (map number->string)
        (foldl string-append "0")))
   '(1 2 3 4 5)))

Basically the last example raises correctly an exception while the "Qi foldr under left threading" truly behaves like "Qi foldr under right threading". Which is the weird behavior you've already noticed.

Should the semantics remain the same (I assume they should) than foldr must work only with the right chirality and foldl only with left chirality.

Right now only the optimized foldr misbehaves - which means this is the right time to make sure it not only gets fixed but we put at least some tests in place so that this does not hit us in the future with other optimizations. I am pretty confident that other fusion-terminating operations will have similar problems with chirality. And I used non-commutative operation (string-append) to double-check. Commutative operations hide a lots of errors here so another suggestion is to make sure we include non-commutative operations in any tests for fold-like optimizations.

OTOH I hope I got it right: right chirality means the values go as the first (leftmost arguments) and left chirality means they go last (rightmost). I am not sure about this, on a second thought it might be the other way round. That means I might be using a flipped terminology here - yet still I am fairly confident the examples show a real problem. Can anyone re-check my naming of left/right chirality for threading macros in Qi?

;; Right-threading is just normal threading but with a syntax
;; property attached to the components indicating the chirality
(define-qi-syntax-rule (thread-right onex:right-threading-clause ...)
  (~> onex.chiral ...))

This snippet probably means that default really is left and if chirality is set, the default is right (reverse).

I like these reports, thanks.

1 Like

And a little side note (I'll officially announce it later), the official Actor Basic is now hosted at:

It is just one package out of a collection of packages aimed at writing roguelike (more like rogue-lite as I am interested in the real-time nature of such environments) games.
The splicing case syntax @countvajhula mentioned in the notes is:

It's very rudimentary, but shows the purpose and simple implementation of splicing syntax in dispatching expressions.

Interesting, I'll have to think about this some more (or you could explain it at the next meeting!), but I think you may be confusing the "chirality" in the code with a different idea. In the code, chirality refers to where in the original expression the argument(s) should be placed. Right chirality means that the argument will be passed in the last argument position (it's very similar to the right threading operator in the threading library):

(~>> (filter odd?) (map sqr))

would be equivalent to

(~> (filter odd? _) (map sqr _))

(just for illustration), which, in the reference (non-optimizing) compiler (to use your terminology :slight_smile: ), compiles down to:

(map sqr (filter odd (list 1 2 3)))

Left chirality is the default and means the argument will be passed in the first position of each application.

(~> (filter odd?) (map sqr))

would be equivalent to

(~> (filter _ odd?) (map _ sqr))

which the reference compiler compiles to:

(map (filter (list 1 2 3) odd?) sqr)

So use of left-threading should be an error for both foldl and foldr since all of these functions expect the list in the last position rather than the first position.

But it's quite possible that going through the stream elements from left-to-right vs right-to-left (which I think is what you're talking about) would have different considerations for foldl vs foldr. I don't recall how it is in St-Amour's writeup (mentioned in several places in recent meeting notes - for anyone following along) but maybe he already accounts for this?

Here are today's notes:

Threading a Needle in a Haystack

Enjoy!

1 Like

Today's notes:

Start Somewhere, and then, Continue.

1 Like

Today's:

Validly Verifying that we're Compiling Correctly

Among other things, we discussed the feedback on compiler testing approaches from the other thread. We'll likely continue that discussion in the coming weeks.

Enjoy, and hope to see you next time :slight_smile:

Try this in macro stepper :slight_smile:

It only covers the before/after optimization (just before introducing bindings), but this is probably the right approach. It allows us to keep some expansion to happen in different order than the plain macro expansion going from the outermost to the innermost expressions and yet present it as distinct transformation steps. All we need to do is to use emit-local-step in the right places of the Qi compiler (like it is probably used in the normal Racket macro expander).

I still have to verify that this is what the normal Racket expander does internally. Or perhaps @ryanc can chime in?

1 Like

The macro expander emits lots of different kinds of events that allow the macro stepper to reconstruct the context and steps of expansion. In the expander, the interface with the macro stepper is mainly in log.rkt. Look for calls to log-expand etc in expr.rkt and other files in that directory.

For example, in the expansion of a lambda expression, the expander would emit a visit event ("I'm about to start expanding this expression"), then a prim-lambda event ("After inspecting the expression and resolving the head identifier, looks like Racket's lambda syntax"), then a lambda-renames event ("Here are the formal parameters with the new lexical scope applied"), then events for its body, then various events to indicate that it's exiting the context of the lambda expression. (I've skipped a few for simplicity.)

The macro stepper parses that sequence of events into a tree --- yes, parses, using Racket's LALR(1) parser generator --- and then it uses that tree to generate reduction steps. The tree structure is used to determine the context of each expansion step, so macro steps within the lambda expression's body show the surrounding lambda expression, show arrows between lambda-bound variables and apparent references, etc.

The macro-debugger/emit API doesn't provide a way to set the context of a remark or artificial step, but that could probably be added, at least in a limited form.

3 Likes

I Once Was Blind but Now I See (or, the Spectre of Friday the Thirteenth)

Enjoy :laughing:

Today's: Challenges with Multiple Values in Streams

Btw just want to put Context is Everything (from last time) on your radar @ryanc and @michaelballantyne .

1 Like

Friday's notes:

Deforesting Vindaloo

The discussion on how to allow users to extend Qi compiler optimizations (starting at "deforestation for bepoke datatypes") was an especially interesting topic that came up.

Incidentally, next time, @dominik.pantucek is planning to give a demo of the 3D rendering engine for the Racket Roguelike Library, powered by Actor Basic (another flow-oriented language like Qi). If you're a fan of 80s/90s era games, I think you wouldn't wanna miss it.

1 Like

Friday's notes:

Rogue's Gambit

Highlights:

  • Dominik gave an awesome demo of the Racket Roguelike Library
  • Brad (@Gambiteer ), author of SRFI 231, described at a high level a deforestation-like approach in generalized arrays
  • Why bother deforesting range when it doesn't improve performance on its own?
  • Why do we still need begin-encourage-inline when we have already said define-inline? (we don't know. Any ideas?)
  • The huge potential of Qi's "no accidental side effects" policy

We also finally returned to the discussion about good ways to test the compiler, and adopted a clever technique suggested by @samth and @gus-massa to test the compiler's initial normalization pass. We're going to continue exploring this next time so you're welcome to join if you're interested in either sharing or learning more here.

2 Likes

The notes on delayed computation by building a description and executing is (IIUC) essentially the monad-interpreter pattern: an algebraic datatype describes the operations and is equipped with monadic structure; the functions (such as map) return a value of the datatype rather than performing the computation. One can then independently specify how to execute the computation, such as via a reference interpreter or via an optimizing compiler, by writing programs over the type of the computation.

The classic IO monad does something like this internally in Haskell, and I think the pattern is commonly used for other problems.

1 Like

I’m interested in helping with the inlining issue if you’d like to provide more information.

Edit: I should probably leave a general note on this issue. The approach of define-inline is that it’s a macro, not normal inlining done by the compiler, while begin-encourage-inline attaches a 'compiler-hint:cross-module-inline syntax property that, well, encourages inlining. In particular, higher-order use isn’t inlined by define-inline (it expands to a variable reference in this case), while there can be more things done by the compiler. This is likely what is causing the issue, because Qi is probably relying on the higher-order usage? I would suggest just simply use begin-encourage-inline and let the compiler figure out the optimization.

1 Like

This commit is the context. If you clone the repo locally (you may find Getting Started helpful) and run:

$ make profile-competitive

The filter-map and filter-map (large list) benchmarks shows the most improvement with both begin-encourage-inline and define-inline, but as I recall, if we remove either of those, it gets noticeably slower.

So only begin-encourage-inline doesn’t work well because the inlining very easily exceeds schemify’s fuel (which governs cross-module inlining), I guess. Actually, in such cases, I think a more aggressive define-inline form (in the sense that it also inlines variable-reference case) will be useful. This may be a good addition to define-inline itself. (And it also doesn’t handle case-lambda, right? So it’s another thing that can be done.)

1 Like

A fine-grained migraine

Highlights (some of these issues affect compilers in general and aren't specific to Qi):

  • we continued discussing good ways to test a compiler (still iterating there -- please join next week if you're interested)
  • how to report good error messages in terms of the source code (rather than the target code)?
  • how to deal with a combinatorial explosion in match patterns in deforesting "fine grained" argument templates like (~> (range _ _ 10) (filter odd? _) (map sqr _))?
  • can we simpify match patterns by reducing inferred syntax properties like "chirality" (i.e. left- or right-threading) to an explicit use of a core language?
  • can we deforest all of racket/list? Should we?

Last week's notes:

Manic Macromancy

There's a lot in there, this was a long one. Some highlights:

  • Qi's error messages with deforestation are better than Racket's and "even better than Qi" :smile:
  • "Tower of languages" vs "compiler macros" as ways to extend the Qi compiler (esp. for deforesting all of racket/list)
  • We've adopted some suggestions from the community for testing the compiler (cc @gus-massa @samth @stamourv -- btw, we still hope to adopt the "logging" approach to trace compilation, but haven't gotten there yet!)
  • Cameo appearance by Typed Qi @scolobb @NoahStoryM
3 Likes