Can someone please give me some tips regarding a macro I’m trying to write? I’d like to take syntax of the form: (:= A (+ A M C))
and transform it into a form like this: (lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor)
such that the identifier M stays the same, and the identifiers A and C are transformed into Processor-A-lens and Processor-C-lens. Additionally, any instances of those identifiers that appear in the expression half of the syntax (the (+ A M C) half) are wrapped with a lens-view and have “processor” appended to them. So, any instance of A in the expression half will become: (lens-view Processor-A-lens processor)
and any instances of M will become (lens-view M processor) ;because M isn't transformed, it just remains as M
can someone give me some advice on how I should go about doing this, and what syntax functions are best suited? I could probably kludge it together with a combination of syntax->list and lots of car-cdring to replace what needs to be replaced, but I get the feeling that’s not how I should be going about doing this. There’s a fixed amount of literal identifiers which are valid (A C and a few others).
Thank you :)
Let’s take one thing at time. Let’s look at generating the result: (lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor)
A nice approach is to fill in the missing pieces in: (with-syntax ( <binding-missing-pieces-here> )
(syntax/loc stx
(lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor)))
The missing pieces are the identifiers that doesn’t appear in the input.
(with-syntax ( [Processor-A-lens <mumble>]
[Processor-C-lens <mumble>])
(syntax/loc stx
(lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor)))
Here <mumble> needs to turn A into Processor-A-lens.
And similarly for C.
Since we need to do the same twice, let’s make a function.
(with-syntax ( [Processor-A-lens (build-processor-id-lens #'A)]
[Processor-C-lens (build-processor-id-lens #'C)])
(syntax/loc stx
(lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor)))
Now actually building the identifier can be done in several ways, but I like to use format-id
.
It is used as: (format-id lctx fmt v ...)
(I am ignoring the optional keyword arguments.
Here lctx
provides the lexical context - which is the same as the input syntax — so we can use stx
.
The argument fmt
is a format string, which means it is a string with where occurences of ~a can be filled in by the values v … .
Our format string becomes: “Processor-~a-lens” and the the value v will be either #’A or #’C.
Wow! I figured modifying pieces of syntax would be much more difficult that just using an equivalent of format that takes a context! that’s very awesome.
We have: (define (build-processor-id-lens id) (format-id stx "Processor-~a-lens" id))
Let’s assemble it, and try it out.
do we need to pass a stx argument to build-processor-id-lens?
#lang racket
(require (for-syntax syntax/parse racket/syntax))
(define-syntax (transform stx)
(syntax-parse stx
[(_transform A M C) ; for now
(define (build-processor-id-lens id)
(format-id stx "Processor-~a-lens" id))
(with-syntax ([Processor-A-lens (build-processor-id-lens #'A)]
[Processor-C-lens (build-processor-id-lens #'C)])
(syntax/loc stx
(lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor)))]))
(transform A M C)
We don’t if we define it locally.
oh, defined as a local function, gotcha.
To test it, I added a quote, like this: (syntax/loc stx
'(lens-set ...))
This produces the expansion as a datum, which sometimes is nice to see.
Here I can’t test it without, because I don’t have the code where Processor-A-lens etc are defined.
Now we need to make it more general.
It looks like, there are two transformations: (+ A M C)
becomes (+ ...)
as above, and (:= ...)
correponds to the lens-set.
Exactly.
I suppose the outer lens-set is (almost) the same as the inner transformations to lens-view, in a way.
Why is M special in: (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
?
The mode functions return lenses: (define (MODE-IMM processor)
(lens-compose (vector-ref-lens 1) Processor-MEM-lens))
So M stands for memory, A for accumulator and C for carry?
Yup, that’s right. Though, in the context of some opcodes (ones defined for the MODE-ACCUMULATOR mode) the M would refer to A also, if that makes sense.
I wanted to make things LOAD and STORE agnostic, so these function-defining-macros don’t care whether you’re loading a register or storing to a location, the MODE functions just a return a lens, for storing or loading.
Hope I explained that alright.
Does that mean that the only names are M, A, C, X and Y?
I wasn’t going to mention this because I didn’t want to get too ahead of myself, but the full syntax I had in mind was something like: (define-op ADC
(:= A (+ A M C))
#:imm 69
#:zp 65
#:zpx 75
#:abs 6D
#:absx 7D
#:absy 79
#:indx 61
#:indy 71
#:z (zero? A)
#:n (twos-complement-negative? A))
so, in total, A X Y M C Z I D B V N.
I like it!
Thank you! :)
I dare say I can accomplish (most of) the rest of what’s there with just the tools you’ve shown me thus far. Would you agree?
Yes.
The only other part I had in mind, was iterating over all modes in a for loop, and using hash-set! to add the newly-generated functions to a table, with the opcode’s hex as the key.
unless that doesn’t sound like the best way to do it?
Sounds fine.
Btw you can expand (+ A M C)
into: (let ([A (lens-view Processor-A-lens processor)])
[M (lens-view M processor)]
[C (lens-view Processor-C-lens processor)])
(+ A M C))
The idea is that the body expression is the one from the input syntax, and each clause in the let corresponds to a register/thing.
It might be easier than figuring out how to substitute things in the original expression.
Ah, so regardless of what the expression is, say (another example I wrote out long-form): (define-op ROR
(:= M (shift-right M))
;etc
they all just get transformed into a let of all the potentially-relevant bindings (whether utilised or not) with the body being the expression.
I wasn’t thinking generically enough, I reckon. That seems much better, thanks.
as one last thing, could you please let me know what I need to look into regarding using #:keys in a macro the way I have planned?
I don’t think you need anything special. I believe ’#:foo can be used as a pattern.
Thank you. I really appreciate you walking me through that macro. Also, I wouldn’t have even considered this approach if it wasn’t for you and @notjack confirming that it was viable, so thanks for that too :)
@sydney.lambda drive by tip: when using syntax-parse
, most uses of with-syntax
can be replaced with the #:with
pattern directive. So instead of this: (syntax-parse stx
[(_transform A M C) ; for now
(define (build-processor-id-lens id)
(format-id stx "Processor-~a-lens" id))
(with-syntax ([Processor-A-lens (build-processor-id-lens #'A)]
[Processor-C-lens (build-processor-id-lens #'C)])
(syntax/loc stx
(lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor)))])
You can write this: (syntax-parse stx
[(_transform A M C) ; for now
#:do [(define (build-processor-id-lens id)
(format-id stx "Processor-~a-lens" id))]
#:with Processor-A-lens (build-processor-id-lens #'A)
#:with Processor-C-lens (build-processor-id-lens #'C)
(syntax/loc stx
(lens-set Processor-A-lens (+ (lens-view Processor-A-lens processor)
(lens-view M processor)
(lens-view Processor-C-lens processor))
processor))])
You need to use #:do
so you can insert definitions that are visible to #:with
, since you can’t use #:with
after you’ve used (define ...)
in a pattern clause.
You can read more about pattern directives like #:with
and #:do
here: https://docs.racket-lang.org/syntax/stxparse-specifying.html#%28part._.Pattern_.Directives%29
@notjack thanks for the tip :)
I think I’ve got something that’s passable: (define-syntax (:= stx)
(syntax-parse stx
[(_ dst src)
(define (transform e)
(datum->syntax stx (for/list ([elem (syntax->datum e)])
(if (member elem '(M A X Y C Z I D B V N))
(list 'lens-view elem 'processor)
elem))))
(with-syntax ([new-src (transform #'src)])
(syntax/loc stx
(λ (processor) (let ([A Processor-A-lens] [C Processor-C-lens])
(lens-set dst processor new-src)))))]))
(:= A (+ A M C))
however, I’m getting unbound identifier errors for A, M, and C when called like so: (:= A (+ A M C))
is there a way I can have these identifiers “ignored” when used in said macro, if that makes sense? At the time of generating the functions, no processor, A, M, or C will be bound. Or is it something to do with how I’m replacing the elements using for/list?
Eventually what will be produced will be a function like: (λ (processor)
(define M (mode processor)
(let ([A Processor-A-lens]
[C Processor-C-lens]
;....)
;the given expression)))
such that M A and C are only “given meaning” when the function is actually called later (during the runtime of the emulator) and a processor is provided. When the := macro is called, A M and C are just “symbols”.
Hmmm. What about something like this? (define ((:= lens f) x)
(lens-set lens x (f (lens-view x))))
(define ((lensify func . lenses) x)
(apply func (for/list ([lens (in-list lenses)]) (lens-view lens x))))
(define A Processor-A-Lens)
(define C Processor-C-Lens)
(define M (mode processor))
(:= A (lensify + A M C))
That is, ditch the macros entirely and introduce some local aliases that make the lensy nature of the operation easier to understand. You’ll need to explicitly say which parts of the expression are lensy and which aren’t (using lensify
), but IMO that’s a good thing. This approach is kind of haskell-y, it’s based on something called “idiom brackets” which is like Haskell’s version of do-notation but for applicative expressions.
I just really like the (:= A (+ A M C)) syntax due to how clearly it expresses what’s going on with the opcodes.
Purely aesthetic.
(oh and just in case you haven’t seen it before, (define ((f x) y) body ...)
is equivalent to (define (f x) (lambda (y) body ...))
)
I think it is clearer. In this case I’d probably not go that far though and just stick to the lensify
approach because it’s very close, and getting all the way there in a way that’s robust is hard
tradeoff of surface syntax clarity v.s. implementation complexity
thanks for the advice @notjack. Being completely new to macros, this is the type of thing I have no past experience with, so my brain is stopping at “wow, that looks nice!” versus “that looks nice, but putting the time and effort into implementing it isn’t worth it” haha.
example of how the macro approach can get thorny: what if someone does this? (:= A (+ A M (let ([x C]) x)))
Should that work?
also, does it? I actually don’t know, I think with your impl it might work fine. Something similar involving let-syntax
and constructing the reference to C
unhygienically would probably break it though.
I guess what I had in mind was only arithmetic expressions, with no binding clauses or anything fancy. I tried out the widest variety of opcodes I could think of, and almost all of them can be expressed in this way.
Oh wait, this is a simpler example of something that I know will break: (:= A (let ([V +]) (V A M C))
Yeah I think you’ll be fine if the expressions allowed are limited to a fixed grammar that you specify ahead of time and check for. You could make a syntax class for it with define-syntax-class
too, which would give you error checking at compile time.
In that case, can you suggest how I can progress from here? I’m still at “it’s giving undefined errors for A, C etc.” I’m afraid, haha.
the closest I could find that seemed relevant was syntax parameters, but I’m not sure they’re what I’m looking for.
I think that would probably be a hygiene thing. I think syntax parameters are what you want, but with a nice little wrapper API over them. One sec, I’ll try something out in drracket.
I really appreciate you taking the time to help me figure this out, thank you :)
happy to help :)
@sydney.lambda I ended up with this: https://gist.github.com/jackfirth/260173a7f7af6cabcdeb960bd8e44483
I don’t have the ability to test it, but it does compile and seems to expand to what you want, I think
hmm, I can’t seem to find where (define-location-alias) comes from?
From this part:
(define-simple-macro
(define-location-alias id:id (~optional (~seq #:default lens:id)))
(~?
(define-syntax-parameter id (λ (_) #'(lens-view lens the-processor)))
(define-syntax-parameter id
(λ (stx) (raise-undefined-location-alias-error #'id stx)))))
I tend to indent macros like that when their header is too long to fit on a single line.
edit: I’m getting the error: syntax-parameterize: not bound as a syntax parameter
at: the-processor
but let me keep trying with it.
There’s a (define-syntax-parameter the-processor #f)
at the top that I suspect you removed, since it was next to the other (define <thing> #f)
definitions that were just to get it to compile
https://gist.github.com/dys-bigwig/dcda1640cf751468d2e5bacd2974ad52 I couldn’t work it out at the finish, I’m afraid. After adding back in the lines you mentioned, all I can seem to get is “application: not a procedure”. Thank you for all your help, I’ll try again tomorrow and see if I can figure it out :)
worth a shot ¯_(ツ)_/¯
sorry it didn’t quite work