If I have two templates/macros dsl and options and I want to ensure that the options one can be used only in an englobing dsl one, is it possible?
dsl:
options:
# This construct is correct
...
options:
# This one is not
...
options:
dsl:
# Neither this one...
...
I've tried having the dsl declare a variable and test its presence in options with declaredInScope or compiles, but I can't get it right. How would you do it?
Why not declare options in the dsl scope?
template dsl(body: untyped) =
block:
template options(optionsBody: untyped) =
# ...
body
Because I forgot to say that dsl and options are for users and must be exported. And Nim prevent exporting templates that are not at the top level. The following code does not compile:
template dsl*(body: untyped) =
block:
template options*(optionsBody: untyped) =
# ...
body
As an example this is what unittest does with templates setup and teardown inside suite:
https://github.com/nim-lang/Nim/blob/version-1-2/lib/pure/unittest.nim#L466
As a demonstration of point #3
import macros
template dsl(dslBody: untyped): untyped =
block:
template options(optionsBody: untyped): untyped =
block:
template foo(fooBody: untyped): untyped =
echo "Enter foo"
fooBody
echo "Leave foo"
echo "Enter options"
optionsBody
echo "Leave options"
template options(status: string; optionsBody: untyped): untyped =
block:
echo "Enter options with " & status
options(optionsBody)
echo "Leave options with " & status
echo "Enter dsl"
dslBody
echo "Leave dsl"
expandMacros:
dsl:
options:
echo "Youpi!"
#foo: #<== Undeclared identifier
# echo "1000 $!!!"
options "Oops!":
echo "I failed"
foo:
echo "1000 $!!!"
You see that you can't call the first foo as the compiler greats you with a Error: Undeclared identifier message but accepts the second one even if foo is not declared in that block. What's the heck! Looking at the output gives you a clue:
block:
template options(optionsBody`gensym14461030: untyped): untyped =
block:
template foo(fooBody`gensym14461031_14465004: untyped): untyped =
echo "Enter foo"
fooBody`gensym14461031_14465004
echo "Leave foo"
echo "Enter options"
optionsBody`gensym14461030
echo "Leave options"
template options(status`gensym14461032: string;
optionsBody`gensym14461033: untyped): untyped =
block:
echo "Enter options with " & status`gensym14461032
options(optionsBody`gensym14461033)
echo "Leave options with " & status`gensym14461032
echo ["Enter dsl"]
block:
template foo(fooBody`gensym14461031`gensym14465018: untyped): untyped =
echo "Enter foo"
fooBody`gensym14461031`gensym14465018
echo "Leave foo"
echo ["Enter options"]
echo ["Youpi!"]
echo ["Leave options"]
block:
echo ["Enter options with Oops!"]
block:
template foo(fooBody`gensym14461031`gensym14475005: untyped): untyped =
echo "Enter foo"
fooBody`gensym14461031`gensym14475005
echo "Leave foo"
echo ["Enter options"]
echo ["I failed"]
echo ["Enter foo"]
echo ["1000 $!!!"]
echo ["Leave foo"]
echo ["Leave options"]
echo ["Leave options with Oops!"]
echo ["Leave dsl"]
The templates rewrites have been more prolific than expected!
Using non-imbricated templates is simpler to reason about:
import macros
template foo(fooBody: untyped): untyped =
echo "Enter foo"
fooBody
echo "Leave foo"
template options(optionsBody: untyped): untyped =
block:
echo "Enter options"
optionsBody
echo "Leave options"
template options(status: string; optionsBody: untyped): untyped =
block:
echo "Enter options with " & status
options(optionsBody)
echo "Leave options with " & status
template dsl(dslBody: untyped): untyped =
block:
echo "Enter dsl"
dslBody
echo "Leave dsl"
expandMacros:
dsl:
options:
echo "Youpi!"
foo:
echo "1000 $!!!"
options "Oops!":
echo "I failed"
foo:
echo "1000 $!!!"
And the result matches expectations
block:
echo ["Enter dsl"]
block:
echo ["Enter options"]
echo ["Youpi!"]
echo ["Leave options"]
block:
echo ["Enter options with Oops!"]
block:
echo ["Enter options"]
echo ["I failed"]
echo ["Enter foo"]
echo ["1000 $!!!"]
echo ["Leave foo"]
echo ["Leave options"]
echo ["Leave options with Oops!"]
echo ["Leave dsl"]
So how do you solve the initial template scoping problem? I have to validate it in my project to confirm, but you can try something like:
import macros
template foo(fooBody: untyped): untyped =
when not declaredInScope(inOptions):
{. fatal: "foo car be used only in `options:` block" .}
echo "Enter foo"
fooBody
echo "Leave foo"
template options(optionsBody: untyped): untyped =
when not declaredInScope(inDsl):
{. fatal: "options car be used only in `dsl:` block" .}
block:
let inOptions {. inject, used .} = true
echo "Enter options"
optionsBody
echo "Leave options"
template dsl(dslBody: untyped): untyped =
block:
let inDsl {. inject, used .} = true
echo "Enter dsl"
dslBody
echo "Leave dsl"
expandMacros:
dsl:
options:
echo "Youpi!"
foo:
echo "1000 $!!!"
#options: <== Will not compile, not in dsl!
# echo "I failed"
#foo: <== Will not compile, not in options!
# echo "Not better"
I think you confuse nim templates with template from another language.
Maybe not, but writing DSLs without learning macros is simply not a good idea.
Be sure to checkout the multibody macro: https://github.com/nim-lang/Nim/pull/12567
macro myMacro(body1,body2: untyped): untyped {.multiBodyMacro.} =
echo body1.lispRepr # (StmtList (Command (Ident "echo") (IntLit 1)))
echo body2.lispRepr # (StmtList (Command (Ident "echo") (IntLit 2)))
myMacro:
body1:
echo 1
body2:
echo 2
What's the heck!
The manual is not completely clear AFAIU, but the mistake is very likely that your code expects overloading to work with templates as it does with procedures, which it doesn't. The first options template is an immediate one and doesn't overload with the second (see https://nim-lang.org/docs/manual.html#templates-typed-vs-untyped-parameters). The code can be fixed by giving the first options template a typed dummy status parameter, which makes it non-immediate so that overloading works again.
A more elegant solution is to take the first options template out of the overloading hierarchy like this:
import macros
template dsl(dslBody: untyped): untyped =
block:
macro options(args: varargs[untyped]): untyped =
template tpl(postFix: untyped; args: varargs[untyped]) =
`options postFix`(args)
let postFix = if nnkStmtList == args[0].kind:
"UT".ident
else:
"T".ident
result = getAst(tpl(postFix, args))
template optionsUT(optionsBody: untyped): untyped =
block:
template foo(fooBody: untyped): untyped =
echo "Enter foo"
fooBody
echo "Leave foo"
echo "Enter options"
optionsBody
echo "Leave options"
template optionsT(status: string; optionsBody: untyped): untyped =
block:
echo "Enter options with string " & status
options(optionsBody)
echo "Leave options with string " & status
# add other `optionsT` templates with `status: <sometype>` here..
echo "Enter dsl"
dslBody
echo "Leave dsl"
expandMacros:
dsl:
options:
echo "Youpi!"
foo:
echo "1000 $!!!"
options "Oops!":
echo "I failed"
foo:
echo "1000 $!!!"
(Term rewriting macros might be able to do this in a more compact way, but they are experimental.)
So maybe we add these to your list:
;-)
Good catch! I did not even noticed that my solution was not correct. Templates are so mysterious when you use them out of their sweet spot...
Learning meta-programming in Nim is not easy. First, there are two ways to doing it: templates and macros. ¸The first one seems like programming with usual procs but is limited in what it can do, and the other one requires to learn working with AST but is more powerful. Which one you choose for your task is not clearly explained in the documentation, as it is not that current to find multiple meta-programming layers in a language. But the path to meta-programming is bordered with pitfalls (and I'm quite good at falling into all I encounter!). And the documentation is quite terse and not really didactical on the subject. Presently, I'm using:
I try to document what I find for others, either in that forum or on Nim wiki. But there are still many subjects I haven't enough experience on like:
Perhaps, the Macros Tutorial could be improved by experienced users. Referring beginners to @krux02 projects in More Examples is quite a steep path... Easier examples would be welcome. And no @mratsim, the multibody macro is not a simple example!
I've followed @Araq's advice and converted the most complex templates to macros. I still have some work to do to be sure that the recognized DSL syntax is robust...
We're always improving the documentation but here is a good rule of thumb:
Whenever you need to analyse the AST, you need a macro.