diff --git a/README.md b/README.md index 8da07a4..31887d1 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/Runic.jl b/src/Runic.jl index c9c0901..f6d4732 100644 --- a/src/Runic.jl +++ b/src/Runic.jl @@ -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_literals(ctx, node) @return_something format_oct_literals(ctx, node) + @return_something format_float_literals(ctx, node) # If the node is unchanged at this point, just keep going. diff --git a/src/runestone.jl b/src/runestone.jl index 85cfd62..d20ce62 100644 --- a/src/runestone.jl +++ b/src/runestone.jl @@ -87,3 +87,55 @@ function format_oct_literals(ctx::Context, node::JuliaSyntax.GreenNode) node′ = JuliaSyntax.GreenNode(JuliaSyntax.head(node), nb, ()) return node′ end + +function format_float_literals(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"^(?\d*)(?:\.?(?\d*))?(?:(?[eEf][+-]?)(?\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 diff --git a/test/runtests.jl b/test/runtests.jl index 73f2538..3bfd82a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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