Compiler support on production ready error codes

I was wondering if Crystal could fully handle the following:

  1. identify all assertions in the code
  2. number them consecutively (“error code”)
  3. if any of the assertions fails, it has to fail with its error code (this is for the end user)
  4. 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:

  1. I couldn’t get Crystal to put the numbers in the file as well
  2. my solution somehow feels clumsy

Any ideas? Thanks! :slight_smile:

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 :wink:):

  1. I need to tweak the assert logic the way I did it in my first post, but that’s a different story
  2. 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