diff --git a/Makefile b/Makefile index 0d0b17a6..3ffd6e48 100644 --- a/Makefile +++ b/Makefile @@ -104,25 +104,28 @@ test: raco test -exp $(PACKAGE-NAME)-{lib,test,doc,probe} test-flow: - racket $(PACKAGE-NAME)-test/tests/flow.rkt + racket -y $(PACKAGE-NAME)-test/tests/flow.rkt test-on: - racket $(PACKAGE-NAME)-test/tests/on.rkt + racket -y $(PACKAGE-NAME)-test/tests/on.rkt test-threading: - racket $(PACKAGE-NAME)-test/tests/threading.rkt + racket -y $(PACKAGE-NAME)-test/tests/threading.rkt test-switch: - racket $(PACKAGE-NAME)-test/tests/switch.rkt + racket -y $(PACKAGE-NAME)-test/tests/switch.rkt test-definitions: - racket $(PACKAGE-NAME)-test/tests/definitions.rkt + racket -y $(PACKAGE-NAME)-test/tests/definitions.rkt test-macro: - racket $(PACKAGE-NAME)-test/tests/macro.rkt + racket -y $(PACKAGE-NAME)-test/tests/macro.rkt test-util: - racket $(PACKAGE-NAME)-test/tests/util.rkt + racket -y $(PACKAGE-NAME)-test/tests/util.rkt + +test-compiler: + racket -y $(PACKAGE-NAME)-test/tests/compiler.rkt test-probe: raco test -exp $(PACKAGE-NAME)-probe @@ -193,4 +196,4 @@ performance-report: performance-regression-report: @racket $(PACKAGE-NAME)-sdk/profile/report.rkt -r $(REF) -.PHONY: help install remove build build-docs build-all clean check-deps test test-flow test-on test-threading test-switch test-definitions test-macro test-util test-probe test-with-errortrace errortrace errortrace-flow errortrace-on errortrace-threading errortrace-switch errortrace-definitions errortrace-macro errortrace-util errortrace-probe docs cover coverage-check coverage-report cover-coveralls profile-local profile-loading profile-selected-forms profile-competitive profile-nonlocal profile performance-report performance-regression-report +.PHONY: help install remove build build-docs build-all clean check-deps test test-flow test-on test-threading test-switch test-definitions test-macro test-util test-compiler test-probe test-with-errortrace errortrace errortrace-flow errortrace-on errortrace-threading errortrace-switch errortrace-definitions errortrace-macro errortrace-util errortrace-probe docs cover coverage-check coverage-report cover-coveralls profile-local profile-loading profile-selected-forms profile-competitive profile-nonlocal profile performance-report performance-regression-report diff --git a/qi-lib/flow.rkt b/qi-lib/flow.rkt index ad49312f..8a5639c1 100644 --- a/qi-lib/flow.rkt +++ b/qi-lib/flow.rkt @@ -5,7 +5,7 @@ (all-from-out "flow/extended/expander.rkt") (all-from-out "flow/extended/forms.rkt")) -(require syntax-spec +(require syntax-spec-v1 (for-syntax racket/base syntax/parse (only-in "private/util.rkt" diff --git a/qi-lib/flow/aux-syntax.rkt b/qi-lib/flow/aux-syntax.rkt index c9151245..e5cf653a 100644 --- a/qi-lib/flow/aux-syntax.rkt +++ b/qi-lib/flow/aux-syntax.rkt @@ -10,7 +10,6 @@ (define-syntax-class literal (pattern - ;; TODO: would be ideal to also match literal vectors, boxes and prefabs (~or* expr:boolean expr:char expr:string diff --git a/qi-lib/flow/core/compiler.rkt b/qi-lib/flow/core/compiler.rkt index fbd0d08b..c8677872 100644 --- a/qi-lib/flow/core/compiler.rkt +++ b/qi-lib/flow/core/compiler.rkt @@ -1,27 +1,52 @@ #lang racket/base -(provide (for-syntax compile-flow)) +(provide (for-syntax compile-flow + normalize-pass)) (require (for-syntax racket/base syntax/parse racket/match (only-in racket/list make-list) "syntax.rkt" - "../aux-syntax.rkt") + "../aux-syntax.rkt" + "util.rkt" + "debug.rkt" + "normalize.rkt" + "deforest.rkt") "impl.rkt" (only-in racket/list make-list) racket/function racket/undefined - (prefix-in fancy: fancy-app)) + (prefix-in fancy: fancy-app) + racket/list) (begin-for-syntax + ;; note: this does not return compiled code but instead, ;; syntax whose expansion compiles the code (define (compile-flow stx) (process-bindings (optimize-flow stx))) + (define (deforest-pass stx) + (find-and-map/qi (fix deforest-rewrite) + stx)) + + (define-qi-expansion-step (~deforest-pass stx) + ;; Note: deforestation happens only for threading, + ;; and the normalize pass strips the threading form + ;; if it contains only one expression, so this would not be hit. + (deforest-pass stx)) + + (define (normalize-pass stx) + (find-and-map/qi (fix normalize-rewrite) + stx)) + + (define-qi-expansion-step (~normalize-pass stx) + (normalize-pass stx)) + (define (optimize-flow stx) - stx)) + (~deforest-pass + (~normalize-pass stx)))) ;; Transformation rules for the `as` binding form: ;; @@ -53,26 +78,6 @@ (begin-for-syntax - (define (find-and-map f stx) - ;; f : syntax? -> (or/c syntax? #f) - (match stx - [(? syntax?) (let ([stx^ (f stx)]) - (or stx^ (datum->syntax stx - (find-and-map f (syntax-e stx)) - stx - stx)))] - [(cons a d) (cons (find-and-map f a) - (find-and-map f d))] - [_ stx])) - - (define (find-and-map/qi f stx) - ;; #%host-expression is a Racket macro defined by syntax-spec - ;; that resumes expansion of its sub-expression with an - ;; expander environment containing the original surface bindings - (find-and-map (syntax-parser [((~datum #%host-expression) e:expr) this-syntax] - [_ (f this-syntax)]) - stx)) - ;; (as name) → (~> (esc (λ (x) (set! name x))) ⏚) ;; TODO: use a box instead of set! (define (rewrite-all-bindings stx) @@ -98,7 +103,7 @@ (with-syntax ([(v ...) ids]) #`(let ([v undefined] ...) #,stx))) - (define (process-bindings stx) + (define-qi-expansion-step (process-bindings stx) ;; TODO: use syntax-parse and match ~> specifically. ;; Since macros are expanded "outside in," presumably ;; it will naturally wrap the outermost ~> @@ -206,23 +211,15 @@ [((~datum #%fine-template) (prarg-pre ... (~datum _) prarg-post ...)) #'(fancy:#%app prarg-pre ... _ prarg-post ...)] - ;; Pre-supplied arguments without a template - [((~datum #%partial-application) (natex prarg ...+)) - ;; we use currying instead of templates when a template hasn't - ;; explicitly been indicated since in such cases, we cannot - ;; always infer the appropriate arity for a template (e.g. it - ;; may change under composition within the form), while a - ;; curried function will accept any number of arguments - #:do [(define chirality (syntax-property this-syntax 'chirality))] - (if (and chirality (eq? chirality 'right)) - #'(lambda args - (apply natex prarg ... args)) - ;; TODO: keyword arguments don't work for the left-chiral case - ;; since we can't just blanket place the pre-supplied args - ;; and need to handle the keyword arguments differently - ;; from the positional arguments. - #'(lambda args - ((kw-helper natex args) prarg ...)))])) + ;; if in the course of optimization we ever end up with a fully + ;; simplified host expression, the compiler would a priori reject it as + ;; not being a core Qi expression. So we add this extra rule here + ;; to simply pass this expression through. + ;; TODO: should `#%host-expression` be formally declared as being part + ;; of the core language by including it in the syntax-spec grammar + ;; in extended/expander.rkt? + [((~datum #%host-expression) hex) + this-syntax])) ;; The form-specific parsers, which are delegated to from ;; the qi0->racket macro: @@ -493,16 +490,28 @@ the DSL. (define (blanket-template-form-parser stx) (syntax-parse stx ;; "prarg" = "pre-supplied argument" + ;; Note: use of currying here doesn't play well with bindings + ;; because curry / curryr immediately evaluate their arguments + ;; and resolve any references to bindings at compile time. + ;; That's why we use a lambda which delays evaluation until runtime + ;; when the reference is actually resolvable. See "anaphoric references" + ;; in the compiler meeting notes, + ;; "The Artist Formerly Known as Bindingspec" [((~datum #%blanket-template) (natex prarg-pre ...+ (~datum __) prarg-post ...+)) - #'(curry (curryr natex - prarg-post ...) - prarg-pre ...)] + ;; "(curry (curryr ...) ...)" + #'(lambda largs + (apply + (lambda rargs + ((kw-helper natex rargs) prarg-post ...)) + prarg-pre ... + largs))] [((~datum #%blanket-template) (natex prarg-pre ...+ (~datum __))) - #'(curry natex prarg-pre ...)] + ;; "curry" + #'(lambda args + (apply natex prarg-pre ... args))] [((~datum #%blanket-template) (natex (~datum __) prarg-post ...+)) - #'(curryr natex prarg-post ...)] - ;; TODO: this should be a compiler optimization - [((~datum #%blanket-template) (natex (~datum __))) - #'natex]))) + ;; "curryr" + #'(lambda args + ((kw-helper natex args) prarg-post ...))]))) diff --git a/qi-lib/flow/core/debug.rkt b/qi-lib/flow/core/debug.rkt new file mode 100644 index 00000000..fd5b0e92 --- /dev/null +++ b/qi-lib/flow/core/debug.rkt @@ -0,0 +1,22 @@ +#lang racket/base + +(provide qi-expansion-step + define-qi-expansion-step) + +(require macro-debugger/emit) + +;; These macros emit expansion "events" that allow the macro +;; stepper to report stages in the expansion of an expression, +;; giving us visibility into this process for debugging purposes. +;; Note that this currently does not distinguish substeps +;; of a parent expansion step. +(define-syntax-rule (qi-expansion-step name stx0 stx1) + (let () + (emit-local-step stx0 stx1 #:id #'name) + stx1)) + +(define-syntax-rule (define-qi-expansion-step (name stx0) + body ...) + (define (name stx0) + (let ([stx1 (let () body ...)]) + (qi-expansion-step name stx0 stx1)))) diff --git a/qi-lib/flow/core/deforest.rkt b/qi-lib/flow/core/deforest.rkt new file mode 100644 index 00000000..ba43520a --- /dev/null +++ b/qi-lib/flow/core/deforest.rkt @@ -0,0 +1,414 @@ +#lang racket/base + +(provide deforest-rewrite) + +(require (for-syntax racket/base) + syntax/parse + racket/syntax-srcloc + racket/performance-hint + racket/match + racket/list + racket/contract/base) + +;; These bindings are used for ~literal matching to introduce implicit +;; producer/consumer when none is explicitly given in the flow. +(define-syntax cstream->list #'-cstream->list) +(define-syntax list->cstream #'-list->cstream) + +;; "Composes" higher-order functions inline by directly applying them +;; to the result of each subsequent application, with the last argument +;; being passed to the penultimate application as a (single) argument. +;; This is specialized to our implementation of stream fusion in the +;; arguments it expects and how it uses them. +(define-syntax inline-compose1 + (syntax-rules () + [(_ f) f] + [(_ [op f] rest ...) (op f (inline-compose1 rest ...))])) + +;; Partially reconstructs original flow expressions. The chirality +;; is lost and the form is already normalized at this point though! +(define (prettify-flow-syntax stx) + (syntax-parse stx + #:datum-literals (#%host-expression esc #%blanket-template #%fine-template) + (((~literal thread) + expr ...) + #`(~> #,@(map prettify-flow-syntax (syntax->list #'(expr ...))))) + (((~or #%blanket-template #%fine-template) + (expr ...)) + (map prettify-flow-syntax (syntax->list #'(expr ...)))) + ((#%host-expression expr) #'expr) + ((esc expr) (prettify-flow-syntax #'expr)) + (expr #'expr))) + +;; Special "curry"ing for #%fine-templates. All #%host-expressions +;; are passed as they are and all (~datum _) are replaced by wrapper +;; lambda arguments. +(define ((make-fine-curry argstx minargs maxargs form-stx) ctx name) + (define argstxlst (syntax->list argstx)) + (define numargs (length argstxlst)) + (cond ((< numargs minargs) + (raise-syntax-error (syntax->datum name) + (format "too few arguments - given ~a - accepts at least ~a" + numargs minargs) + (prettify-flow-syntax ctx) + (prettify-flow-syntax form-stx))) + ((> numargs maxargs) + (raise-syntax-error (syntax->datum name) + (format "too many arguments - given ~a - accepts at most ~a" + numargs maxargs) + (prettify-flow-syntax ctx) + (prettify-flow-syntax form-stx)))) + (define temporaries (generate-temporaries argstxlst)) + (define-values (allargs tmpargs) + (for/fold ((all '()) + (tmps '()) + #:result (values (reverse all) + (reverse tmps))) + ((tmp (in-list temporaries)) + (arg (in-list argstxlst))) + (syntax-parse arg + #:datum-literals (#%host-expression) + ((#%host-expression ex) + (values (cons #'ex all) + tmps)) + ((~datum _) + (values (cons tmp all) + (cons tmp tmps)))))) + (with-syntax (((carg ...) tmpargs) + ((aarg ...) allargs)) + #'(λ (proc) + (λ (carg ...) + (proc aarg ...))))) + +;; Special curry for #%blanket-template. Raises syntax error if +;; there are too many arguments. If the number of arguments is +;; exactly the maximum, wraps into lambda without any arguments. If +;; less than maximum, curries it from both left and right. +(define ((make-blanket-curry prestx poststx maxargs form-stx) ctx name) + (define prelst (syntax->list prestx)) + (define postlst (syntax->list poststx)) + (define numargs (+ (length prelst) (length postlst))) + (with-syntax (((pre-arg ...) prelst) + ((post-arg ...) postlst)) + (cond ((> numargs maxargs) + (raise-syntax-error (syntax->datum name) + (format "too many arguments - given ~a - accepts at most ~a" + numargs maxargs) + (prettify-flow-syntax ctx) + (prettify-flow-syntax form-stx))) + ((= numargs maxargs) + #'(λ (v) + (λ () + (v pre-arg ... post-arg ...)))) + (else + #'(λ (v) + (λ rest + (apply v pre-arg ... + (append rest + (list post-arg ...))))))))) + +;; Unifying producer curry makers. The ellipsis escaping allows for +;; simple specification of pattern variable names as bound in the +;; syntax pattern. +(define-syntax make-producer-curry + (syntax-rules () + ((_ min-args max-args + blanket? pre-arg post-arg + fine? arg + form-stx) + (cond + ((attribute blanket?) + (make-blanket-curry #'(pre-arg (... ...)) + #'(post-arg (... ...)) + max-args + #'form-stx + )) + ((attribute fine?) + (make-fine-curry #'(arg (... ...)) min-args max-args #'form-stx)) + (else + (λ (ctx name) #'(λ (v) v))))))) + +;; Used for producing the stream from particular +;; expressions. Implicit producer is list->cstream-next and it is +;; not created by using this class but rather explicitly used when +;; no syntax class producer is matched. +(define-syntax-class fusable-stream-producer + #:attributes (next prepare contract name curry) + #:datum-literals (#%host-expression #%blanket-template #%fine-template esc __) + ;; Explicit range producers. + (pattern (~and (~or (esc (#%host-expression (~literal range))) + (~and (#%fine-template + ((#%host-expression (~literal range)) + arg ...)) + fine?) + (~and (#%blanket-template + ((#%host-expression (~literal range)) + (#%host-expression pre-arg) ... + __ + (#%host-expression post-arg) ...)) + blanket?)) + form-stx) + #:attr next #'range->cstream-next + #:attr prepare #'range->cstream-prepare + #:attr contract #'(->* (real?) (real? real?) any) + #:attr name #'range + #:attr curry (make-producer-curry 1 3 + blanket? pre-arg post-arg + fine? arg + form-stx)) + + ;; The implicit stream producer from plain list. + (pattern (~literal list->cstream) + #:attr next #'list->cstream-next + #:attr prepare #'list->cstream-prepare + #:attr contract #'(-> list? any) + #:attr name #''list->cstream + #:attr curry (λ (ctx name) #'(λ (v) v)))) + +;; Matches any stream transformer that can be in the head position +;; of the fused sequence even when there is no explicit +;; producer. Procedures accepting variable number of arguments like +;; `map` cannot be in this class. +(define-syntax-class fusable-stream-transformer0 + #:attributes (f next) + #:datum-literals (#%host-expression #%blanket-template __ _ #%fine-template) + (pattern (~or (#%blanket-template + ((#%host-expression (~literal filter)) + (#%host-expression f) + __)) + (#%fine-template + ((#%host-expression (~literal filter)) + (#%host-expression f) + _))) + #:attr next #'filter-cstream-next)) + +;; All implemented stream transformers - within the stream, only +;; single value is being passed and therefore procedures like `map` +;; can (and should) be matched. +(define-syntax-class fusable-stream-transformer + #:attributes (f next) + #:datum-literals (#%host-expression #%blanket-template __ _ #%fine-template) + (pattern (~or (#%blanket-template + ((#%host-expression (~literal map)) + (#%host-expression f) + __)) + (#%fine-template + ((#%host-expression (~literal map)) + (#%host-expression f) + _))) + #:attr next #'map-cstream-next) + + (pattern (~or (#%blanket-template + ((#%host-expression (~literal filter)) + (#%host-expression f) + __)) + (#%fine-template + ((#%host-expression (~literal filter)) + (#%host-expression f) + _))) + #:attr next #'filter-cstream-next)) + +;; Terminates the fused sequence (consumes the stream) and produces +;; an actual result value. +(define-syntax-class fusable-stream-consumer + #:attributes (end) + #:datum-literals (#%host-expression #%blanket-template _ __ #%fine-template esc) + (pattern (~or (#%blanket-template + ((#%host-expression (~literal foldr)) + (#%host-expression op) + (#%host-expression init) + __)) + (#%fine-template + ((#%host-expression (~literal foldr)) + (#%host-expression op) + (#%host-expression init) + _))) + #:attr end #'(foldr-cstream-next op init)) + + (pattern (~or (#%blanket-template + ((#%host-expression (~literal foldl)) + (#%host-expression op) + (#%host-expression init) + __)) + (#%fine-template + ((#%host-expression (~literal foldl)) + (#%host-expression op) + (#%host-expression init) + _))) + #:attr end #'(foldl-cstream-next op init)) + + (pattern (~or (esc (#%host-expression (~literal car))) + (#%fine-template + ((#%host-expression (~literal car)) + _)) + (#%blanket-template + ((#%host-expression (~literal car)) + __))) + #:attr end #'(car-cstream-next)) + + (pattern (~literal cstream->list) + #:attr end #'(cstream-next->list))) + +;; Used only in deforest-rewrite to properly recognize the end of +;; fusable sequence. +(define-syntax-class non-fusable + (pattern (~not (~or _:fusable-stream-transformer + _:fusable-stream-producer + _:fusable-stream-consumer)))) + +;; Generates a syntax for the fused operation for given +;; sequence. The syntax list must already be in the following form: +;; (producer transformer ... consumer) +(define (generate-fused-operation ops ctx) + (syntax-parse (reverse ops) + [(c:fusable-stream-consumer + t:fusable-stream-transformer ... + p:fusable-stream-producer) + ;; A static runtime contract is placed at the beginning of the + ;; fused sequence. And runtime checks for consumers are in + ;; their respective implementation procedure. + #`(esc + (#,((attribute p.curry) ctx (attribute p.name)) + (contract p.contract + (p.prepare + (#,@#'c.end + (inline-compose1 [t.next t.f] ... + p.next) + '#,(prettify-flow-syntax ctx) + #,(syntax-srcloc ctx))) + p.name + '#,(prettify-flow-syntax ctx) + #f + #,(syntax-srcloc ctx))))])) + +;; Performs one step of deforestation rewrite. Should be used as +;; many times as needed - until it returns the source syntax +;; unchanged. +(define (deforest-rewrite stx) + (syntax-parse stx + [((~datum thread) _0:non-fusable ... + p:fusable-stream-producer + ;; There can be zero transformers here: + t:fusable-stream-transformer ... + c:fusable-stream-consumer + _1 ...) + #:with fused (generate-fused-operation + (syntax->list #'(p t ... c)) + stx) + #'(thread _0 ... fused _1 ...)] + [((~datum thread) _0:non-fusable ... + t1:fusable-stream-transformer0 + t:fusable-stream-transformer ... + c:fusable-stream-consumer + _1 ...) + #:with fused (generate-fused-operation + (syntax->list #'(list->cstream t1 t ... c)) + stx) + #'(thread _0 ... fused _1 ...)] + [((~datum thread) _0:non-fusable ... + p:fusable-stream-producer + ;; Must be 1 or more transformers here: + t:fusable-stream-transformer ...+ + _1 ...) + #:with fused (generate-fused-operation + (syntax->list #'(p t ... cstream->list)) + stx) + #'(thread _0 ... fused _1 ...)] + [((~datum thread) _0:non-fusable ... + f1:fusable-stream-transformer0 + f:fusable-stream-transformer ...+ + _1 ...) + #:with fused (generate-fused-operation + (syntax->list #'(list->cstream f1 f ... cstream->list)) + stx) + #'(thread _0 ... fused _1 ...)] + [_ this-syntax])) + +(begin-encourage-inline + + ;; Producers + + (define-inline (list->cstream-next done skip yield) + (λ (state) + (cond [(null? state) (done)] + [else (yield (car state) (cdr state))]))) + + (define-inline (list->cstream-prepare next) + (case-lambda + [(lst) (next lst)] + [rest (void)])) + + (define-inline (range->cstream-next done skip yield) + (λ (state) + (match-define (list l h s) state) + (cond [(< l h) + (yield l (cons (+ l s) (cdr state)))] + [else (done)]))) + + (define-inline (range->cstream-prepare next) + (case-lambda + [(h) (next (list 0 h 1))] + [(l h) (next (list l h 1))] + [(l h s) (next (list l h s))] + [rest (void)])) + + ;; Transformers + + (define-inline (map-cstream-next f next) + (λ (done skip yield) + (next done + skip + (λ (value state) + (yield (f value) state))))) + + (define-inline (filter-cstream-next f next) + (λ (done skip yield) + (next done + skip + (λ (value state) + (if (f value) + (yield value state) + (skip state)))))) + + ;; Consumers + + (define-inline (cstream-next->list next ctx src) + (λ (state) + (let loop ([state state]) + ((next (λ () null) + (λ (state) (loop state)) + (λ (value state) + (cons value (loop state)))) + state)))) + + (define-inline (foldr-cstream-next op init next ctx src) + (λ (state) + (let loop ([state state]) + ((next (λ () init) + (λ (state) (loop state)) + (λ (value state) + (op value (loop state)))) + state)))) + + (define-inline (foldl-cstream-next op init next ctx src) + (λ (state) + (let loop ([acc init] [state state]) + ((next (λ () acc) + (λ (state) (loop acc state)) + (λ (value state) + (loop (op value acc) state))) + state)))) + + (define-inline (car-cstream-next next ctx src) + (λ (state) + (let loop ([state state]) + ((next (λ () ((contract (-> pair? any) + (λ (v) v) + 'car-cstream-next ctx #f + src) '())) + (λ (state) (loop state)) + (λ (value state) + value)) + state)))) + + ) diff --git a/qi-lib/flow/core/impl.rkt b/qi-lib/flow/core/impl.rkt index 658778d9..8cfc523a 100644 --- a/qi-lib/flow/core/impl.rkt +++ b/qi-lib/flow/core/impl.rkt @@ -29,7 +29,8 @@ racket/list racket/format syntax/parse/define - (for-syntax racket/base)) + (for-syntax racket/base) + racket/performance-hint) (define-syntax-parse-rule (values->list body:expr ...+) (call-with-values (λ () body ...) list)) @@ -121,6 +122,8 @@ [(cons v vs) (append (values->list (f v)) (~map f vs))])) +;; Note: can probably get rid of implicit packing to args, and the +;; final apply values (define (map-values f . args) (apply values (~map f args))) diff --git a/qi-lib/flow/core/normalize.rkt b/qi-lib/flow/core/normalize.rkt new file mode 100644 index 00000000..bda0ba15 --- /dev/null +++ b/qi-lib/flow/core/normalize.rkt @@ -0,0 +1,64 @@ +#lang racket/base + +(provide normalize-rewrite) + +(require syntax/parse) + +;; 0. "Qi-normal form" +(define (normalize-rewrite stx) + (syntax-parse stx + ;; "deforestation" for values + ;; (~> (pass f) (>< g)) → (>< (if f g ⏚)) + [((~datum thread) _0 ... ((~datum pass) f) ((~datum amp) g) _1 ...) + #'(thread _0 ... (amp (if f g ground)) _1 ...)] + ;; merge amps in sequence + [((~datum thread) _0 ... ((~datum amp) f) ((~datum amp) g) _1 ...) + #'(thread _0 ... (amp (thread f g)) _1 ...)] + ;; merge pass filters in sequence + [((~datum thread) _0 ... ((~datum pass) f) ((~datum pass) g) _1 ...) + #'(thread _0 ... (pass (and f g)) _1 ...)] + ;; collapse deterministic conditionals + [((~datum if) (~datum #t) f g) #'f] + [((~datum if) (~datum #f) f g) #'g] + ;; trivial threading form + [((~datum thread) f) + #'f] + ;; associative laws for ~> + [((~datum thread) _0 ... ((~datum thread) f ...) _1 ...) ; note: greedy matching + #'(thread _0 ... f ... _1 ...)] + ;; left and right identity for ~> + [((~datum thread) _0 ... (~datum _) _1 ...) + #'(thread _0 ... _1 ...)] + ;; composition of identity flows is the identity flow + [((~datum thread) (~datum _) ...) + #'_] + ;; identity flows composed using a relay + [((~datum relay) (~datum _) ...) + #'_] + ;; amp and identity + [((~datum amp) (~datum _)) + #'_] + ;; trivial tee junction + [((~datum tee) f) + #'f] + ;; merge adjacent gens in a tee junction + [((~datum tee) _0 ... ((~datum gen) a ...) ((~datum gen) b ...) _1 ...) + #'(tee _0 ... (gen a ... b ...) _1 ...)] + ;; dead gen elimination + [((~datum thread) _0 ... ((~datum gen) a ...) ((~datum gen) b ...) _1 ...) + #'(thread _0 ... (gen b ...) _1 ...)] + ;; prism identities + ;; Note: (~> ... △ ▽ ...) can't be rewritten to `values` since that's + ;; only valid if the input is in fact a list, and is an error otherwise, + ;; and we can only know this at runtime. + [((~datum thread) _0 ... (~datum collect) (~datum sep) _1 ...) + #'(thread _0 ... _1 ...)] + ;; collapse `values` and `_` inside a threading form + [((~datum thread) _0 ... (~literal values) _1 ...) + #'(thread _0 ... _1 ...)] + [((~datum thread) _0 ... (~datum _) _1 ...) + #'(thread _0 ... _1 ...)] + [((~datum #%blanket-template) (hex (~datum __))) + #'hex] + ;; return syntax unchanged if there are no applicable normalizations + [_ stx])) diff --git a/qi-lib/flow/core/util.rkt b/qi-lib/flow/core/util.rkt new file mode 100644 index 00000000..2466c7e3 --- /dev/null +++ b/qi-lib/flow/core/util.rkt @@ -0,0 +1,41 @@ +#lang racket/base + +(provide find-and-map/qi + fix) + +(require racket/match + syntax/parse) + +(define (find-and-map f stx) + ;; f : syntax? -> (or/c syntax? #f) + (match stx + [(? syntax?) (let ([stx^ (f stx)]) + (or stx^ (datum->syntax stx + (find-and-map f (syntax-e stx)) + stx + stx)))] + [(cons a d) (cons (find-and-map f a) + (find-and-map f d))] + [_ stx])) + +(define (find-and-map/qi f stx) + ;; #%host-expression is a Racket macro defined by syntax-spec + ;; that resumes expansion of its sub-expression with an + ;; expander environment containing the original surface bindings + (find-and-map (syntax-parser [((~datum #%host-expression) e:expr) this-syntax] + [_ (f this-syntax)]) + stx)) + +;; Applies f repeatedly to the init-val terminating the loop if the +;; result of f is #f or the new syntax object is eq? to the previous +;; (possibly initial) one. +;; +;; Caveats: +;; * the syntax object is not inspected, only eq? is used +;; * comparison is performed only between consecutive steps (does not handle cyclic occurences) +(define ((fix f) init-val) + (let ([new-val (f init-val)]) + (if (or (not new-val) + (eq? new-val init-val)) + init-val + ((fix f) new-val)))) diff --git a/qi-lib/flow/extended/expander.rkt b/qi-lib/flow/extended/expander.rkt index 76ea3f1a..0c152f19 100644 --- a/qi-lib/flow/extended/expander.rkt +++ b/qi-lib/flow/extended/expander.rkt @@ -12,7 +12,7 @@ [sep △] [collect ▽]))) -(require syntax-spec +(require syntax-spec-v1 (for-syntax "../aux-syntax.rkt" "syntax.rkt" racket/base @@ -168,23 +168,37 @@ ;; by wrapping them with #%-prefixed forms, similar to Racket's ;; approach to a similiar case - "interposition points." These ;; new forms can then be treated as core forms in the compiler. + ;; + ;; Be careful with these tagging rules, though -- if they are too + ;; lax in their match criteria they may produce infinite code + ;; unless their output is matched prior to reaching the tagging rule. + ;; So core forms expected to be produced by these tagging rules + ;; should generally occur before the tagging rule + (#%blanket-template (arg:arg-stx ...)) (~> f:blanket-template-form #'(#%blanket-template f)) - (#%blanket-template (arg:arg-stx ...)) - + (#%fine-template (arg:arg-stx ...)) (~> f:fine-template-form #'(#%fine-template f)) - (#%fine-template (arg:arg-stx ...)) - - ;; The core rule must come before the tagging rule here since - ;; the former as a production of the latter would still match - ;; the latter (i.e. it is still a parenthesized expression), - ;; which would lead to infinite code generation. - (#%partial-application (arg:arg-stx ...)) + ;; When there is a partial application where a template hasn't + ;; explicitly been indicated, we rewrite it to an equivalent use + ;; of a blanket template. + ;; We use a blanket rather than fine template since in such cases, + ;; we cannot always infer the appropriate arity for a template + ;; (e.g. it may change under composition within the form), while a + ;; blanket template will accept any number of arguments (~> f:partial-application-form - #'(#%partial-application f)) + #:do [(define chirality (syntax-property this-syntax 'chirality))] + (if (and chirality (eq? chirality 'right)) + (datum->syntax this-syntax + (append (syntax->list this-syntax) + (list '__))) + (datum->syntax this-syntax + (let ([stx-list (syntax->list this-syntax)]) + (cons (car stx-list) + (cons '__ (cdr stx-list))))))) ;; literally indicated function identifier ;; ;; functions defined in the Qi binding space take precedence over diff --git a/qi-lib/flow/extended/forms.rkt b/qi-lib/flow/extended/forms.rkt index b324a0a7..a1080b8f 100644 --- a/qi-lib/flow/extended/forms.rkt +++ b/qi-lib/flow/extended/forms.rkt @@ -10,7 +10,6 @@ [effect ε]))) (require (for-syntax racket/base - (only-in racket/list make-list) "syntax.rkt" "../aux-syntax.rkt") syntax/parse/define diff --git a/qi-lib/flow/extended/syntax.rkt b/qi-lib/flow/extended/syntax.rkt index 20067ab2..a289c89e 100644 --- a/qi-lib/flow/extended/syntax.rkt +++ b/qi-lib/flow/extended/syntax.rkt @@ -30,6 +30,12 @@ onex:clause #:with parsed #'onex)) +(define-syntax-class pre-supplied-argument + (pattern + (~not + (~or (~datum _) + (~datum __))))) + (define (make-right-chiral stx) (syntax-property stx 'chirality 'right)) @@ -54,7 +60,7 @@ (define-syntax-class partial-application-form ;; "prarg" = "pre-supplied argument" (pattern - (natex prarg ...+))) + (natex prarg:pre-supplied-argument ...+))) (define-syntax-class any-stx (pattern _)) diff --git a/qi-lib/info.rkt b/qi-lib/info.rkt index 630025a2..a8b349bd 100644 --- a/qi-lib/info.rkt +++ b/qi-lib/info.rkt @@ -4,9 +4,8 @@ (define collection "qi") (define deps '("base" ("fancy-app" #:version "1.1") - ;; this git URL should be changed to a named package spec - ;; once syntax-spec is on the package index - "git://github.com/michaelballantyne/syntax-spec.git#main")) + "syntax-spec-v1" + "macro-debugger")) (define build-deps '()) (define clean '("compiled" "private/compiled")) (define pkg-authors '(countvajhula)) diff --git a/qi-sdk/profile/nonlocal/intrinsic.rkt b/qi-sdk/profile/nonlocal/intrinsic.rkt index 6607f184..b5c04ad1 100755 --- a/qi-sdk/profile/nonlocal/intrinsic.rkt +++ b/qi-sdk/profile/nonlocal/intrinsic.rkt @@ -48,6 +48,8 @@ #:when (member (bm-name spec) benchmarks-to-run)) (let ([name (bm-name spec)] [exerciser (bm-exerciser spec)] - [f (eval (read (open-input-string (bm-name spec))) namespace)] + [f (eval + ;; the first datum in the benchmark name needs to be a function name + (read (open-input-string (bm-name spec))) namespace)] [n-times (bm-times spec)]) (run-nonlocal-benchmark name exerciser f n-times))))) diff --git a/qi-sdk/profile/nonlocal/qi/main.rkt b/qi-sdk/profile/nonlocal/qi/main.rkt index c4dc012c..7d9f154a 100644 --- a/qi-sdk/profile/nonlocal/qi/main.rkt +++ b/qi-sdk/profile/nonlocal/qi/main.rkt @@ -7,8 +7,13 @@ pingala eratosthenes collatz + range-map-car filter-map + filter-map-foldr + filter-map-foldl + long-functional-pipeline filter-map-values + range-map-sum double-list double-values) @@ -54,8 +59,47 @@ cons)])) +;; (define-flow filter-map +;; (~> △ (>< (if odd? sqr ⏚)) ▽)) + (define-flow filter-map - (~> △ (>< (if odd? sqr ⏚)) ▽)) + (~>> (filter odd?) + (map sqr))) + +(define-flow filter-map-foldr + (~>> (filter odd?) + (map sqr) + (foldr + 0))) + +(define-flow filter-map-foldl + (~>> (filter odd?) + (map sqr) + (foldl + 0))) + +(define-flow range-map-car + (~>> (range 0) + (map sqr) + car)) + +(define-flow range-map-sum + ;; TODO: this should be written as (apply +) + ;; and that should be normalized to (foldr/l + 0) + ;; (depending on which of foldl/foldr is more performant) + (~>> (range 0) (map sqr) (foldr + 0))) + +(define-flow long-functional-pipeline + (~>> (range 0) + (filter odd?) + (map sqr) + values + (filter (λ (v) (< (remainder v 10) 5))) + (map (λ (v) (* 2 v))) + (foldl + 0))) + +;; (define filter-double +;; (map (☯ (when odd? +;; (-< _ _))) +;; (list 1 2 3 4 5))) (define-flow filter-map-values (>< (if odd? sqr ⏚))) diff --git a/qi-sdk/profile/nonlocal/racket/main.rkt b/qi-sdk/profile/nonlocal/racket/main.rkt index 4e80ae24..89769805 100644 --- a/qi-sdk/profile/nonlocal/racket/main.rkt +++ b/qi-sdk/profile/nonlocal/racket/main.rkt @@ -7,8 +7,13 @@ pingala eratosthenes collatz + range-map-car filter-map + filter-map-foldr + filter-map-foldl + long-functional-pipeline filter-map-values + range-map-sum double-list double-values) @@ -58,6 +63,28 @@ (define (filter-map lst) (map sqr (filter odd? lst))) +(define (filter-map-foldr lst) + (foldr + 0 (map sqr (filter odd? lst)))) + +(define (filter-map-foldl lst) + (foldl + 0 (map sqr (filter odd? lst)))) + +(define (range-map-car v) + (car (map sqr (range 0 v)))) + +(define (range-map-sum n) + (apply + (map sqr (range 0 n)))) + +(define (long-functional-pipeline v) + (foldl + + 0 + (map (λ (v) (* 2 v)) + (filter (λ (v) (< (remainder v 10) 5)) + (values + (map sqr + (filter odd? + (range 0 v)))))))) + (define (filter-map-values . vs) (apply values (map sqr (filter odd? vs)))) diff --git a/qi-sdk/profile/nonlocal/spec.rkt b/qi-sdk/profile/nonlocal/spec.rkt index addaa412..eb7b5388 100644 --- a/qi-sdk/profile/nonlocal/spec.rkt +++ b/qi-sdk/profile/nonlocal/spec.rkt @@ -9,6 +9,8 @@ #:transparent) (define specs + ;; the first datum in the benchmark name needs to be the name + ;; of the function that will be exercised (list (bm "conditionals" check-value 300000) @@ -18,9 +20,27 @@ (bm "root-mean-square" check-list 500000) + (bm "range-map-car" + check-value-large + 50000) (bm "filter-map" check-list 500000) + (bm "filter-map (large list)" + check-large-list + 50000) + (bm "filter-map-foldr" + check-large-list + 50000) + (bm "filter-map-foldl" + check-large-list + 50000) + (bm "long-functional-pipeline" + check-value-large + 5000) + (bm "range-map-sum" + check-value-large + 5000) (bm "filter-map-values" check-values 500000) @@ -37,7 +57,7 @@ check-value 10000) (bm "eratosthenes" - check-value-primes + check-value-medium-large 100) ;; See https://en.wikipedia.org/wiki/Collatz_conjecture (bm "collatz" diff --git a/qi-sdk/profile/regression.rkt b/qi-sdk/profile/regression.rkt index 0e1e072b..20ec8b6c 100644 --- a/qi-sdk/profile/regression.rkt +++ b/qi-sdk/profile/regression.rkt @@ -12,7 +12,7 @@ racket/pretty) (define LOWER-THRESHOLD 0.75) -(define HIGHER-THRESHOLD 1.5) +(define HIGHER-THRESHOLD 1.33) (define (parse-json-file filename) (call-with-input-file filename @@ -39,7 +39,9 @@ (define-flow calculate-ratio (~> (-< (hash-ref after _) - (hash-ref before _)) + (~> (hash-ref before _) + ;; avoid division by zero + (if (= 0) 1 _))) / (if (< low _ high) 1 diff --git a/qi-sdk/profile/util.rkt b/qi-sdk/profile/util.rkt index a751e212..27a0be0e 100644 --- a/qi-sdk/profile/util.rkt +++ b/qi-sdk/profile/util.rkt @@ -3,8 +3,11 @@ (provide average measure check-value - check-value-primes + check-value-medium-large + check-value-large + check-value-very-large check-list + check-large-list check-values check-two-values run-benchmark @@ -57,7 +60,11 @@ (set! i (remainder (add1 i) len)) (fn (vector-ref inputs i))))) -(define check-value-primes (curryr check-value #(100 200 300))) +(define check-value-medium-large (curryr check-value #(100 200 300))) + +(define check-value-large (curryr check-value #(1000))) + +(define check-value-very-large (curryr check-value #(100000))) ;; This uses the same list input each time. Not sure if that ;; may end up being cached at some level and thus obfuscate @@ -70,6 +77,12 @@ (for ([i how-many]) (fn vs)))) +(define (check-large-list fn how-many) + ;; call a function with a single list argument + (let ([vs (range 1000)]) + (for ([i how-many]) + (fn vs)))) + ;; This uses the same input values each time. See the note ;; above for check-list in this connection. (define (check-values fn how-many) diff --git a/qi-test/tests/compiler.rkt b/qi-test/tests/compiler.rkt new file mode 100644 index 00000000..99a400d6 --- /dev/null +++ b/qi-test/tests/compiler.rkt @@ -0,0 +1,21 @@ +#lang racket/base + +(provide tests) + +(require rackunit + rackunit/text-ui + (prefix-in semantics: "compiler/semantics.rkt") + (prefix-in rules: "compiler/rules.rkt") + (prefix-in util: "compiler/util.rkt")) + +(define tests + (test-suite + "compiler tests" + + semantics:tests + rules:tests + util:tests)) + +(module+ main + (void + (run-tests tests))) diff --git a/qi-test/tests/compiler/rules.rkt b/qi-test/tests/compiler/rules.rkt new file mode 100644 index 00000000..9f28e5ca --- /dev/null +++ b/qi-test/tests/compiler/rules.rkt @@ -0,0 +1,537 @@ +#lang racket/base + +(provide tests) + +(require (for-template qi/flow/core/compiler) + qi/flow/core/deforest + rackunit + rackunit/text-ui + (only-in math sqr) + racket/string + (only-in racket/list + range) + syntax/parse/define) + +(define-syntax-parse-rule (test-normalize msg a b ...+) + (begin + (check-equal? (syntax->datum + (normalize-pass a)) + (syntax->datum + (normalize-pass b)) + msg) + ...)) + +(define (deforested? exp) + (string-contains? (format "~a" exp) "cstream")) + + +(define tests + (test-suite + "Compiler rule tests" + + (test-suite + "deforestation" + ;; Note that these test deforestation in isolation + ;; without necessarily taking normalization (a preceding + ;; step in compilation) into account + + (test-suite + "general" + (let ([stx #'(thread + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-false (deforested? (syntax->datum + (deforest-rewrite + stx))) + "does not deforest single stream component in isolation")) + (let ([stx #'(thread + #%blanket-template + ((#%host-expression map) + (#%host-expression sqr) + __) + ((#%host-expression filter) + (#%host-expression odd?) + __))]) + (check-false (deforested? (syntax->datum + (deforest-rewrite + stx))) + "does not deforest map in the head position")) + ;; (~>> values (filter odd?) (map sqr) values) + (let ([stx #'(thread + values + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)) + (#%blanket-template + ((#%host-expression map) + (#%host-expression sqr) + __)) + values)]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "deforestation in arbitrary positions")) + (let ([stx #'(thread + values + (#%blanket-template + ((#%host-expression filter) + (#%host-expression string-upcase) + __)) + (#%blanket-template + ((#%host-expression foldl) + (#%host-expression string-append) + (#%host-expression "I") + __)) + values)]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "deforestation in arbitrary positions"))) + + (test-suite + "transformers" + (let ([stx #'(thread + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)) + (#%blanket-template + ((#%host-expression map) + (#%host-expression sqr) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "filter")) + (let ([stx #'(thread + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)) + (#%blanket-template + ((#%host-expression map) + (#%host-expression sqr) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "filter-map (two transformers)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression filter) + (#%host-expression odd?) + _)) + (#%fine-template + ((#%host-expression map) + (#%host-expression sqr) + _)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "fine-grained template forms"))) + + (test-suite + "producers" + (let ([stx #'(thread + (esc (#%host-expression range)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "range")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range _)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range _ _)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 _)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range _ 10)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range _ _ _)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range _ _ 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range _ 10 _)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range _ 10 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 _ _)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 _ 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 10 _)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range __)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 __)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range __ 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 10 __)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range __ 10 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 __ 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 10 1 __)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 10 __ 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range 0 __ 10 1)")) + (let ([stx #'(thread + (#%fine-template + ((#%host-expression range) + _ + _)) + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "(range __ 0 10 1)"))) + + (test-suite + "consumers" + (let ([stx #'(thread + (#%blanket-template + ((#%host-expression filter) + (#%host-expression odd?) + __)) + (esc (#%host-expression car)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "car")) + (let ([stx #'(thread + (#%blanket-template + ((#%host-expression filter) + (#%host-expression string-upcase) + __)) + (#%blanket-template + ((#%host-expression foldl) + (#%host-expression string-append) + (#%host-expression "I") + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "foldl")) + (let ([stx #'(thread + (#%blanket-template + ((#%host-expression filter) + (#%host-expression string-upcase) + __)) + (#%blanket-template + ((#%host-expression foldr) + (#%host-expression string-append) + (#%host-expression "I") + __)))]) + (check-true (deforested? (syntax->datum + (deforest-rewrite + stx))) + "foldr")))) + + (test-suite + "normalization" + (test-normalize "pass-amp deforestation" + #'(thread + (pass f) + (amp g)) + #'(amp (if f g ground))) + (test-normalize "merge amps in sequence" + #'(thread (amp f) (amp g)) + #'(amp (thread f g))) + (test-normalize "merge pass filters in sequence" + #'(thread (pass f) (pass g)) + #'(pass (and f g))) + (test-normalize "collapse deterministic conditionals" + #'(if #t f g) + #'f) + (test-normalize "collapse deterministic conditionals" + #'(if #f f g) + #'g) + (test-normalize "trivial threading is collapsed" + #'(thread f) + #'f) + (test-normalize "associative laws for ~>" + #'(thread f (thread g h) i) + #'(thread f g (thread h i)) + #'(thread (thread f g) h i) + #'(thread f g h i)) + (test-normalize "left and right identity for ~>" + #'(thread f _) + #'(thread _ f) + #'f) + + (test-normalize "line composition of identity flows" + #'(thread _ _ _) + #'(thread _ _) + #'(thread _) + #'_) + (test-normalize "relay composition of identity flows" + #'(relay _ _ _) + #'(relay _ _) + #'(relay _) + #'_) + (test-normalize "amp under identity" + #'(amp _) + #'_) + (test-normalize "trivial tee junction" + #'(tee f) + #'f) + (test-normalize "merge adjacent gens in a tee junction" + #'(tee (gen a b) (gen c d)) + #'(tee (gen a b c d))) + (test-normalize "remove dead gen in a line" + #'(thread (gen a b) (gen c d)) + #'(thread (gen c d))) + (test-normalize "prism identities" + #'(thread collect sep) + #'_) + (test-normalize "redundant blanket template" + #'(#%blanket-template (f __)) + #'f) + (test-normalize "values is collapsed inside ~>" + #'(thread values f values) + #'(thread f)) + (test-normalize "_ is collapsed inside ~>" + #'(thread _ f _) + #'(thread f)) + (test-normalize "consecutive amps are combined" + #'(thread (amp f) (amp g)) + #'(thread (amp (thread f g))))) + + (test-suite + "compilation sequences" + null))) + +(module+ main + (void (run-tests tests))) diff --git a/qi-test/tests/compiler/semantics.rkt b/qi-test/tests/compiler/semantics.rkt new file mode 100644 index 00000000..1663bae6 --- /dev/null +++ b/qi-test/tests/compiler/semantics.rkt @@ -0,0 +1,150 @@ +#lang racket/base + +(provide tests) + +(require qi + rackunit + rackunit/text-ui + (only-in math sqr) + (only-in racket/list range) + racket/function) + +(define tests + (test-suite + "Compiler preserves semantics" + + (test-suite + "deforestation" + (check-equal? ((☯ (~>> (filter odd?) (map sqr))) + (list 1 2 3 4 5)) + (list 1 9 25)) + (check-exn exn:fail? + (thunk + ((☯ (~> (map sqr) (map sqr))) + (list 1 2 3 4 5))) + "(map) doforestation should only be done for right threading") + (check-exn exn:fail? + (thunk + ((☯ (~> (filter odd?) (filter odd?))) + (list 1 2 3 4 5))) + "(filter) doforestation should only be done for right threading") + (check-exn exn:fail? + (thunk + ((☯ (~>> (filter odd?) (~> (foldr + 0)))) + (list 1 2 3 4 5))) + "(foldr) doforestation should only be done for right threading") + (check-equal? ((☯ (~>> values (filter odd?) (map sqr) values)) + (list 1 2 3 4 5)) + (list 1 9 25) + "optimizes subexpressions") + (check-equal? ((☯ (~>> (filter odd?) (map sqr) (foldr + 0))) + (list 1 2 3 4 5)) + 35) + (check-equal? ((☯ (~>> (filter odd?) (map sqr) (foldl + 0))) + (list 1 2 3 4 5)) + 35) + (check-equal? ((☯ (~>> (map string-upcase) (foldr string-append "I"))) + (list "a" "b" "c")) + "ABCI") + (check-equal? ((☯ (~>> (map string-upcase) (foldl string-append "I"))) + (list "a" "b" "c")) + "CBAI") + (check-equal? ((☯ (~>> (range 10) (map sqr) car))) + 0) + (test-suite + "range (stream producer)" + ;; Semantic tests of the range producer that cover all combinations: + (test-equal? "~>>range [1-3] (10)" + (~>> (10) range (filter odd?) (map sqr)) + '(1 9 25 49 81)) + (test-equal? "~>range [1-3] (10)" + (~> (10) range (~>> (filter odd?) (map sqr))) + '(1 9 25 49 81)) + (test-equal? "~>> range [1-3] (5 10)" + (~>> (5 10) range (filter odd?) (map sqr)) + '(25 49 81)) + (test-equal? "~> range [1-3] (5 10)" + (~> (5 10) range (~>> (filter odd?) (map sqr))) + '(25 49 81)) + (test-equal? "~>> range [1-3] (5 10 3)" + (~>> (5 10 3) range (filter odd?) (map sqr)) + '(25)) + (test-equal? "~> range [1-3] (5 10 3)" + (~> (5 10 3) range (~>> (filter odd?) (map sqr))) + '(25)) + + (test-equal? "~>> (range 10) [0-2] ()" + (~>> () (range 10) (filter odd?) (map sqr)) + '(1 9 25 49 81)) + (test-equal? "~> (range 10) [0-2] ()" + (~> () (range 10) (~>> (filter odd?) (map sqr))) + '(1 9 25 49 81)) + (test-equal? "~>> (range 5) [0-2] (10)" + (~>> (10) (range 5) (filter odd?) (map sqr)) + '(25 49 81)) + (test-equal? "~> (range 10) [0-2] (5)" + (~> (5) (range 10) (~>> (filter odd?) (map sqr))) + '(25 49 81)) + (test-equal? "~>> (range 3) [0-2] (5 10)" + (~>> (3) (range 5 10) (filter odd?) (map sqr)) + '(25)) + (test-equal? "~> (range 5) [0-2] (10 3)" + (~> (5) (range 10 3) (~>> (filter odd?) (map sqr))) + '(25)) + + (test-equal? "~>> (range 5 10) [0-1] ()" + (~>> () (range 5 10) (filter odd?) (map sqr)) + '(25 49 81)) + (test-equal? "~> (range 5 10) [0-1] ()" + (~> () (range 5 10) (~>> (filter odd?) (map sqr))) + '(25 49 81)) + (test-equal? "~>> (range 5 10) [0-1] (3)" + (~>> (3) (range 5 10) (filter odd?) (map sqr)) + '(25)) + (test-equal? "~> (range 10 3) [0-1] (5)" + (~> (5) (range 10 3) (~>> (filter odd?) (map sqr))) + '(25)) + + (test-equal? "~>> (range 5 10 3) [0] ()" + (~>> () (range 5 10 3) (filter odd?) (map sqr)) + '(25)) + + (test-equal? "~>> (range _) [1] (10)" + (~>> (10) (range _) (filter odd?) (map sqr)) + '(1 9 25 49 81)) + (test-equal? "~>> (range _ _) [2] (5 10)" + (~>> (5 10) (range _ _) (filter odd?) (map sqr)) + '(25 49 81)) + (test-equal? "~>> (range _ _ _) [3] (5 10 3)" + (~>> (5 10 3) (range _ _ _) (filter odd?) (map sqr)) + '(25)) + + (test-equal? "~>> (range 5 _) [1] (10)" + (~>> (10) (range 5 _) (filter odd?) (map sqr)) + '(25 49 81)) + (test-equal? "~>> (range _ 10) [1] (5)" + (~>> (5) (range _ 10) (filter odd?) (map sqr)) + '(25 49 81)) + + (test-equal? "~>> (range 5 _ _) [2] (10 3)" + (~>> (10 3) (range 5 _ _) (filter odd?) (map sqr)) + '(25)) + (test-equal? "~>> (range _ 10 _) [2] (5 3)" + (~>> (5 3) (range _ 10 _) (filter odd?) (map sqr)) + '(25)) + (test-equal? "~>> (range _ _ 3) [2] (5 10)" + (~>> (5 10) (range _ _ 3) (filter odd?) (map sqr)) + '(25)) + + (test-equal? "~>> (range 5 10 _) [1] (3)" + (~>> (3) (range 5 10 _) (filter odd?) (map sqr)) + '(25)) + (test-equal? "~>> (range 5 _ 3) [1] (10)" + (~>> (10) (range 5 _ 3) (filter odd?) (map sqr)) + '(25)) + (test-equal? "~>> (range _ 10 3) [1] (5)" + (~>> (5) (range _ 10 3) (filter odd?) (map sqr)) + '(25)))))) + +(module+ main + (void (run-tests tests))) diff --git a/qi-test/tests/compiler/util.rkt b/qi-test/tests/compiler/util.rkt new file mode 100644 index 00000000..7977483c --- /dev/null +++ b/qi-test/tests/compiler/util.rkt @@ -0,0 +1,24 @@ +#lang racket/base + +(provide tests) + +(require qi/flow/core/util + rackunit + rackunit/text-ui + (only-in racket/function + curryr)) + +(define tests + (test-suite + "Compiler utilities tests" + + (test-suite + "fixed point" + (check-equal? ((fix abs) -1) 1) + (check-equal? ((fix abs) -1) 1) + (let ([integer-div2 (compose floor (curryr / 2))]) + (check-equal? ((fix integer-div2) 10) + 0))))) + +(module+ main + (void (run-tests tests))) diff --git a/qi-test/tests/flow.rkt b/qi-test/tests/flow.rkt index 9c94ba3e..e2f16142 100644 --- a/qi-test/tests/flow.rkt +++ b/qi-test/tests/flow.rkt @@ -354,6 +354,22 @@ (check-equal? ((☯ (~> (as v) (+ v))) 3) 3 "binds a single value") + (check-equal? ((☯ (~> (-< (as v) + _) (+ 3 _ v))) 3) + 9 + "reference in a fine template") + (check-equal? ((☯ (~> (-< (as v) + _) (+ 3 v))) 3) + 9 + "reference in a left-chiral partial application") + (check-equal? ((☯ (~>> (-< (as v) + _) (+ 3 v))) 3) + 9 + "reference in a right-chiral partial application") + (check-equal? ((☯ (~> (-< (as v) + _) (+ 3 __ v))) 3) + 9 + "reference in a blanket template") (check-false ((☯ (~> (as v) live?)) 3) "binding does not propagate the value") (check-equal? ((☯ (~> (as v w) (+ v w))) 3 4) @@ -420,6 +436,10 @@ "routing forms" (test-suite "~>" + (test-equal? "basic threading" + ((☯ (~> sqr add1)) + 3) + 10) (check-equal? ((☯ (~> add1 (* 2) number->string @@ -449,6 +469,10 @@ "p" "q") "pabqab" "threading without template") + (check-equal? ((☯ (~> (sort 3 1 2 #:key sqr))) + <) + (list 1 4 9) + "pre-supplied keyword arguments with left chirality") (check-equal? ((☯ (thread add1 (* 2) number->string @@ -482,10 +506,10 @@ "p" "q") "abpq" "right-threading without template") - (check-equal? ((☯ (~>> △ (sort < #:key identity))) + (check-equal? ((☯ (~>> △ (sort < #:key sqr))) (list 2 1 3)) - (list 1 2 3) - "right-threading with keyword arg") + (list 1 4 9) + "pre-supplied keyword arguments with right chirality") ;; TODO: propagate threading side to nested clauses ;; (check-equal? (on ("p" "q") ;; (~>> (>< (string-append "a" "b")) @@ -685,9 +709,21 @@ "abc") (check-equal? ((☯ (string-append __ "c")) "a" "b") - "abc")) + "abc") + (check-equal? ((☯ (sort __ 1 2 #:key sqr)) + < 3) + (list 1 4 9) + "keyword arguments in a left chiral blanket template") + (check-equal? ((☯ (sort < 3 #:key sqr __)) + 1 2) + (list 1 4 9) + "keyword arguments in a right chiral blanket template") + (check-equal? ((☯ (sort < __ #:key sqr)) + 3 1 2) + (list 1 4 9) + "keyword arguments in a vindaloo blanket template")) (test-suite - "template with single argument" + "fine template with single argument" (check-false ((☯ (apply > _)) (list 1 2 3))) (check-true ((☯ (apply > _)) @@ -706,13 +742,21 @@ (check-equal? ((☯ (foldl string-append "" _)) (list "a" "b" "c")) "cba" - "foldl in predicate")) + "foldl in predicate") + (check-equal? ((☯ (sort < 3 _ 2 #:key sqr)) + 1) + (list 1 4 9) + "keyword arguments in a fine template")) (test-suite - "template with multiple arguments" + "fine template with multiple arguments" (check-true ((☯ (< 1 _ 5 _ 10)) 3 7) "template with multiple arguments") (check-false ((☯ (< 1 _ 5 _ 10)) 3 5) - "template with multiple arguments")) + "template with multiple arguments") + (check-equal? ((☯ (sort < _ _ 2 #:key sqr)) + 3 1) + (list 1 4 9) + "keyword arguments in a fine template")) (test-suite "templating behavior is contained to intentional template syntax" (check-exn exn:fail:syntax? @@ -1483,7 +1527,34 @@ (check-equal? ((☯ (~> (pass positive?) +)) 1 -3 5) 6 - "runtime arity changes in threading form")))) + "runtime arity changes in threading form")) + + (test-suite + "nonlocal semantics" + ;; these are collected from counterexamples to candidate equivalences + ;; that turned up during code review. They ensure that some tempting + ;; "equivalences" that are not really equivalences are formally checked + (test-suite + "counterexamples" + (let () + (define-flow g (-< add1 sub1)) + (define-flow f positive?) + (define (f* x y) (= (sub1 x) (add1 y))) + (define (amp-pass g f) (☯ (~> (>< g) (pass f) ▽))) + (define (amp-if g f) (☯ (~> (>< (~> g (if f _ ground))) ▽))) + (check-equal? (apply (amp-pass g f) (range -3 4)) + (list 1 2 3 1 4 2)) + (check-exn exn:fail? + (thunk (apply (amp-if g f) (range -3 4)))) + (check-exn exn:fail? + (thunk (apply (amp-pass g f*) (range -3 4)))) + (check-equal? (apply (amp-if g f*) (range -3 4)) + (list -2 -4 -1 -3 0 -2 1 -1 2 0 3 1 4 2))) + (let () + (check-equal? ((☯ (~> (>< string->number) (pass _))) "a" "2" "c") + 2) + (check-equal? ((☯ (~> (>< (if _ string->number ground)) ▽)) "a" "2" "c") + (list #f 2 #f))))))) (module+ main (void (run-tests tests))) diff --git a/qi-test/tests/qi.rkt b/qi-test/tests/qi.rkt index c3f67523..26b9c36a 100644 --- a/qi-test/tests/qi.rkt +++ b/qi-test/tests/qi.rkt @@ -8,7 +8,8 @@ (prefix-in threading: "threading.rkt") (prefix-in definitions: "definitions.rkt") (prefix-in macro: "macro.rkt") - (prefix-in util: "util.rkt")) + (prefix-in util: "util.rkt") + (prefix-in compiler: "compiler.rkt")) (define tests (test-suite @@ -20,8 +21,9 @@ threading:tests definitions:tests macro:tests - util:tests)) + util:tests + compiler:tests)) -(module+ test +(module+ main (void (run-tests tests)))