Hacker News .hnnew | past | comments | ask | show | jobs | submit | kbolino's commentslogin

Floating-point non-determinism issues usually come from one of these sources:

- Inconsistent use of floating-point precision, which can arise surprisingly easily if e.g. you had optional hardware acceleration support, but didn't truncate the intermediate results after every operation to match the smaller precision of the software version and other platforms.

- Treating floating-point arithmetic as associative, whether at the compiler level, the network level, or even the code level. Common violations here include: changing compiler optimization flags between versions (or similar effects from compiler bugs/changes), not annotating network messages or individual events with strictly consistent sequence numbers, and assuming it's safe to "batch" or "accumulate" deltas before applying them.

- Relying on hardware-accelerated trigonometric etc. functions, which are implemented slightly differently from FPU to FPU, since these, unlike basic arithmetic, are not strictly specified by IEEE. There are many reasons for them to vary, too, like: which argument ranges should be closest to correct, should a lookup table be used to save time or avoided to save space, after how many approximation iterations should the computation stop, etc.


There's another gotcha. Consider positive, normal x and y where ulp(y) != ulp(x). Bitwise comparison, regardless of tolerance, will consider x to be far from y, even though they might be adjacent numbers, e.g. if y = x+ulp(x) but y is a power of 2.

This case actually works because for finite numbers of a given sign, the integer bit representations are monotonic with the value due to the placement of the exponent and mantissa fields and the implicit mantissa bit. For instance, 1.0 in IEEE float is 0x3F800000, and the next immediate representable value below it 1.0-e is 0x3F7FFFFF.

Signed zero and the sign-magnitude representation is more of an issue, but can be resolved by XORing the sign bit into the mantissa and exponent fields, flipping the negative range. This places -0 adjacent to 0 which is typically enough, and can be fixed up for minimal additional cost (another subtract).


I interpreted OP's "bit-cast to integer, strip few least significant bits and then compare for equality" message as suggesting this kind of comparison (Go):

  func equiv(x, y float32, ignoreBits int) bool {
      mask := uint32(0xFFFFFFFF) << ignoreBits
      xi, yi := math.Float32bits(x), math.Float32bits(y)
      return xi&mask == yi&mask
  }
with the sensitivity controlled by ignoreBits, higher values being less sensitive.

Supposing y is 1.0 and x is the predecessor of 1.0, the smallest value of ignoreBits for which equiv would return true is 24.

But a worst case example is found at the very next power of 2, 2.0 (bitwise 0x40000000), whose predecessor is quite different (bitwise 0x3FFFFFFF). In this case, you'd have to set ignoreBits to 31, and thus equivalence here is no better than checking that the two numbers have the same sign.


Yeah, that's effectively quantization, which will not work for general tolerance checks where you'd convert float similarity to int similarity.

There are cases where the quantization method is useful, hashing/binning floats being an example. Standard similarity checks don't work there because of lack of transitivity. But that's fundamentally a different operation than is-similar.


I don't think this is true. Modulo the sign bit, the "next float" operator is equivalent to the next bitstring or the integer++.

Sure, but that operator can propagate a carry all the way to the most significant bit, so a check for bitwise equality after "strip[ping] few least significant bits" will yield false in some cases. The pathologically worst case for single precision, for example, is illustrated by the value 2.0 (bitwise 0x40000000) and its predecessor, which differ in all bits except the sign.

Fil-C has two major downsides: it slows programs down and it doesn't interoperate with non-Fil-C code, not even libc. That second problem complicates using it on systems other than Linux (even BSDs and macOS) and integrating it with other safe languages.

You’re not wrong but both problems could be alleviated by sending patches :-)

I would never say it's impossible, and you've done some amazing work, but I do wonder if the second problem is feasibly surmountable. Setting aside cross-language interop, BYOlibc is not really tolerated on most systems. Linux is fairly unique here with its strongly compatible syscall ABI.

You're right that it's challenging. I don't think it's infeasible.

Here's why:

1. For the first year of Fil-C development, I was doing it on a Mac, and it worked fine. I had lots of stuff running. No GUI in that version, though.

2. You could give Fil-C an FFI to Yolo-C. It would look sort of like the FFIs that Java, Python, or Ruby do. So, it would be a bit annoying to bridge to native APIs, but not infeasible. I've chosen not to give Fil-C such an FFI (except a very limited FFI to assembly for constant time crypto) because I wanted to force myself to port the underlying libraries to Fil-C.

3. Apple could do a Fil-C build of their userland, and MS could do a Fil-C build of their userland. Not saying they will do it. But the feasibility of this is "just" a matter of certain humans making choices, not anything technical.


> it slows programs down

Interesting, how costly would be hardware acceleration support for Fil-C code.


