Skip to content

Commit 91c8dac

Browse files
[3/15] Apply R's type-promotion lattice: promoted result modes, operand casts, and a narrowing-reassignment error (t-kalinowski#139)
* Report and apply promoted result modes in operators and constructors reduce_promoted_mode() implemented R's mode lattice correctly but was only used for c()/reduction output declarations; conform() reported the first non-scalar operand's mode, and no operand was ever cast. Since `<-` copies the reported mode verbatim into the target's declaration, mixed integer/double arithmetic silently truncated (x + 0.5 on integer x returned integers), and every context where Fortran requires uniform argument types -- array constructors, min/max, modulo -- failed at the gfortran stage for mixed modes. Logical operands could not participate in arithmetic or comparisons at all. Centralize the mechanism in two helpers and wire it through: - cast_to_mode(x, mode, context): the one place cast spellings live. Handles integer->double, logical->integer/double (booleanizing or relabeling bind(c) integer-backed logicals as needed), and errors cleanly on casts it cannot spell. bind_cast_value() folds into it. - promote_operands(args, context): join + cast for uniform-type contexts (c(), multi-arg min/max/sum/prod, modulo). - conform() now reports the lattice join; the lattice gains complex as its top so complex results no longer depend on operand order. - ^ always returns double like R, casting the base but keeping integer exponents integer (real ** int is exact and defined for negative bases, matching R_pow); the parenthesized exponent fixes the non-standard `x ** -1_c_int` spelling. - Arithmetic and comparisons cast logical operands to integer (R: TRUE + TRUE is 2L), including unary +/-. Snapshot churn: heat-diffusion's dx^2/dy^2 gain the base cast and exponent parens; all runtime results are unchanged there. * Error when reassignment would narrow a variable's mode R re-types a binding on assignment (x <- x + 0.5 promotes an integer x to double); Fortran cannot re-type a variable, so quickr kept the declared mode and the assignment silently truncated the value back into it -- quick() returned 2 where R returns 2.5, with no diagnostic. check_assignment_compatible() only checked rank, not mode. Add check_reassignment_narrowing(): reassigning a value whose mode sits above the target's on the lattice (logical < integer < double < complex) is now a compile-time error naming the variable and both modes. Same-mode reassignment and assigning a lower-mode value into a wider variable (x_dbl <- x_dbl + 1L) are unaffected. * Compute double %/% in the real domain Fortran FLOOR() returns a default-kind integer, so the double branch of %/% silently overflowed large quotients: 1e20 %/% 3 came back as -2147483648 instead of ~3.33e19. The sibling floor() handler already avoids this with aint() plus a merge() adjustment in real arithmetic; use the same expression here, hoisting the quotient so it is evaluated once instead of three times. * Extend the narrowing check to subassignment and superassignment check_reassignment_narrowing() only guarded whole-variable `<-`, so `x[1L] <- 2.5` on a declared-integer x silently truncated to 2L where R promotes the whole vector to double -- the same contract hole the reassignment check was added to close. `x <<- val` and `x[i] <<- val` inside local closures had the identical gap. Run the same check against the base variable in the `[<-`, `<<-`, and `[<<-` handlers. * Treat logicals as integers in abs() and single-arg reductions The logical-as-integer arithmetic rule was spelled inline at each call site, and two sites missed it: abs() passed logical expressions straight to Fortran's abs(), and the single-argument reduction path emitted sum((x /= 0)) / minval((x /= 0)) / etc. -- all gfortran type errors, where R returns integer results (abs(TRUE) is 1L, sum(TRUE) is 1L). Name the rule once as arith_join_mode() and use it in abs(), reduce_arg, and the existing inline spellings (unary +/-, %%, multi-arg reductions, promote_arith_pair), so the next consumer cannot miss it. * Spell the mode lattice once: shared mode_lattice + mode_rank() The narrowing check hand-spelled the promotion order that reduce_promoted_mode() encoded as an if-chain, so the two rules this branch ties together -- what a join promotes to, and what a reassignment may not narrow from -- could drift apart. One lattice constant and a mode_rank() helper now back both: the join takes the highest rank present (modes outside the lattice are ignored, as the old chain did), the narrowing check compares ranks. No behavior change. * Format with air * Use result unpacking for more compact and readable code. * Create snapshots of assignmetn narrowing errors
1 parent 0c443e2 commit 91c8dac

14 files changed

Lines changed: 1262 additions & 70 deletions

R/r2f-arithmetic.R

