Join GitHub today
GitHub is home to over 36 million developers working together to host and review code, manage projects, and build software together.Sign up
Pass typedesc as NimNode to macros #148
I continuously try to fight surprise special cases in the Nim
for example, changing
import macros macro foo(a: int, b: string, c: float): untyped = echo typeof(a) # NimNode echo typeof(b) # NimNode echo typeof(c) # NimNode foo(1,"string", 123.456) # use macro bar(a,b,c: typedesc): untyped = echo typeof(a) # None, should probably be typedesc[int] echo typeof(b) # None, should probably be typedesc[string] echo typeof(c) # None, should probably be typedesc[float] bar(int, string, float)
To make sure that this breaking change does not cause too much harm, I would like to collect all use cases where a macro might take a
The first use case for a
macro baz(a: typedesc[int], b: untyped): untyped = echo "baz int" macro baz(a: typedesc[string], b: untyped): untyped = echo "baz string" macro baz(a: typedesc[float], b: untyped): untyped = echo "baz float" baz(int, a+b) # output: baz int baz(string, a+b) # output: baz string baz(float, a+b) # output: baz float
The second use case is a macro that has several types as an argument, and then it does some logic to select one of them. This result type can then be used in a type expression:
macro selectType(context: typed, a,b,c: typedesc): typedesc = # some random logic here case context.repr.len mod 3: of 0: result = a of 1: result = b else: result = c var myValue = 1 var myOtherValue : selectType(myValue, int, float, string) echo myOtherValue
Since both, argument and result would be changed at the same time
The only direct API that the
macro baz(arg: typedesc): untyped = echo arg baz(seq[int]) # output: None
Well I guess that never worked in the first place, so we don't need to
macro baz2(arg: typedesc): untyped = echo arg.getTypeInst.repr baz2(seq[int]) # output: typeDesc[seq[int]]
macro baz3(arg: typed): untyped = echo arg.getTypeInst.repr baz3(seq[int]) # output: typeDesc[seq[int]]
So as far as I can tell, there isn't any code that would be broken in a horrible way.
So now I would like to take a look at code that would be required to be changed and see how it could be changed it a way that it works in both versions of the compiler old and new.
Assigning a type expression to the result of a macro wont't compile anymore:
# this won't be legal anymore, works only in old macro baz4(): typedesc = result = int # this would work only in new macro baz4(): typedesc = result = bindSym"int" # this would work in both old and new macro baz4(): untyped = result = bindSym"int"
# consistently a typedesc in both now and old macro baz5(arg: static[typedesc]): untyped = arg # consistently a NimNode in both new and old macro baz5(arg: typed): untyped = arg
+1 Ran into this a couple days ago. It was confusing behavior for me. If nothing else comes of this rfc, docs should mention somewhere how typedesc get treated in macros (at least I couldn't find it). Now time to go update my code to use
A bit of history for how we arrived at the current state:
const myTypeList = [int, float, string] var foo: myTypeList
import macros macro myArity(t: typedesc): untyped = let x = t.getTypeImpl.getImpl return newLit(x.len) type MyProcType = proc (x, y, z: int) static: echo MyProcType.myArity macro printArity(procType: typedesc): untyped = let arity = procType.myArity echo arity printArity MyProcType
The expected compile-time output here is
I've had various plans for how to solve this. The nicest solution I can think of is to map the
Please note that a reasonable way to define a "type trait" may be through a simple template like this:
template ElementType(T: typedesc): typedesc = type(default(T)) macro printElementType(T: typedesc) = echo T.ElementType printElementType seq[int]
It would be nice if these work as well.
You can get creative here - Nim should support defining record types with varying set of fields, attaching type-bound operators to the returned types, doing some of the work in helper procs and so on.
This is a breaking change that is arguably less convenient than the current behaviour, but it has the nice property that you can inspect the precise expression that was passed to the macro. This may be important for some DSL and I think it's a property that should be enforced for the
Thank you for clearing things up, it is nice to know about some of the original design goal. I think there are a few problems with the original design goal. I had the same problem, I wanted to access traits of types, but I wanted my own traits, not something that is available from a typetraits module. For example I introduced my own type traits to map Nim types to GLSL types, and I used templates for it:
template glslType(arg: typedesc[Vec4f]): string = "vec4" template glslType(arg: typedesc[Vec4i]): string = "ivec4" const x = glslType(Vec4f) # "vec4" const y = glslType(typeof(myExpr)) # "ivec4"
This pattern allows me attach arbitrary traits to types. But it is not available from within a macro. In macros I usually don't pass in the types as typedesc arguments, I get them by calling
# the explicit call to convert the type to a typedesc is necessary, but it is probably a bug that it isn't implicit. macro foobar(arg: typed): string = result = newCall(bindSym"glslType", newCall(bindSym"typedesc",getTypeInst(arg)))
This does not allow me to access the type trait value at execution time of the macro, but it does allow me to generate calls that access the type traits. When I need the macro value at macro execution time, I just go one level deeper and generate another macro call from the macro:
macro foobar_inner(arg: typed): untyped = echo "foobar_inner: ", arg.lispRepr macro foobar(arg: typed): untyped = result = newCall(bindSym"foobar_inner", newCall(bindSym"glslType", newCall(bindSym"typedesc",getTypeInst(arg)))) var v1: Vec4f var v2: Vec4i foobar(v1) # foobar_inner: (StrLit "vec4") foobar(v2) # foobar_inner: (StrLit "ivec4")
Second, part is. Typetraits API that is in the standard library, for example size or alignment, can easiliy have a second API within macros so that those values can be accessed from NimNodes. Which is already true for NimNode.
The inner-macro solution still uses delayed expansion, which makes it a bit more troublesome to work with. The eager expansion provided by
Otherwise, I agree that the "user-defined traits" through simple templates ought to be supported. My own message covered that. Did you understand the solution I'm proposing for automatically detecting and lifting the expressions where
But if it were NimNode internally in
@Araq, I think you are mistaken. If you don't use
@zah, yes I do understand where you would use the
macro foobar2(arg: typed): untyped = let typeArg = newCall(bindSym"typedesc",getTypeInst(arg)) let tmp = getAst(glslType(typeArg)) echo tmp.treeRepr var v1: Vec4f var v2: Vec4i foobar2(v1) # StrLit "vec4" foobar2(v2) # StrLit "ivec4"
currently this does not compile with the following error:
I know the inner-marco solution is more troublesome to work with, not just a bit. Especially with more than just one attribute call, you have to construct an AST that exists purely to be typechecked, after it is passed to the inner macro, it is deconstructed again. But it is a solution that works today.