In practice, most modern statically typed languages maintain soundness through a combination of compile time and runtime checking.
Rust, C, C++ - all do not keep around this information.
You're right that the JVM languages (and thus, naturally, C#) do it differently. I think it is a bit silly to name out each of the JVM languages separately.
I think basically every managed statically typed language ends up carrying around runtime representation and uses some amount of runtime checks.
AFAIK - and this might be either dated or just wrong - the two places GHC doesn't fully erase are 1) polymorphic recursion (where if we squint it's probably not unreasonable to treat the dictionaries as type tags), and 2) when the programmer explicitly asks for run-time type information with Typeable.
However, such matches are considered valid code: "non-exhaustive match" is a warning by default, even if most people advise to turn that warning into an error.
In other words, the `Match_failure` exception is part of OCaml non-typed semantics and there is no soundness issue involved here.
For instance
let f x = match x with
| [] -> ()
is translated into (is_empty/267 =
(function x/269 : int
(if x/269
(raise (makeblock 0 (global Match_failure/18!) [0: "r.ml" 1 17]))
1)))
where you can see the exception being build and raised in the `then` branch of the test.
Contrarily, the total function let is_empty x = match x with
| [] -> true
| _ :: _ -> false
becomes (let (is_empty/267 = (function x/269 : int (if x/269 0 1)))
And since the function doesn't need to handle the failure case, it doesn't have that `(raise ...)` case.Another important point is that type system information is only needed to check the exhautiveness of pattern matching in presence of GADTs. Otherwise the exhautiveness of pattern matching can be checked using syntatic criteria on the pattern matching and type definitions.
In the GADTs case, the type system is only used to remove the failure branch. For instance,
type 'a t = A: int t | B: float t
let always_a (x:int t) = match x with
| A -> ()
| _ -> .
is translated to (let (always_a/270 = (function x/272[int] : int 0))
because the typechecker can prove that the `B` case is impossible.
In other words, GADTs are yet another instance where the type system can be used to eliminate dead code in the untyped IR.Makes sense to me.