Lines changed: 49 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@ r2f_handlers[["+"]] <- function(args, scope, ...) {
77
# Support both binary and unary plus
88
if (length(args) == 1L) {
99
x <- r2f(args[[1L]], scope, ...)
10+
# R: +TRUE is 1L
11+
x <- cast_to_mode(x, arith_join_mode(x), "unary +")
1012
Fortran(glue("(+{x})"), Variable(x@value@mode, x@value@dims))
1113
} else {
1214
.[left, right] <- lapply(args, r2f, scope, ...)
13-
reshaped <- maybe_reshape_vector_matrix(left, right)
14-
left <- reshaped$left
15-
right <- reshaped$right
15+
.[left, right] <- promote_arith_pair(left, right, "+")
16+
.[left, right] <- maybe_reshape_vector_matrix(left, right)
1617
Fortran(glue("({left} + {right})"), conform(left@value, right@value))
1718
}
1819
}
@@ -21,40 +22,52 @@ r2f_handlers[["-"]] <- function(args, scope, ...) {
2122
# Support both binary and unary minus
2223
if (length(args) == 1L) {
2324
x <- r2f(args[[1L]], scope, ...)
25+
# R: -TRUE is -1L
26+
x <- cast_to_mode(x, arith_join_mode(x), "unary -")
2427
Fortran(glue("(-{x})"), Variable(x@value@mode, x@value@dims))
2528
} else {
2629
.[left, right] <- lapply(args, r2f, scope, ...)
27-
reshaped <- maybe_reshape_vector_matrix(left, right)
28-
left <- reshaped$left
29-
right <- reshaped$right
30+
.[left, right] <- promote_arith_pair(left, right, "-")
31+
.[left, right] <- maybe_reshape_vector_matrix(left, right)
3032
Fortran(glue("({left} - {right})"), conform(left@value, right@value))
3133
}
3234
}
3335

3436
r2f_handlers[["*"]] <- function(args, scope = NULL, ...) {
3537
.[left, right] <- lapply(args, r2f, scope, ...)
36-
reshaped <- maybe_reshape_vector_matrix(left, right)
37-
left <- reshaped$left
38-
right <- reshaped$right
38+
.[left, right] <- promote_arith_pair(left, right, "*")
39+
.[left, right] <- maybe_reshape_vector_matrix(left, right)
3940
Fortran(glue("({left} * {right})"), conform(left@value, right@value))
4041
}
4142

4243
r2f_handlers[["/"]] <- function(args, scope = NULL, ...) {
4344
.[left, right] <- lapply(args, r2f, scope, ...)
4445
left <- maybe_cast_double(left)
4546
right <- maybe_cast_double(right)
46-
reshaped <- maybe_reshape_vector_matrix(left, right)
47-
left <- reshaped$left
48-
right <- reshaped$right
47+
.[left, right] <- maybe_reshape_vector_matrix(left, right)
4948
Fortran(glue("({left} / {right})"), conform(left@value, right@value))
5049
}
5150

5251
r2f_handlers[["^"]] <- function(args, scope, ...) {
5352
.[left, right] <- lapply(args, r2f, scope, ...)
54-
reshaped <- maybe_reshape_vector_matrix(left, right)
55-
left <- reshaped$left
56-
right <- reshaped$right
57-
Fortran(glue("({left} ** {right})"), conform(left@value, right@value))
53+
# R's ^ always returns double (R_pow), so cast the base. Keep an integer
54+
# exponent as integer: Fortran `real ** int` is exact and, unlike
55+
# `real ** real`, defined for negative bases -- matching R, which
56+
# special-cases whole-number exponents.
57+
left <- maybe_cast_double(left)
58+
if (identical(right@value@mode, "logical")) {
59+
right <- cast_to_mode(right, "integer", "^")
60+
}
61+
.[left, right] <- maybe_reshape_vector_matrix(left, right)
62+
mode <- reduce_promoted_mode(left, right)
63+
if (!identical(mode, "complex")) {
64+
mode <- "double"
65+
}
66+
# Parenthesizing the exponent avoids non-standard `** -1_c_int`.
67+
Fortran(
68+
glue("({left} ** ({right}))"),
69+
conform(left@value, right@value, mode = mode)
70+
)
5871
}
5972

6073

