For instance, I have a small dsl like:
dsl:
algo1:
evaluate:
echo "Evaluation is " & $value
algo2:
evaluate:
echo "Evaluation is " & $value
The role of evaluate is to retrieve the result of calculation using algo1 or algo2 that use different data structures in an external API. So when evaluate is called in the context of algo1, it must call a different API than when used in the context of algo2.
The simplest solution would be to use different names for evaluate in context 1 vs 2, like evaluate1 and evaluate2.
Another solution, in evaluate, would be being able to detect when walking up the AST if it contains NimNode calls with "algo1" or "algo2". But I don't think that you can walk up the AST to the root like you can do down to the children with findChild for instance.
Another idea is for algo1 and algo2 to set the algorithm used in a variable and let evaluate retrieve the value of that variable before calling the API. But if the macro evaluate is executed in an untyped context from algo1 / algo2, it can't access that variable content because that context hasn't been semantically checked yet, if I understand correctly.
What would be an approach to solve that problem? Please note that I've simplified my problem and that in the real DSL, there could be multiple layers of block calls between algoX and evaluate.
Well, the DSL is more a language than a fixed structure. I gave a small sample code to feel it but you could have code like:
dsl:
algo1:
evaluate:
# Do something with algo1 result
let x = getValue()
algo2:
# Using x to calculate algo2
init(x)
evaluate:
# Getting a new value
let y = getValue()
etc.
That's the reason why dsl, algo1 and algo2, evaluate are macros in that example that can be combined by the user. dsl is used by the user to start using the DSL syntax. algo1 and algo2 are two keywords of the DSL to do some operations, and evaluate is another keyword to get access to result from these operations.
Like I said, eventually the solution would be to change the DSL syntax, either by using dedicated evaluate1 and evaluate2, but in the real DSL I could have more that 2 algoN and having a single keyword for a common action is more orthogonal. Or do you have a better idea?
Night has been enlightening. I understand my error. Instead of having multiple macros algo1, evaluate, etc. that are called when Nim compiler evaluates the syntax tree from the dsl```call, I must see ``dsl as a compiler itself. Now, I have only one macro, dsl, that is used to convert its argument to an AST, and then this AST is parsed/translated by procs in a new AST that will be used by the Nim compiler.
Instead of having multiple small macros that don't play well together and have only a local context, I'll apply a visitor-like pattern and I can control the scopes in the DSL.
Thanks @dawkot for forcing me to think out of the box with your example.
Instead of having multiple small macros that don't play well together and have only a local context, I'll apply a visitor-like pattern and I can control the scopes in the DSL.
Yes! Now you're cooking.
I reopen this thread as I'm facing a similar problem, but now when calling an API.
Is it possible to write a proc whose behaviour is based on existence or not of macro injected variables? I've tried to reproduce a sample case below:
import macros
template foo(body: untyped) =
block:
let inFoo {. inject, used .} = true
body
template bar(body: untyped) =
block:
let inBar {. inject, used .} = true
body
proc callApi =
if declaredInScope(inFoo):
echo "Calling API with foo scope"
elif declaredInScope(inBar):
echo "Calling API with bar scope"
else:
echo "You can call API only in foo or bar scopes!"
proc foobar =
foo:
callApi()
bar:
callApi()
foobar()
This code does not give the expected result!
What I'm trying to do and that does not work: the callApi proc takes no parameters and depend on the scope it is used to call different API entry points. Its behaviour is really given in the scope it is used. In order to determine in which scope it is used, I've tried to inject 2 different variables inFoo and inBar and base callApi behaviour on the presence or absence of these variables in the scope.
Having changed the way my DSL works, I'm now able to know in which scope/context the AST is parsed. But in that case, callApi is a Nim function that can be called by the user and I don't want for these types of functions to have a complex syntax: the user knows if she is using it in the for or bar contexts and that's the reason it doesn't have arguments in my example.
Behind the scene, I have two functions callApiFromFoo and calApiFromBar with all the parameters required. I want to be able to find which function to call...
Is the only way to get the expected result to parse the whole AST tree and when finding a callApi call NimNode, replace it with callApiFromFoo (resp. callApiFromBar) when in the foo (resp. bar) scope?
Or is there another way to solve this problem?
You could do it by wrapping in a template
import macros
template foo(body: untyped) =
block:
let inFoo {. inject, used .} = true
body
template bar(body: untyped) =
block:
let inBar {. inject, used .} = true
body
proc callApiFromFoo =
echo "Calling API with foo scope"
proc callApiFromBar =
echo "Calling API with bar scope"
template callApi =
when declaredInScope(inFoo):
callApiFromFoo()
elif declaredInScope(inBar):
callApiFromBar()
else:
echo "You can call API only in foo or bar scopes!"
proc foobar =
foo:
callApi()
bar:
callApi()
foobar()
Could you explain how/why it works?
I have reproduced that pattern in my project and it works sometimes but not always, and I don't understand why.
When it does not work, I have changed the callApi template to be like:
template callApi =
when declaredInScope(inFoo):
callApiFromFoo()
elif declaredInScope(inBar):
callApiFromBar()
else:
echo "inFoo scope? = ", declaredInScope(inFoo)
echo "inBar scope? = ", decalredInScope(inBar)
error("You can call API only in foo or bar scopes!")
And in the foobar proc (a macro in my case), I dump the whole AST tree. I can see that inFoo or inBar are correctly defined and deeper in the tree I find
LetSection
IdentDefs
PragmaExpr
Ident "inFoo"
Pragma
Ident "inject"
Ident "used"
Sym "Bool"
Call
...
Command
Ident "echo"
Call
Ident "callApi"
(in that specific case, callApi returns a string)
But the call callApi goes in the else: branch and I see
echo ["inFoo scope? = ", "false"]
echo ["inBar scope? = ", "false"]
error("You can call API only in foo or bar scopes!",
nil)
The case where it fails is more complex than this example. It is like declaredInScope does not return the correct result in that particular situation. I've tried to look at the code for declaredInScope but that's a magic function and I can't find it to understand how it works.
In case it could be useful to find the source of the problem, compilation fails with the error message (which is true because callApi expects to return a string but that's the error case...)
Error: expression has no type:
echo ["inFoo scope? = ", "false"]
echo ["inBar scope? = ", "false"]
error("You can call API only in foo or bar scopes!",
nil)
Ah, you probably want declared instead of declaredInScope. The latter only works for the immediately enclosing scope and not the ones above it. The documentation may not be very clear on that https://nim-lang.org/docs/system.html#declared%2Cuntyped.
import macros
template foo(body: untyped) =
block:
let inFoo {. inject, used .} = true
body
template bar(body: untyped) =
block:
let inBar {. inject, used .} = true
body
proc callApiFromFoo =
echo "Calling API with foo scope"
proc callApiFromBar =
echo "Calling API with bar scope"
template callApi =
when declared(inFoo):
callApiFromFoo()
elif declared(inBar):
callApiFromBar()
else:
echo "You can call API only in foo or bar scopes!"
proc foobar =
foo:
block:
callApi()
bar:
block:
callApi()
foobar()