diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-23 01:24:49 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-23 01:24:49 +0000 |
| commit | 0111978da7b1643d92fb70461cb44ab9723896c7 (patch) | |
| tree | cef7da32e3c41a4e99bb4d1e1a11eab2c341c509 | |
| parent | a0dba2acdeb6e2e9af8e53a4d886fba79c7a3afe (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.md | 2 | ||||
| -rw-r--r-- | doc/language/function/cpsc-function-is_num.7.md | 2 | ||||
| -rw-r--r-- | lib/CodeGen/InputGen.cc | 17 | ||||
| -rw-r--r-- | pseudocode-lib/README.md | 4 | ||||
| -rw-r--r-- | pseudocode-lib/string.cc | 4 |
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); |
