n, Crystal has chosen to aggressively optimize away unreachable code
Not exactly. You look at the language expecting full, strict, exact typing, likely with type coercion, everything’s supposed to be compiled as-is, methods declared as-is, etc. This is the complete opposite of what Crystal was ever meant to be: types are optional first, and everything’s basically templated.
Types are always inferred from reality (with some caveats like ivars where we can’t always infer and do ask for the actual type): we only consider concrete types, not potential ones. If some code is unreachable, it’s discarded, because yeah, it’s dead code, but also because we don’t have any information to properly type that piece of code: we don’t have the concrete types to do the analysis and thus can’t do it.
Types are abstract constraints to the actual types. When an argument is typed, anything we give it must match a subset of the full constraint, same for the returned type. Crystal won’t generate one generic method that does everything but multiple specialized methods that will only do what is really called.
Note: the optimization is aggressive for runtime performance reasons, not for faster compile times: it would be simpler to generate one method rather than many.
This shifts the onus of correctness checking from the compiler back to the developer
Types are enforced, they must match any constraint, methods must exists, you can do lots of things with types at compile time, but that only applies to concrete types in reachable code, for the reasons explained above.
without giving the developer any hint on the what or the why
We won’t complain about any unreachable bit of code. The compiler comes with some tools, though:
crystal tool unreachablefor listing unreachable codecrystal too hierarchyto see the whole tree of types
Then, as @nobodywasishere pointed to, a linter could help.