aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-22 15:20:26 +0000
committer_ov4 <ov4@ov4.org>2026-10-22 15:20:26 +0000
commit9ef1cc2424e5afa21d3980b56046f5fb71bff89f (patch)
tree7b1fff9f3114601e3fa7a29ecc9e31481a4bccf6
parent5c491c54a22044ef1570c3fb709e14264e367fb1 (diff)
feat(output)!: print REAL values in shortest round-trip form
OUTPUT and NUM_TO_STR now share Python repr formatting instead of %f. BREAKING CHANGE: REAL output changes from six fixed decimals (2.500000) to the shortest round-trip text (2.5). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS/PITFALLS/core-language.md5
-rw-r--r--doc/language/function/cpsc-function-num_to_str.7.md2
-rw-r--r--doc/language/keyword/cpsc-keyword-output.7.md2
-rw-r--r--lib/CodeGen/OutputGen.cc101
-rw-r--r--pseudocode-lib/README.md2
-rw-r--r--pseudocode-lib/string.cc23
-rw-r--r--tests/i/template-classes/expected.txt4
-rw-r--r--tests/i/template-explicit-args/expected.txt6
-rw-r--r--tests/i/template-functions/expected.txt2
-rw-r--r--tests/i/template-variadic/expected.txt6
-rw-r--r--tests/library/exp-string/expected.txt2
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