#lang scribble/lp2 @(require "../lib/doc.rkt") @doc-lib-setup @title[#:style manual-doc-style]{Graph implementation} This module provides (a simplified form of) recursive algebraic data structures, with the ability to handle the structure as a collection of nodes, and process them all in a way similar to what @tc[map] provides. Traditionally, immutable data structures can't form cycles, but can easily be traversed to reach all nodes. Conversely, iterating over a cyclic data structure (created via lazy evaluation or thunks) is difficult if at all possible. More formally, this module offers fold operations on heterogeneous, richly typed graphs. @(table-of-contents) @section{Notes on complex transform result types} We wish at one point to support complex result types for the transforms, instead of only allowing a single node type. We have to impose a constraint: do not have a cycle inside the transform's result that doesn't go through a node, since we break cycles by replacing nodes with a promise. The safest way to satisfy that constraint is to enforce the absence of loops at the type level. We would then inline the called transform's results, breaking the cycles by replacing nodes with a thunk that returns the desired node. That thunk will be wrapped into a Promise that calls it, so that typed/racket's occurrence typing is happy, but we don't rely on the memoization semantics. @subsection{Compile-time handling of complex transform result types} During macro-expansion, we generate procedures that process nodes found in transforms' results, by inlining the results of called transforms. If we find a @tc[transform/link-request] type in some place we don't know how to rewrite (like a function type, for example), we throw an error. Similarly, if we encounter a cycle in the type that does not go through a node type, we throw an error. These procedures will help generate code to make a facade node from the incomplete one. When inlining results from called transforms, they will request other incomplete nodes from the database. @subsection{Two-step graph creation} Writing a graph-generation macro that allows complex return types for transforms seems difficult, and it would be easier to write a simple graph-generation macro, that only accepts transforms with return a single node type. We could build on top of that a more flexible macro, that would first generate a graph where each transform's result is wrapped in an ad-hoc single-field node. Then, we would automatically generate a second graph transformation that produces the desired nodes from that graph. Example: transform @tc[t1] takes a list of numbers as input, and produces a list of either calls to transform @tc[t2] or nodes @tc[ni] as output. The @tc[t2] transform generates a pair of nodes @tc[(ni [x Number])] and @tc[(nj [y String])]. The user would describe the graph like this: @chunk[ (make-graph ([root (Listof (U ni (Pairof ni nj)))] [ni [x Number]] [nj [y String]]) [(t1 [ln : (Listof Number)] : (Listof (U ni t2)) (map (λ (x) (if (even? x) (t2 x) (ni x))) ln))] [(t2 [n : Number] : (Pairof ni nj) (cons (ni n) (nj (format "~a" n))))])] In the above, the result type of @tc[t1] has to be @tc[(Listof (U ni t2))] instead of @tc[(Listof (U ni (Pairof ni nj)))], because otherwise we can't easily automatically infer that @tc[(Pairof ni nj)] was actually @tc[t2], without looking at the body of the transform. In a more advanced version, we could substitute every @tc[result-type] found in another transform's @tc[result-type] by @tc[(U result-type transform/link-request)], however that would likely produce spurious cycles that do not go through a node, so it's probably best to make things explicit, and let the user write @tc[U]. @chunk[ (graph ([r-t1 [result (Listof (U ni t2))]] [r-t2 [result (Pairof ni nj)]]) [(t1 [ln : (Listof Number)] : r-t1 (r-t1 (map (λ (x) (if (even? x) (t2 x) (ni x))) ln)))] [(t2 [n : Number] : r-t2 (r-t2 (cons (ni n) (nj (format "~a" n)))))])] Then use this graph transform: @chunk[ (make-graph ([root [result (Listof (Pairof ni nj))]] [ni [x Number]] [nj [y String]]) [(r-t1→root [t1 : r-t1]) : root (root (map (λ (v) (match v [(? list?) (r-t2-result v)] [(ni _) v])) (r-t1-result t1)))])] @subsection{Many to one transforms} This example covers one to many transforms. What about many to one transforms? The macro we are building allows generating graphs, but does not care about the input. In the case were transforming a graph of @tc[house]s, @tc[street]s and a @tc[city], and we want to condense all the @tc[house]s on one side of each @tc[street] to a @tc[suburb], we would write a transform @tc[t1] for@tc[street] which passes the whole list of @tc[house]s to a transform @tc[t2]. The @tc[t2] transform would create a @tc[suburb] from those, without calling a transform for each @tc[house]. @subsection{Implicit rule names} In order to allow implicit rule names, when there's only one rule with the desired result node, we can use the node's name as the transform name. We should think about naming conflicts: when calling @tc[n], should it insert a link request for the transform, or should it create an incomplete node? @subsection[#:tag "graph|complex-transforms-return-type-conclusion"]{Conclusion} With this approach, we can write the graph creation macro with the guaranty that the result of a transform always is exactly one node type. More complex transform result types can be decomposed into to two passes. A downside is that we can't inspect the result of a call to another transform, since it's not actually calling it, and we're only getting an opaque link request back. We couldn't call the other transform anyway, because it could half of the time return a value immediately, and half of the time call us back (with the same arguments), causing an infinite loop. For that, we could declare some #:helper transforms, that get called immediately (but if they run into an infinite loop it's not our fault). @section{Comparison with @racket[make-placeholder] and @racket[make-reader-graph]} Comparison of this approach with @tc[make-placeholder] and @tc[make-reader-graph]: @itemlist[ @item{They don't guarantee at compile-time that you'll fill in all placeholders. We could use @racket[make-placeholder] and @racket[make-reader-graph] wrapped inside a macro that makes sure that all placeholders are filled (same approach as we have).} @item{I don't think you can iterate over all the nodes or over the nodes of a specific type, and @racket[make-placeholder] isn't typed (yet) anyway I guess).}] @section{Constructor} Here is an overview of the architecture of the graph constructor: @itemlist[ @item{We first save the parameter types in the old context, because we later shadow the node names, and the parameters should refer to the old types. Depending on how we write the rest, this might not be necessary though, since it is possible we need to write @racket[(og node)] to refer to nodes types from the old graph @racket[og].} @item{We then define the node names as constructors for incomplete types — which means that they can contain link requests for the results other transforms} @item{We define data structures representing link requests. Each link request encapsulates a thunk that performs the transform's work when called, as well as the name of the transform and its arguments, used to detect when we have two identical link requests (which can be due to cycles in the resulting graph, for example).} @item{We then define the transforms as procedures that return a link request.}] @chunk[ (define-syntax/parse (make-graph-constructor ([node (field:id field-type:expr) ...] ...) [transform:id (param:id param-type:expr) ... (~literal :) result-type:id body ...] ...) #`(let () (let () make-graph-database)))] @chunk[ (define make-g (make-graph-constructor ([ma (fav String) (faa ma) (fab mb)] [mb (fbv String) (fba ma)]) [transform-a (s String) : ma (ma s (transform-a s) (transform-b "b"))] [transform-b (s String) : mb (mb s (transform-a s))])) (make-g "root-arg")] @subsection{Saving parameter types in old context} @chunk[ (define/with-syntax ((param-type/old ...) ...) (stx-map (λ (ps) (with-syntax ([(t sps ...) ps]) (format-temp-ids "~a/~a/memorized-type" #'t #'(sps ...)))) #'((transform param ...) ...)))] @chunk[ (define-type param-type/old param-type) ... ...] @subsection{Incomplete nodes} When a transform returns an object, it is incomplete (it potentially contains link requests instead of actual references to the nodes). We prepare some template variables. The first is the name of the tagged variant representing an incomplete node: @chunk[ (define/with-syntax (node/incomplete ...) (format-temp-ids "~a/incomplete" #'(node ...)))] Then, we build a reverse map, which from a node type obtains all the transforms returning that node type. More specifically, we are interested in the transform's link request type. @chunk[ (define/with-syntax ((node/link-request-types ...) ...) (for/list ([x (in-syntax #'(node ...))]) (multiassoc-syntax x #'([result-type . transform/link-request] ...))))] The third template variable we define maps transforms to the incomplete type for their returned node. @chunk[ (define/with-syntax (transform/result-node/incomplete ...) (for/list ([x (in-syntax #'(result-type ...))]) (assoc-syntax x #'([node . node/incomplete] ...))))] @CHUNK[ (define-type node (U node/link-request-types ...) #:omit-define-syntaxes) ... (define-tagged node/incomplete [field field-type] ...) ... (define-multi-id node #:match-expander-id node/incomplete #:call-id node/incomplete) ...] @subsection{Link requests for nodes} When a transform wants to produce a reference to the result of another transform of some data, it generates instead a link request, which encapsulates the desired transform and arguments, without actually performing it. @chunk[ (define/with-syntax (transform/link-request ...) (format-temp-ids "~a/link-request" #'(transform ...)))] Due to an issue with @tc[typed/racket] (@tc[struct]s aren't properly declared inside a @tc[let]), we need to pre-declare the @tc[transform/link-request] @tc[struct]. Since the call to make-graph could itself be inside a @tc[let], we need to pre-declare it in this file, instead of declaring it at the top of the macro. We're making the structure transparent for easier debugging, but at the time of writing this, it needs not be. @chunk[ (struct (TKey) transform/link-request-pre-declared ([key : TKey]) #:transparent)] @chunk[ (define-type transform/link-request (transform/link-request-pre-declared (List 'transform param-type/old ...))) ...] @subsection{Transforms} @chunk[ (define/with-syntax (transform/link-request→incomplete ...) (format-temp-ids "~a/link-request→incomplete" #'(transform ...)))] @chunk[ (begin (: transform/link-request→incomplete (→ param-type/old ... transform/result-node/incomplete)) (define (transform/link-request→incomplete param ...) body ...)) ...] @chunk[ (begin (: transform (→ param-type/old ... transform/link-request)) (define (transform param ...) ((inst transform/link-request-pre-declared (List 'transform param-type/old ...)) (list 'transform param ...)))) ...] @section{Queue} @chunk[ (define/with-syntax (root-transform . _) #'(transform ...)) (define/with-syntax ((root-transform/param-type ...) . _) #'((param-type ...) ...)) (define/with-syntax ((root-transform/param ...) . _) #'((param ...) ...)) (define/with-syntax (transform/transformed ...) (format-temp-ids "~a/transformed" #'(transform ...))) (define/with-syntax (root-transform/link-request . _) #'(transform/link-request ...)) (define/with-syntax recursive-call #'(process-queue pending-requests processed-requests transform/transformed ...)) (define/with-syntax (node/extract-link-requests ...) (format-temp-ids "~a/extract-link-requests" #'(node ...))) ] To build the graph database, we take the parameters for the root transform, and return lists incomplete nodes (one for each transform). The parameters for the root transform, addition to the transform's name, form the first link request. To fulfil this link request and the ones found later, we call the desired transform which returns an incomplete node. We extract any link requests found in that incomplete node, and queue them. The incomplete node itself is added to the appropriate list, to be returned once the queue has been fully processed. @CHUNK[ (: make-graph-database (→ root-transform/param-type ... (List (Listof transform/result-node/incomplete) ...)))] The @tc[make-graph-database] function consists mainly in the process-queue function, which takes a queue for each transform, and a list of already-processed incomplete nodes for each transform, and returns these lists, once all queues are empty. @CHUNK[ (define (make-graph-database root-transform/param ...) (: process-queue (→ (Setof (U transform/link-request ...)) (Setof (U transform/link-request ...)) (Listof transform/result-node/incomplete) ... (List (Listof transform/result-node/incomplete) ...))) (define (process-queue pending-requests processed-requests transform/transformed ...) ;; TODO: Can probably be moved out. ) )] The @tc[process-queue] function is initially called with empty lists for all queues and all result lists, except for the root transform's queue, which contains the initial link request. @CHUNK[ (process-queue (set (root-transform root-transform/param ...)) (set) (begin 'transform/transformed '()) ...)] Process-queue is a standard queue handler using sets. @CHUNK[ (if (set-empty? pending-requests) (list transform/transformed ...) (let* ([request (set-first pending-requests)] [pending-requests (set-rest pending-requests)] [processed-requests (set-add processed-requests request)] [tag (car (transform/link-request-pre-declared-key request))]) ))] To process each link request, we first match on its type, and once we found it, we call the result thunk, extract any link requests contained within, and add those to the queue. @CHUNK[ (cond [(eq? tag 'transform) (let* ([transformed : transform/result-node/incomplete (apply transform/link-request→incomplete (cdr (transform/link-request-pre-declared-key request)))] [transform/transformed (cons transformed transform/transformed)] [extracted (list->set (transform/result-node/extract-link-requests transformed))] [pending-requests (set-union pending-requests (set-subtract extracted processed-requests))]) recursive-call)] ...)] @subsection[#:tag "graph|TODO3"]{TODO} We need to traverse the @tc[transformed] node (which is an incomplete node), and find the link requests within. These link requests will be added to the corresponding @tc[pending-requests] queue. Below is the body of a for-syntax function that transforms a type with link-requests into the @tc[match] patterns that will be used at run-time to traverse the incomplete node. In most cases, there is only one pattern, but the @tc[U] requires one for each possibility. When we encounter a link request, we prepend it to the corresponding queue. For the type @tc[(List Number n/link-request)], the function will look like this: @chunk[ (match transformed [(list a b) (match a [a2 a2]) (match b [(and t (transform/link-request-pre-declared (cons 'transform1 _))) (set! pending-requests (cons t pending-requests))])])] @subsubsection{Match clauses} We first transform the type into the different match clauses. For that, we define the @tc[fold-type-clauses] function, which takes the identifier to destructure at run-time, and its type. The function returns a list of clauses. @chunk[ (define (fold-type-clauses val t) (syntax-parse t ))] When a link request is found in the type, we produce the corresponding match clause, which body prepends the request to the queue of pending requests. For now we use @racket[set!] to prepend the request, but it would be cleaner to use recursion. We wouldn't even need to flatten the pending-requests list, because it could be a tree instead of a flat list, since we only need to add to it and later pop elements. TODO: we currently ignore potential hiding of identifiers due to type variables bound by Rec, for example. This is a case where having a fold-type function provided by the type-expander library would be interesting. @CHUNK[ [x:id #:when (ormap (curry free-identifier=? #'x) (syntax->list #'(node/incomplete ...))) (define/with-syntax (this-field-type ...) (assoc-syntax #'x #'((node/incomplete field-type ...) ...))) (define/with-syntax (tmp ...) (generate-temporaries #'(this-field-type ...))) #`([(x tmp ...) (append #,@(stx-map fold-type #'(tmp ...) #'(this-field-type ...)))])]] @CHUNK[ [x:id #:when (ormap (curry free-identifier=? #'x) (syntax->list #'(node ...))) #`([(and t (transform/link-request-pre-declared (cons 'transform _))) (cons (ann t transform/link-request) '())] ...)]] We handle fixed-length lists by calling @tc[fold-type] on each element type. @CHUNK[ [((~literal List) a ...) (define/with-syntax (tmp ...) (generate-temporaries #'(a ...))) #`([(list tmp ...) (append #,@(stx-map fold-type #'(tmp ...) #'(a ...)))])]] We iterate variable-length lists at run-time. @CHUNK[ [((~literal Listof) a) #`([(list tmp (... ...)) (append-map (λ (tmp1) #,(fold-type #'tmp1 #'a)) tmp)])]] Pairs and vectors are handled similarly: @CHUNK[ [((~literal Pairof) a b) #`([(cons tmpa tmpb) (list #,(fold-type #'tmpa #'a) #,(fold-type #'tmpb #'b))])]] @CHUNK[ [((~literal Vectorof) a) #'([(vector tmp (... ...)) (append-map (λ (tmp1) #,(fold-type #'tmp1 #'a)) tmp)])]] For unions, we return several clauses, obtained via a recursive call to @tc[fold-type-clauses]. @CHUNK[ [((~literal U) a ...) #`(#,@(stx-map fold-type-clauses val #'(a ...)))]] We handle other cases by leaving them as-is, but we still check that they don't contain a reference to a node type, because we would otherwise leave the link-request there. And the fourth maps transforms to the link-requests extraction procedure for their returned node. @chunk[ (define/with-syntax (transform/result-node/extract-link-requests ...) (for/list ([x (in-syntax #'(result-type ...))]) (assoc-syntax x #'([node . node/extract-link-requests] ...))))] The last case is when we encounter an unknown type. We assume that it does not contain any link-requests and therefore return an empty list. @CHUNK[ [x:id #`([_ '()])]] @subsubsection{Folding the type: extracting link requests} The for-syntax function @tc[fold-type] generates code that uses @tc[match] to extract the @tc[link-request]s from an incomplete node (or part of it) with type @tc[t]. The match clauses are those returned by @tc[fold-type-clauses] defined above. @CHUNK[ (define (fold-type val t) #`(begin (match #,val #,@(fold-type-clauses val t))))] @subsubsection{Fold function for each incomplete node} For each node type, we wish to declare a function that extracts link requests from the incomplete type. We should work on the expanded type. @chunk[ (define-template-metafunction (fold-type-tmpl stx) (syntax-case stx () [(_ val t) (fold-type #'val #'t)]))] @CHUNK[ #,@(for/list ([name (in-syntax #'(node/extract-link-requests ...))] [val-type (in-syntax #'(node/incomplete ...))] [field-types (in-syntax #'((field-type ...) ...))]) #`(define (#,name [val : #,val-type]) : (Listof (U transform/link-request ...)) #,(fold-type #'val val-type)))] @subsubsection[#:tag "graph|TODO1"]{TODO} Later, we will replace link requests with thunks returning the desired node, wrapped in a promise in order to please occurrence typing. Below is the body of the for-syntax function that transforms a type with link-requests into a type with actual nodes. It's probably not useful, because we obtain the same result with scopes. @CHUNK[ [x:id #:when (ormap (curry free-identifier=? #'x) (syntax->list #'(node/link-request ...))) #`(Promise (→ #,(assoc-syntax #'x #'((node/link-request . node) ...))))] [((~literal List) a ...) #`(List #,@(stx-map fold-type #'(a ...)))] [((~literal Listof) a) #`(Listof #,@(stx-map fold-type #'(a ...)))] [((~literal Pairof) a b) #`(Pairof #,(fold-type #'a) #,(fold-type #'b))] [((~literal Vectorof) a) #'(Vectorof #,(fold-type #'a))] [((~literal U) a ...) #'(U #,(stx-map fold-type #'(a ...)))]] @section{@racket[incomplete] type-expander} We define a @tc[type-expander] @tc[(incomplete n)] that returns the incomplete node type for the node type @tc[n]. This type-expander allows the user to refer to the incomplete type of the node in the body of a transform, if annotations are needed for a value containing such a node. @chunk[ (define-type-expander (incomplete stx) (syntax-case stx () [(_ n) (raise-syntax-error 'incomplete (format "Type doesn't have an incomplete counterpart: ~a" (syntax->datum #'n)) #'n)]))] @chunk[ (define-type-expander (outer-incomplete stx) (syntax-case stx () [(_ n) #'(incomplete n)]))] @chunk[ (let () (let () (define-type node (tagged node [field (Promise field-type)] ...)) ... (define-type node/incomplete ;; TODO: substitute link-requests here (tagged node [field (Promise field-type)] ...)) (define-type-expander (incomplete stx) (syntax-parse stx () [(_ (~litral node)) #'node/incomplete] [_ #'(outer-incomplete n)])) ))] @section{Transforming @racket[incomplete] nodes into complete ones} @subsection{Initial version} We will start with a very simple traversal function, that will just substitute link requests immediately in the fields of a node. @chunk[ (define (substitute-link-requests v) (match v [(node/incomplete field ...) (node ...)] ...))] @chunk[ (match field [(transform/link-request key _) (transform/key→promise key)] ;; TODO ...)] @chunk[ ] @subsection{More complex attempt} We know for sure that all references to future nodes are actually incomplete ones, but we have no guarantee about the contents of the fields of a node. Since they may contain a mix of link requests and primitives (via a @tc[U] type for example), and may contain lists of nodes etc. we need to traverse them at run-time, in order to find and replace references to link requests. However, if we were to write this as a simple recursive function, we wouldn't be able to express its type without knowing anything about the node's type: @chunk[ (case→ (→ node/link-request node) ... (→ (Pairof may-contain-link-request may-contain-link-request) (Pairof doesnt-contain-link-request doesnt-contain-link-request)))] Writing the @tc[may-contain-link-request] and @tc[doesnt-contain-link-request] as functions, while expressing the contraint that the output is the same type as the input — except for the link requests that turned into nodes, would be impossible in typed/racket. I suppose that with GADTs one could write such a type. Instead, we will, during macro-expansion, traverse the type, and generate conversion procedures accordingly. @chunk[ [(~literal node/link-request) #''link-request] ... [((~literal List) a ...) #'(List #,@(stx-map fold-type #'(a ...)))] [((~literal Listof) a) #''Listof] [((~literal Pairof) a) #''Pairof] [((~literal Vectorof) a) #''Vectorof] [((~literal U) a ...) #''U]] @chunk[ (→ (List a ...) (List replaced-a ...))] @chunk[ [(list? v) (map traverse-list v)] [(pair? v) (cons (traverse-list (car v)) (traverse-list (cdr v)))] [(vector? v) ]] @subsection{Unions} Unions are difficult to handle: At one extreme, we confuse two different types like @tc[(Listof Number)] and @tc[(Listof String)], by using just the @tc[list?] predicate. On the other end of the spectrum, we try to distinguish them with @tc[typed/racket]'s @tc[make-predicate], which doesn't work in all cases. Handling this in the best way possible is out of the scope of this project, so we will just add special cases as-needed. @subsection{Unhandled} We currently don't handle structure types, prefab structures, hash tables, syntax objects and lots of other types. On the other hand, we can't handle fixed-length @tc[(Vector ...)] types, because occurrence typing currently can't track which case we are in when we check the length with @tc[(vector-length constant)]. We also can't handle functions, for hopefully obvious reasons. @; TODO: insert a link to the type-expander document in the paragraph below. We run into a problem though with types declared via define-type without informing the type-expander. The type-expander handles these by expanding just their arguments, and leaving the type untouched, but we can't ignore them in our case. For all these other cases, we'll just check that they don't contain any reference to a link-request type. @chunk[ [other (fold-check-no-link-requests #'other) #'other]] The checker below is approximate, and is just meant to catch the error as soon as possible, and we include a fall-back case for anything we couldn't handle properly. If we let a link-request slip, it should be caught by the type checker, unless it is absorbed by a larger type, like in @tc[(U Any link-request)], in which case it doesn't matter. @chunk[ (define (fold-check-no-link-requests stx) (syntax-parse stx [(~and whole (~or (~literal node/link-request) ...)) (raise-syntax-error 'graph "Found a link request buried somewhere I can't access" whole)] [(~and whole (t ...)) (stx-map fold-check-no-link-requests #'(t ...))] [whole whole]))] @section[#:tag "graph|TODO2"]{TODO} @chunk[ (define (multiassoc-syntax query alist) (map stx-cdr (filter (λ (xy) (free-identifier=? query (stx-car xy))) (syntax->list alist)))) (define (assoc-syntax query alist) (let ([res (assoc query (map syntax-e (syntax->list alist)) free-identifier=?)]) (unless res (raise-syntax-error '? (format "Can't find ~a in ~a" query alist))) (cdr res)))] @CHUNK[ ;; The actual traversal code: ;; TODO: write a tail-recursive version, it's cleaner than using set!. (: make-graph-database (→ root-transform.param.type ... (case→ (→ 'node.name (Listof (Pairof Any node.incomplete))) ...))) (define (make-graph-database root-transform.param.name ...) (let ([pending : (Listof (U node.link-request ...)) (list (cons (list 'root-transform.name root-transform.param.name ...) (λ () (root-transform.function root-transform.param.name ...))))] [all-transformed : (Listof (Pairof Symbol Any)) '()] ;; the key is actually the second element in a ;; link-request-???, but should be just a number like in ;; the C# version. [node.transformed : (Listof (Pairof Any node.incomplete)) '()] ...) (do : (case→ (→ 'node.name (Listof (Pairof Any node.incomplete))) ...) () [(null? pending) (ann (λ (selector) (cond [(eq? selector 'node.name) node.transformed] ...)) (case→ (→ 'node.name (Listof (Pairof Any node.incomplete))) ...))] (let ((request (car pending))) ;; Must be immediately after the (let (...), because we cons to ;; that list in the block below. (set! pending (cdr pending)) ;; Skip already-transformed link requests. TODO: map a number ;; for each. (unless (member (car request) all-transformed) ;; Call the lambda-part of the request. (let ([transformed ((cdr request))]) (cond [(eq? (car transformed) 'node.name) (set! pending (list* ((cdr transformed) 'node/field-filter-out-primitives/name) ... pending)) (set! all-transformed (cons (car request) all-transformed)) (set! node.transformed (cons (cons (car request) (cdr transformed)) node.transformed))] ... ;; Make sure all cases are treated, at compile-time. [else (typecheck-fail #'#,stx "incomplete coverage")])))))))] @section{Tests} @chunk[ (values)] @section{Conclusion} @chunk[<*> (begin (module main typed/racket (require (for-syntax racket/sequence ;; in-syntax on older versions ;;;unstable/sequence syntax/parse syntax/parse/experimental/template racket/syntax racket/function syntax/stx racket/pretty "../lib/low-untyped.rkt" "../lib/untyped.rkt") (prefix-in DEBUG-tr: typed/racket) syntax/parse "../lib/low.rkt" "structure.lp2.rkt" "variant.lp2.rkt" "../type-expander/multi-id.lp2.rkt" "../type-expander/type-expander.lp2.rkt") (provide make-graph-constructor #|graph|#) (begin-for-syntax ) #||#) (require 'main) (provide (all-from-out 'main)) (module* test typed/racket (require (submod "..") "../type-expander/type-expander.lp2.rkt" "../lib/test-framework.rkt") ;; Debug (require syntax/parse "../lib/low.rkt" "structure.lp2.rkt" "variant.lp2.rkt" "../type-expander/multi-id.lp2.rkt" "../type-expander/type-expander.lp2.rkt") ;; ))]