I was wondering if Crystal could fully handle the following:
- identify all
assert
ions in the code
- number them consecutively (“error code”)
- if any of the
assert
ions fails, it has to fail with its error code (this is for the end user)
- in addition create a file with a full mapping of all “error codes” to “source files and lines” (this is for the author only)
This is as far as I could get:
ERROR_CODEFILE = "generated-errorcodes.txt"
{% system("rm #{ERROR_CODEFILE}; true") %}
ENV["XX"] = "0" # since assert() always increments first, we effectively start with "1"
macro assert(invariant, f=__FILE__, l=__LINE__)
ENV["XX"] = (ENV["XX"].to_i+1).to_s
{% system("echo #{f}, #{l} >> #{ERROR_CODEFILE}") %}
{% if invariant %}
if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
else
raise("error code ##{ENV["XX"]}")
end
{% else %}
raise("error code ##{ENV["XX"]}")
{% end %}
end
assert(false)
assert(false)
I get something like this after build:
# generated-errorcodes.txt
.../macro.cr, 18
.../macro.cr, 19
And this after running:
Unhandled exception: error code #1 (Exception)
So, I practically have only those two points open:
- I couldn’t get Crystal to put the numbers in the file as well
- my solution somehow feels clumsy
Any ideas? Thanks! 
Neat use case! It looks like you’re storing and using the error code at runtime, when “hardcoding” the error code at compile time will probably tidy things up a bit. Also, we can take advantage that the macro language can manipulate its own variables too (including constants, to a degree):
ERROR_CODE_PROPERTIES = {
file_name: "generated-errorcodes.txt",
current_code: 0
}
{% system("rm #{ERROR_CODE_PROPERTIES[:file_name]}; true") %}
macro assert(invariant, f=__FILE__, l=__LINE__)
{%
ERROR_CODE_PROPERTIES[:current_code] += 1
system("echo #{f}, #{l}, #{ERROR_CODE_PROPERTIES[:current_code]} >> #{ERROR_CODE_PROPERTIES[:file_name]}")
%}
raise("error code #{{ERROR_CODE_PROPERTIES[:current_code]}}") unless {{invariant}}
end
assert(false)
assert(false)
Or something like that. It can probably be cleaned up a bit more too, and I haven’t tested the above either, but have written macros that update and modify constants in this way before at compile time.
Thanks a lot, this definitely looks way better! It also works for my simple examples.
And, of course, there’s a “but” (actually it’s two
):
- I need to tweak the assert logic the way I did it in my first post, but that’s a different story
- this is the bigger one: it breaks the compiler once more:
ERROR_CODE_PROPERTIES = {
file_name: "generated-errorcodes.txt",
current_code: 0
}
{% system("rm #{ERROR_CODE_PROPERTIES[:file_name]}; true") %}
macro assert(invariant, f=__FILE__, l=__LINE__)
{%
ERROR_CODE_PROPERTIES[:current_code] += 1
system("echo \"#{ERROR_CODE_PROPERTIES[:current_code]}; #{f}; #{l}\" >> #{ERROR_CODE_PROPERTIES[:file_name]}")
%}
{% if invariant %}
if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
else
raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
end
{% else %}
raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
{% end %}
end
class X
def initialize
end
def do(&block)
assert(false)
end
end
x = X.new
x.do {}
x.do {}
… gives…
Module validation failed: GEP base pointer is not a vector or a vector of pointers
%42 = getelementptr inbounds %"NamedTuple(file_name: String, current_code: Int32)", i8 %41, i32 0, i32 1, !dbg !97
(Exception)
from /crystal/src/llvm/module.cr:73:9 in 'codegen'
from /crystal/src/compiler/crystal/compiler.cr:172:16 in 'compile:combine_rpath'
from /crystal/src/compiler/crystal/compiler.cr:165:56 in 'compile:combine_rpath'
from /crystal/src/compiler/crystal/command.cr:227:5 in 'run_command'
from /crystal/src/compiler/crystal/command.cr:125:10 in 'run'
from /crystal/src/compiler/crystal.cr:11:1 in '__crystal_main'
from /crystal/src/crystal/main.cr:129:5 in 'main'
from src/env/__libc_start_main.c:95:2 in 'libc_start_main_stage2'
Error: you've found a bug in the Crystal compiler. Please open an issue, including source code that will allow us to reproduce the bug: https://github.com/crystal-lang/crystal/issues
(showing up on both 1.9.2 and 1.10.1, Ubuntu 18.04)
I’ll post it on crystal…
If you’re curious about my “but #1” from above, here is some code:
ERROR_CODE_PROPERTIES = {
file_name: "generated-errorcodes.txt",
current_code: 0
}
{% system("rm #{ERROR_CODE_PROPERTIES[:file_name]}; true") %}
# -------------- three different implementations for `assert` follow
# variant A: this yields a compiler error message for some use cases (see tests below)
macro assert(invariant, f=__FILE__, l=__LINE__)
{%
ERROR_CODE_PROPERTIES[:current_code] += 1
system("echo #{f}, #{l}, #{ERROR_CODE_PROPERTIES[:current_code]} >> #{ERROR_CODE_PROPERTIES[:file_name]}")
%}
raise("error code #{{ERROR_CODE_PROPERTIES[:current_code]}}") unless {{invariant}}
end
# # variant B: this macro works for all tests below
# macro assert(invariant, f=__FILE__, l=__LINE__)
# {% if invariant %}
# if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
# else
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# end
# {% else %}
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# {% end %}
# end
# # variant C: this macro crashes compiler for some use cases (see tests below)
# macro assert(invariant, f=__FILE__, l=__LINE__)
# {%
# ERROR_CODE_PROPERTIES[:current_code] += 1
# system("echo \"#{ERROR_CODE_PROPERTIES[:current_code]}; #{f}; #{l}\" >> #{ERROR_CODE_PROPERTIES[:file_name]}")
# %}
# {% if invariant %}
# if {{invariant}} # see https://github.com/crystal-lang/crystal/issues/13209
# else
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# end
# {% else %}
# raise("error code #{ERROR_CODE_PROPERTIES[:current_code]}")
# {% end %}
# end
# ------------------- some tests follow
def x(arg : Int32|Nil) : Int32
assert(!arg.nil?) # this compiles with all three variants
arg + 1
end
def y : Int32
# raise("foo") # this compiles with all three variants
assert(false) # this only compiles with variant B (error on A, crash on C)
end
x(ARGV.size==0 ? nil : 42)
y
assert(false)
assert(false)
1 Like