tala-compiler-0.1.0: test/RuntimeText.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | A reference for the text functions of the runtime, used only by tests.
The runtime library that programs call is written in C++ and works on
machine integers, a buffer of scalars and a bounded natural number. This
module states what each of its functions returns in the most direct way the
language of the tests allows: unbounded integers, exact rationals and lists.
It shares nothing with the library, so a test that runs a program in the
reference evaluator and through the native pipeline compares two
independent answers.
A floating-point value is an exact rational here: the value of the binary
number, not of the decimal literal it was written as. @%f@ rounds that exact
value to the digits asked for, a tie to the even digit.
-}
module RuntimeText
( Conversion (..)
, plain
, textOfSigned
, textOfBool
, formatInteger
, formatFloating
, formatText
, lineTerminator
) where
import Data.Bits ((.&.))
import Numeric (showHex)
import Progmasoft.Tala.RuntimeCall
-- | The flags, the width and the precision of one conversion, as the runtime takes them.
data Conversion = Conversion
{ conversionFlags :: Integer
, conversionWidth :: Integer
, conversionPrecision :: Integer
}
deriving (Eq, Show)
-- | A conversion with nothing written in it.
plain :: Conversion
plain = Conversion 0 absent absent
has :: Conversion -> Integer -> Bool
has conversion flag = conversionFlags conversion .&. flag /= 0
{- | The line terminator the reference writes. The native runtime writes the
terminator of its platform; a test that compares the two reads a carriage
return and line feed as this.
-}
lineTerminator :: String
lineTerminator = "\n"
-- | An integer in decimal, with a minus sign when negative.
textOfSigned :: Integer -> String
textOfSigned = show
textOfBool :: Bool -> String
textOfBool flag = if flag then "true" else "false"
{- | The field of a conversion: what stands before the body, the body, and the
padding that brings the two to the width. Zeros stand between the two.
-}
field :: Conversion -> Bool -> String -> String -> String
field conversion zerosAllowed prefix body
| has conversion flagLeft = prefix ++ body ++ replicate padding ' '
| zerosAllowed && has conversion flagZero = prefix ++ replicate padding '0' ++ body
| otherwise = replicate padding ' ' ++ prefix ++ body
where
padding = max 0 (fromInteger (conversionWidth conversion) - length prefix - length body)
sign :: Conversion -> Bool -> String
sign conversion negative
| negative = "-"
| has conversion flagPlus = "+"
| has conversion flagSpace = " "
| otherwise = ""
-- | Apostrophes between groups of three digits, counted from the right.
grouped :: String -> String
grouped digits = reverse (go (reverse digits))
where
go (a : b : c : rest@(_ : _)) = a : b : c : '\'' : go rest
go remaining = remaining
{- | @%d@, @%u@ and @%x@. A negative number is its sign and its magnitude in
either base.
-}
formatInteger :: Conversion -> Integer -> String
formatInteger conversion value = field conversion True prefix body
where
hexadecimal = has conversion flagHexadecimal
magnitude = abs value
digits = if hexadecimal then showHex magnitude "" else show magnitude
body = if not hexadecimal && has conversion flagGroup then grouped digits else digits
prefix = sign conversion (value < 0) ++ (if hexadecimal && has conversion flagAlternate then "0x" else "")
{- | @%f@ of an exact value. The flag says whether the value is negative zero,
which a rational cannot say for itself.
-}
formatFloating :: Conversion -> Bool -> Rational -> String
formatFloating conversion negativeZero value = field conversion True (sign conversion negative) body
where
negative = value < 0 || negativeZero
precision = if conversionPrecision conversion < 0 then 6 else conversionPrecision conversion
-- 'round' on a rational rounds a tie to the even integer.
scaled = round (abs value * 10 ^ precision) :: Integer
digits = show scaled
padded = replicate (fromInteger precision + 1 - length digits) '0' ++ digits
(whole, fraction) = splitAt (length padded - fromInteger precision) padded
integer = if has conversion flagGroup then grouped whole else whole
body = if precision > 0 then integer ++ "." ++ fraction else integer
-- | @%s@ and @%c@: at most the precision's number of characters, when there is one.
formatText :: Conversion -> String -> String
formatText conversion value = field conversion False "" kept
where
kept = if conversionPrecision conversion >= 0 then take (fromInteger (conversionPrecision conversion)) value else value