I think there's two main avenues for hardware acceleration: pointer provenance and garbage collection. The first dovetails with things like CHERI [1] but the second doesn't seem to be getting much hardware attention lately. It has been decades since Lisp Machines were made, and I'm not aware of too many other architectures with hardware-level GC support. There are more efficient ways to use the existing hardware for GC though, as e.g. Go has experimented with recently [2].

[1]: https://en.wikipedia.org/wiki/Capability_Hardware_Enhanced_R...

[2]: https://go.dev/blog/greenteagc


There are algorithms to align allocations and use metadata in unused pointer bits to encode object start addresses. That would allow Fil-C's shadow memory to be reduced to a tag bit per 8-byte word (like 32-bit CHERI), at the expense of more bit shuffling. But that shuffling could certainly be a candidate for hardware acceleration.

There is a startup working on "Object Memory Addressing" (OMA) with tracing GC in hardware [1], and its model seems to map quite well to Fil-C's. I have also seen a discussion on RISC-V's "sig-j" mailing list about possible hardware support for ZGC's pointer colours in upper pointer bits, so that it wouldn't have to occupy virtual memory bits — and space — for those.

However, I think that tagged pointers with reference counting GC could be a better choice for hardware acceleration than tracing GC. The biggest performance bottleneck with RC in software are the many atomic counter updates, and I think those could instead be done transparently in parallel by a dedicated hardware unit. Cycles would still have to be reclaimed by tracing but modern RC algorithms typically need to trace only small subsets of the object graph.

[1]: "Two Paths to Memory Safety: CHERI and OMA" https://hackernews.hn/item?id=45566660


I think these are all valid arguments, but I do want to point out that Java is addressing them.

The first bullet is possible with the JetBrainsRuntime, a fork of OpenJDK: https://github.com/JetBrains/JetBrainsRuntime

The second bullet is a headline goal of Project Valhalla, however it is unlikely to be delivered in quite the way that a C# (or Go or Rust etc.) developer might expect. The ideal version would allow any object with purely value semantics [1] to be eligible for heap flattening [2] and/or scalarization [3], but in experimental builds that are currently available, the objects must be from a class marked with the "value" qualifier; importantly, this is considered an optimization and not a guarantee. More details: https://openjdk.org/projects/valhalla/value-objects

The third bullet (IIUC) is addressed with the Foreign Function & Memory API, though I'll admit what I've played around with so far is not nearly as slick as P/Invoke. See e.g. https://openjdk.org/jeps/454

[1] value semantics means: the object is never on either side of an == or != comparison; the equals and hashCode methods are never called, or are overridden and their implementation doesn't rely on object identity; no methods are marked synchronized and the object is never the target of a synchronized block; the wait, notify, and notifyAll methods are never called; the finalize method is not overridden and no cleaner is registered for the object; no phantom or weak references are taken of the object; and probably some other things I can't think of

[2] heap flattening means that an object's representation when stored in another object's field or in an array is reduced to just the object's own fields, removing the overhead from storing references to its class and monitor lock

[3] scalarization means that an object's fields would be stored directly on the stack and passed directly through registers


The third bullet is also presumably referring to C#'s ancient wider support for unsafe { } blocks for low level pointer math as well as the modern tools Span<T> and Memory<T> which are GC-safe low level memory management/access/pointer math tools in modern .NET. Span<T>/Memory<T> is a bit like a modest partial implementation of Rust's borrowing mechanics without changing a lot of how .NET's stack and heap work or compromising as much on .NET's bounds checking guarantees through an interesting dance of C# compiler smarts and .NET JIT smarts.

The FFM API actually does cover a lot of the same ground, albeit with far worse ergonomics IMO. To wit,

- There is no unsafe block, instead certain operations are "restricted", which currently causes them to emit warnings that can be suppressed on a per-module basis; it seems the warnings will turn into exceptions in the future

- There is no "fixed" statement and frankly nothing like it all, native code is just not allowed to access managed memory period; instead, you set up an arena to be shared between managed and native code

- MemorySegment is kinda like Memory<T>/Span<T> but harder to actually use because Java's type-erased generics are useless here

- Setting up a MemoryLayout to describe a struct is just not as nice as slapping layout attributes on an actual struct

- Working with VarHandle is just way more verbose than working with pointers


> - There is no unsafe block, instead certain operations are "restricted", which currently causes them to emit warnings that can be suppressed on a per-module basis; it seems the warnings will turn into exceptions in the future

