Files
ef79aafea9 codegen: emit negative-zero float constants via math.Float*frombits (#27)
Go has no negative-zero literal: constant arithmetic is exact rational,
so `float64(-0)` is +0. Every emission path that rendered a finite float
constant as a decimal literal therefore collapsed f32.const/f64.const
-0.0 to +0, and the global-initializer path skipped -0 entirely behind
its `fv == 0` zero-value check. Found by go-spidermonkey's test262 run:
SpiderMonkey's CharsToNumber, fdlibm atan2 and Math.sumPrecise all
return the constant -0.0, so Number("-0") lost the sign of zero.

Route -0 through the same math.Float{32,64}frombits path NaN/Inf
already use (shared predicate floatNeedsBitsEmission), in goConstF32 /
goConstF64, floatInitExpr, and the global-initializer zero-skip. The
gcasm backend inherits the fix, since it compiles the generated Go.

Verified: new fixture cg_negzero.wat (constants direct, through calls,
global initializers, arithmetic identities, NaN payload) failed against
the wazero reference before the fix and passes after; `go test ./...`
exit 0.

Co-authored-by: Claude Fable 5 <[email protected]>
2026-07-12 09:08:50 +09:00

163 lines
4.4 KiB
Go

package codegen
import (
"encoding/binary"
"fmt"
"go/ast"
"go/token"
"math"
"strconv"
"strings"
"github.com/goccy/wasm2go/internal/wasm"
)
// evalConstExprI64 evaluates a wasm constant expression that must produce an
// integer value (i32 or i64). Used for data/element segment offsets and
// integer global initializers. global.get of an *imported* global is not yet
// supported (returns an error). The expression bytes include the trailing
// 0x0b end byte.
func evalConstExprI64(expr []byte, m *wasm.Module) (int64, error) {
if len(expr) == 0 {
return 0, fmt.Errorf("empty const expr")
}
op := expr[0]
switch op {
case 0x41: // i32.const
v, _, err := readSLEB(expr[1:])
if err != nil {
return 0, err
}
return v, nil
case 0x42: // i64.const
v, _, err := readSLEB(expr[1:])
if err != nil {
return 0, err
}
return v, nil
case 0x23: // global.get
return 0, fmt.Errorf("global.get in const expr not yet supported")
}
return 0, fmt.Errorf("unsupported const-expr opcode 0x%02x", op)
}
// evalConstExprFloat evaluates a wasm constant expression that produces a
// float value (f32.const = 0x43, f64.const = 0x44). Returns the value as a
// float64; for f32 the caller narrows to float32. The expression bytes
// include the trailing 0x0b end byte.
func evalConstExprFloat(expr []byte) (float64, error) {
if len(expr) == 0 {
return 0, fmt.Errorf("empty const expr")
}
switch expr[0] {
case 0x43: // f32.const — 4 little-endian bytes
if len(expr) < 5 {
return 0, fmt.Errorf("truncated f32.const")
}
bits := binary.LittleEndian.Uint32(expr[1:5])
return float64(math.Float32frombits(bits)), nil
case 0x44: // f64.const — 8 little-endian bytes
if len(expr) < 9 {
return 0, fmt.Errorf("truncated f64.const")
}
bits := binary.LittleEndian.Uint64(expr[1:9])
return math.Float64frombits(bits), nil
}
return 0, fmt.Errorf("unsupported float const-expr opcode 0x%02x", expr[0])
}
// floatNeedsBitsEmission reports whether v cannot be reproduced by a Go
// decimal literal: NaN and ±Inf have no literal syntax, and Go constant
// arithmetic has no negative zero (`float64(-0)` is +0), so -0 must be
// materialized via math.Float{32,64}frombits as well.
func floatNeedsBitsEmission(v float64) bool {
return math.IsNaN(v) || math.IsInf(v, 0) || (v == 0 && math.Signbit(v))
}
// floatInitExpr builds a Go expression for a float global's initial value.
// Finite values use a decimal literal cast to the target type; NaN/Inf/-0
// values route through math.Float{32,64}frombits since Go has no literal
// syntax for them. isF32 selects between float32 and float64 emission.
func floatInitExpr(v float64, isF32 bool) ast.Expr {
special := floatNeedsBitsEmission(v)
if isF32 {
if special {
return &ast.CallExpr{
Fun: &ast.SelectorExpr{X: newID("math"), Sel: newID("Float32frombits")},
Args: []ast.Expr{&ast.BasicLit{
Kind: token.INT,
Value: fmt.Sprintf("0x%x", math.Float32bits(float32(v))),
}},
}
}
return &ast.CallExpr{Fun: newID("float32"), Args: []ast.Expr{&ast.BasicLit{
Kind: token.FLOAT,
Value: strconv.FormatFloat(v, 'g', -1, 32),
}}}
}
if special {
return &ast.CallExpr{
Fun: &ast.SelectorExpr{X: newID("math"), Sel: newID("Float64frombits")},
Args: []ast.Expr{&ast.BasicLit{
Kind: token.INT,
Value: fmt.Sprintf("0x%x", math.Float64bits(v)),
}},
}
}
return &ast.CallExpr{Fun: newID("float64"), Args: []ast.Expr{&ast.BasicLit{
Kind: token.FLOAT,
Value: strconv.FormatFloat(v, 'g', -1, 64),
}}}
}
func readSLEB(b []byte) (int64, int, error) {
var v int64
var s uint
var i int
var by byte
for i < len(b) {
by = b[i]
i++
v |= int64(by&0x7f) << s
s += 7
if by&0x80 == 0 {
break
}
if s > 70 {
return 0, i, fmt.Errorf("sleb overflow")
}
}
if s < 64 && (by&0x40) != 0 {
v |= -1 << s
}
return v, i, nil
}
// goEscapedBytes returns a Go string-literal representation (double-quoted,
// with \xNN escapes) of the given bytes.
func goEscapedBytes(b []byte) string {
var sb strings.Builder
sb.Grow(len(b)*4 + 2)
sb.WriteByte('"')
for _, c := range b {
switch {
case c == '\\':
sb.WriteString(`\\`)
case c == '"':
sb.WriteString(`\"`)
case c == '\n':
sb.WriteString(`\n`)
case c == '\t':
sb.WriteString(`\t`)
case c == '\r':
sb.WriteString(`\r`)
case c >= 0x20 && c < 0x7f:
sb.WriteByte(c)
default:
fmt.Fprintf(&sb, `\x%02x`, c)
}
}
sb.WriteByte('"')
return sb.String()
}