From 8af5109bbb1d4dc08ad3b76fe1995b60d604ddbd Mon Sep 17 00:00:00 2001 From: itsfuad Date: Sun, 4 Oct 2026 13:56:46 +0600 Subject: [PATCH] Preserve signed zero in floating unary negation Emit LLVM fneg through existing SSA/debug output and preserve scalar checks. Reuse float fixture comparison for signed-zero and exceptional-value regressions. Validation passed: focused and full Go tests, vet, fresh bundle, executable fixtures, and Clang object checks for 32-/64-bit targets. --- docs/language-spec.md | 5 ++ internal/backend/llvm/emitter_test.go | 65 ++++++++++++++++++++ internal/backend/llvm/instruction_emit.go | 7 ++- internal/constvalue/value_test.go | 45 ++++++++++++++ x_test/runtime_float_remainder/src/main.peep | 29 +++++++++ 5 files changed, 150 insertions(+), 1 deletion(-) diff --git a/docs/language-spec.md b/docs/language-spec.md index e26e4b2f..7b0ac404 100644 --- a/docs/language-spec.md +++ b/docs/language-spec.md @@ -380,6 +380,11 @@ unrepresentable signed case `MIN / -1` wraps to `MIN`, and `MIN % -1` is zero. Integer division or remainder by zero traps at runtime. Floating-point division keeps IEEE behavior. +Floating-point unary `-` reverses the operand's sign without changing its `f32` +or `f64` type. It maps `+0.0` to `-0.0` and `-0.0` to `+0.0`, and reverses the +sign of infinities. Negating NaN produces NaN; no NaN payload or sign encoding +is specified. Constant evaluation and runtime execution follow the same rule. + Floating-point `%` uses truncating remainder (LLVM `frem`, C `fmod`), not IEEE nearest-quotient remainder. For finite operands and a nonzero divisor, its mathematical result is `a - trunc(a / b) * b`, with the quotient computed in diff --git a/internal/backend/llvm/emitter_test.go b/internal/backend/llvm/emitter_test.go index 861b7d6d..c33b1e91 100644 --- a/internal/backend/llvm/emitter_test.go +++ b/internal/backend/llvm/emitter_test.go @@ -690,6 +690,71 @@ func TestGenerateLLVMIRGuardsIntegerDivisionAndRemainder(t *testing.T) { } } +func TestGenerateLLVMIRLowersUnaryNegation(t *testing.T) { + for _, compilerTarget := range []target.Info{testLinux386, testLinuxAMD64} { + for _, test := range []struct { + name string + typ ir.Type + instruction string + }{ + {name: "f32", typ: ir.Type{Kind: ir.TypeFloat, Bits: 32}, instruction: "fneg float %value"}, + {name: "f64", typ: ir.Type{Kind: ir.TypeFloat, Bits: 64}, instruction: "fneg double %value"}, + {name: "i32", typ: ir.Type{Kind: ir.TypeInteger, IsSigned: true, Bits: 32}, instruction: "sub i32 0, %value"}, + } { + t.Run(fmt.Sprintf("%d/%s", compilerTarget.PointerBits, test.name), func(t *testing.T) { + types := newLLVMTypeFixture(compilerTarget.PointerBits) + typeID := types.table.Intern(test.typ) + location := source.NewLocation(unixTestPath, source.Position{Line: 2, Column: 4}, source.Position{Line: 2, Column: 10}) + mod := &mir.Module{ + Name: "test", Types: types.table, FilePath: unixTestPath, + Funcs: []*mir.Function{{ + Name: "negate", + Params: []ir.Param{{Name: "value", Type: typeID}}, + ReturnType: typeID, + Location: source.NewLocation(unixTestPath, source.Position{Line: 1, Column: 1}, source.Position{Line: 3, Column: 1}), + Blocks: []*mir.Block{{ + ID: 0, + Instrs: []mir.Instr{&mir.Assign{Name: "result", Location: location, Value: &mir.Unary{ + Op: "-", Arg: &mir.RefName{Name: "value", Type: typeID}, Type: typeID, Location: location, + }}}, + Term: &mir.Ret{Value: &mir.RefName{Name: "result", Type: typeID}}, + }}, + }}, + } + out := GenerateLLVMIR(mod, diagnostics.NewDiagnosticBag(), compilerTarget, true) + _, after, found := strings.Cut(out, " = "+test.instruction+", !dbg !") + if !found { + t.Fatalf("expected debug-tagged %s, got:\n%s", test.instruction, out) + } + debugID, _, _ := strings.Cut(after, "\n") + if !strings.Contains(out, "!"+debugID+" = !DILocation(line: 2, column: 4,") { + t.Fatalf("negation must retain its source location, got:\n%s", out) + } + clang, err := exec.LookPath("clang") + if err != nil { + t.Skip("clang unavailable for LLVM IR validation") + } + cmd := exec.Command(clang, "-target", compilerTarget.LLVMTriple, "-x", "ir", "-c", "-o", filepath.Join(t.TempDir(), "negation.o"), "-") + cmd.Stdin = strings.NewReader(out) + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("clang rejected unary negation LLVM IR: %v\n%s\n%s", err, output, out) + } + }) + } + } +} + +func TestLLVMEmitterRejectsNonScalarFloatNegation(t *testing.T) { + types := newLLVMTypeFixture(testLinuxAMD64.PointerBits) + floatType := types.table.Intern(ir.Type{Kind: ir.TypeFloat, Bits: 32}) + emitter := &llvmEmitter{mod: &mir.Module{Types: types.table}} + requireLLVMInvariant(t, func() { + emitValueExpr(newLLVMBuilder(&strings.Builder{}, emitter, -1), &mir.Unary{ + Op: "-", Arg: &mir.RefConst{Value: "null", Type: types.rawptr}, Type: floatType, + }) + }) +} + func TestGenerateLLVMIRLeavesFloatDivRemUnguarded(t *testing.T) { for _, bits := range []int{32, 64} { for _, test := range []struct{ op, instruction string }{{"/", "fdiv"}, {"%", "frem"}} { diff --git a/internal/backend/llvm/instruction_emit.go b/internal/backend/llvm/instruction_emit.go index c3e0737c..fb893e86 100644 --- a/internal/backend/llvm/instruction_emit.go +++ b/internal/backend/llvm/instruction_emit.go @@ -518,7 +518,12 @@ func emitValueExpr(b *llvmBuilder, expr mir.ValueExpr) llvmValue { return arg case "-": if isFloatType(b.emitter.mod.Types, e.Type) { - return b.arithmetic("fsub", b.value("0.0", arg.Layout), arg) + if arg.Layout.Kind != llvmLayoutScalar { + b.invariant("fneg requires scalar operand, got %s", arg.Layout.Text) + } + result := b.nextValue(arg.Layout) + b.line(fmt.Sprintf("%s = fneg %s %s", result.Text, arg.Layout.Text, arg.Text)) + return result } return b.arithmetic("sub", b.value("0", arg.Layout), arg) case "!": diff --git a/internal/constvalue/value_test.go b/internal/constvalue/value_test.go index f2ae232e..880541a8 100644 --- a/internal/constvalue/value_test.go +++ b/internal/constvalue/value_test.go @@ -213,6 +213,51 @@ func TestFoldFloatBinaryRoundsF32(t *testing.T) { } } +func TestFoldFloatUnaryNegation(t *testing.T) { + for _, typeID := range []string{"f32", "f64"} { + tiny := math.SmallestNonzeroFloat64 + rounded := float64(16777217) + if typeID == "f32" { + tiny = math.SmallestNonzeroFloat32 + rounded = 16777216 + } + for _, test := range []struct { + name string + input, want float64 + }{ + {name: "positive zero", input: 0, want: math.Copysign(0, -1)}, + {name: "negative zero", input: math.Copysign(0, -1), want: 0}, + {name: "positive finite", input: 5.5, want: -5.5}, + {name: "negative finite", input: -5.5, want: 5.5}, + {name: "positive subnormal", input: tiny, want: -tiny}, + {name: "negative subnormal", input: -tiny, want: tiny}, + {name: "positive infinity", input: math.Inf(1), want: math.Inf(-1)}, + {name: "negative infinity", input: math.Inf(-1), want: math.Inf(1)}, + {name: "nan", input: math.NaN(), want: math.NaN()}, + {name: "rounded input", input: 16777217, want: -rounded}, + } { + t.Run(typeID+"/"+test.name, func(t *testing.T) { + input, ok := NewFloat(test.input, typeID) + if !ok { + t.Fatalf("NewFloat(%v, %s) failed", test.input, typeID) + } + folded, ok := FoldUnary("-", input) + got, isFloat := folded.(*FloatConst) + if !ok || !isFloat || got.TypeText() != typeID { + t.Fatalf("FoldUnary(-) = %#v, want %s", folded, typeID) + } + if math.IsNaN(test.want) { + if !math.IsNaN(got.Float()) { + t.Fatalf("got %v, want NaN", got.Float()) + } + } else if math.Float64bits(got.Float()) != math.Float64bits(test.want) { + t.Fatalf("got %v (%x), want %v (%x)", got.Float(), math.Float64bits(got.Float()), test.want, math.Float64bits(test.want)) + } + }) + } + } +} + func TestFoldFloatRemainder(t *testing.T) { for _, typeID := range []string{"f32", "f64"} { tiny := math.SmallestNonzeroFloat64 diff --git a/x_test/runtime_float_remainder/src/main.peep b/x_test/runtime_float_remainder/src/main.peep index 858fead5..01d5401e 100644 --- a/x_test/runtime_float_remainder/src/main.peep +++ b/x_test/runtime_float_remainder/src/main.peep @@ -4,9 +4,13 @@ const Inf32: f32 = 1.0f32 / 0.0f32; const Inf64: f64 = 1.0f64 / 0.0f64; const NaN32: f32 = 0.0f32 / 0.0f32; const NaN64: f64 = 0.0f64 / 0.0f64; +const NegativeZero32: f32 = -0.0f32; +const NegativeZero64: f64 = -0.0f64; fn Rem32(left: f32, right: f32) -> f32 { return left % right; } fn Rem64(left: f64, right: f64) -> f64 { return left % right; } +fn Neg32(value: f32) -> f32 { return -value; } +fn Neg64(value: f64) -> f64 { return -value; } // Widening f32 results is exact; compare both paths with an independent result. fn Matches(actual: f64, folded: f64, expected: f64) -> bool { @@ -52,5 +56,30 @@ fn main() -> i32 { // Inputs round to their declared width before remainder, not afterward. if !Matches(Rem32(16777217.0, 2.0), 16777217.0f32 % 2.0f32, 0.0) { return 31; } if !Matches(Rem64(16777217.0, 2.0), 16777217.0f64 % 2.0f64, 1.0) { return 32; } + // Parameter-backed negation must preserve the sign that constant folding flips. + if !Matches(Neg32(0.0), NegativeZero32, -0.0) { return 33; } + if !Matches(Neg64(0.0), NegativeZero64, -0.0) { return 34; } + if !Matches(Neg32(NegativeZero32), -NegativeZero32, 0.0) { return 35; } + if !Matches(Neg64(NegativeZero64), -NegativeZero64, 0.0) { return 36; } + if !Matches(Neg32(5.5), -5.5f32, -5.5) { return 37; } + if !Matches(Neg64(5.5), -5.5f64, -5.5) { return 38; } + if !Matches(Neg32(-5.5), -(-5.5f32), 5.5) { return 39; } + if !Matches(Neg64(-5.5), -(-5.5f64), 5.5) { return 40; } + if !Matches(Neg32(1e-45f32), -1e-45f32, -1e-45f32) { return 41; } + if !Matches(Neg64(5e-324f64), -5e-324f64, -5e-324f64) { return 42; } + if !Matches(Neg32(-1e-45f32), -(-1e-45f32), 1e-45f32) { return 43; } + if !Matches(Neg64(-5e-324f64), -(-5e-324f64), 5e-324f64) { return 44; } + if !Matches(Neg32(Inf32), -Inf32, -Inf64) { return 45; } + if !Matches(Neg64(Inf64), -Inf64, -Inf64) { return 46; } + if !Matches(Neg32(-Inf32), -(-Inf32), Inf64) { return 47; } + if !Matches(Neg64(-Inf64), -(-Inf64), Inf64) { return 48; } + if !Matches(Neg32(NaN32), -NaN32, NaN64) { return 49; } + if !Matches(Neg64(NaN64), -NaN64, NaN64) { return 50; } + if !Matches(Neg32(Neg32(0.0)), -(-0.0f32), 0.0) { return 51; } + if !Matches(Neg64(Neg64(0.0)), -(-0.0f64), 0.0) { return 52; } + if !Matches(Neg32(Neg32(NegativeZero32)), -(-NegativeZero32), NegativeZero64) { return 53; } + if !Matches(Neg64(Neg64(NegativeZero64)), -(-NegativeZero64), NegativeZero64) { return 54; } + if !Matches(Neg32(16777217.0), -16777217.0f32, -16777216.0) { return 55; } + if !Matches(Neg64(16777217.0), -16777217.0f64, -16777217.0) { return 56; } return 0; }