From 11266ea732e2533081283290c3c865c234e79b2a Mon Sep 17 00:00:00 2001 From: _ov4 Date: Thu, 22 Oct 2026 15:04:43 +0000 Subject: fix(stdlib): make IS_NUM agree with STR_TO_NUM and explain conversion errors Co-Authored-By: Claude Sonnet 5.5 --- AGENTS/PITFALLS/core-language.md | 2 +- doc/language/function/cpsc-function-is_num.7.md | 2 +- .../function/cpsc-function-str_to_num.7.md | 2 +- pseudocode-lib/README.md | 4 +-- pseudocode-lib/string.cc | 35 ++++++++++++++++------ tests/library/exp-string/expected-stderr.txt | 2 +- 6 files changed, 32 insertions(+), 15 deletions(-) diff --git a/AGENTS/PITFALLS/core-language.md b/AGENTS/PITFALLS/core-language.md index 29c3fa4..219c0d8 100644 --- a/AGENTS/PITFALLS/core-language.md +++ b/AGENTS/PITFALLS/core-language.md @@ -30,7 +30,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 an owning one-byte STRING, not CHAR; CHR(0) retains one NUL byte. - - STR_TO_NUM always returns REAL and uses std::stod with full-consumption checks. + - 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. - **`getExprTypeInfo` returns nullptr** for expressions it can't classify (null-check diff --git a/doc/language/function/cpsc-function-is_num.7.md b/doc/language/function/cpsc-function-is_num.7.md index efb5f1a..3b9641d 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 for a nonempty string fully consumed by the runtime's numeric conversion. Leading whitespace is accepted; trailing whitespace or other trailing text is rejected. +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. # EXAMPLE diff --git a/doc/language/function/cpsc-function-str_to_num.7.md b/doc/language/function/cpsc-function-str_to_num.7.md index acdf60b..48d24db 100644 --- a/doc/language/function/cpsc-function-str_to_num.7.md +++ b/doc/language/function/cpsc-function-str_to_num.7.md @@ -14,7 +14,7 @@ STR_TO_NUM(text : STRING) RETURNS REAL Provided by the standard string library, loaded and linked by default. -Return REAL, including for integer-looking text. The whole string must be consumed. Invalid text, trailing junk and values outside the REAL range throw a catchable exception. Use IS_NUM first to check numeric text without throwing for invalid input. +Return REAL, including for integer-looking text. The whole string must be consumed. Invalid text and trailing junk throw a catchable exception reading `STR_TO_NUM: '' is not a number`, and overflowing values throw `STR_TO_NUM: '' is out of the REAL range`. Underflow is accepted. Use IS_NUM first to check numeric text without throwing for invalid input. # EXAMPLE diff --git a/pseudocode-lib/README.md b/pseudocode-lib/README.md index e0819bf..1e360a4 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 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. -- `STR_TO_NUM(S)` returns REAL using `std::stod` and requires full consumption. Invalid text, trailing junk, and out-of-range values throw. -- `IS_NUM(S)` tests full consumption by `strtod` without throwing for invalid text. Leading whitespace is accepted; trailing whitespace, trailing junk, empty input, and embedded NUL bytes are rejected. +- `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: '' 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. 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 f2f03f6..34f1ec5 100644 --- a/pseudocode-lib/string.cc +++ b/pseudocode-lib/string.cc @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -20,6 +22,20 @@ decltype(auto) checked(Operation &&Op) { } } std::string &text(void *Handle) { return *static_cast(Handle); } + +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; + char *End = nullptr; + errno = 0; + double Number = std::strtod(Source.c_str(), &End); + if (End != Source.c_str() + Source.size()) return NumberStatus::Invalid; + if (errno == ERANGE && std::isinf(Number)) return NumberStatus::OutOfRange; + Result = Number; + return NumberStatus::Ok; +} } extern "C" { @@ -58,11 +74,14 @@ void _string_impl_from_real(void *Target, double Number) { } double _string_impl_to_real(void *Handle) { return checked([&] { - std::size_t Consumed = 0; - double Number = std::stod(text(Handle), &Consumed); - if (Consumed != text(Handle).size()) - throw std::invalid_argument("unconsumed characters in string conversion"); - return Number; + double Number = 0; + switch (parseNumber(text(Handle), Number)) { + case NumberStatus::Ok: return Number; + case NumberStatus::OutOfRange: + throw std::out_of_range("STR_TO_NUM: '" + text(Handle) + "' is out of the REAL range"); + case NumberStatus::Invalid: break; + } + throw std::invalid_argument("STR_TO_NUM: '" + text(Handle) + "' is not a number"); }); } long long _string_impl_length(void *Handle) { @@ -130,10 +149,8 @@ void _string_impl_chr(void *Target, long long Code) { } int _string_impl_is_num(void *Handle) { return checked([&]() -> int { - const auto &Source = text(Handle); - char *End = nullptr; - std::strtod(Source.c_str(), &End); - return !Source.empty() && End == Source.c_str() + Source.size(); + double Number = 0; + return parseNumber(text(Handle), Number) == NumberStatus::Ok; }); } const char *_string_impl_c_str(void *Handle) { diff --git a/tests/library/exp-string/expected-stderr.txt b/tests/library/exp-string/expected-stderr.txt index 6dc689f..7153023 100644 --- a/tests/library/exp-string/expected-stderr.txt +++ b/tests/library/exp-string/expected-stderr.txt @@ -1,2 +1,2 @@ -[Fatal] line 27: unconsumed characters in string conversion +[Fatal] line 27: STR_TO_NUM: '12x' is not a number [Fatal] line 32: negative string length -- cgit v1.3.1-4-g156e