Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/ops/builtins.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
/** I/O builtins, type casting, and misc builtins extracted from eval.c.
*/

#include <errno.h>
#include "lang/eval.h"
#include "lang/internal.h"
#include "lang/env.h"
Expand Down Expand Up @@ -1342,8 +1343,13 @@ ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) {
const char* sp = ray_str_ptr(val);
if (!sp) return ray_error("domain", "as: cannot parse empty str as i64");
char* end;
errno = 0;
int64_t v = strtoll(sp, &end, 10);
if (end == sp) return ray_error("domain", "as: cannot parse str as i64");
if (*end != '\0')
return ray_error("domain", "as: cannot parse str as i64, unexpected trailing characters");
if (errno == ERANGE)
return ray_error("domain", "as: cannot parse str as i64, value out of int64 range");
return make_i64(v);
}
/* Vector/list cast */
Expand All @@ -1364,8 +1370,13 @@ ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) {
if (val->type == -RAY_TIMESTAMP) return ray_i32((int32_t)val->i64);
if (val->type == -RAY_STR) {
const char* sp = ray_str_ptr(val); char* end;
errno = 0;
long v = strtol(sp, &end, 10);
if (end == sp) return ray_error("domain", "as: cannot parse str as i32");
if (*end != '\0')
return ray_error("domain", "as: cannot parse str as i32, unexpected trailing characters");
if (errno == ERANGE)
return ray_error("domain", "as: cannot parse str as i32, value out of int64 range");
return ray_i32((int32_t)v);
}
/* Vector cast */
Expand All @@ -1386,8 +1397,13 @@ ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) {
if (val->type == -RAY_TIMESTAMP) return ray_i16((int16_t)val->i64);
if (val->type == -RAY_STR) {
const char* sp = ray_str_ptr(val); char* end;
errno = 0;
long v = strtol(sp, &end, 10);
if (end == sp) return ray_error("domain", "as: cannot parse str as i16");
if (*end != '\0')
return ray_error("domain", "as: cannot parse str as i16, unexpected trailing characters");
if (errno == ERANGE)
return ray_error("domain", "as: cannot parse str as i16, value out of int64 range");
return ray_i16((int16_t)v);
}
/* Vector cast */
Expand All @@ -1410,8 +1426,11 @@ ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) {
const char* sp = ray_str_ptr(val);
if (!sp) return ray_error("domain", "as: cannot parse empty str as f64");
char* end;
errno = 0;
double v = strtod(sp, &end);
if (end == sp) return ray_error("domain", "as: cannot parse str as f64");
if (*end != '\0')
return ray_error("domain", "as: cannot parse str as f64, unexpected trailing characters");
/* STAGE 2 (ingest/cast STR→F64): canonicalize at the ingest entry
* point. strtod("inf")/strtod("1e400")/strtod("nan") would yield a
* non-finite F64; make_f64 maps every non-finite to NULL_F64 (0Nf),
Expand Down Expand Up @@ -1900,8 +1919,13 @@ ray_t* ray_cast_fn(ray_t* type_sym, ray_t* val) {
if (val->type == -RAY_F64) return ray_u8(ray_cast_f64_to_u8_null(val->f64));
if (val->type == -RAY_STR) {
const char* sp = ray_str_ptr(val);
char* end; long v = strtol(sp, &end, 10);
char* end; errno = 0;
long v = strtol(sp, &end, 10);
if (end == sp) return ray_error("domain", "as: cannot parse str as u8");
if (*end != '\0')
return ray_error("domain", "as: cannot parse str as u8, unexpected trailing characters");
if (errno == ERANGE)
return ray_error("domain", "as: cannot parse str as u8, value out of int64 range");
return ray_u8((uint8_t)v);
}
/* Vector cast */
Expand Down
30 changes: 30 additions & 0 deletions test/rfl/type/as.rfl
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,36 @@
(as 'i64 (as 'i32 "-2147483648")) -- 0Nl
(as 'i64 (as 'i32 "2147483647")) -- 2147483647

;; ========== STRING → NUMERIC: FULL CONSUMPTION + OVERFLOW ==========
;; The old strtoll/strtol/strtod path stopped at the first non-digit and
;; never checked the end pointer or ERANGE, so trailing garbage and true
;; overflow were silently accepted ("1e19" → 1, "9999...9" → INT64_MAX).
;; All of these must now REJECT with a domain error, matching the strict
;; DATE/TIME/TIMESTAMP string casts. Narrow-int truncation itself still
;; wraps (documented narrow-int rule); only unparseable/overflowing input
;; errors.
(as 'i64 "123abc") !- domain
(as 'i64 "1e19") !- domain
(as 'i64 "12.5") !- domain
(as 'i64 "999999999999999999999999") !- domain
(as 'i64 "-999999999999999999999999") !- domain
(as 'i32 "123x") !- domain
(as 'i32 "1e9") !- domain
(as 'i16 "40000junk") !- domain
(as 'u8 "12zz") !- domain
(as 'u8 "300abc") !- domain
(as 'f64 "1.5x") !- domain
(as 'f64 "1.5e3junk") !- domain
;; Clean values still parse.
(as 'i64 " 123") -- 123
(as 'i64 "-5") -- -5
(as 'f64 "0x10") -- 16.0
;; The string-VECTOR cast routes through the same per-element parser: a
;; malformed element rejects the whole cast, clean elements parse.
(as 'I64 (list "42" "-7" "100")) -- [42 -7 100]
(as 'I64 (list "42" "123abc")) !- domain
(as 'F64 (list "3.14" "1.5x")) !- domain

;; ========== NULL PRESERVATION ACROSS CASTS ==========
;; Casting any null returns null of target type, never sentinel/INT_MIN.
(nil? (as 'i64 0Nh)) -- true
Expand Down
Loading