Hacker News new | ask | show | jobs
by LoganDark 4 days ago
> Someone in the audience also pointed out that a tool like this could be used to compile rust programs, not just C programs, in which case it's odd to hear Fil-C repeatedly framed as a language in opposition to rust rather than as a tool which might complement it.

You can combine both approaches for sure, but that doesn't change that they are indeed separate approaches. One (Rust) intends to characterize and prevent undefined behavior at compile time, and another (Fil-C) intends to make undefined behavior impossible at runtime, as evidenced by decisions like (for example) making unsafe usage of setjmp/longjmp safe.

You could sort of achieve "safety-in-depth" by combining both approaches, but Rust prefers to prevent all undefined behavior statically, and Fil-C prefers to focus on dynamic approaches. They have real differences, so that's probably what comes off as "oppositional"

3 comments

This presentation wasn’t too bad in terms of us vs theming, but in general throughout the lifetime of the project there’s been a strong us vs them rhetoric. I think it’s working as a marketing tactic to some extent but there are better, albeit harder ways to market the thing.
Rust generally invites us vs them everywhere because it's just so ambitious and attractive (to some). Fil-C probably receives questions all the time about why they don't just do X or Y that Rust does. Maybe they end up supporting their position against that type of pressure and it looks like they're opposing Rust when all they're doing is justifying themselves.
Because by making Cyclone's ideas mainstream, which AT&T started alongside Cornell University, the C and C++ devs that so far felt safe from all those RC/GC languages, now had an actually problem as companies started paying attention and embracing the language's ideas, even extending the type system of existing RC/GC languages.

Thus anyone that identifies themselves with the programming language they work with, gets dragged into a us vs them discussion.

Arguably the most important memory safety property, i.e. bounds checking for dynamic arrays, is also not statically checked in Rust.
It's possible to do this (WUFFS does it) but it's so invasive that, well, you basically end up with WUFFS. You end up having to track every set of possible and impossible values at every location, etc. (during each program location too.) WUFFS certainly has its place and I've kind of been itching to try it out myself, but I think getting to the point where dynamic arrays can be statically bounds-checked would have highly disabled Rust.
How would Rust perform static checks on dynamic bounds? That seems like an impossible order, i.e. not a reasonable evaluation criteria for any (general-purpose) language.

(I'm separately skeptical that it's the most important memory safety property; I suspect that a review of Chrome and Firefox 0days would show that UAFs and type confusion are, at least to attackers, equally if not more important.)