Which sounds funny because C# effectively has gone the other direction. .NET's Code Access Security (CAS) used to heavily penalize unsafe blocks (and unchecked blocks, another relative that C# has that I don't think has a direct Java equivalent), limiting how libraries could use such blocks without extra mandatory code signing and permissions, throwing all sorts of weird runtime exceptions in CAS environments with slightly wrong permissions. CAS is mostly gone today so most C# developers only ever really experience compiler warnings and warnings-as-errors when trying to use unsafe (and/or unchecked) blocks. More libraries can use it for low level things than used to. (But also fewer libraries need to now than used to, thanks to Memory<T>/Span<T>.)

> There is no "fixed" statement and frankly nothing like it all, native code is just not allowed to access managed memory period; instead, you set up an arena to be shared between managed and native code

Yeah, this seems to be an area that .NET has a lot of strengths in. Not just the fixed keyword, but also a direct API for GC pinning/unpinning/locking and many sorts of "Unsafe Marshalling" tools to provide direct access to pointers into managed memory for native code. (Named "Unsafe" in this case because they warrant careful consideration before using them, not because they rely on unsafe blocks of code.)

> MemorySegment is kinda like Memory<T>/Span<T> but harder to actually use because Java's type-erased generics are useless here

It's the ease of use that really makes Memory<T>/Span<T> shine. It's a lot more generally useful throughout the .NET ecosystem (beyond just "foreign function interfaces") to the point where a large swathe of the BCL (Base Class Library; standard library) uses Span<T> in one fashion or another for easy performance improvements (especially with the C# compiler quietly preferring Span<T>/ReadOnlySpan<T> overloads of functions over almost any other data type, when available). Span<T> has been a "quiet performance revolution" under the hood of a lot of core libraries in .NET, especially just about anything involving string searching, parsing, or manipulation. Almost none of those gains have anything to do with calling into native code and many of those performance gains have also been achieved by eliminating native code (and the overhead of transitions to/from it) by moving performance-optimized algorithms that were easier to do unsafely in native code into "safe" C#.

It's really cool what has been going on with Span<T>. It's really wild some of the micro-benchmarks of before/after Span<T> migrations.

Related to the overall topic, it's said Span<T> is one of the reasons Unity wants to push faster to modern .NET, but Unity still has a ways to go to where it uses enough of the .NET coreclr memory model to take real advantage of it.


Yeah, coming to C# from Rust (in a project using both), I’ve been extremely impressed by the capabilities of Span<T> and friends.

I’m finding that a lot of code that would traditionally need to be implemented in C++ or Rust can now be implemented in C# at no or very little performance cost.

I’m still using Rust for certain areas where the C# type system is too limited, or where the borrow checker is a godsend, but the cooperation between these languages is really smooth.


For those wondering, Unity overloaded the == operator in two specific situations, such that obj == null will return true even though obj is not actually null. More details on this archived blogpost: https://web.archive.org/web/20140519040926/https://blogs.uni...

Actually, Windows by default will not trust code signed by CAs that issue certificates to websites. It will only trust code signed by CAs that are approved for code-signing, which isn't a very large set anymore. Moreover, recent CA/Browser Forum policies forbid dual-use CAs anyway. If Let's Encrypt issues you a certificate for a web site, it cannot be used for code signing.

It's possible that they could start issuing separate certificates upon specific request for code signing purposes, but it's doubtful they would be willing to meet Microsoft's requirements for such certificates, so their code-signing CA would not be added to Windows's trust store, rendering the certificates it issues useless.

The ACME protocol, the key automation technology that makes Let's Encrypt possible, performs domain validation only. It verifies that you, the person (or bot) making the request, are an authorized administrator of the DNS records, or port-80 HTTP server, for that domain. This is directly relevant to, and generally considered sufficient for, HTTPS.

However, domain validation is almost completely irrelevant to, and insufficient for, code signing. Microsoft's rules (and Apple's, incidentally) require establishing the identity of a legal person (individual, or preferably, company). There is no way the ACME protocol can do this, which means that the process is totally out of Let's Encrypt's wheelhouse.


> However, domain validation is almost completely irrelevant to, and insufficient for, code signing.

It's actually the only thing that provides any kind of assurance to users. It's not like end users know if FuzzCo is the correct developer for FooApp but they know fooapp.com.


A web site is identified by its URL, which contains its domain. Any good HTTPS implementation cross-checks the requested domain against the SANs of the cert, and does so automatically.

There is nothing in a piece of random software obtained from some random source that authoritatively connects it with a particular domain. Without bringing an App Store or other walled garden into the picture, the operating system must evaluate an executable file according to the contents of the file itself. On cold launch, the information in the certificate can be presented to the user, and the certificate issuer can be checked against the O/S trust store, but nothing equivalent to the HTTPS domain check can be done.

