For those of you designing languages

25 views
Skip to first unread message

Alan Karp

unread,
May 8, 2026, 9:47:27 PM (7 days ago) May 8
to <friam@googlegroups.com>

Matt Rice

unread,
May 8, 2026, 10:41:05 PM (7 days ago) May 8
to fr...@googlegroups.com
On Fri, May 8, 2026 at 6:47 PM Alan Karp <alan...@gmail.com> wrote:
>
> https://www.gingerbill.org/article/2026/05/03/signed-by-default/
>

I suppose there is also a "unsigned integers are unnatural numbers" camp, where
we point out that the natural numbers never have a subtraction
operator, they can have a truncated subtraction like monus.
But we can also have forms of subtraction that return some form
including a pair of "spaceship operator" (< or == or >) and absolute
difference.

I'd rather see systems languages that attempt to do some form of
bounded natural numbers, than keep on repeating these same
algebraic mistakes. Instead of just cutting them down a peg by doing
them not by default. I suppose this is really just saying if you need
to ensure sane algebraic rules followed, you probably want something
dependently typed.

But even when not default, whenever you reach for the unsigned
integers for all the millions of cases where you don't have negative
indices
it isn't like these pitfalls have just disappeared because they are no
longer default. shrug

Rob Meijer

unread,
May 9, 2026, 5:52:16 AM (7 days ago) May 9
to Design
This touches on two subjects I've been trying to address in my DSL, the first being virtual integer types and integer width type math ( https://hive.blog/hive-139531/@pibara/version-03-of-the-merg-e-language-specification--robust-integers-and-integer-bitwidth-generic-programming )  and the second, a connected subject I'm currently working on for my DSL but might abandon if I find something simpmer,  operator result type graphs and result type operator modifiers. 

I recently extended the language spec with rationals (integer based) and complex numbers (float based) and I'm contemplating adding rational and int based complex types too to make the operator result graphs more consistent, but I'm not sure yet because except for some niche lattice stuff, most complex number operations tend to revert to float pretty quickly with e or pi in the mix. 

In my current draft, unsigned integers are similarly short lived in practice for similar reasons. A '-' between to unsigned expressions results in a signed result unless an operator modifier is used telling the operator to use a different underflow wrapping implementation. 

It's still notes for now, and I'm not sure about the type graph model (I feel something simpler must be possible), but I'll share my blog post here when I feel I have a coherent spec for it. 

I hope this bunch of underspecified fundamentals is making sense. I feel I'm on the right track for my DSL though it may not be suitable for less "weird" languages because it requires thinking about numeric type safety quite a lot or risk exploding to multi kbit numeric types without 1:1 machine mapping that most languages probably don't want to implement.



On Sat, 9 May 2026, 03:47 Alan Karp, <alan...@gmail.com> wrote:

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/friam/CANpA1Z1Q--Sfz8RNpzQ-dF_%2Bae0v5fZeek1_HWNbFX-kk_6ofg%40mail.gmail.com.

Rob Meijer

unread,
May 9, 2026, 7:07:10 AM (7 days ago) May 9
to Design


On Sat, 9 May 2026, 04:41 Matt Rice, <rat...@gmail.com> wrote:
On Fri, May 8, 2026 at 6:47 PM Alan Karp <alan...@gmail.com> wrote:
>
> https://www.gingerbill.org/article/2026/05/03/signed-by-default/
>

I suppose there is also a "unsigned integers are unnatural numbers" camp, where
we point out that the natural numbers never have a subtraction
operator, they can have a truncated subtraction like monus.
But we can also have forms of subtraction that return some form
including a pair of "spaceship operator" (< or == or >)  and absolute
difference.

I'd rather see systems languages that attempt to do some form of
bounded natural numbers, than keep on repeating these same
algebraic mistakes. Instead of just cutting them down a peg by doing
them not by default. I suppose this is really just saying if you need
to ensure sane algebraic rules followed, you probably want something
dependently typed.


That's my default approach now for the DSL that I'm working on.  But with modifiers to allow for alternate overfow/underflow results (and appropriate implementations) if required. 

