diff options
| -rw-r--r-- | AGENTS/PITFALLS/core-language.md | 5 | ||||
| -rw-r--r-- | doc/language/function/cpsc-function-num_to_str.7.md | 2 | ||||
| -rw-r--r-- | doc/language/keyword/cpsc-keyword-output.7.md | 2 | ||||
| -rw-r--r-- | lib/CodeGen/OutputGen.cc | 101 | ||||
| -rw-r--r-- | pseudocode-lib/README.md | 2 | ||||
| -rw-r--r-- | pseudocode-lib/string.cc | 23 | ||||
| -rw-r--r-- | tests/i/template-classes/expected.txt | 4 | ||||
| -rw-r--r-- | tests/i/template-explicit-args/expected.txt | 6 | ||||
| -rw-r--r-- | tests/i/template-functions/expected.txt | 2 | ||||
| -rw-r--r-- | tests/i/template-variadic/expected.txt | 6 | ||||
| -rw-r--r-- | tests/library/exp-string/expected.txt | 2 |
11 files changed, 133 insertions, 22 deletions
diff --git a/AGENTS/PITFALLS/core-language.md b/AGENTS/PITFALLS/core-language.md index 219c0d8..60fbd11 100644 --- a/AGENTS/PITFALLS/core-language.md +++ b/AGENTS/PITFALLS/core-language.md @@ -31,8 +31,9 @@ source, most also by compiling and running test programs. DIV/MOD only. - CHR returns an owning one-byte STRING, not CHAR; CHR(0) retains one NUL byte. - STR_TO_NUM always returns REAL and shares parseNumber (strtod, full consumption, overflow rejected) with IS_NUM. - - NUM_TO_STR uses std::to_string, preserving the full signed i64 range and six - fractional digits for REAL. + - NUM_TO_STR keeps the full signed i64 range for INTEGER; REAL uses the shortest + round-trip text (realText in string.cc), byte-identical to OUTPUT's IR helper in + OutputGen.cc (`__cps_output_real`); change both together. C_DOUBLE output keeps `%f`. - **`getExprTypeInfo` returns nullptr** for expressions it can't classify (null-check it at any new call site) and internally defaults to INTEGER for unclassified arithmetic expressions; unknown or unmatched calls diagnose and return nullptr. diff --git a/doc/language/function/cpsc-function-num_to_str.7.md b/doc/language/function/cpsc-function-num_to_str.7.md index 39b6890..8e895ee 100644 --- a/doc/language/function/cpsc-function-num_to_str.7.md +++ b/doc/language/function/cpsc-function-num_to_str.7.md @@ -14,7 +14,7 @@ NUM_TO_STR(number : INTEGER or REAL) RETURNS STRING Provided by the standard string library, loaded and linked by default. -Format INTEGER as signed decimal text and REAL with six digits after the decimal point. +Format INTEGER as signed decimal text and REAL in the same shortest round-trip form that OUTPUT prints. The text is the shortest decimal text that reads back as the same value (as Python's `repr`): fixed notation with at least one fractional digit when 1e-4 <= |x| < 1e16 (`2.0`, `87.5`, `0.0001`), otherwise scientific notation with an exponent of at least two digits (`1e-07`, `1.5e-07`, `1e+16`); special values print as `-0.0`, `inf`, `-inf` and `nan`. # EXAMPLE diff --git a/doc/language/keyword/cpsc-keyword-output.7.md b/doc/language/keyword/cpsc-keyword-output.7.md index 06d9910..d115ace 100644 --- a/doc/language/keyword/cpsc-keyword-output.7.md +++ b/doc/language/keyword/cpsc-keyword-output.7.md @@ -12,7 +12,7 @@ OUTPUT expression, ... # DESCRIPTION -Print expressions from left to right without automatic separators, then add a newline. Strings print their contents; BOOLEAN prints TRUE or FALSE. Whole arrays and partially indexed arrays print their elements one per line; a final array adds no extra newline. Other records, classes, and pointers cannot be printed directly. +Print expressions from left to right without automatic separators, then add a newline. Strings print their contents; BOOLEAN prints TRUE or FALSE. REAL values use the shortest decimal text that reads back as the same value (as Python's `repr`): fixed notation with at least one fractional digit when 1e-4 <= |x| < 1e16 (`2.0`, `87.5`, `0.0001`), otherwise scientific notation with an exponent of at least two digits (`1e-07`, `1.5e-07`, `1e+16`); special values print as `-0.0`, `inf`, `-inf` and `nan`. OUTPUT and NUM_TO_STR produce identical text. Whole arrays and partially indexed arrays print their elements one per line; a final array adds no extra newline. Other records, classes, and pointers cannot be printed directly. C scalars print with their native type's format and C argument promotions. Plain char prints a character, other C integers print numbers, and _Bool prints TRUE or FALSE. C_STRING is a pointer: cast it to STRING to print its contents. See [cpsc-keyword-c_type(7)](cpsc-keyword-c_type.7.md). diff --git a/lib/CodeGen/OutputGen.cc b/lib/CodeGen/OutputGen.cc index c1ea4b6..faf21df 100644 --- a/lib/CodeGen/OutputGen.cc +++ b/lib/CodeGen/OutputGen.cc @@ -3,6 +3,98 @@ using namespace llvm; using namespace cps; +namespace { +// Defines (once per module) `void __cps_output_real(double, i1 Newline)`, which prints a REAL in +// the shortest round-trip form of Python's repr: fixed notation (at least one decimal) for +// 1e-4 <= |x| < 1e16, otherwise scientific with a two-digit exponent. The text is +// NUM_TO_STR-identical to _string_impl_from_real in pseudocode-lib/string.cc; keep both in sync. +FunctionCallee getOutputRealHelper(Module &M) { + LLVMContext &Ctx = M.getContext(); + Type *VoidTy = Type::getVoidTy(Ctx), *I32 = Type::getInt32Ty(Ctx), *I64 = Type::getInt64Ty(Ctx); + Type *DoubleTy = Type::getDoubleTy(Ctx), *I1 = Type::getInt1Ty(Ctx); + PointerType *Ptr = PointerType::getUnqual(Ctx); + FunctionType *HelperTy = FunctionType::get(VoidTy, {DoubleTy, I1}, false); + if (Function *Existing = M.getFunction("__cps_output_real")) return {HelperTy, Existing}; + + FunctionCallee Printf = M.getOrInsertFunction("printf", FunctionType::get(I32, {Ptr}, true)); + FunctionCallee Snprintf = M.getOrInsertFunction("snprintf", FunctionType::get(I32, {Ptr, I64, Ptr}, true)); + FunctionCallee Strtod = M.getOrInsertFunction("strtod", FunctionType::get(DoubleTy, {Ptr, Ptr}, false)); + + Function *F = Function::Create(HelperTy, Function::InternalLinkage, "__cps_output_real", &M); + Value *X = F->getArg(0), *Newline = F->getArg(1); + BasicBlock *Entry = BasicBlock::Create(Ctx, "entry", F); + BasicBlock *NanBB = BasicBlock::Create(Ctx, "nan", F), *NotNan = BasicBlock::Create(Ctx, "notnan", F); + BasicBlock *InfBB = BasicBlock::Create(Ctx, "inf", F), *Finite = BasicBlock::Create(Ctx, "finite", F); + BasicBlock *FixedHead = BasicBlock::Create(Ctx, "fixed.head", F), *FixedBody = BasicBlock::Create(Ctx, "fixed.body", F); + BasicBlock *SciHead = BasicBlock::Create(Ctx, "sci.head", F), *SciBody = BasicBlock::Create(Ctx, "sci.body", F); + BasicBlock *Print = BasicBlock::Create(Ctx, "print", F); + + IRBuilder<> B(Entry); + constexpr uint64_t BufSize = 64; + Value *Buf = B.CreateAlloca(ArrayType::get(Type::getInt8Ty(Ctx), BufSize), nullptr, "buf"); + auto Str = [&](const char *Text, const char *Name) { return B.CreateGlobalString(Text, Name, 0, &M); }; + Value *SizeV = ConstantInt::get(I64, BufSize); + Value *NanCheck = B.CreateFCmpUNO(X, X); + B.CreateCondBr(NanCheck, NanBB, NotNan); + + B.SetInsertPoint(NanBB); + B.CreateCall(Snprintf, {Buf, SizeV, Str("%s", "fmt_real_s"), Str("nan", "real_nan")}); + B.CreateBr(Print); + + B.SetInsertPoint(NotNan); + Value *IsNeg = B.CreateFCmpOLT(X, ConstantFP::get(DoubleTy, 0.0)); + Value *Abs = B.CreateSelect(IsNeg, B.CreateFNeg(X), X, "abs"); + B.CreateCondBr(B.CreateFCmpOEQ(Abs, ConstantFP::getInfinity(DoubleTy)), InfBB, Finite); + + B.SetInsertPoint(InfBB); + B.CreateCall(Snprintf, {Buf, SizeV, Str("%s", "fmt_real_s"), + B.CreateSelect(IsNeg, Str("-inf", "real_ninf"), Str("inf", "real_inf"))}); + B.CreateBr(Print); + + B.SetInsertPoint(Finite); + Value *IsFixed = B.CreateOr( + B.CreateAnd(B.CreateFCmpOGE(Abs, ConstantFP::get(DoubleTy, 1e-4)), + B.CreateFCmpOLT(Abs, ConstantFP::get(DoubleTy, 1e16))), + B.CreateFCmpOEQ(Abs, ConstantFP::get(DoubleTy, 0.0))); + B.CreateCondBr(IsFixed, FixedHead, SciHead); + + // Fixed notation: the fewest decimals (at least one) that round-trip. + B.SetInsertPoint(FixedHead); + PHINode *Decimals = B.CreatePHI(I32, 2, "decimals"); + Value *FixedFmt = Str("%.*f", "fmt_real_f"); + B.CreateCall(Snprintf, {Buf, SizeV, FixedFmt, Decimals, X}); + Value *FixedBack = B.CreateCall(Strtod, {Buf, ConstantPointerNull::get(Ptr)}); + Value *FixedDone = B.CreateOr(B.CreateFCmpOEQ(FixedBack, X), + B.CreateICmpUGE(Decimals, ConstantInt::get(I32, 40))); + B.CreateCondBr(FixedDone, Print, FixedBody); + B.SetInsertPoint(FixedBody); + Value *NextDecimals = B.CreateAdd(Decimals, ConstantInt::get(I32, 1)); + B.CreateBr(FixedHead); + Decimals->addIncoming(ConstantInt::get(I32, 1), Finite); + Decimals->addIncoming(NextDecimals, FixedBody); + + // Scientific notation: the fewest significant digits (1..17) that round-trip. + B.SetInsertPoint(SciHead); + PHINode *Precision = B.CreatePHI(I32, 2, "precision"); + Value *SciFmt = Str("%.*e", "fmt_real_e"); + B.CreateCall(Snprintf, {Buf, SizeV, SciFmt, Precision, X}); + Value *SciBack = B.CreateCall(Strtod, {Buf, ConstantPointerNull::get(Ptr)}); + Value *SciDone = B.CreateOr(B.CreateFCmpOEQ(SciBack, X), + B.CreateICmpUGE(Precision, ConstantInt::get(I32, 16))); + B.CreateCondBr(SciDone, Print, SciBody); + B.SetInsertPoint(SciBody); + Value *NextPrecision = B.CreateAdd(Precision, ConstantInt::get(I32, 1)); + B.CreateBr(SciHead); + Precision->addIncoming(ConstantInt::get(I32, 0), Finite); + Precision->addIncoming(NextPrecision, SciBody); + + B.SetInsertPoint(Print); + B.CreateCall(Printf, {B.CreateSelect(Newline, Str("%s\n", "fmt_real_out_nl"), Str("%s", "fmt_real_out")), Buf}); + B.CreateRetVoid(); + return {HelperTy, F}; +} +} + Value *CodeGen::emitStringNullGuard(Value *StrVal) { if (!StrVal || !StrVal->getType()->isPointerTy()) return StrVal; Value *IsNull = Builder->CreateICmpEQ(StrVal, @@ -28,12 +120,9 @@ void CodeGen::emitOutputValue(Value *Val, const TypeInfo *Info, bool AppendNewli // kind is added without deciding its OUTPUT story. switch (Info->Kind) { case TypeKind::Real: - Fmt = AppendNewline - ? PrintfFloatFormatStr - : Builder->CreateGlobalString("%f", "fmt_flt_inline", 0, TheModule.get()); - Args.push_back(Fmt); - Args.push_back(Val); - break; + Builder->CreateCall(getOutputRealHelper(*TheModule), + {coerceValueToType(Val, resolveType("REAL")), Builder->getInt1(AppendNewline)}); + return; case TypeKind::Boolean: { Value *BoolVal = coerceValueToType(Val, resolveType("BOOLEAN")); diff --git a/pseudocode-lib/README.md b/pseudocode-lib/README.md index 1e360a4..4e70a44 100644 --- a/pseudocode-lib/README.md +++ b/pseudocode-lib/README.md @@ -34,7 +34,7 @@ The library provides `&`, six case-sensitive content comparisons, and these ordi - `LCASE(CHAR)` and `UCASE(CHAR)` return CHAR, following guide section 5.5. STRING overloads return independent strings. Nonletters and embedded NUL bytes are preserved; use ASCII for predictable case conversion. - `ASC(CHAR)` and `ASC(STRING)` return the unsigned character or first-byte code; an empty string returns zero. - `CHR(Code)` returns a one-byte STRING containing the low eight bits. `LENGTH(CHR(0))` is 1. -- `NUM_TO_STR(INTEGER)` and `NUM_TO_STR(REAL)` use C++17 `std::to_string`, with six fractional digits for REAL. +- `NUM_TO_STR(INTEGER)` and `NUM_TO_STR(REAL)` convert INTEGER to decimal text and REAL to the shortest round-trip text (Python `repr` style, identical to OUTPUT: `2.0`, `0.1`, `1e-07`, `inf`). - `STR_TO_NUM(S)` returns REAL through the same `parseNumber` helper as IS_NUM (`strtod`, full consumption). Invalid text and trailing junk throw `STR_TO_NUM: '<text>' is not a number`; overflow throws `... is out of the REAL range`; underflow is accepted. - `IS_NUM(S)` agrees with STR_TO_NUM: full consumption by `strtod` and no overflow, without throwing. Leading whitespace is accepted; trailing whitespace, trailing junk, empty input, embedded NUL bytes, and overflowing values (`1e999`) are rejected. diff --git a/pseudocode-lib/string.cc b/pseudocode-lib/string.cc index 34f1ec5..a4c7343 100644 --- a/pseudocode-lib/string.cc +++ b/pseudocode-lib/string.cc @@ -23,6 +23,27 @@ decltype(auto) checked(Operation &&Op) { } std::string &text(void *Handle) { return *static_cast<std::string *>(Handle); } +// Python-repr-style shortest round-trip text for a REAL. Keep in sync with getOutputRealHelper in +// lib/CodeGen/OutputGen.cc, which OUTPUT uses. +std::string realText(double Number) { + if (std::isnan(Number)) return "nan"; + if (std::isinf(Number)) return Number < 0 ? "-inf" : "inf"; + char Buffer[64]; + double Abs = std::fabs(Number); + if (Abs == 0 || (Abs >= 1e-4 && Abs < 1e16)) { + for (int Decimals = 1; Decimals < 40; ++Decimals) { + std::snprintf(Buffer, sizeof Buffer, "%.*f", Decimals, Number); + if (std::strtod(Buffer, nullptr) == Number) break; + } + } else { + for (int Precision = 0; Precision < 17; ++Precision) { + std::snprintf(Buffer, sizeof Buffer, "%.*e", Precision, Number); + if (std::strtod(Buffer, nullptr) == Number) break; + } + } + return Buffer; +} + enum class NumberStatus { Ok, Invalid, OutOfRange }; // Shared by IS_NUM and STR_TO_NUM so that they always agree. @@ -70,7 +91,7 @@ void _string_impl_from_int(void *Target, long long Number) { checked([&] { text(Target) = std::to_string(Number); }); } void _string_impl_from_real(void *Target, double Number) { - checked([&] { text(Target) = std::to_string(Number); }); + checked([&] { text(Target) = realText(Number); }); } double _string_impl_to_real(void *Handle) { return checked([&] { diff --git a/tests/i/template-classes/expected.txt b/tests/i/template-classes/expected.txt index 9413c45..6c891be 100644 --- a/tests/i/template-classes/expected.txt +++ b/tests/i/template-classes/expected.txt @@ -2,7 +2,7 @@ FALSE 0 7 3 -2.500000 +2.5 30 20 10 @@ -11,5 +11,5 @@ FALSE alpha beta 1 -2.500000 +2.5 c diff --git a/tests/i/template-explicit-args/expected.txt b/tests/i/template-explicit-args/expected.txt index 1c46cb5..5e2993d 100644 --- a/tests/i/template-explicit-args/expected.txt +++ b/tests/i/template-explicit-args/expected.txt @@ -1,8 +1,8 @@ -2.000000 +2.0 3 tail -0.000000 -2.500000 +0.0 +2.5 9 200 100 diff --git a/tests/i/template-functions/expected.txt b/tests/i/template-functions/expected.txt index 0f27430..72f49e8 100644 --- a/tests/i/template-functions/expected.txt +++ b/tests/i/template-functions/expected.txt @@ -1,5 +1,5 @@ 7 -2.500000 +2.5 pear 3 42 diff --git a/tests/i/template-variadic/expected.txt b/tests/i/template-variadic/expected.txt index a3177f6..f776296 100644 --- a/tests/i/template-variadic/expected.txt +++ b/tests/i/template-variadic/expected.txt @@ -1,11 +1,11 @@ 1 -2.500000 +2.5 c str 1 -2.500000 +2.5 7 -3.500000 +3.5 30 10 20 diff --git a/tests/library/exp-string/expected.txt b/tests/library/exp-string/expected.txt index 030a4c2..3211dab 100644 --- a/tests/library/exp-string/expected.txt +++ b/tests/library/exp-string/expected.txt @@ -3,7 +3,7 @@ helloworld 3 20500 20500 -421.250000 +421.25 TRUE conversion caught length caught |