@@ -72,21 +85,38 @@ r2f_handlers[["^"]] <- function(args, scope, ...) {
7285

7386
r2f_handlers[["%%"]] <- function(args, scope, ...) {
7487
.[left, right] <- lapply(args, r2f, scope, ...)
88+
# `modulo` requires same-typed arguments, so cast both operands to the
89+
# join (logical joins as integer: R's TRUE %% TRUE is 0L).
90+
mode <- arith_join_mode(left, right)
91+
left <- cast_to_mode(left, mode, "%%")
92+
right <- cast_to_mode(right, mode, "%%")
7593
out_val <- conform(left@value, right@value)
7694
# MODULO gives result with sign(right) - matches R %% behaviour
7795
Fortran(glue("modulo({left}, {right})"), out_val)
7896
}
7997

80-
r2f_handlers[["%/%"]] <- function(args, scope, ...) {
81-
.[left, right] <- lapply(args, r2f, scope, ...)
98+
r2f_handlers[["%/%"]] <- function(args, scope, ..., hoist = NULL) {
99+
.[left, right] <- lapply(args, r2f, scope, ..., hoist = hoist)
100+
.[left, right] <- promote_arith_pair(left, right, "%/%")
82101
out_val <- conform(left@value, right@value)
83102

84103
expr <- switch(
85104
out_val@mode,
86105
integer = glue(
87106
"int(floor(real({left}, kind=c_double) / real({right}, kind=c_double)), kind=c_int)"
88107
),
89-
double = glue("floor({left} / {right})"),
108+
double = {
109+
# Fortran FLOOR() returns an integer, so a large double quotient
110+
# (e.g. 1e20 %/% 3) would silently overflow. Stay in the real domain
111+
# as the floor() handler does; the quotient is spliced three times,
112+
# so hoist it to evaluate once.
113+
q <- hoist_unless_name(
114+
Fortran(glue("({left} / {right})"), out_val),
115+
hoist
116+
)
117+
aint <- glue("aint({q})")
118+
glue("({aint} - merge(1.0_c_double, 0.0_c_double, ({q} < {aint})))")
119+
},
90120
stop("%/% only implemented for numeric types")
91121
)
92122

R/r2f-assign.R

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ register_r2f_handler(
222222
var@mode <- value@value@mode
223223
var@dims <- value@value@dims
224224
}
225+
check_reassignment_narrowing(name, var, value@value)
225226
check_assignment_compatible(var, value@value)
226227
var@modified <- TRUE
227228
# could probably drop this @modified property, and instead track
@@ -259,6 +260,12 @@ register_r2f_handler(
259260
lhs <- compile_subscript_lhs(target_call, scope, ..., target = "local")
260261
value <- r2f(args[[2L]], scope, ...)
261262

263+
# Subassignment cannot re-type the base variable any more than
264+
# whole-variable reassignment can: `x[1L] <- 2.5` on an integer `x`
265+
# would silently truncate where R promotes `x` to double.
266+
base_name <- as.character(target_call[[2L]])
267+
check_reassignment_narrowing(base_name, get0(base_name, scope), value@value)
268+
262269
Fortran(str_flatten_lines(lhs$pre, glue("{lhs$lhs} = {value}")))
263270
}
264271
)
@@ -313,6 +320,7 @@ register_r2f_handler(
313320
host_scope[[name]] <- host_var
314321

315322
value <- r2f(args[[2L]], scope, ..., hoist = hoist)
323+
check_reassignment_narrowing(name, host_var, value@value)
316324
check_assignment_compatible(host_var, value@value)
317325

318326
Fortran(glue("{host_var@name} = {value}"))
@@ -366,6 +374,7 @@ register_r2f_handler(
366374
target = "host"
367375
)
368376
value <- r2f(args[[2L]], scope, ..., hoist = hoist)
377+
check_reassignment_narrowing(name, host_var, value@value)
369378
Fortran(glue("{lhs$lhs} = {value}"))
370379
}
371380
)

R/r2f-constructors.R

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
r2f_handlers[["c"]] <- function(args, scope = NULL, ...) {
88
ff <- lapply(args, r2f, scope, ...)
9+
# Fortran array constructors require uniform element types; cast every
10+
# element whose mode differs from the promoted mode (R: c(1L, 2.5) is
11+
# double, c(TRUE, 2L) is integer).
12+
promoted <- promote_operands(ff, context = "c()")
13+
ff <- promoted$args
14+
mode <- promoted$mode
915
s <- glue("[ {str_flatten_commas(ff)} ]")
1016
lens <- lapply(ff[order(map_int(ff, \(f) f@value@rank))], function(e) {
1117
rank <- e@value@rank
@@ -17,7 +23,6 @@ r2f_handlers[["c"]] <- function(args, scope = NULL, ...) {
1723
stop("all args passed to c() must be scalars or 1-d arrays")
1824
}
1925
})
20-
mode <- reduce_promoted_mode(ff)
2126
len <- Reduce(
2227
\(l1, l2) {
2328
if (is_scalar_na(l1) || is_scalar_na(l2)) {

R/r2f-logical.R

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,41 +7,53 @@
77

88
r2f_handlers[[">="]] <- function(args, scope, ...) {
99
.[left, right] <- lapply(args, r2f, scope, ...)
10+
# R compares logicals as integers; Fortran has no logical comparison.
11+
.[left, right] <- promote_arith_pair(left, right, "comparison")
1012
var <- conform(left@value, right@value)
1113
var@mode <- "logical"
1214
Fortran(glue("({left} >= {right})"), var)
1315
}
1416

1517
r2f_handlers[[">"]] <- function(args, scope, ...) {
1618
.[left, right] <- lapply(args, r2f, scope, ...)
19+
# R compares logicals as integers; Fortran has no logical comparison.
20+
.[left, right] <- promote_arith_pair(left, right, "comparison")
1721
var <- conform(left@value, right@value)
1822
var@mode <- "logical"
1923
Fortran(glue("({left} > {right})"), var)
2024
}
2125

2226
r2f_handlers[["<"]] <- function(args, scope, ...) {
2327
.[left, right] <- lapply(args, r2f, scope, ...)
28+
# R compares logicals as integers; Fortran has no logical comparison.
29+
.[left, right] <- promote_arith_pair(left, right, "comparison")
2430
var <- conform(left@value, right@value)
2531
var@mode <- "logical"
2632
Fortran(glue("({left} < {right})"), var)
2733
}
2834

2935
r2f_handlers[["<="]] <- function(args, scope, ...) {
3036
.[left, right] <- lapply(args, r2f, scope, ...)
37+
# R compares logicals as integers; Fortran has no logical comparison.
38+
.[left, right] <- promote_arith_pair(left, right, "comparison")
3139
var <- conform(left@value, right@value)
3240
var@mode <- "logical"
3341
Fortran(glue("({left} <= {right})"), var)
3442
}
3543

3644
r2f_handlers[["=="]] <- function(args, scope, ...) {
3745
.[left, right] <- lapply(args, r2f, scope, ...)
46+
# R compares logicals as integers; Fortran has no logical comparison.
47+
.[left, right] <- promote_arith_pair(left, right, "comparison")
3848
var <- conform(left@value, right@value)
3949
var@mode <- "logical"
4050
Fortran(glue("({left} == {right})"), var)
4151
}
4252

4353
r2f_handlers[["!="]] <- function(args, scope, ...) {
4454
.[left, right] <- lapply(args, r2f, scope, ...)
55+
# R compares logicals as integers; Fortran has no logical comparison.
56+
.[left, right] <- promote_arith_pair(left, right, "comparison")
4557
var <- conform(left@value, right@value)
4658
var@mode <- "logical"
4759
Fortran(glue("({left} /= {right})"), var)

R/r2f-math.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ r2f_handlers[["log10"]] <- function(args, scope, ...) {
121121
r2f_handlers[["abs"]] <- function(args, scope, ...) {
122122
stopifnot(length(args) == 1L)
123123
arg <- r2f(args[[1]], scope, ...)
124+
# R: abs(logical) is integer (abs(TRUE) is 1L); Fortran's abs() requires
125+
# a numeric argument, so a logical operand participates as integer.
126+
arg <- cast_to_mode(arg, arith_join_mode(arg), "abs()")
124127
out_mode <- if (arg@value@mode == "complex") "double" else arg@value@mode
125128
Fortran(glue("abs({arg})"), Variable(mode = out_mode, dims = arg@value@dims))
126129
}

R/r2f-matrix.R

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -224,29 +224,6 @@ bind_output_mode <- function(values, context) {
224224
stop(context, " does not support input mode(s): ", str_flatten_commas(modes))
225225
}
226226

227-
bind_cast_value <- function(value, mode, context) {
228-
if (identical(value@value@mode, mode)) {
229-
return(value)
230-
}
231-
if (identical(mode, "double")) {
232-
return(maybe_cast_double(value))
233-
}
234-
if (identical(mode, "integer") && identical(value@value@mode, "logical")) {
235-
return(Fortran(
236-
glue("merge(1_c_int, 0_c_int, {value})"),
237-
Variable("integer", value@value@dims)
238-
))
239-
}
240-
stop(
241-
context,
242-
" does not support coercion from ",
243-
value@value@mode,
244-
" to ",
245-
mode,
246-
call. = FALSE
247-
)
248-
}
249-
250227
bind_dim_sum <- function(values, context, label) {
251228
if (!length(values)) {
252229
return(0L)
@@ -381,7 +358,7 @@ register_r2f_handler(
381358
}
382359

383360
mode <- bind_output_mode(values, context)
384-
values <- lapply(values, bind_cast_value, mode = mode, context = context)
361+
values <- lapply(values, cast_to_mode, mode = mode, context = context)
385362

386363
dims <- lapply(values, matrix_dims, orientation = "colvec")
387364
scalar_flags <- map_lgl(values, \(val) passes_as_scalar(val@value))
@@ -437,7 +414,7 @@ register_r2f_handler(
437414
}
438415

439416
mode <- bind_output_mode(values, context)
440-
values <- lapply(values, bind_cast_value, mode = mode, context = context)
417+
values <- lapply(values, cast_to_mode, mode = mode, context = context)
441418

442419
dims <- lapply(values, matrix_dims, orientation = "rowvec")
443420
scalar_flags <- map_lgl(values, \(val) passes_as_scalar(val@value))

0 commit comments

Comments
 (0)