That it, uiint  minus uint returns an int of appropriate bits width, not an uint. Int divided by int returns a rational of appropriate bits width, and so on. 

Then if I need things to stay uint over int or int over rational, the operator gets a modifier token. It's a bit clunky but I'm trying to keep my pet projects manageable. 


So:

inert uint8 a = 408;
inert uint8 b = 17;
inert int16 c = a - b;
inert rational16 d = a / b;

Or

inert uint8 c = a - b nopromote;
inert uint8 d = a / b nopromote;

And if you don't want runtime errors:

inert uint8 c = a - b hazardous nopromote;





But even when not default, whenever you reach for the unsigned
integers for all the millions of cases where you don't have negative
indices
it isn't like these pitfalls have just disappeared because they are no
longer default. shrug

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Douglas Crockford

unread,
May 9, 2026, 8:29:33 AM (7 days ago) May 9
to friam
The problem is with the int class of types. Overflow is inevitable, signed or unsigned, and the consequences can be devastating. The solution is to eliminate int in all of its forms. Instead, have a single floating point type that has excellemt performance on integers. You get the performance and eliminate the danger. That is what DEC64 does.

Rob Meijer

unread,
May 9, 2026, 8:47:00 AM (7 days ago) May 9
to Design


On Sat, 9 May 2026, 14:29 Douglas Crockford, <dou...@crockford.com> wrote:
The problem is with the int class of types. Overflow is inevitable, signed or unsigned, and the consequences can be devastating.

I think it's only inevitable once you run out of types to promote to, and then only if you decide that runtime overflows are preferable over compile time type-errors. 


The solution is to eliminate int in all of its forms. Instead, have a single floating point type that has excellemt performance on integers. You get the performance and eliminate the danger. That is what DEC64 does.

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Raoul Duke

unread,
May 9, 2026, 2:08:33 PM (6 days ago) May 9
to fr...@googlegroups.com
I do not follow how it is a solution to over/underflow. Limited bits in the physical universe means all calculations are finitely bounded, no?

How does DEC64 let me successfully exactly represent 

(int) 2^55

?

Seems like DEC64 is kicking the can by adding more bits as we have always done, but nevertheless remains finite?

On Sat, May 9, 2026 at 05:29 Douglas Crockford <dou...@crockford.com> wrote:
The problem is with the int class of types. Overflow is inevitable, signed or unsigned, and the consequences can be devastating. The solution is to eliminate int in all of its forms. Instead, have a single floating point type that has excellemt performance on integers. You get the performance and eliminate the danger. That is what DEC64 does.

Tony Arcieri

unread,
May 9, 2026, 3:06:28 PM (6 days ago) May 9
to fr...@googlegroups.com
After reading the post I'm a bit lost what it means "by default". It's seemingly talking about languages with strong static typing, namely Odin. Is it asking what the type of an integer should be absent any other inference hints? If so, inferring a signed type by default is relatively uncontroversial, I think. For example, Rust uses i32.

That said, I am very firmly on the side of using unsigned integers where they make sense (for example, the vast majority of cryptography). Having implemented a bignum arithmetic library, it started with unsigned integers, and signed integers are modeled using two's complement as a newtype for an unsigned integer, so to me when you talk about "how integers work on the machine" I see signed integers as unsigned integers with extra steps. For mixed operations between unsigned and signed values where there's a possibility of returning unsigned or signed results, I suppose both Rust and my library "default" to unsigned, with `*_signed` variants that return signed values.

> The most common problem is the mentality of using unsigned types to “enforce” that a value is never negative. The irony is that these same people do arithmetic assuming normal algebra rules apply, where subexpressions (e.g. the a-b part of a-b+c) can go negative even if the final result is positive. This leads to infinite loops and out-of-bounds errors.

To me these are cases where you should use checked, fallible arithmetic, which always errors in the event of an out-of-bounds condition. If anything I'd like something closer to dependent typing where I can ensure a value is within specific ranges at the type-level. I will say Rust's compromise as noted by the post's author of doing checked arithmetic in debug builds and wrapping in release is a bit awkward, but fortunately they do provide explicitly checked and explicitly wrapping arithmetic which always works the same way regardless of if it's a debug or release build.

