Debugging the compiler
The Lume compiler isn't impervious to bugs - far from it! This chapter exists to outline how one would start with debugging the compiler, given some bug. Most of this should be transferable to most parts of the compiler.
Helpful flags
When using a development build of the compiler, additional flags are possible to pass to lume. Some of them are
listed here, since they can be useful for some specific issues.
Getting a backtrace on errors
It can sometimes be hard to find out where an error is being emitted from. The --panic-on-error and --track-diagnostics flags can be useful, since they can help you place the exact line an error is being pushed.
-
--panic-on-errordoes exactly what the name implies - when a diagnostic is pushed, it panics. This makes it possible for a developer to print a stacktrace, locating where the error was pushed:$ RUST_BACKTRACE=1 lume build ./sample --panic-on-error [track_diagnostics] pushed from compiler/lume_typech/src/query/mod.rs:238:32 thread 'main' (1317499) panicked at compiler/lume_typech/src/query/mod.rs:238:32: error emitted with `panic_on_error` enabled: mismatched types stack backtrace: 0: __rustc::rust_begin_unwind at /rustc/f04e3dfc87d7e2b6ad53e7a52253812cd62eba50/library/std/src/panicking.rs:698:5 1: core::panicking::panic_fmt at /rustc/f04e3dfc87d7e2b6ad53e7a52253812cd62eba50/library/core/src/panicking.rs:80:14 2: lume_errors::DiagCtxInner::push at ./crates/lume_errors/src/lib.rs:71:13 3: lume_errors::DiagCtx::emit at ./crates/lume_errors/src/lib.rs:156:22 4: lume_typech::query::<impl lume_typech::TyCheckCtx>::check_params at ./compiler/lume_typech/src/query/mod.rs:238:32 5: lume_typech::query::<impl lume_typech::TyCheckCtx>::check_signature at ./compiler/lume_typech/src/query/mod.rs:78:14 (~~~~ LINES REMOVED FOR BREVITY ~~~~) -
--track-diagnosticsis very similar, expect it does not panic. Instead, it prints a single line for each diagnostic, outlining where the diagnostic was pushed from:$ lume build ./sample --track-diagnostics [track_diagnostics] pushed from compiler/lume_typech/src/query/mod.rs:238:32 × error[LM4001]: mismatched types ╭─[/Users/max/Documents/Projects/Lume/samples/playground/main.lm:17:17] 16 │ let a = 1_i32; 17 │ let b = a + false; ∶ ^^^^^ expected type Int32, but found type Boolean... 18 │ } ╰── ╭─[int.lm:80:34] 79 │ use Add<Int32> in Int32 { 80 │ fn external add(self, other: Int32) -> Int32 ∶ ^^^^^ ...because of type defined here 81 │ } ╰── help: expected type Int32 found type Boolean
Dumping the MIR
When debugging an issue which only shows itself later in the compilation process, it might be useful to verify
that the MIR looks correct. To dump the MIR to the console output, you can pass the --dump-mir flag:
$ lume build ./sample --dump-mir
(~~~~ LINES REMOVED FOR BREVITY ~~~~)
@!3848283137416547773 fn "std::Range::new" (start: i32 #0, end: i32 #1) -> ptr std::Range {
B0:
let #2: metadata std::Range = metadata std::Range()
#3 = alloc std::Range
mark object(#3)
*#3[+x0] = #2
*#3[+x8] = #0
*#3[+xC] = #1
let #4: ptr std::Range = +(#3, 8_u64)
mark object(#4)
return #4
}
Sometimes, an issue crops up because of a specific optimization pass. You can print the MIR before a specific pass
is invoked by passing the name of the pass to the --dump-mir flag:
$ lume build ./sample --dump-mir=mark_gc_refs
(~~~~ LINES REMOVED FOR BREVITY ~~~~)
@!3848283137416547773 fn "std::Range::new" (start: i32 #0, end: i32 #1) -> ptr std::Range {
B0:
let #2: metadata std::Range = metadata std::Range()
#3 = alloc std::Range
*#3[+x0] = #2
*#3[+x8] = #0
*#3[+xC] = #1
let #4: ptr std::Range = +(#3, 8_u64)
return #4
}
Tracing the compiler
It might sometimes be important to see the execution path of the compiler, to see why a certain operation
is executed. For that reason, the compiler has #[traced] attributes on most methods, which can print out
some information about the method call - passed arguments, return values, whether an error was returned, etc.
To see these logs, you need to enable the tracing feature on the compiler. Tracing is disabled by default, since
it can cause significant performance loss.
To enable it, you must have a local copy of the compiler and have passed the given command when building:
$ cargo build --bin lume --features tracing
You can then pass log filters via the LUMEC_LOG environment variable when running the compiler.
Filtering traces
While you could print all traces to the console, this is likely not what you want. Given a function like this:
#[traced(level = Debug, fields(id))]
fn compiler_function(id: NodeId) -> TypeRef {}
you can print all it's invocations by using:
LUMEC_LOG=compiler_function=debug
which will cause all calls to compiler_function to be logged, along with the passed id argument.
This is under the assumption that type_of_expr exists without any parent module or external crate. You'll more
like see log filters like the following:
LUMEC_LOG=lume_infer::query::type_of=trace
lume_infer::query::type_ofis the full path of the method, including crate and module path.
Filter specific invocations
You can also filter traces by the arguments which are passed to them. Each trace has a list of arguments
which are added as "fields" to the trace. For example, the given method within the compiler
(location in the lume_typech crate, inside the query module):
#[traced(level = Trace, fields(name = expr.name()), err, ret)]
fn check_params(
&self,
expr: lume_hir::CallExpression<'_>,
parameters: &lume_types::Parameters,
arguments: &[&lume_hir::Expression],
) -> Result<bool> {}
has a field name equal to the name of the passed call expression. So, to filter all calls to check_params
which received a call expression to my_function, we can use the following filter:
LUMEC_LOG=lume_typech::query::check_params[name=my_function]=trace
For more information about the filtering, you can read the libftrace documentation about the feature.