As I define new types in a DSL language, I have templates that generate new templates code for operations supported by these new types. That's code generation.
Also, these types don't support automatic coercion. That is, if the user wrote an int constant, I don't want the nim compiler automatically transform it to a float without the user knowing it. And I want to have precise error messages when the user does type errors when using these new types.
Let's try to reproduce the problem:
import macros
type
Real = object
val: float
# Generated templates for the new type Real
###########################################
template `==`(a: Real, b: float): bool =
a.val == b
template `==`(a: float, b: Real): bool =
a == b.val
# User code
###########
let x = Real(val: 3)
echo x == 4
echo 2 == x
dumpTree:
let x = Real(val: 3)
echo x == 4
echo 2 == x
When this code is compiled, there are no error messages and compilation succeeds. We see from the dumpTree that the compiler has parsed int literals but it has coerced (automatically converted) the 4 and 2 literal int values to floats and was able to resolve the == operators! That's not what I want because the DSL does not mix types the way Nim does...
OK, that's because the templates use typed arguments. Let's change them to untyped ones.
import macros
type
Real = object
val: float
# Generated templates for the new type Real
###########################################
template `==`(a: Real, b: untyped): bool =
if typeof(b) is not float:
raise newException(CatchableError, "Second param " & $b & " is not a float")
a.val == b
template `==`(a: untyped, b: Real): bool =
if typeof(a) is not float:
raise newException(CatchableError, "First param " & $a & " is not a float")
a == b.val
# User code
###########
let x = Real(val: 3)
echo x == 4
echo 2 == x
Again, the user code compiles without error, but now at least I'm able to catch the type mismatch at run time! But the user does not know when he compile his code that there are syntax (types) errors. He will discover later when executing the code, too late!
$ ./foo
foo.nim(17) foo
Error: unhandled exception: Second param 4 is not a float [CatchableError]
Is it possible to write code that will catch the error at compilation time and print a precise error message to the user (not the like of Error: type mismatch: got <Real, int literal(4)> but expected one of ... 30 other mismatching symbols have been suppressed)?
You can use the {.error.} pragma with when to cause a compilation error, you can also give a message
template `==`(a: Real, b: typed): bool =
when typeof(b) is not float:
{.error: "Reals can only be compared to floats".}
a.val == b
If a distinct type is defined for the val field of Real, automatic type conversion is not lifted to it:
type
strictlyFloat = distinct float
Real = object
val: strictlyFloat
Should do what you want.
I already gave an answer in your previous thread and people have answered already, but I'll give an answer that shows more info in the error message. You don't need to import macros for any of this, astToStr and typeof are in the system module.
template `==`(a: Real, b: untyped): bool =
when typeof(b) is not float:
{.error: "Second param " & astToStr(b) & " of == with Real is not a float, but is " & $typeof(b).}
a.val == b
template `==`(a: untyped, b: Real): bool =
when typeof(a) is not float:
{.error: "First param " & astToStr(b) & " of == with Real is not a float, but is " & $typeof(b).}
a == b.val
Thanks TinBryn, gemath and Hlaaftana. All your solutions work in my tiny example. I'll adapt the last one to work in the macro that generate the templates.
The missing ingredient from the other thread is the when test that guarantee that the {.error: msg... .} pragma is printed at compilation time only.
Unfortunately, that is not that simple...
Like I said in the introduction, the templates are generated by another template. So the real code of my project looks like:
template generate(op: untyped, Typ1, Typ2, Tout: untyped) =
template `op`(a: `Typ1`, b: typed): `Tout` =
when typeof(b) isnot `Typ2`:
{.error: "Second param " & astToStr(b) & " of == with Real is not a float, but is " & $typeof(b).}
a.val == b
template `op`(a: typed, b: `Typ1`): `Tout` =
when typeof(a) isnot `Typ2`:
{.error: "First param " & astToStr(b) & " of == with Real is not a float, but is " & $typeof(b).}
a == b.val
# Generate operators for all types
generate(`==`, Real, float, bool)
etc.
...
Now the when code executes when I compile my module while generating the templates for all operators x types, and it aborts compilation (with a cryptic error message like Type expected: got bool or Error: type expected, but got symbol 'bool' of kind 'Template' if I change is not for isnot in the module code!)
Instead, it should happen when the user compiles his code with invalid parameters.
At least, that's how I interpret what's happening. The bug occurs only when I have the 2 lines in the generated templates:
when typeof(b) isnot `Typ2`:
{.error: "Second param " & astToStr(b) & " of == with Real is not a float, but is " &
But there must be another real reason because when I try with all the other types, I don't get the problem; only with the bool parameter. I spent too much time today on that bug to have a clear vision. At some time during the day, when I tried different forms of the code, I got the same error, but now with Nim 1.2.0 the error has changed...
Perhaps it's time to convert this template to a macro and learn how to inject the when ... lines?