The biggest influence on me in regard to this overall topic was probably trying to implement cryptography in Java (back in the mid-'90s when it was a new language), a language that famously eschewed a signed/unsigned type-level separation as being too difficult for programmers to understand, and made all of the integer primitive types signed. The algorithms I was trying to implement depended on bitwise and wrapping arithmetic around unsigned values, which you have to emulate in Java using a twice-width signed value and copious masking. They also had to add a special "unsigned shift" operator >>>, and in Java 8 added a whole host of other unsigned operations, even though they were fundamentally still on signed types. It all felt like awkward workarounds for a missing language feature.

Alan Karp

unread,
May 9, 2026, 11:45:42 PM (6 days ago) May 9
to fr...@googlegroups.com
The edges are a problem for any computer representation of numbers.  You can eliminate the edges for integers because you know how many digits are in every result.  Perhaps the only integer types should be bigint, signed and unsigned.  Any wrapping, like what cryptography needs, would be an explicit operation.  Performance should not be a problem since most computed numbers are small enough to fit into registers.

Posits do almost the same thing for floating point as bigints do for integers.  You can swap bits between exponent and fraction to extend the exponent range.  That doesn't completely eliminate the edges the way bigint does for integers, but it's a reasonable approximation.

--------------
Alan Karp


Kevin Reid

unread,
May 10, 2026, 12:00:31 AM (6 days ago) May 10
to fr...@googlegroups.com
I’m writing code (in Rust) that is designed to never overflow. I have gotten a lot of mileage out of careful choice of integer types, but in some cases, it would be valuable to be able to easily define types with explicitly chosen ranges that slightly differ from the machine integers. For example, a type of range [−2³¹+1, +2³¹−1] (whereas an ordinary signed integer would be [−2³¹, +2³¹−1]) would be useful because it gains the property that negating it cannot overflow, and thus coordinate rotations about the origin cannot overflow. If I had such a feature (and I admit I have not looked into what libraries already exist for it), the trick after that would be to choose ranges, and sequences of arithmetic operations, such that the cost of implementing operations that obey the range restriction is near zero (after optimization).

Mike Stay

unread,
May 10, 2026, 12:19:09 AM (6 days ago) May 10
to fr...@googlegroups.com
There's a really interesting post on replacing FP operations with
integer ops on CP4 this week:
https://cp4space.hatsya.com/2026/05/03/schanuels-conjecture-and-the-semantics-of-fpsan/

On Sat, May 9, 2026 at 10:00 PM Kevin Reid <kpr...@switchb.org> wrote:
>
> I’m writing code (in Rust) that is designed to never overflow. I have gotten a lot of mileage out of careful choice of integer types, but in some cases, it would be valuable to be able to easily define types with explicitly chosen ranges that slightly differ from the machine integers. For example, a type of range [−2³¹+1, +2³¹−1] (whereas an ordinary signed integer would be [−2³¹, +2³¹−1]) would be useful because it gains the property that negating it cannot overflow, and thus coordinate rotations about the origin cannot overflow. If I had such a feature (and I admit I have not looked into what libraries already exist for it), the trick after that would be to choose ranges, and sequences of arithmetic operations, such that the cost of implementing operations that obey the range restriction is near zero (after optimization).
>
> --
> You received this message because you are subscribed to the Google Groups "friam" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.
> To view this discussion visit https://groups.google.com/d/msgid/friam/CANkSj9VA4XxGjhcQQkaZJUrMo8NzKS8a_mJKbbOGJ_%2BbD%2BUH8w%40mail.gmail.com.



--
Mike Stay - meta...@gmail.com
https://math.ucr.edu/~mike
https://reperiendi.wordpress.com

Matt Rice

unread,
May 10, 2026, 12:47:51 AM (6 days ago) May 10
to fr...@googlegroups.com
On Sat, May 9, 2026 at 9:00 PM Kevin Reid <kpr...@switchb.org> wrote:
>
> I’m writing code (in Rust) that is designed to never overflow. I have gotten a lot of mileage out of careful choice of integer types, but in some cases, it would be valuable to be able to easily define types with explicitly chosen ranges that slightly differ from the machine integers. For example, a type of range [−2³¹+1, +2³¹−1] (whereas an ordinary signed integer would be [−2³¹, +2³¹−1]) would be useful because it gains the property that negating it cannot overflow, and thus coordinate rotations about the origin cannot overflow. If I had such a feature (and I admit I have not looked into what libraries already exist for it), the trick after that would be to choose ranges, and sequences of arithmetic operations, such that the cost of implementing operations that obey the range restriction is near zero (after optimization).
>

This is basically wanting the pattern types rfc,
https://github.com/rust-lang/rust/issues/123646 which it seems is
already used internally to implement the `NonZero*` types,
I've definitely also wanted this specifically for `NonMax*` which
allowed nicer math in some cases than `NonZero` in particular it
allows you to open up a niche so that `size_of::<Option<NonMaxT>>() ==
size_of::<NonMaxT>()` while still preserving 0 based arithmetic... If
rust ever gets this, I'm certain it'll be a great and busy day.

Matt Rice

unread,
May 10, 2026, 1:36:38 AM (6 days ago) May 10
to fr...@googlegroups.com
Also forgot to say that there are some rust crates with some
interesting usage of wrapping, in particular the left-right crate
relies on the fact that wrapping an integer preserves the transition
from oddness to evenness for overflow, and vice versa for underflow on
two's complement, it uses that to partition changes into two sets (the
current one and one undergoing changes), with a few modifications it
could perhaps be used like a keykos checkpoint mechanism (there are
some issues about it I filed, `rollback` but nobody has needed it
enough to implement that yet).

