nurpax / c64jasm

C64 6502 assembler in TypeScript
51 stars 14 forks source link

figure out a way to pass implicit params to macros #57

Closed nurpax closed 5 years ago

nurpax commented 5 years ago

In reference to issue #56

It's looking likely that macros will not be able to access the enclosing scope where the macro is expanded, e.g. this used to work:

!macro foo() {
    lda #zp.x
}
func: {
    !let zp = { x: 3}
    +foo()
}

but soon it won't.

However, this way of implicitly passing arguments can be useful for things like passing a context object like a dict containing zero page slot allocation, which can then be overridden locally by declaring a shadowed context parameter. See https://nurpax.github.io/posts/2019-07-27-c64jasm-object-literals.html for details.

This issue is filed to figure out a design that'd allow doing this ergonomically.

One approach I had in mind was default parameter values for macro args. E.g.,:

!macro foo(z = zp) {
    lda #z.x
}
func: {
    +foo({ x: 5 })
}
func2: {
    !let zp = { x: 6 }
    +foo()    ; pass zp implicitly
}

But this syntax is confusing as it looks like z = zp is evaluated at the point where the macro is declared, not where it's called.

I guess some sort of lazy binding semantics would work so that the macro default argument value is (optionally?) evaluated on the macro call site.

neochrome commented 5 years ago

Another idea would be to introduce a minor variation on the namespacing operator, where if you leave LHS empty it means "look up symbol in scope at place of macro expansion. Example:

!macro foo() {
    lda #::zp.x
}
!let zp = { x: 5 }
func: {
    +foo()    ; results in lda#5
}
func2: {
    !let zp = { x: 6 }
    +foo()    ; results in lda #6
}

Would that take care of the case described in your post? I'm not 100% sure about the readability when using globals like this - but that is mostly about taste and one can still use explicit arguments if that feels better.

That said, some kind of default argument values could still be useful.

nurpax commented 5 years ago

This syntax in fact already exists but it serves a different purpose. Acessing a symbol like ::foo::bar means it will look up foo::bar rooted in the top-most, root scope. Kind of like in C++. This can be useful for something like this (probably contrived, but I can imagine this happening in some cases):

foo: {
bar: {
!let sym = 1
}
    lda #::bar::sym    ; access 'bar' from below, not from foo::bar
}

bar: {
!let sym = 0
}

I'm not sure if the docs mention it. :)

However, I think I figured out a reasonably nice approach for implementing "implicit macro args" by using... wait for it... global variables! So basically you can assign zp some value where you declare the macros, then if you need them to change, just assign another value to zp and restore it back to its original values once you're done.

Since c64jasm is in fact a programmable assembler, this save/restore functionality can be implemented using a stack in JavaScript. :) I wrote a test case that does exactly this: https://github.com/nurpax/c64jasm/commit/e670695942e772415db0d73e64cf8e0563b016d7

It's a bit longer because I wanted the same test case to test a few different things at the same time, but the idea should still come across.

Had to fix one bug in c64jasm w.r.t extension functions to support this type of an extension nicely.

I'm pretty happy with this. In my demo code I tend to have macros for "irq start" and "irq end" when I declare an IRQ handler. So the push/pop calls could go into these macros and all IRQs would automatically use safe ZP slots.

If you look at the test, you will notice that c64jasm doesn't support running "statements". I'd like some syntax for this so that I don't need to do:

!let dummy = stack.push(2)
!let dummy2 = stack.pop()

IIRC KA has .eval for this.

nurpax commented 5 years ago

@neochrome Do you have any preference on how the syntax for running statements should look like?

Right now only assignment statements are supported:

!let foo = 3
foo = 4

IIRC KickAssembler implements statements by .eval, e.g., .eval foo = 4.

Side-effectful statements can be expressed currently by only abusing let but this is ugly:

!let dummy = x.push(1)   ; dummy will not be referenced

I'm not a big fan of !eval, mainly because I've had some plans to support things like !eval "lda #0" (which could be useful if an extension generates assembly code as string). Although I suppose this feature could also be !compile <string>.

How about using !! <stmt> instead?

!! x.push(1)

Using ! for side-effects is not completely unheard of in other languages. For example, to set a mutable variable in Scheme, you'd use (set! var 3).

neochrome commented 5 years ago

Funny that you thought about this - I was on the verge of suggesting a mechanism for executing statements. So that you could implement e.g a print function in JavaScript to get printing during assembly without having to further extend the assembler syntax.

In OCaml you can use ignore <expr> if you're only interested in the side effects, so maybe !ignore could work. On the other hand it may come across like you're skipping the statement altogether. I guess in an expression based language it might make more sense...

Yet another could be !exec or !execute.

Otherwise I kinda like the !! It's short and sweet.

On Sun, Aug 4, 2019, 21:44 Janne Hellsten notifications@github.com wrote:

@neochrome https://github.com/neochrome Do you have any preference on how the syntax for running statements should look like?

Right now only assignment statements are supported:

!let foo = 3 foo = 4

IIRC KickAssembler implements statements by .eval, e.g., .eval foo = 4.

Side-effectful statements can be expressed currently by only abusing let but this is ugly:

!let dummy = x.push(1) ; dummy will not be referenced

I'm not a big fan of !eval, mainly because I've had some plans to support things like !eval "lda #0" (which could be useful if an extension generates assembly code as string). Although I suppose this feature could also be !compile .

How about using !! instead?

!! x.push(1)

Using ! for side-effects is not completely unheard of in other languages. For example, to set a mutable variable in Scheme, you'd use (set! var 3).

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/nurpax/c64jasm/issues/57?email_source=notifications&email_token=AACR6UROOYGY5HXRPJPMQQTQC4WQ7A5CNFSM4IJD3H6KYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD3QIK2Y#issuecomment-518030699, or mute the thread https://github.com/notifications/unsubscribe-auth/AACR6UTL7VG432X52Q54ALDQC4WQ7ANCNFSM4IJD3H6A .

nurpax commented 5 years ago

Oh, another OCaml user! :) I'm using some FP code in c64jasm too.. the HTML docs are built in Haskell (c64jasm/docs-src/src folder).

Yeah, I think ignore can be confusing.. And so would Haskell's void. Ditto for !let _ = x.push(1). The mindset of assembly coders might not be very functional.

I guess !! is a strong contender. I'll give this some more thinking and try how it works in the parser. Hopefully it won't conflict with something like !let foo = !!bar (force a value to boolean).

nurpax commented 5 years ago

Closing.. the context global variable thing is fine for this.

nurpax commented 5 years ago

The normal variable assignment syntax now became sort of obsolete as it'd be better done by

!! var_name = 3

instead of

var_name = 3

But I didn't change this as a bunch of my own code would break. Both are now allowed in the syntax.