DV certs work for the web because of that intrinsic connection between web site and domain. They fail for arbitrary software because of the lack of such a connection. The trustworthiness of code-signing certs comes from the relatively difficult process necessary to obtain them, and not the name attached to them. The identifiable legal entity to which the certificate was issued is more useful to the O/S vendor, as a harder-to-evade ban target, than it is to the end user.


Both Borgo and now Lisette seem to act as though (T, error) returns are equivalent to a Result<T, error> sum type, but this is not semantically valid in all cases. The io.Reader interface's Read method, for example, specifies not only that (n!=0, io.EOF) is a valid return pattern, but moreover that it is not even an error condition, just a terminal condition. If you treat the two return values as mutually exclusive, you either can't see that you're supposed to stop reading, or you can't see that some number of valid bytes were placed into the buffer. This is probably well known enough to be handled specifically, but other libraries have been known to make creative use of the non-exclusivity in multiple return values too.


You are right, and thank you for pointing this out. I've opened an issue:

https://github.com/ivov/lisette/issues/12

I have a few approaches in mind and will be addressing this soon.


I gave Lisette a run today. I really like it, its a clear improvement to Go.

Here a few things that i noticed.

- Third party Go code support (like go-chi) is a absolute must have. This is THE feature that will possibly sky-rocket Lisette adoption. So something like stubs etc, maybe something like ReScript has for its JS interop (https://rescript-lang.org/docs/manual/external). The cli tool could probably infer and make these stubs semi-easily, as the go typesystem is kind of simple.

- The HM claim did confuse me. It does not infer when matching on an Enum, but i have to manually type the enum type to get the compiler to agree on what is being matched on. Note, this is a HARD problem (ocaml does this probably the best), and maybe outside the scope of Lisette, but maybe tweak the docs if this is the case. (eg. infers somethings, but not all things)

- Can this be adopted gradually? Meaning a part is Go code, and a part generated from Lisette. Something like Haxe perhaps. This ties to issue 1 (3rd party interop)

But so far this is the BEST compile to Go language, and you are onto something. This might get big if the main issues are resolved.


Thanks for trying it out!

Variant qualification is a name resolution requirement - Lis follows Rust's scoping model where variants are namespaced under the enum. The implementation correctly infers the type, as shown e.g. in the hint `help: Use Shape.Circle to match this variant` My understanding is HM has nothing to say about this; it operates after names are resolved.

Re: Go third-party packages + incremental adoption, I'll do my best! Thanks for the encouragement.


To be fair, I feel like the language is widely criticized for this particular choice and it's not a pattern you tend to see with newer APIs.

It's a really valid FFI concern though! And I feel like superset languages like this live or die on their ability to be integrated smoothly side-by-side with the core language (F#, Scala, Kotlin, Typescript, Rescript)


To be honest you could easily mark this as an additional (adt) type if that suits you better. Its a halting situation no matter how you twist it.


I think you're assuming that LCDs all have framebuffers, but this is not the case. A basic/cheap LCD does not store the state of its pixels anywhere. It electrically refreshes them as the signal comes in, much like a CRT. The pixels are blocking light instead of emitting it, but they will still fade out if left unrefreshed for long. So, the simple answer is, you can't get direct access to something when it doesn't even exist in the first place.


I touched on the issues with FTP itself in another comment, but who can forget the issues with HTTP+FTP, like: modes (644 or 755? wait, what is a umask?), .htaccess, MIME mappings, "why isn't index.html working?", etc. Every place had a different httpd config and a different person you had to email to (hopefully) get it fixed.


There are multiple reasons why FTP by itself became obsolete. Some of them I can think of off the top of my head:

1) Passive mode. What is it and why do I need it? Well, you see, back in the old days, .... It took way too long for this critical "option" to become well supported and used by default.

2) Text mode. No, I don't want you to corrupt some of my files based on half-baked heuristics about what is and isn't a text file, and it doesn't make any sense to rewrite line endings anymore anyway.

3) Transport security. FTPS should have become the standard decades ago, but it still isn't to this day. If you want to actually transfer files using an FTP-like interface today, you use SFTP, which is a totally different protocol built on SSH.


Why would you say FTP is obsolete? For what it's worth, I still use it (for bulk file transfer).


chrome and firefox dropped support for it 5 years or so ago, it has had a lot of security issues over the years, was annoying over NAT, and there are better options for secure bulk transfers (sftp, rsync, etc)


I see, I assumed by ftp you also meant sftp.


Depending on your hardware (SBC), FTP can also be several times faster than SFTP for transferring files over a LAN. Though I'll admit to having used other protocols like torrents for large files that had bad transfers or other issues (low-quality connection issues causing dropped connections, etc).


TFTP is also a good choice for transferring files over trusted networks to/from underpowered devices.


Consider applying for YC's Summer 2026 batch! Applications are open till May 4

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: