michael-buschbeck / mychs-macro-magic

A simple, sane, and friendly little scripting language for your Roll20 macros.
MIT License
1 stars 1 forks source link

Support custom functions #160

Closed michael-buschbeck closed 2 years ago

michael-buschbeck commented 2 years ago

(Copied from this comment in phylll#32)

I've felt inspired to think about MMM support for custom functions. Shouldn't be too difficult to implement. Syntax is more interesting to ponder.

Define function – scoped to the script that contains the definition, like variables:

!mmm function foo(bar, baz)
!mmm     chat: Foo, my ${bar} is ${baz}'d!
!mmm     return sender.(bar & "_" & baz)
!mmm end function

Call function – like any other:

!mmm do foo("ammo", "use")
!mmm set remaining = foo("life", "challenge")

That's the baseline. What else?

michael-buschbeck commented 2 years ago

Access to the main script's variables: yes or no?

phylll commented 2 years ago

All of this is very much like Christmas in MMMland, quite appropriately timed :)

  • Maybe: Function code gets read-only access to the script's variables through a special variable called script – e.g. script.cOwnID to access the cOwnID variable defined on the main script's level.

This would be extremely useful. In my very pedestrian experiments with "library" code, I found quite a few instances where I need to use a lot of variables global to each script, particularly as long as I won't be able to handle struct variables. This would make for loooooong list of parameters and providing easy read-only access to all of this would be super helpful, including the read-only part in terms of avoiding mistakes.

  • Maybe: Function code gets read-only access to the parent function call's variables through another special variable called caller. I struggle to imagine what the use case for that could be though.

I struggle to see the need for this, as well. If we had the script.var feature, anything else I can think of can easily be handled using explicit function parameters.

michael-buschbeck commented 2 years ago

Very good!

phylll commented 2 years ago
  • Global functions – those defined outside of a script block – are supported. They're lost as soon as the API sandbox is stopped or restarted; we may have to think of some way to work with that fact.

Would it be one way of working with that fact to simply make sure that every script "loads" all required functions upon execution? I think that would not be difficult to do, in particular if MMM had no trouble with the same function being loaded several times (and thereby overwritten). If that would be fine, MMM scripts could simply include all required functions using the Roll20 include syntax (%{Sheet|Ability} or #Macro).

michael-buschbeck commented 2 years ago

It'd work for sure.

It just seems unnecessarily wasteful to not take advantage of only having to parse functions once. :)

phylll commented 2 years ago

O-kay. Right. So what do we do? Could MMM simply skip any duplicate global function blocks? I don't see a way from my end to only include the code if it hasn't been included before. I guess you could call some predefined autorun macro the first time any particular player calls MMM...?

In other news, I am happy to have implemented my first MMM functions using the currently installed 1.24.0-pre :)

michael-buschbeck commented 2 years ago

Something like "autorun" macros was on my mind, too. Or, well, have players manually run the "library macro" at session start.

For now, simply redefining all functions on each run will be fine. Performance issues are non-issues until observed.

Glad you like 1.24.0-pre – it's still got some issues with accessing outside data from within a global function especially when calls are nested. Working on it!

michael-buschbeck commented 2 years ago

One of the things that are yet to make to work is being able to call functions that were defined on script level from a global function called by that script:

!mmm function foo()
!mmm     do script.exclaim("It is I!")
!mmm     do script.exclaim("Who dares to challenge me?")
!mmm end function

!mmm script
!mmm     function exclaim(msg)
!mmm         chat: Hark!, ${msg}
!mmm     end function
!mmm     do foo()
!mmm end script

!mmm script
!mmm     function exclaim(msg)
!mmm         chat: ${msg}, I say!
!mmm     end function
!mmm     do foo()
!mmm end script

For that matter, you can also pass functions as parameters or assign them to variables.

phylll commented 2 years ago

I didn't have a use case for this problem, yet. What I did encounter is the inability so far to call one global function from another, if I'm not doing anything wrong here.

This...

!mmm function bar()
!mmm     return "Hello!"
!mmm end function

!mmm function foo()
!mmm     return bar()
!mmm end function

!mmm script
!mmm     chat: ${foo()}
!mmm end script

... leads to this error:

(From Mych's Macro Magic): During execute, During execute, unknown function in expression in script: return bar❌() in expression in template in script: !mmm chat: ${❌foo()}

phylll commented 2 years ago

And as you mentioned above, access to script.var from a global function does not work yet:

!mmm function foo()
!mmm     return "The answer is " & script.testVar
!mmm end function

!mmm script
!mmm     set testVar = 42
!mmm     chat: ${foo()}
!mmm end script

... results in ...

The answer is

michael-buschbeck commented 2 years ago

Both issues are connected – fix upcoming.

michael-buschbeck commented 2 years ago

The latest prerelease seems to fix all known issues.

Things left to do:

michael-buschbeck commented 2 years ago

The latest prerelease fixes the !mmm diagnostics inconsistency and improves nested-call error messages, which now look something like this:

During execute, unknown function in expression in script: !mmm     do runtimeError❌()
Called from: !mmm     do ❌test3()
Called from: !mmm     do ❌test2()
Called from: !mmm do ❌test1()

The "diagnostics for errors in parameter lists" amazingly turns out to be a non-issue because the parser consumes as much of the parameter list as can be interpreted as a valid one, and then points to the character afterwards for the error – close enough! (It's really just a matter of taste whether the error in test(foo,) is the presence of the comma or the absence of a following parameter name.)

michael-buschbeck commented 2 years ago

Speaking of error diagnostics and the location of an error – I've been wondering what kind of information might make it easier to identify which line the error occurred in?

phylll commented 2 years ago

Speaking of error diagnostics and the location of an error – I've been wondering what kind of information might make it easier to identify which line the error occurred in? ...

  • Would spiri support for end-of-line comments help to mitigate the ambiguity?

What does "spiri support" mean? "Boost Spirit" was my closest match on Auntie G...

  • Line numbers are tricky: Do non-!mmm lines count or not? If they do, what about non-!mmm lines leading the !mmm script line? If not, how useful is a line number if you can't use your text editor to address it?

Agree. With various %{...} includes, in particular, it's quite unrealistic for anyone to match MMM's line numbers as received from Roll20's auto expansion to their scripts in a way that would be helpful for debugging.

How about thinking in a slightly different direction? Could MMM add more info to identify the active block(s)? Function names as part of error messages would help, and functions should not be so complicated as to contain lots of identical lines, should they? Maybe error messages could even contain other open blocks ("in if foo==bar, in combine chat, in for item in list, in m3mgdMyCrazyLoopingFunction()")? And script blocks could get (optional) names, too (!mmm script rangedAttack)?

michael-buschbeck commented 2 years ago

"spiri support" was Siri channeling… dunno, the spirit of Christmas maybe? I really just meant plain "support" (for end-of-line comments). You could disambiguate several identical do func() lines by adding a comment like do func() // first try.

Identifying a call stack of blocks sounds interesting. I'll think about that.