Presumably, they're talking about the possibility of explicitly replacing dynamic bounds checks in cases where it's statically provable that the access will not overrun. That doesn't necessarily mean you lose cases where you genuinely can't prove such a thing, but rather that it would be possible to opt into compiler assistance with proving it, rather than hoping LLVM will have your back. At least, that's how I'd envision such a thing for Rust. This would be similar to how you can already choose whether to invite more of the borrow checker by using references directly, or to do the checks at runtime with Rc/RefCell, etc.
Yeah, that seems like a nice thing that Rust could offer. It strikes me as a weird thing to get hung up on, though, given that the norm in compiled languages - including C - is to express your bounds such that an optimizing compiler can (but won't necessarily) eliminate them.

(You mentioned WUFFS below, which is why I qualified with general-purpose! One thing that WUFFS does that I think Rust could add pretty easily is provable indexing, e.g. allow me to use a `u8` to index a `[u8; 256]` without having to widen to a `usize` first and hope that LLVM optimizes it back out.)

> rather than hoping LLVM will have your back

My thought here is to proactively verify that LLVM elided the automatic bounds checks in places where you believe that your explicit checks should be sufficient.

That was a key part of my article on "No-Panic Rust": https://blog.reverberate.org/2025/02/03/no-panic-rust.html ("A Dance With The Optimizer")

I merely pointed out that the statement "Rust prefers to prevent all undefined behavior statically" is misleading in the sense that Rust does not do this for all undefined behavior.
Maybe if you think of [] as offsetting a pointer rather than calling into an Index (or IndexMut) implementation. Since the return type isn't optional, a panic is the only way to avoid performing an invalid access or manufacturing an unfaithful return value. There are also optional accessors which do not panic, and unsafe/unchecked accessors.
Maybe? I think my statement is clearly correct in the way I formulated it.
ATS could probably do some of this by constructing proofs about the bounds and indices - not in the completely general case of course, but for at least some fraction of what a language without dependent types would have to defer to runtime
those are not dynamic bounds.
All "dynamic" means is that you don't know or don't prove the precise value statically. However you may know the range of possible values, or you may know properties of your algorithm that mean it can never attempt an out-of-bounds access. Sometimes you don't know any of these things, but sometimes you do.
What I was (badly) trying to express was more that given static bounds rust could also eliminate dynamic checks. So saying e.g. ATS can eliminate static checks, is kind of switching the target.
Rust can not do this. But the original claim is that it is all statically checked in Rust, and this is an obvious counter example. Some dependently types languages can prove this statically, also model checking can do this, etc. So it can be done statically as well, but not in Rust.
> But the original claim is that it is all statically checked in Rust

Where? The GP's comment says "prefers," which I didn't read to mean a blanket statement. I don't think anybody with more than passing experience in Rust (or C) would make such a claim.

> Some dependently types languages can prove this statically, also model checking can do this, etc. So it can be done statically as well, but not in Rust.

You're couching the part where it can't be done with full generality or can be done with full generality, but with punishing semantics. The appropriate comparison here is with other normal general purpose compiled languages.

I read "Rust prefers to prevent all undefined behavior statically," to also imply that it actually does this, because otherwise the word "all" would not make sense to me in this sentence.

Dependently types languages and model checking do exist. They come with tradeoffs, but this is also true for Rust.

This isn't entirely true. Rust's indexing operator performs a dynamic check but the language and stdlib have a number of ways to access array elements without indexing, which is effectively a form of static checking. For example, the following loop is statically guaranteed to not go OOB, and will not emit any unnecessary bounds checks:

  for i in some_arr {
    println!("{i}");
  }
I suspect most techniques you could think of to statically avoid bounds checks are possible in rust's type system.
These are not terribly interesting cases though, as it would also be impossible to get an oob access in other languages either.
Yes, rust didn't invent the concept of an iterator, and is thus not the only language which offers a solution to statically avoid bounds checks. Feel free to give a more "interesting" case you believe rust wouldn't be able to handle.
Rust fails to prove basic indirection to be statically safe and instead does a run-time check.

  fn main() {
    let v = vec![1, 2, 3];
    v[5];
  }
If this was an array, that is, a built-in type with a static length, this example does give a compile-time error: https://play.rust-lang.org/?version=stable&mode=debug&editio...

However, a vector's length is inherently a runtime concept, and is a user-defined library type, so the compiler does not attempt to directly reason about this at compile time. However, for this example, post-optimizations, it doesn't do a runtime check at all: it calls the panic directly, because the optimizer does in fact reason that this always panics and removes the dynamic check.

It is true that Rust inserts dynamic checks for things that it can't prove statically, but so do dependently typed languages. "Please read an integer from stdin and then load that element of an array" is not possible to statically check, it must rely on runtime checks, definitionally.

Now it's your example that's too simple. I can't present a counter-example that wouldn't look totally different: if you avoid the indexing operator then trivially incorrect code like that becomes unrepresentable. It's like if I said destructors help you avoid memory leaks and you asked how you'd avoid a leak in a single-function program that does nothing but call `Box::leak()`.

Again: I'm not disputing the fact that the rust index operator does a dynamic bounds check.

But that's also arguably pretty boring because it's very easy to dynamically check and the cost for doing so is relatively small compared to what needs to be done for other properties.

On the other hand statically checking bounds for (dynamic and non) arrays is very very hard in the general case.

The combination of static safety gracefully degrading to runtime safety where not provable is interesting to me
This is sort of already possible in Rust when you use safe primitives (e.g., RefCell), but it doesn't cover unsafe code invoking Undefined Behavior. Placing something like Fil-C around unsafe code could prove to be interesting, especially since current approaches rely on interpretation (Miri, Soteria Rust) rather than translation.