Rob Meijer

unread,
May 10, 2026, 5:53:42 AM (6 days ago) May 10
to Design
Result promotion, virtual integer types, big integer types and integer bit width generics can prevent that and allow some space to solve this without  resorting to variable sized integer types or in most cases to loss of fidelity by moving to floating point types. I'm doing this in my current DSL design. 

Evaluate whole expressions at compile time using intermediate integer types and bidtwidth generics. Then upscale the result type to the nearest runtime fixed size integer type. 

Always using the minimum bidwidth needed for full fidelity to determine if compilation of the assignment should be permitted. So assigning a Vint35 result to an int32 without operator modifiers gives a compiler error, but assigning it to an int64 will compile. 

I'm using this for my DSL and I feel strongly that it's "close to" the ideal solution. Not the ideal solution because it's a one man spare time pet project and I don't have time to make everything perfect. I think in the ideal solution, the compiler would keep perfect track of the minimum virtual type (for example Vint32) of the result type (for example int64) and use that bit width in integer bit width generics instead of the runtime width, so there is no undue promotion just because virtual types are fully single expression bound. 

Douglas advocated for using floats, give up on full fidelity to avoid overflows and underflows, and I personally not only think it's a bad idea, I think we should avoid floats completely unless our math starts using irrationals like e and pi. Preserve full fidelity as long as we can by having rational types too of different bit widths.

My DSL language-spec currently defines a naively skewed rational type made up of an equal sized int and uint. I've been thinking that inequel bit size with (next to a sign bit) a big/small bit might be a better rational, but the implementation is harder and I'm not really sure it's actually better. But I do feel strongly that rationals are important for fidelity and switching to floats should be reserved to situations where we are working with irrationals like pi. 

Both floats (as Douglas proposes) and bigint (as Alan proposes) seem more like solutions to a problem that a multi size type system and a smarter compiler could make not even exist. 



Tony Arcieri

unread,
May 10, 2026, 11:05:51 AM (6 days ago) May 10
to fr...@googlegroups.com
On Sun, May 10, 2026 at 3:53 AM Rob Meijer <pib...@gmail.com> wrote:
Result promotion, virtual integer types, big integer types and integer bit width generics can prevent that and allow some space to solve this without  resorting to variable sized integer types

I did find this case interesting:

a: u8  = 53
b: u8  = 34
c: u16 = a * b
The question is: what happens to a * b? 

