Skip to content

Commit

Permalink
Formal floating point literals.
Browse files Browse the repository at this point in the history
  • Loading branch information
fredrikekre committed May 25, 2024
1 parent b93535a commit 78b4a5a
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 12 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@
This is a list of the rules and formatting transformations performed by Runic:

- No trailing whitespace
- Normalized line endings (`\r\n` -> `\n`) (TODO: Is this bad on Windows with Git's autocrlf?)
- Hex/octal/binary literals are padded with zeroes
- Normalized line endings (`\r\n` -> `\n`) (TODO: Is this bad on Windows with Git's autocrlf? gofmt does it...)
- Hex/octal/binary literals are padded with zeroes to better highlight the resulting UInt
type
- Floating point literals are normalized to always have an integral and fractional part.
`E`-exponents are normalized to `e`-exponents. Unnecessary trailing/leading zeros from
integral, fractional, and exponent parts are removed.
1 change: 1 addition & 0 deletions src/Runic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ function format_node!(ctx::Context, node::JuliaSyntax.GreenNode)::Union{JuliaSyn
@return_something trim_trailing_whitespace(ctx, node)
@return_something format_hex_literal(ctx, node)
@return_something format_oct_literal(ctx, node)
@return_something format_float_literal(ctx, node)

# If the node is unchanged at this point, just keep going.

Expand Down
52 changes: 52 additions & 0 deletions src/runestone.jl
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,55 @@ function format_oct_literal(ctx::Context, node::JuliaSyntax.GreenNode)
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), nb, ())
return node′
end

function format_float_literal(ctx::Context, node::JuliaSyntax.GreenNode)
JuliaSyntax.kind(node) in KSet"Float Float32" || return nothing
@assert JuliaSyntax.flags(node) == 0
@assert !JuliaSyntax.haschildren(node)
str = String(node_bytes(ctx, node))
# Check and shortcut the happy path first
r = r"""
^
(?:(?:[1-9]\d*)|0) # Non-zero followed by any digit, or just a single zero
\. # Decimal point
(?:(?:\d*[1-9])|0) # Any digit with a final nonzero, or just a single zero
(?:[ef][+-]?[1-9]\d*)?
$
"""x
if occursin(r, str)
return nothing
end
if occursin('_', str) || startswith(str, "0x")
# TODO: Hex floats and floats with underscores are ignored
return nothing
end
# Split up the pieces
r = r"^(?<int>\d*)(?:\.?(?<frac>\d*))?(?:(?<epm>[eEf][+-]?)(?<exp>\d+))?$"
m = match(r, str)
io = IOBuffer() # TODO: Could be reused?
# Strip leading zeros from integral part
int_part = isempty(m[:int]) ? "0" : m[:int]
int_part = replace(int_part, r"^0*((?:[1-9]\d*)|0)$" => s"\1")
write(io, int_part)
# Always write the decimal point
write(io, ".")
# Strip trailing zeros from fractional part
frac_part = isempty(m[:frac]) ? "0" : m[:frac]
frac_part = replace(frac_part, r"^((?:\d*[1-9])|0)0*$" => s"\1")
write(io, frac_part)
# Write the exponent part
if m[:epm] !== nothing
write(io, replace(m[:epm], "E" => "e"))
@assert m[:exp] !== nothing
# Strip leading zeros from integral part
exp_part = isempty(m[:exp]) ? "0" : m[:exp]
exp_part = replace(exp_part, r"^0*((?:[1-9]\d*)|0)$" => s"\1")
write(io, exp_part)
end
bytes = take!(io)
nb = write_and_reset(ctx, bytes)
@assert nb == length(bytes)
# Create new node and return it
node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), nb, ())
return node′
end
37 changes: 27 additions & 10 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,39 @@ end
"0o4" * z(42) => "0o4" * z(42), # typemax(UInt128) + 1
"0o7" * z(42) => "0o7" * z(42),
]
# Test the test cases :)
mod = Module()
for (a, b) in test_cases
c = Core.eval(mod, Meta.parse(a))
d = Core.eval(mod, Meta.parse(b))
@test c == d
@test typeof(c) == typeof(d)
@test format_string(a) == b
end
# Join all cases to a single string so that we only need to call the formatter once
input_str = let io = IOBuffer()
join(io, (case.first for case in test_cases), '\n')
String(take!(io))
end
output_str = let io = IOBuffer()
join(io, (case.second for case in test_cases), '\n')
String(take!(io))
end

@testset "Floating point literals" begin
test_cases = [
["1.0", "1.", "01.", "001.", "001.00", "1.00"] => "1.0",
["0.1", ".1", ".10", ".100", "00.100", "0.10"] => "0.1",
["1.1", "01.1", "1.10", "1.100", "001.100", "01.10"] => "1.1",
["1e3", "01e3", "01.e3", "1.e3", "1.000e3", "01.00e3"] => "1.0e3",
["1e+3", "01e+3", "01.e+3", "1.e+3", "1.000e+3", "01.00e+3"] => "1.0e+3",
["1e-3", "01e-3", "01.e-3", "1.e-3", "1.000e-3", "01.00e-3"] => "1.0e-3",
["1E3", "01E3", "01.E3", "1.E3", "1.000E3", "01.00E3"] => "1.0e3",
["1E+3", "01E+3", "01.E+3", "1.E+3", "1.000E+3", "01.00E+3"] => "1.0e+3",
["1E-3", "01E-3", "01.E-3", "1.E-3", "1.000E-3", "01.00E-3"] => "1.0e-3",
["1f3", "01f3", "01.f3", "1.f3", "1.000f3", "01.00f3"] => "1.0f3",
["1f+3", "01f+3", "01.f+3", "1.f+3", "1.000f+3", "01.00f+3"] => "1.0f+3",
["1f-3", "01f-3", "01.f-3", "1.f-3", "1.000f-3", "01.00f-3"] => "1.0f-3",
]
mod = Module()
for (as, b) in test_cases
for a in as
c = Core.eval(mod, Meta.parse(a))
d = Core.eval(mod, Meta.parse(b))
@test c == d
@test typeof(c) == typeof(d)
@test format_string(a) == b
end
end
@test format_string(input_str) == output_str
end

0 comments on commit 78b4a5a

Please sign in to comment.