aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-23 01:24:49 +0000
committer_ov4 <ov4@ov4.org>2026-10-23 01:24:49 +0000
commit0111978da7b1643d92fb70461cb44ab9723896c7 (patch)
treecef7da32e3c41a4e99bb4d1e1a11eab2c341c509
parenta0dba2acdeb6e2e9af8e53a4d886fba79c7a3afe (diff)
fix(stdlib): reject NaN, infinity and hexadecimal text in IS_NUM, STR_TO_NUM and INPUT
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--AGENTS/PITFALLS/core-language.md2
-rw-r--r--doc/language/function/cpsc-function-is_num.7.md2
-rw-r--r--lib/CodeGen/InputGen.cc17
-rw-r--r--pseudocode-lib/README.md4
-rw-r--r--pseudocode-lib/string.cc4
5 files changed, 17 insertions, 12 deletions
diff --git a/AGENTS/PITFALLS/core-language.md b/AGENTS/PITFALLS/core-language.md
index 5ff97ca..5018fb3 100644
--- a/AGENTS/PITFALLS/core-language.md
+++ b/AGENTS/PITFALLS/core-language.md
@@ -32,7 +32,7 @@ source, most also by compiling and running test programs.
- **`/` always yields REAL** (double FDiv), even for two INTEGERs; integer division is
DIV/MOD only.
- CHR returns CHAR, not STRING; CHR(0) is a NUL CHAR that converts to a one-byte STRING.
- - STR_TO_NUM always returns REAL and shares parseNumber (strtod, full consumption, overflow rejected) with IS_NUM.
+ - STR_TO_NUM always returns REAL and shares parseNumber (strtod, full consumption, overflow rejected, only `0-9 . e E + -` after leading whitespace, so no nan/inf/hex) with IS_NUM and INPUT REAL (`__cps_parse_real` in InputGen.cc); change them together.
- 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`.
diff --git a/doc/language/function/cpsc-function-is_num.7.md b/doc/language/function/cpsc-function-is_num.7.md
index 3b9641d..8b07b02 100644
--- a/doc/language/function/cpsc-function-is_num.7.md
+++ b/doc/language/function/cpsc-function-is_num.7.md
@@ -14,7 +14,7 @@ IS_NUM(text : STRING) RETURNS BOOLEAN
Provided by the standard string library, loaded and linked by default.
-Return true exactly when STR_TO_NUM would succeed: a nonempty string fully consumed by `strtod`, with a value inside the REAL range. Leading whitespace, NaN, inf and hexadecimal forms are accepted; trailing whitespace, trailing text, embedded NUL bytes and overflowing values such as `1e999` are rejected. Underflow to zero or a denormal is accepted.
+Return true exactly when STR_TO_NUM would succeed: a nonempty string fully consumed by `strtod`, with a value inside the REAL range. After any leading whitespace, only the characters `0123456789.eE+-` may appear, so NaN, inf/infinity and hexadecimal forms are rejected. Leading whitespace, exponents (`1e3`), a leading `+`, `.5` and `5.` are accepted; trailing whitespace, trailing text, embedded NUL bytes and overflowing values such as `1e999` are rejected. Underflow to zero or a denormal is accepted.
# EXAMPLE
diff --git a/lib/CodeGen/InputGen.cc b/lib/CodeGen/InputGen.cc
index 7c2b181..b90daae 100644
--- a/lib/CodeGen/InputGen.cc
+++ b/lib/CodeGen/InputGen.cc
@@ -244,18 +244,20 @@ FunctionCallee getParseUIntHelper(Module &M) {
}
// Defines `i1 __cps_parse_real(ptr token, ptr out)`: the rule of parseNumber in pseudocode-lib/
-// string.cc (shared by IS_NUM and STR_TO_NUM): strtod must consume the whole token and must not
-// overflow to infinity. A finite overflow is told from an "inf" literal by the missing letter i.
+// string.cc (shared by IS_NUM and STR_TO_NUM): the token holds only 0-9 . e E + -, which rules
+// out nan, inf/infinity and hexadecimal; strtod must consume the whole token and must not
+// overflow to infinity, so an infinite result can only come from overflow.
FunctionCallee getParseRealHelper(Module &M) {
LLVMContext &Ctx = M.getContext();
- Type *I1 = Type::getInt1Ty(Ctx), *I8 = Type::getInt8Ty(Ctx), *I32 = Type::getInt32Ty(Ctx);
+ Type *I1 = Type::getInt1Ty(Ctx), *I8 = Type::getInt8Ty(Ctx), *I64 = Type::getInt64Ty(Ctx);
Type *DoubleTy = Type::getDoubleTy(Ctx);
PointerType *Ptr = PointerType::getUnqual(Ctx);
FunctionType *HelperTy = FunctionType::get(I1, {Ptr, Ptr}, false);
if (Function *Existing = M.getFunction("__cps_parse_real")) return {HelperTy, Existing};
FunctionCallee Strtod = M.getOrInsertFunction("strtod", FunctionType::get(DoubleTy, {Ptr, Ptr}, false));
- FunctionCallee Strchr = M.getOrInsertFunction("strchr", FunctionType::get(Ptr, {Ptr, I32}, false));
+ FunctionCallee Strspn = M.getOrInsertFunction("strspn", FunctionType::get(I64, {Ptr, Ptr}, false));
+ FunctionCallee Strlen = M.getOrInsertFunction("strlen", FunctionType::get(I64, {Ptr}, false));
Function *F = Function::Create(HelperTy, Function::InternalLinkage, "__cps_parse_real", &M);
Value *Token = F->getArg(0), *Out = F->getArg(1);
IRBuilder<> B(BasicBlock::Create(Ctx, "entry", F));
@@ -265,11 +267,10 @@ FunctionCallee getParseRealHelper(Module &M) {
Value *Progress = B.CreateICmpNE(B.CreateLoad(Ptr, EndSlot), Token);
Value *IsInf = B.CreateFCmpOEQ(B.CreateUnaryIntrinsic(Intrinsic::fabs, Number),
ConstantFP::getInfinity(DoubleTy));
- Value *NoLowerI = B.CreateIsNull(B.CreateCall(Strchr, {Token, ConstantInt::get(I32, 'i')}));
- Value *NoUpperI = B.CreateIsNull(B.CreateCall(Strchr, {Token, ConstantInt::get(I32, 'I')}));
- Value *Overflowed = B.CreateAnd(IsInf, B.CreateAnd(NoLowerI, NoUpperI));
+ Value *Decimal = B.CreateICmpEQ(B.CreateCall(Strspn, {Token, B.CreateGlobalString("0123456789.eE+-")}),
+ B.CreateCall(Strlen, {Token}));
B.CreateStore(Number, Out);
- B.CreateRet(B.CreateAnd(B.CreateAnd(Consumed, Progress), B.CreateNot(Overflowed)));
+ B.CreateRet(B.CreateAnd(Decimal, B.CreateAnd(B.CreateAnd(Consumed, Progress), B.CreateNot(IsInf))));
return {HelperTy, F};
}
diff --git a/pseudocode-lib/README.md b/pseudocode-lib/README.md
index dbdebf3..a12620a 100644
--- a/pseudocode-lib/README.md
+++ b/pseudocode-lib/README.md
@@ -35,7 +35,7 @@ The library provides `&`, six case-sensitive content comparisons, and these ordi
- `ASC(CHAR)` and `ASC(STRING)` return the unsigned character or first-byte code; an empty string returns zero.
- `CHR(Code)` returns a CHAR containing the low eight bits. `LENGTH(CHR(0))` is 1.
- `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)` accepts STRING or CHAR and 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.
+- `STR_TO_NUM(S)` accepts STRING or CHAR and returns REAL through the same `parseNumber` helper as IS_NUM (`strtod`, full consumption, decimal text only: nan, inf/infinity and hexadecimal are rejected). 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; nan, inf/infinity, hexadecimal, trailing whitespace, trailing junk, empty input, embedded NUL bytes, and overflowing values (`1e999`) are rejected.
Library C++ exceptions print the user call line and can be caught with TRY/CATCH. Library functions follow the guide where specified; LEFT, STRING case conversion, byte-code and numeric-conversion functions remain compatibility extensions.
diff --git a/pseudocode-lib/string.cc b/pseudocode-lib/string.cc
index 4395304..a20bebe 100644
--- a/pseudocode-lib/string.cc
+++ b/pseudocode-lib/string.cc
@@ -49,6 +49,10 @@ enum class NumberStatus { Ok, Invalid, OutOfRange };
// Shared by IS_NUM and STR_TO_NUM so that they always agree.
NumberStatus parseNumber(const std::string &Source, double &Result) {
if (Source.empty()) return NumberStatus::Invalid;
+ // Only decimal text: reject nan, inf/infinity and hexadecimal forms that strtod accepts.
+ if (Source.find_first_not_of("0123456789.eE+-", Source.find_first_not_of(" \t\n\v\f\r")) !=
+ std::string::npos)
+ return NumberStatus::Invalid;
char *End = nullptr;
errno = 0;
double Number = std::strtod(Source.c_str(), &End);