I think it would be quite interesting if that did a concatenating widening multiplication. Effectively what Rust calls `u8::widening_mul`, but concatenating the result into a twice-width value, i.e. `u16`: https://doc.rust-lang.org/std/primitive.u8.html#method.widening_mul

Unfortunately Rust's `Mul` trait design doesn't allow overlapping impls which differ in their `Output` type, since it's implemented as an associated type. Though, if it were a generic parameter of the trait instead of an associated type to allow overlapping impls, you now have an inference problem if e.g. `u8` multiplication can now return a `u8` or widening `u16` result and you now need to hint to the compiler which one you want. It works in the above example where every variable is notated with its type, but if you want inference to work, permitting overlapping impls probably isn't going to fly, so Rust probably made the right call. Maybe if there were some way to pick a "default" impl to use in the event there are overlapping/ambiguous impls, if only for compiler built-in primitives? Seems hard.

--
Tony Arcieri

Chris Hibbert

unread,
May 10, 2026, 12:13:20 PM (6 days ago) May 10
to fr...@googlegroups.com
My problem with going straight to floats is that you end up having to
carry around 5.0001, and decide whether to tell the user it might as
well have been 5. And similarly, dividing by 3 and then multiplying by
3 loses precision.

I built a ratio package when I was at Agoric so that we could multiply
and divide financial numbers and manage the rounding. (If you worry
about sensitivity analysis, it often makes sense to put off the
divisions as long as possible.) The other thing we did was attach units
to all ratios so you could tell when you accidentally multiplied when
you should have divided or used the wrong exchange rate.

I thought it worked pretty well.

Chris
--
Currently reading: Rivalry and Central Planning, Don Lavoie;
Vault of the Ages, Poul Anderson; Salt, Adam Roberts; Why
Plato Matters Now, Angie Hobbs.

Chris Hibbert
hib...@mydruthers.com
Blog: http://www.pancrit.org
http://mydruthers.com





Rob Meijer

unread,
May 10, 2026, 12:58:44 PM (5 days ago) May 10
to Design
Just added a blog post on the subject to my series of blog posts.  It outlines how I've been trying to address both issues (over/under-flow prevention and fidelity preservation) with seven families of fixed size numeric types and an operator system with modifiers:

https://hive.blog/hive-139531/@pibara/version-03-of-the-merg-e-language-specification--interaction-between-operators-integer-bitwidth-generics-and-the-full-numeric-

Hope this contributes to the discussion.

Rob Meijer

unread,
May 10, 2026, 7:07:10 PM (5 days ago) May 10
to fr...@googlegroups.com
On Sun, May 10, 2026 at 6:13 PM Chris Hibbert <hib...@mydruthers.com> wrote:
On 5/9/26 5:29 AM, Douglas Crockford wrote:
> The problem is with the int class of types. Overflow is inevitable,
> signed or unsigned, and the consequences can be devastating. The
> solution is to eliminate int in all of its forms. Instead, have a single
> floating point type that has excellemt performance on integers. You get
> the performance and eliminate the danger. That is what DEC64 does.

My problem with going straight to floats is that you end up having to
carry around 5.0001, and decide whether to tell the user it might as
well have been 5.   And similarly, dividing by 3 and then multiplying by
3 loses precision.

By choosing floats you gain range by giving up on exact results. I think it should be a last resort when either you run out of bits and have no other option, or when you need fundamantaly limited fidelity representations of irrationals that bind any results to a non-perfect accuracy level.
And once that happens, I believe that you end up with a split.  Once you go float, mixing types converges to the lowest fidelity. As long as you can avoid it, muxing types converges to bigger and bigger representation types.

 
I built a ratio package when I was at Agoric so that we could multiply
and divide financial numbers and manage the rounding. (If you worry
about sensitivity analysis, it often makes sense to put off the
divisions as long as possible.) The other thing we did was attach units
to all ratios so you could tell when you accidentally multiplied when
you should have divided or used the wrong exchange rate.


