885 lines
36 KiB
Racket
885 lines
36 KiB
Racket
#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[<example-1674389-2>
|
|
(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[<example-1674389>
|
|
(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[<example-1674389-2>
|
|
(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[<make-graph-constructor>
|
|
(define-syntax/parse
|
|
(make-graph-constructor ([node (field:id field-type:expr) ...] ...)
|
|
[transform:id (param:id param-type:expr) ...
|
|
(~literal :) result-type:id
|
|
body ...]
|
|
...)
|
|
<stx-transform/link-request>
|
|
<stx-make-graph-database>
|
|
<stx-node/incomplete>
|
|
<stx-param-type/old>
|
|
<stx-transform/result-node/extract-link-requests>
|
|
<stx-transform/link-request→incomplete>
|
|
#`(let ()
|
|
<param-type/old>
|
|
(let ()
|
|
<define-incomplete-types>
|
|
<define-make-link-requests>
|
|
<transform/link-request→incomplete>
|
|
<define-transforms>
|
|
<make-graph-database>
|
|
make-graph-database)))]
|
|
|
|
@chunk[<test-make-graph-constructor>
|
|
(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[<stx-param-type/old>
|
|
(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[<param-type/old>
|
|
(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[<stx-node/incomplete>
|
|
(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[<stx-node/incomplete>
|
|
(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[<stx-node/incomplete>
|
|
(define/with-syntax (transform/result-node/incomplete ...)
|
|
(for/list ([x (in-syntax #'(result-type ...))])
|
|
(assoc-syntax x #'([node . node/incomplete] ...))))]
|
|
|
|
@CHUNK[<define-incomplete-types>
|
|
(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[<stx-transform/link-request>
|
|
(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[<pre-declare-transform/link-request>
|
|
(struct (TKey)
|
|
transform/link-request-pre-declared
|
|
([key : TKey])
|
|
#:transparent)]
|
|
|
|
@chunk[<define-make-link-requests>
|
|
(define-type transform/link-request
|
|
(transform/link-request-pre-declared
|
|
(List 'transform
|
|
param-type/old ...)))
|
|
...]
|
|
|
|
@subsection{Transforms}
|
|
|
|
@chunk[<stx-transform/link-request→incomplete>
|
|
(define/with-syntax (transform/link-request→incomplete ...)
|
|
(format-temp-ids "~a/link-request→incomplete" #'(transform ...)))]
|
|
|
|
@chunk[<transform/link-request→incomplete>
|
|
(begin
|
|
(: transform/link-request→incomplete
|
|
(→ param-type/old ... transform/result-node/incomplete))
|
|
(define (transform/link-request→incomplete param ...)
|
|
body ...))
|
|
...]
|
|
|
|
@chunk[<define-transforms>
|
|
(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[<stx-make-graph-database>
|
|
(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 ...)))
|
|
<fold-type-clauses>
|
|
<fold-type-stx>
|
|
<stx-extract-link-requests>]
|
|
|
|
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>
|
|
(: 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[<make-graph-database>
|
|
(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
|
|
...)
|
|
<define-extract-link-requests> ;; TODO: Can probably be moved out.
|
|
<process-queue-body>)
|
|
|
|
<process-queue-initial-call>)]
|
|
|
|
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-initial-call>
|
|
(process-queue (set (root-transform root-transform/param ...))
|
|
(set)
|
|
(begin 'transform/transformed '())
|
|
...)]
|
|
|
|
Process-queue is a standard queue handler using sets.
|
|
|
|
@CHUNK[<process-queue-body>
|
|
(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))])
|
|
<process-queue-body-tags>))]
|
|
|
|
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[<process-queue-body-tags>
|
|
(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[<fold-type-match-example>
|
|
(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[<fold-type-clauses>
|
|
(define (fold-type-clauses val t)
|
|
(syntax-parse t
|
|
<fold-type-clauses-body>))]
|
|
|
|
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[<fold-type-clauses-body>
|
|
[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[<fold-type-clauses-body>
|
|
[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[<fold-type-clauses-body>
|
|
[((~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[<fold-type-clauses-body>
|
|
[((~literal Listof) a)
|
|
#`([(list tmp (... ...))
|
|
(append-map (λ (tmp1) #,(fold-type #'tmp1 #'a))
|
|
tmp)])]]
|
|
|
|
Pairs and vectors are handled similarly:
|
|
|
|
@CHUNK[<fold-type-clauses-body>
|
|
[((~literal Pairof) a b)
|
|
#`([(cons tmpa tmpb)
|
|
(list #,(fold-type #'tmpa #'a)
|
|
#,(fold-type #'tmpb #'b))])]]
|
|
|
|
@CHUNK[<fold-type-clauses-body>
|
|
[((~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[<fold-type-clauses-body>
|
|
[((~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[<stx-transform/result-node/extract-link-requests>
|
|
(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[<fold-type-clauses-body>
|
|
[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[<fold-type-stx>
|
|
(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[<stx-extract-link-requests>
|
|
(define-template-metafunction (fold-type-tmpl stx)
|
|
(syntax-case stx () [(_ val t) (fold-type #'val #'t)]))]
|
|
@CHUNK[<define-extract-link-requests>
|
|
#,@(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[<fold-type-cases>
|
|
[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[<outermost-incomplete>
|
|
(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[<save-outer-incomplete>
|
|
(define-type-expander (outer-incomplete stx)
|
|
(syntax-case stx () [(_ n) #'(incomplete n)]))]
|
|
|
|
@chunk[<incomplete>
|
|
(let ()
|
|
<save-outer-incomplete>
|
|
(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)]))
|
|
<body>))]
|
|
|
|
@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[<substitute-link-requests>
|
|
(define (substitute-link-requests v)
|
|
(match v
|
|
[(node/incomplete field ...)
|
|
(node <link-request→promise> ...)]
|
|
...))]
|
|
|
|
@chunk[<link-request→promise>
|
|
(match field
|
|
[(transform/link-request key _) (transform/key→promise key)] ;; TODO
|
|
...)]
|
|
|
|
@chunk[<transform/key→promise>
|
|
]
|
|
|
|
@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[<attempt-at-typing-traverse>
|
|
(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[<fold-type-cases2>
|
|
[(~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[<traverse-list-type>
|
|
(→ (List a ...) (List replaced-a ...))]
|
|
|
|
@chunk[<traverse-list-code>
|
|
[(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[<fold-type-cases2>
|
|
[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[<fold-check-no-link-requests>
|
|
(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[<multiassoc-syntax>
|
|
(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[<old-make-graph-database>
|
|
;; 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[<test-graph>
|
|
(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
|
|
<multiassoc-syntax>)
|
|
<pre-declare-transform/link-request>
|
|
|
|
<make-graph-constructor>
|
|
|
|
#|<graph>|#)
|
|
|
|
(require 'main)
|
|
(provide (all-from-out 'main))
|
|
|
|
(module* test typed/racket
|
|
(require (submod "..")
|
|
"../type-expander/type-expander.lp2.rkt"
|
|
"../lib/test-framework.rkt")
|
|
|
|
;; Debug
|
|
<pre-declare-transform/link-request>
|
|
(require syntax/parse
|
|
"../lib/low.rkt"
|
|
"structure.lp2.rkt"
|
|
"variant.lp2.rkt"
|
|
"../type-expander/multi-id.lp2.rkt"
|
|
"../type-expander/type-expander.lp2.rkt")
|
|
;;
|
|
|
|
<test-graph>
|
|
<test-make-graph-constructor>))]
|