fix: reject inexact integer literals in float context
This commit is contained in:
@@ -71,7 +71,9 @@ type checking (`@source/sema/check`) runs after name resolution. `check.check` b
|
|||||||
|
|
||||||
the checker keeps union-find parent links and group sizes in a compact array separate from per-node inference metadata. root walks and path compression access only that link storage; both temporary arrays are released before `check.check` returns.
|
the checker keeps union-find parent links and group sizes in a compact array separate from per-node inference metadata. root walks and path compression access only that link storage; both temporary arrays are released before `check.check` returns.
|
||||||
|
|
||||||
numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt either family; float literals remain float. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int` values do not implicitly convert to `float`. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid; division involving fixed `int` values requires explicit conversion.
|
numeric typing follows contextual rules within Honey's current `int` and `float` families. integer literals can adopt a supported numeric family only when their value is exactly representable; float literals remain float and opt into ordinary IEEE rounding. numeric demands propagate through arithmetic and inferred bindings before undemanded integers default to `int`. conflicting demands are errors; fixed `int`/`i64`, `usize`, and `float`/`f64` values do not implicitly convert between families. `+`, `-`, `*` and unary `-` require compatible numeric operands; `/` requires and supplies float context. `3 / 2`, `3.0 / 2`, and `ratio float :: 3 / 2` are valid. `value f64 :: 9007199254740993` is diagnosed, including when float context arrives through aliases, division, or array elements; `9007199254740993.0` explicitly permits rounding. exactly representable integers beyond the signed/unsigned integer ranges remain valid in float context. explicit numeric casts are not yet implemented.
|
||||||
|
|
||||||
|
the conversion policy is value-preserving by default: any future implicit widening must preserve every value of the source type, not merely the value observed at one use. narrowing, potentially inexact integer-to-float conversion, float-to-integer conversion, and signedness changes that cannot preserve the full source range require an explicit operation. integer arithmetic remains checked rather than wrapping; floating arithmetic retains IEEE rounding.
|
||||||
|
|
||||||
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed.
|
type errors carry source spans and diagnostic identities; invalid operands propagate their existing identity without duplicate errors, while independent expressions, arguments and nested bodies are still checked. inference cycles without a type anchor are diagnosed. function signatures are checked independently of their bodies. bare `return` and same-line `return expression` are supported inside function bodies and must match the declared result type. nonvoid return-path checking is structural: every path must return or terminate in a diagnostic trap, and both conditional successors are considered without constant folding. a missing `else` or a loop permits fallthrough, even for a literal `true` condition; a later return can close that path. statements after a return and untaken branches or loops are still type-checked but are not executed.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
- [ ] consider whether numeric conversion semantics are properly tuned.
|
|
||||||
- e.g. are we disallowing implicit conversion between types where loss of precission or wrapping could/would occur?
|
|
||||||
- should we do that or should we not?
|
|
||||||
@@ -48,6 +48,7 @@ Code :: enum {
|
|||||||
missing_entry,
|
missing_entry,
|
||||||
invalid_entry,
|
invalid_entry,
|
||||||
literal_out_of_range,
|
literal_out_of_range,
|
||||||
|
inexact_integer_literal,
|
||||||
invalid_escape,
|
invalid_escape,
|
||||||
read_before_initialization,
|
read_before_initialization,
|
||||||
write_before_initialization,
|
write_before_initialization,
|
||||||
@@ -131,6 +132,7 @@ messages :: $enummap.init(Code, []u8, {
|
|||||||
missing_entry = "program requires a main function",
|
missing_entry = "program requires a main function",
|
||||||
invalid_entry = "main must be a function with no parameters",
|
invalid_entry = "main must be a function with no parameters",
|
||||||
literal_out_of_range = "numeric literal is outside its runtime type's range",
|
literal_out_of_range = "numeric literal is outside its runtime type's range",
|
||||||
|
inexact_integer_literal = "integer literal is not exactly representable as float; use a floating literal to allow rounding",
|
||||||
invalid_escape = "invalid string escape",
|
invalid_escape = "invalid string escape",
|
||||||
read_before_initialization = "local value was read before initialization",
|
read_before_initialization = "local value was read before initialization",
|
||||||
write_before_initialization = "local value was assigned before initialization",
|
write_before_initialization = "local value was assigned before initialization",
|
||||||
|
|||||||
@@ -872,6 +872,17 @@ integer_magnitude proc(text []u8, limit u64) ?u64 {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
exact_float_integer proc(text []u8, value f64) bool {
|
||||||
|
# every finite f64 integer has at most 309 decimal digits, plus a terminator.
|
||||||
|
decimal [310]mut u8 := undefined
|
||||||
|
count :: c.snprintf(ptrcast!((&decimal).ptr), c_ulong(decimal.len), "%.0f", value)
|
||||||
|
if (count < 0 or usize(count) >= decimal.len) return false
|
||||||
|
start := usize(0)
|
||||||
|
while (start + 1 < text.len and text[start] == '0') start += 1
|
||||||
|
return mem.eql(text[start..], decimal[..usize(count)])
|
||||||
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) {
|
||||||
span :: ast.node_span(state.tree, id)
|
span :: ast.node_span(state.tree, id)
|
||||||
@@ -894,6 +905,10 @@ lower_number proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError
|
|||||||
value :: f64(c.strtod(ptrcast!(buffer.ptr), null))
|
value :: f64(c.strtod(ptrcast!(buffer.ptr), null))
|
||||||
# libc reports ERANGE for representable subnormals as well as zero/infinity.
|
# libc reports ERANGE for representable subnormals as well as zero/infinity.
|
||||||
if (c.__error()?^ != c.ERANGE or (value > 0 and value < 1)) {
|
if (c.__error()?^ != c.ERANGE or (value > 0 and value < 1)) {
|
||||||
|
if (ast.expr_kind(state.tree, id) == .literal_int and !exact_float_integer(text, value)) {
|
||||||
|
trap(state, try diagnostics.add(state.diagnostics, .inexact_integer_literal, span), span)
|
||||||
|
return ir.no_value
|
||||||
|
}
|
||||||
return try constant(state, ir.Value{ type = .float, data = ir.ValueData{ real = value } }, span)
|
return try constant(state, ir.Value{ type = .float, data = ir.ValueData{ real = value } }, span)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,6 +357,44 @@ parses_float_context_without_integer_range_limits_and_rejects_float_overflow tes
|
|||||||
try expect_trap(trapped, &diagnostic_store, .literal_out_of_range)
|
try expect_trap(trapped, &diagnostic_store, .literal_out_of_range)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rejects_inexact_integer_literals_in_float_context test {
|
||||||
|
sources [4][]u8 :: [
|
||||||
|
"main proc() f64 {\n return 9007199254740993\n}\n",
|
||||||
|
"main proc() f64 {\n value :: 9007199254740993\n return value\n}\n",
|
||||||
|
"main proc() f64 {\n return 9007199254740993 / 1\n}\n",
|
||||||
|
"main proc() f64 {\n values [1]f64 :: [9007199254740993]\n return values[0]\n}\n",
|
||||||
|
]
|
||||||
|
for (sources) |source| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
defer interpreter.deinit(&outcome)
|
||||||
|
try expect_trap(outcome, &diagnostic_store, .inexact_integer_literal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preserves_exact_large_integers_and_explicit_float_rounding test {
|
||||||
|
sources [3][]u8 :: [
|
||||||
|
"main proc() f64 {\n return 9007199254740994 - 9007199254740992\n}\n",
|
||||||
|
"main proc() f64 {\n return 9007199254740993.0 - 9007199254740992\n}\n",
|
||||||
|
"main proc() f64 {\n return 0003\n}\n",
|
||||||
|
]
|
||||||
|
expected [3]f64 :: [2, 0, 3]
|
||||||
|
for sources |source, i| {
|
||||||
|
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||||
|
defer diagnostics.deinit(&diagnostic_store)
|
||||||
|
module := try compile_owned(source, &diagnostic_store)
|
||||||
|
defer ir.deinit(&module)
|
||||||
|
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||||
|
defer interpreter.deinit(&outcome)
|
||||||
|
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||||
|
try testing.expect(outcome.trap == null)
|
||||||
|
try testing.expect_equal(expected[i], outcome.value.data.real)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
accepts_representable_subnormals_and_rejects_underflow_to_zero test {
|
accepts_representable_subnormals_and_rejects_underflow_to_zero test {
|
||||||
zeros [2]usize :: [307, 400]
|
zeros [2]usize :: [307, 400]
|
||||||
for (zeros) |count| {
|
for (zeros) |count| {
|
||||||
|
|||||||
Reference in New Issue
Block a user