Puting of division as long as possible is I think done most natural by having rational types. But these come with their own specific considerations. You can limit overflow issues with narrow rationals by doing canonical divisions early, but these are not a computational free ride. 
In my own pet project I am currently choosing to not do any intermediate canonical divisions in my rational expressions, not because it isn't a good idea, but because it's not high priority for now. 

Did you delay all divisions, or just non-canonical divisions? And what considerations led to that choice? 

 
I thought it worked pretty well.

Chris
--
Currently reading: Rivalry and Central Planning, Don Lavoie;
    Vault of the Ages, Poul Anderson; Salt, Adam Roberts; Why
    Plato Matters Now, Angie Hobbs.

Chris Hibbert
hib...@mydruthers.com
Blog:   http://www.pancrit.org
http://mydruthers.com





--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Matt Rice

unread,
May 10, 2026, 7:42:09 PM (5 days ago) May 10
to fr...@googlegroups.com
On Sun, May 10, 2026 at 4:07 PM Rob Meijer <pib...@gmail.com> wrote:
>
>
> On Sun, May 10, 2026 at 6:13 PM Chris Hibbert <hib...@mydruthers.com> wrote:
>>
>> On 5/9/26 5:29 AM, Douglas Crockford wrote:
>> > The problem is with the int class of types. Overflow is inevitable,
>> > signed or unsigned, and the consequences can be devastating. The
>> > solution is to eliminate int in all of its forms. Instead, have a single
>> > floating point type that has excellemt performance on integers. You get
>> > the performance and eliminate the danger. That is what DEC64 does.
>>
>> My problem with going straight to floats is that you end up having to
>> carry around 5.0001, and decide whether to tell the user it might as
>> well have been 5. And similarly, dividing by 3 and then multiplying by
>> 3 loses precision.
>>
> By choosing floats you gain range by giving up on exact results. I think it should be a last resort when either you run out of bits and have no other option, or when you need fundamantaly limited fidelity representations of irrationals that bind any results to a non-perfect accuracy level.
> And once that happens, I believe that you end up with a split. Once you go float, mixing types converges to the lowest fidelity. As long as you can avoid it, muxing types converges to bigger and bigger representation types.
>

There is also that once you get rid of types restricted to non
negative values, lots of things make no sense when given negative or
fractional value, like array indexing x[-5/3], which just forces the
introduction of a bunch of runtime errors.
> To view this discussion visit https://groups.google.com/d/msgid/friam/CAMpet1UgSEedN2BAnEme7G4VLz%3DTKk1Z6o6BRcFyrSxdp5a_6w%40mail.gmail.com.

Raoul Duke

unread,
May 11, 2026, 11:19:47 AM (5 days ago) May 11
to fr...@googlegroups.com
i believe we lie to ourselves about the complexity of the state space; we fool ourselves that we are mostly on safe happy paths. 

if our type system rubs our nose in the lie, that helps - up until people get tired of the friction, and mark things as `unsafe` or `:any`. 

so people think formalism & checking isn't worth it. 

so i believe we need better ux for formalism. 

un/fortunately i also believe the only way to get that is by applying ai-agents as code cop proof assistants. 

Chris Hibbert

unread,
May 11, 2026, 12:53:59 PM (4 days ago) May 11
to fr...@googlegroups.com
On 5/10/26 4:06 PM, Rob Meijer wrote:
>
> On Sun, May 10, 2026 at 6:13 PM Chris Hibbert <hib...@mydruthers.com wrote:
>
> On 5/9/26 5:29 AM, Douglas Crockford wrote:
> > The problem is with the int class of types. Overflow is inevitable,
> > signed or unsigned, and the consequences can be devastating. The
> > solution is to eliminate int in all of its forms. Instead, have a
> single
> > floating point type that has excellemt performance on integers.
> You get
> > the performance and eliminate the danger. That is what DEC64 does.
>
> My problem with going straight to floats is that you end up having to
> carry around 5.0001, and decide whether to tell the user it might as
> well have been 5.   And similarly, dividing by 3 and then
> multiplying by
> 3 loses precision.
>
> By choosing floats you gain range by giving up on exact results. I think
> it should be a last resort when either you run out of bits and have no
> other option, or when you need fundamantaly limited fidelity
> representations of irrationals that bind any results to a non-perfect
> accuracy level.
> And once that happens, I believe that you end up with a split.  Once you
> go float, mixing types converges to the lowest fidelity. As long as you
> can avoid it, muxing types converges to bigger and bigger representation
> types.

