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
5 changes: 5 additions & 0 deletions docs/language-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 65 additions & 0 deletions internal/backend/llvm/emitter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}} {
Expand Down
7 changes: 6 additions & 1 deletion internal/backend/llvm/instruction_emit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 "!":
Expand Down
45 changes: 45 additions & 0 deletions internal/constvalue/value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions x_test/runtime_float_remainder/src/main.peep
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
Loading