From 0f24e17c1b14f9e161e2f2d65c1c451ec6446714 Mon Sep 17 00:00:00 2001 From: Gilbert Ramirez Date: Tue, 19 May 2026 20:45:19 -0500 Subject: [PATCH] issue 21: parse floats in scientific notation Extends the number parser to accept exponent notation like `1e5`, `1.5E-3`, and `2e+10`, in addition to plain decimal floats. Also changes the `FloatingPoint` variant of `Number.Outcome` to carry `{ val : Float, text : String }` instead of a bare `Float`, so the original source representation is preserved alongside the parsed value. This is useful for formatting and code generation, where we need to know the author's original represenation. --- src/Compiler/Parse/Number.gren | 58 ++++++++++++++++--- tests/src/Test/Compiler/Parse/Expression.gren | 2 +- tests/src/Test/Compiler/Parse/Number.gren | 35 ++++++++++- 3 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/Compiler/Parse/Number.gren b/src/Compiler/Parse/Number.gren index d0050ed..fc24438 100644 --- a/src/Compiler/Parse/Number.gren +++ b/src/Compiler/Parse/Number.gren @@ -26,7 +26,7 @@ import Compiler.Parse.Context exposing (Context) -} type Outcome = Integer Int - | FloatingPoint Float + | FloatingPoint { val : Float, text : String } | Hex Int @@ -70,8 +70,8 @@ parser = Integer int -> Integer (negate int) - FloatingPoint float -> - FloatingPoint (negate float) + FloatingPoint record -> + FloatingPoint { record | val = negate record.val, text = "-" ++ record.text } Hex hex -> Hex (negate hex) @@ -104,6 +104,7 @@ zeroOrHigher = |> Parser.map Hex , Parser.chompChar '.' NotANumber |> Parser.andThen (\_ -> fractalParser "0") + , exponentParser "0" , Parser.chompIf Char.isDigit NotANumber |> Parser.andThen (\_ -> Parser.problem LeadingZero) , Parser.succeed (Integer 0) @@ -122,6 +123,7 @@ zeroOrHigher = Parser.oneOf [ Parser.chompChar '.' NotANumber |> Parser.andThen (\_ -> fractalParser str) + , exponentParser str , Parser.succeed (Integer num) ] ) @@ -135,12 +137,52 @@ fractalParser str = |> Parser.getChompedString |> Parser.andThen (\postDot -> - when String.toFloat (str ++ "." ++ postDot) is - Nothing -> - Parser.problem NotANumber + let + base = str ++ "." ++ postDot + in + Parser.oneOf + [ exponentParser base + , when String.toFloat base is + Nothing -> + Parser.problem NotANumber + + Just float -> + Parser.succeed (FloatingPoint { val = float, text = base }) + ] + ) - Just float -> - Parser.succeed (FloatingPoint float) + +exponentParser : String -> Parser Context Error Outcome +exponentParser base = + Parser.chompIf (\c -> c == 'e' || c == 'E') NotANumber + |> Parser.getChompedString + |> Parser.andThen + (\eChar -> + Parser.oneOf + [ Parser.chompChar '+' NotANumber + |> Parser.getChompedString + , Parser.chompChar '-' NotANumber + |> Parser.getChompedString + , Parser.succeed "" + ] + |> Parser.andThen + (\sign -> + Parser.chompIf Char.isDigit ExpectedInt + |> Parser.skip (Parser.chompWhile Char.isDigit) + |> Parser.getChompedString + |> Parser.andThen + (\expDigits -> + let + fullText = base ++ eChar ++ sign ++ expDigits + in + when String.toFloat fullText is + Nothing -> + Parser.problem NotANumber + + Just float -> + Parser.succeed (FloatingPoint { val = float, text = fullText }) + ) + ) ) {-| A parser that only parses hex-encoded integers, like `0xAB`. diff --git a/tests/src/Test/Compiler/Parse/Expression.gren b/tests/src/Test/Compiler/Parse/Expression.gren index 71f7a91..b049010 100644 --- a/tests/src/Test/Compiler/Parse/Expression.gren +++ b/tests/src/Test/Compiler/Parse/Expression.gren @@ -29,7 +29,7 @@ tests = ) , test "Float" <| \_ -> Parser.run PE.parser Context.empty "3.14" - |> expectExpression (AST.NumberLiteral (Number.FloatingPoint 3.14)) + |> expectExpression (AST.NumberLiteral (Number.FloatingPoint { val = 3.14, text = "3.14" })) , test "Hex" <| \_ -> Parser.run PE.parser Context.empty "0xDE" |> expectExpression (AST.NumberLiteral (Number.Hex 0xDE)) diff --git a/tests/src/Test/Compiler/Parse/Number.gren b/tests/src/Test/Compiler/Parse/Number.gren index b9e20d0..d5a4474 100644 --- a/tests/src/Test/Compiler/Parse/Number.gren +++ b/tests/src/Test/Compiler/Parse/Number.gren @@ -34,13 +34,13 @@ tests = |> Result.map (\outcome -> when outcome is - PN.Integer value -> - PN.FloatingPoint (toFloat value) + PN.Integer value -> + PN.FloatingPoint { val = toFloat value, text = String.fromInt value } _ -> outcome ) - |> Expect.equal (Ok (PN.FloatingPoint float)) + |> Expect.equal (Ok (PN.FloatingPoint { val = float, text = String.fromFloat float })) , test "Requires a number after ." <| \{} -> run "0." |> expectErr PN.ExpectedInt @@ -48,6 +48,35 @@ tests = run "0.15a" |> expectErr PN.NotANumber ] + , describe "Scientific notation" + [ test "integer with exponent" <| \{} -> + run "1e5" + |> Expect.equal (Ok (PN.FloatingPoint { val = 1.0e5, text = "1e5" })) + , test "zero with exponent" <| \{} -> + run "0e3" + |> Expect.equal (Ok (PN.FloatingPoint { val = 0.0e3, text = "0e3" })) + , test "decimal with exponent" <| \{} -> + run "1.5e3" + |> Expect.equal (Ok (PN.FloatingPoint { val = 1.5e3, text = "1.5e3" })) + , test "uppercase E" <| \{} -> + run "1.5E3" + |> Expect.equal (Ok (PN.FloatingPoint { val = 1.5e3, text = "1.5E3" })) + , test "explicit positive exponent" <| \{} -> + run "1.5e+3" + |> Expect.equal (Ok (PN.FloatingPoint { val = 1.5e3, text = "1.5e+3" })) + , test "negative exponent" <| \{} -> + run "1.5e-3" + |> Expect.equal (Ok (PN.FloatingPoint { val = 1.5e-3, text = "1.5e-3" })) + , test "negative number with exponent" <| \{} -> + run "-1.5e3" + |> Expect.equal (Ok (PN.FloatingPoint { val = -1.5e3, text = "-1.5e3" })) + , test "requires digits after e" <| \{} -> + run "1e" + |> expectErr PN.ExpectedInt + , test "requires digits after e sign" <| \{} -> + run "1e+" + |> expectErr PN.ExpectedInt + ] , describe "Hex" [ test "Can parse hex" <| \{} -> run "0xAFFE"