I'm trying to get Decs to work in the repl. I've traced it back to repl_eval/src/eval.rs It looks like the function being passed to app.call_function needs to take a RocDec, which isn't included in the crate. Does this sound right? I want to make sure I'm not I'm not going about this in the wrong way before adding RocDec to the appropriate cargo file
I think you can use deref_i128
and then in the cli and wasm crates (that already import roc_std) do the actual conversion to RocDec
repl_cli
and repl_wasm
, that is
at least that's how we do it with strings and lists today
I'll give that a try. Thanks!
Now that I've dug deeper, what's needed in repl_eval
is an Expr::Num
, which takes a string representation of the number. I see 3 options:
deref_i128
and build the display logic into repl_eval
. This will require leaking implementation details (how many decimal digits) of Dec
into repl_eval
deref_dec(&self, addr: usize) -> &str
to ReplAppMemory
and add a function toStr(&self) -> &str
to RocDec
. This feels the cleanest to me.deref_dec(&self, addr: usize) -> (i128, i32)
to ReplAppMemory
that returns the whole and decimal parts. Then add a function to repl_eval
to display the pair. Note that there is already display logic for other numbers in repl_eval
Thoughts?
that's surprising. What makes it different form list/str ?
deref_str
uses method 2.
right, let's go with that then
toStr
is implemented in zig, but should be easy to port to rust
I don't understand the List structure enough to be sure, but I think it basically uses 1 and leaks that it's implemented as an array.
Should I leave the zig implementation for generating the llvm bytecode, or just convert the function to rust, and call that one?
porting that code and putting it into roc_std
should work
the type signature there doesn't work though
:+1:
it has to be deref_dec(&self, addr: usize) -> i128
, or at least something that is not a reference type
Why not a &str
?
I guess the other possibility is deref_dec(&self, addr: usize) -> f64
and the print it as a f64
lifetimes
you cannot return a &str
there, there is no way to allocate it
it works in the case of deref_str
because self
contains the bytes
but you cannot create a new string there and then return it as a &str
Right
f64
is a good option I think
although
maybe not
we can represent 1/3
exactly I think right? and floats cannot
so converting via float could introduce weird .000000001
problems
you know what, mono
is a dependency of repl_eval
and it already imports roc_std
, so adding it as a dependency has no effect whatsoever to repl_eval
so making it deref_dec(...) -> RocDec
should also be just fine
And then call toStr
to get the representation on the stack, and env.arena.alloc to get it on the heap, right?
let's see, we want to do this right.
1) implement std::fmt::Display for RocStr
2) make a bumpalo::collections::String::with_capacity_in(max_num_of_digits)
3) use write!(bumaplo_string, "{}", roc_dec)
; it converts the RocDec to a string and pushes that result into the bumpalo string efficiently
4) there's probably some .into_bump_slice
equivalent for strings, I'll look this up
step 1 is only efficient if you write!
the individual characters to the formatter, don't create a String in there
the zig implementation does something similar
Here's what's being called for the other numbers.
image.png
oh that's not great
you can use the later steps I described to create a bumpalo string directly
Does this mean we can punt on steps 2-4?
Or does it mean I should rewrite that function to use bumpalo?
yeah, something like
fn number_literal_to_ast<T: std::fmt::Display>(arena: &Bump, num: T) -> Expr<'_> {
let mut string = bumpalo::collections::String::with_capacity_in(64, arena);
write!(string, "{}", num).unwrap();
Expr::Num(string.into_bump_slice())
}
that doesn't quite work yet though, I'll do some debugging
I'll start by getting deref_dec
and Display
implemented
got it
/// This is centralized in case we want to format it differently later,
/// e.g. adding underscores for large numbers
fn number_literal_to_ast<T: std::fmt::Display>(arena: &Bump, num: T) -> Expr<'_> {
use std::fmt::Write;
let mut string = bumpalo::collections::String::with_capacity_in(64, arena);
write!(string, "{}", num).unwrap();
Expr::Num(string.into_bump_str())
}
Oh yeah the original one allocates the string twice! Nice improvement.
Do I need to worry about boxing/unboxing RocDec? Or can I just call this macro with RocDec?
image.png
Well that's tiny
I think the best approach is a default trait method
fn deref_dec(&self, addr: usize) -> RocDec {
bits = self.deref_i128(addr)
...
}
in the definition of
pub trait ReplAppMemory {
wouldn't we need separate methods for i128 and Dec?
oh right sorry, that's what you wrote
I misread
yeah
just, we only need to implement deref_dec
once in this way
Right, that's a good idea!
Folkert de Vries said:
we can represent
1/3
exactly I think right? and floats cannot
No, but if I remember correctly, our dec is essentially a x * 10^-18
. So anything divisible by 10^-18
is always perfectly represented. So 1/3
is still a problem.
wait then what problem do decimals solve?
I thought they were designed to prevent this sort of 0.0000000001
thing
Representing cash and any sort of numbers divisible by 10 is the main thing.
Theoretically we could have the base to make it nicer for other cases. Could make it base 60 so it gets 1/2, 1/3, 1/4, 1/5, 1/6, 1/10, 1/12. That gets most human expected fractions and still makes it work well with base 10 since it is a multiple of 10.
Change the number to x * 60^10
Also, I guess it still has a slightly more reasonable third already. Would be 0.33333....3
with 18 3s. So it shouldn't have as many issues with the 0.000000001
thing.
If I remember correctly, 0.3 isn't exactly representable in base 2.
ah that must be what I'm thinking of
If we're looking to provide nice representations for lots of fractions, we probably want it to be a fractional class, although that will be representable in Roc itself once Abilities are implemented.
Ah yeah, it fixes the 0.3 case and like 0.1 + 0.9 actually equalling 1.
yeah in every language that uses IEEE floats if you put 0.1 + 0.2 into the repl, it does not print 0.3 :sweat_smile:
but withDec
in Roc it would!
so it's better for currency, but is it good?
when you calculate interest, it would still be inaccurate right?
(and potentially accumulating the errors to a point where they matter)
I remember asking some people about this
I forget exactly who told me this, but it's something like "nobody in finance uses fixed-point decimal with more than 6 decimal places, and one time we had a client ask for 8 or 9 and it was the most absurd thing, everyone was laughing about it"
depends what inaccurate is. If everyone accepts that interest is always compounded at a certain time interval and rounded off then it's ok
and Dec
has 18 decimal places, so...seems like we're ok? :sweat_smile:
SQL Server has a fixed-point decimal type where you can configure the number of decimal places, and the common advice there is to always use 4
I remember asking around for various use cases and nobody I talked to had any use cases they knew of where 18 decimal places wouldn't suffice
(unless of course you're doing arbitrary fractions like 1/3, in which case yeah - probably want a different datatype altogether for that!)
basically the goal with Dec
is to have a better default than F32
or F64
, which actually works the way people expect it to in terms of decimal math
as opposed to all the weird footguns of floating-point math, currency or otherwise
and still having reasoanble, if not ideal, performance
while still of course having F32
and F64
as opt-in higher-performance alternatives if you care more about math operation performance than about precision (e.g. for graphics coordinates)
@Brian Carroll It's fairly common to quote daily compounding, and actually use continuous compounding, which adds even more inaccuracies.
I've managed to get myself confused in the RocDec implementation.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub struct RocDec(pub i128);
That i128 is never named. How can I reference it?
roc_dec_value.0
btw if anyone wants to do a bachelors/masters thesis on the Dec type, its implementation (e.g. we'll also need sin
and sqrt
ect) and its accuracy tradeoffs, that would be very cool
:+1:
oh also speed because we do integer arithmetic but it's more operations than the equivalent float operation that happens in hardware
cc @Samuel Dubovec
btw @Derek Gustafson was that field already pub
? that seems suspicious
Yup, already pub
does anything bad happen when you remove it?
I'll give it a try and let you know
I guess we want to construct from an i128
sometimes
but we could add from_le_bytes
Since I'm not interested in writing about the Dec implementation, should I leave it alone once I'm done with this feature?
And, speed wise, Dec should be comparable to f64, if there is a fpu available
not for more specialized operations right? like sqrt
or sin
?
at least I thought those might by "in silicon"
and no if the material interests you go ahead with implementing. Don't want to block on the chance that someone comes along to do a thesis, but if it works out that way then it would be cool
It's been a decade since I've worked on that sort of stuff. You're probably right that those are hardware instructions on the fpu, and we'd be implementing in software
If I remember, I think one operation is already slower than F64. I believe it was multiplication because it requires more expensive normalization.
Probably. The work I was doing was fixed point base 2 arithmatic, so we got the renormalization shr, which is really fast.
from way back when we originally talked about this:
Screen-Shot-2022-03-04-at-5.39.23-PM.png Screen-Shot-2022-03-04-at-5.39.38-PM.png
@Folkert de Vries Removing pub
from RocDec.0
causes compilation to fail in mono with 4 errors. I expect they're fixable using traits that are already defined on RocDec, but one problem at a time. I'll make an issue so it doesn't get lost.
Do we have a safe mode vs release mode implemented?
there's multiple levels to this
so, we can build our compiler with --release
, which instructs the rust compiler to perform more optimizations
but, we can also make our compiler turn Roc code into more optimal code
with the --optimize
flag
optimizations are always safe btw
so --release
is for cargo
, and makes the roc
executable run faster
and --optimize
is an argument you pass to the roc
executable to make the compiled roc code go faster
yeah, that :point_up: :big_smile:
e.g. cargo run -- --optimize --debug examples/benchmarks/Deriv.roc
here --debug
make the roc compiler emit the LLVM bytecode to a file
you will need an extra tool for that though, using that flag should panic with a link to instructions on how to do that
So, I'm going to be calling str.from_utf8
which returns a Result
. The compiler is building the string from the i128, so if we've done our job right, it should always succeed. It's nice to have that check while we're building the compiler, so we know it failed, but in a release mode, we should probably call str.from_utf8unsafe
and skip the check that it's a valid codepoint
oh we just use unsafe in those cases
it makes sense here; we have to trust ourselves
and we add tests ect but we choose speed in this sort of scenario
Okay. I'm used to C where the preprocessor can switch between those choices at compile time
I'm sure you can, but :shrug:
To be fair, you can do that in rust as well. Just have to use cfg
Generally though, I don't think that would add much value.
Yeah part of the explanation here is in the way a compiler is structured. It's really only the parser that needs to handle invalid UTF8. If the user input doesn't make it past the parser, then none of the other stages need to worry about it.
What's the format for 1: Dec
? I can think of 3 reasonable answers
1) 1
2) 1.
3) 1.0
I suppose whatever we do for 1: F64
this should be the same?
That would be option 1)
I think 1) is good for now, unlike other languages (at least today) a float literal can be inferred without having to have a decimal point after it.
Pushed my RocDec fix and I'm getting a number of failing tests regarding recognizing records with a single Num *
in them as Num *
.
The error is in the function num_to_ast
in repl_eval/src/eval.rs
, but I don't understand Content
well enough to understand what's going on.
Anyone have a suggestion of what could be going on, and where I should look?
if you just want to peek at what is in a Content
, then dbg!(roc_types::subs::SubsFmtContent(content, subs))
works
hmm I can reproduce the problem locally and it's odd that your changes would cause this issue
got it, in eval.rs
, change this snippet to
let (newtype_containers, content) = unroll_newtypes(env, content);
let content = unroll_aliases(env, content);
macro_rules! helper {
($ty:ty) => {
app.call_function(main_fn_name, |_, num: $ty| {
num_to_ast(env, number_literal_to_ast(env.arena, num), content)
})
};
}
I think the moving of helper
up to line 281 may be causing the problem, as it seems to be capturing content
before unrolling
crucially, content
must be defined before the macro
yes, exactly
What Folkert said :)
I assumed that my changes exposed an unhandled case of RocDec
no this is why shadowing is not allowed in roc
but it's so convenient sometimes ...
but yeah you have to be careful when moving macros that they capture the same things
Should I revert that change, or rename something
no just swap the lines around
like in the snippet I posted
Okay. Will do when I get home
I'll just push that change then, got it already
repl tests all succeed at least
Last updated: Jul 06 2025 at 12:14 UTC