Yeah, I mostly think it's a mistake in javascript to only offer floats
natively. At Agoric we used Amounts, which pair a BigInt with a unit to
represent financial values. This works well in cryptocurrencies.

> I built a ratio package when I was at Agoric so that we could multiply
> and divide financial numbers and manage the rounding. (If you worry
> about sensitivity analysis, it often makes sense to put off the
> divisions as long as possible.) The other thing we did was attach units
> to all ratios so you could tell when you accidentally multiplied when
> you should have divided or used the wrong exchange rate.
>
>
> Puting of division as long as possible is I think done most natural by
> having rational types. But these come with their own specific
> considerations. You can limit overflow issues with narrow rationals by
> doing canonical divisions early, but these are not a computational free
> ride.
> In my own pet project I am currently choosing to not do any intermediate
> canonical divisions in my rational expressions, not because it isn't a
> good idea, but because it's not high priority for now.
>
> Did you delay all divisions, or just non-canonical divisions? And what
> considerations led to that choice?

I'm not sure what you mean by "canonical" divisions. Could you say more?


We put it off as long as possible by carrying the ratios for most
intermediate representations. You don't even have to reduce the
representation when comparing values - it's only when a value has to be
stored or displayed that you have to reduce it to decimal.

We were doing financial calculations with multiple currencies. We needed
to multiply our Amounts (unlimited precision integer, plus a Unit) by
interest rates, percentage fees, and currency conversions. We found that
it was a hassle to write generic code that was agnostic about the
particular currency, and to write code that made it apparent that we
were keeping the units straight.

When working with numbers that could be very large and very small, we
had to be clear about the order of operations (AKA sensitivity
analysis). When we switched to ratios, the analysis got simpler and we
had fewer issues with surprising outcomes.

https://github.com/Agoric/agoric-sdk/blob/master/packages/ERTP/src/ratio.js

One thing it took me a little while to figure out was that it was
expensive to store ratios that were the result of continuing
calculations. The ratios used bigInts, and the numerator and denominator
would grow over time. We had to identify places where we were
accumulating values over time, and reduce the fraction regularly, or the
storage and computation costs grew over time.


> I thought it worked pretty well.
>
> Chris
> --
Chris
--
Change is not linear. Our expectations are linear, but new
technologies come in "S" curves, so we routinely overestimate
short-term change and underestimate long-term change.
--Paul Saffo

Rob Meijer

unread,
May 11, 2026, 4:24:35 PM (4 days ago) May 11
to Design
Anxample, if you have something like (note, I'm using p ÷ q notation for rational literals):

inert rational32 a = 3 ÷ 7; 
inert rational32 b = 26 ÷ 46;
inert rational32 c = 7 ÷ 15;
inert rational128 d = ( a + b ) * c;


The first and third lines are already in canonical form. For the second line, both p and q can be divided by two to get the canonical form 13  ÷ 23
The 4th line can end up as 630 ÷ 2415, or both p and q can be divided by their common divisors to get the canonical 6  ÷ 23.

That can be usefull because in this example you can be sure you can take the chance on not promoting if you divide both p and q to get that canonical form. 
In my DSL spec with an awkward line like:

inert rational32 d = ( a + b nopromote canonical) * C nopromote canonical;

I love rationals, but even with canonicaluzation, they are quite bit hungry with operator type promotion. 

On the other hand, implicit canonicaluzation can be relatively expensive if done on all operators. 

Right now I'm doing them implicit only for literals and I'm postponing explicit canonicaluzation to a later version of my project.  Because I feel that I need to get a feeling for it first. 

I also considered doing canonicalization as overflow contingency plan, but right now I've put that idea on ice for a bit. 

I'm using a data format where for example my smallest rational, a rational32 is a p/q pair consisting of a int16 for p and an uint16 for q.  That all the way up to a 32 kbit rational consisting of a 16kbit int for p and a 16kbit uint for q. 

Promotion rules are hungry though because addition and subtraction are basically multiplications plus additions unless canonicaluzation is possible and more often than not we need to assume the worst case bitwidth needs.  And "if" you run out of bits, then demoting to floating point or accepting a non zero overflow risk are the only unfortunate option left. 


 
--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Rob Meijer

unread,
May 12, 2026, 6:07:11 AM (4 days ago) May 12
to Design
Some of you might enjoy this. I asked notebooklm to make a video from the numeric type system blog post. I tried to tell it to keep it down with the bling praise, but as usual it manages to still oversell.

https://www.youtube.com/watch?v=wnL_vZ1xjnw&t=1s 


Alan Karp

unread,
May 12, 2026, 1:21:27 PM (3 days ago) May 12
to fr...@googlegroups.com
A very nice explanation.  

I wonder about where the 2KB basic unit comes from rather than just using a general bigint.  

The video didn't mention constant time operations, which are important to avoid leaking bits during crypto operations.

Also, I understand the motivation for the difference between single line operations and using multiple lines, but I worry about developers getting it wrong leading to vulnerability. 

--------------
Alan Karp


Rob Meijer

unread,
May 13, 2026, 2:52:57 AM (3 days ago) May 13
to Design



On Tue, 12 May 2026, 19:21 Alan Karp, <alan...@gmail.com> wrote:
A very nice explanation.  

I wonder about where the 2KB basic unit comes from rather than just using a general bigint.  

It basicly comes down to my memory management and scheduling model and how these two interact. 

I'm defining 

* int and uint types for power of two bytes up to a size of 2KB (16kbit)
* rational types upto 4KB because my rationals consist of a signed int for p and an equal width unsigned int for q
* integer complex types upto 4KB because they consist of two signed integers for obvious reasons. 
* rational complex types are tricky; in theory they could go to 8KB because they consist of two rationals. 


But I'm considering if I should scale these last ones  back to 4KB for memory management purposes, or if I should go for simplicity and be potentially more wasteful in memory management. 

I'm still designing the memory management  for the compiled runtime , but it's mmap based and mostly scheduler managed, with two exceptions (dataframes and versioning/cow membranes) that aren't directly relevant for this discussion. 

The core concequence of my current preliminary memory management model for the compiled runtime (I'm building a testing runtime first, on top of Python, but the design and type system should idealy be both compiled runtime and BEAM runtime ready) is that for simplicity I really want a single scalar to live in a single memory page, which makes any growing type like a bigint hard to work with. 

Only this gives, me a new problem, not all systems have 4k memory pages, so I need to support 16k memory page systems at least. Maybe by abstracting to 16k on 4k systems. I haven't made that choice yet, but I either need to drop the crat65536 type (a 8KB complex rational), or define virtual 16k pages on 4k platforms. 



Chris Hibbert

unread,
May 14, 2026, 12:39:11 PM (2 days ago) May 14
to fr...@googlegroups.com
On 5/11/26 1:24 PM, Rob Meijer wrote:
>
> Anxample, if you have something like (note, I'm using p ÷ q notation for
> rational literals):
>
> inert rational32 a = 3 ÷ 7;
> inert rational32 b = 26 ÷ 46;
> inert rational32 c = 7 ÷ 15;
> inert rational128 d = ( a + b ) * c;

We didn't attempt to reduce to canonical form. Many of the numbers we
were working with represented exact amounts of cryptocurrencies, so they
included factors of 10^9 or 10^18 (Satoshi/Wei). After adding interest
or charging fees, they could be .97 or 1.02 of the original amount.
Finding least common divisors seemed intractable, so after a couple of
rounds of adding interest on compounded values, we just reduced the
denominator to 10^9 again.

We had a lending instrument which we restricted so you had to borrow at
least enough that the minimum interest charge would round to non-zero.


Chris
--
Computers are telescopes we use to see the infosphere.
Some care about telescopes, most want to see the stars.
Paraphrased from Gelernter
Reply all
Reply to author
Forward
0 new messages