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
55 changes: 53 additions & 2 deletions compiler/lowering.go
Original file line number Diff line number Diff line change
Expand Up @@ -6469,8 +6469,11 @@ func lowerCompoundAssignValue(
tok token.Token,
mask bool,
) string {
// Wide integers, int bitwise operations, 32-bit products and integer
// quotients have helpers.
// Complex numbers, wide integers, int bitwise operations, 32-bit
// products and integer quotients have helpers.
if helper, ok := complexArithmeticHelper(targetType, tok); ok {
return complexHelperCall(runtimeOwner, targetType, helper, left, right)
}
if helper, ok := wideIntegerAssignHelper(targetType, tok); ok {
return wideIntegerHelperCall(runtimeOwner, targetType, helper, left, right)
}
Expand Down Expand Up @@ -9874,6 +9877,46 @@ func (o *LoweringOwner) lowerComplexEqualityExpr(ctx lowerFileContext, expr *ast
return value, true
}

// lowerComplexArithmeticExpr lowers an arithmetic operation of complex
// numbers, which are objects of their parts, to its runtime helper.
func (o *LoweringOwner) lowerComplexArithmeticExpr(ctx lowerFileContext, expr *ast.BinaryExpr, left string, right string) (string, bool) {
resultType := ctx.semPkg.source.TypesInfo.TypeOf(expr)
helper, ok := complexArithmeticHelper(resultType, expr.Op)
if !ok {
return "", false
}
return complexHelperCall(o.runtimeOwner, resultType, helper, left, right), true
}

// complexArithmeticHelper returns the runtime helper of the arithmetic
// operator op, or its assignment form, on complex numbers of type typ.
func complexArithmeticHelper(typ types.Type, op token.Token) (RuntimeHelper, bool) {
if !isComplexType(typ) {
return "", false
}
switch op {
case token.ADD, token.ADD_ASSIGN:
return RuntimeHelperComplexAdd, true
case token.SUB, token.SUB_ASSIGN:
return RuntimeHelperComplexSub, true
case token.MUL, token.MUL_ASSIGN:
return RuntimeHelperComplexMul, true
case token.QUO, token.QUO_ASSIGN:
return RuntimeHelperComplexDiv, true
}
return "", false
}

// complexHelperCall calls the complex helper with the operands, rounding a
// complex64 result to the precision of its float32 parts.
func complexHelperCall(runtimeOwner *RuntimeContractOwner, typ types.Type, helper RuntimeHelper, operands ...string) string {
call := runtimeOwner.QualifiedHelper(helper) + "(" + strings.Join(operands, ", ") + ")"
if basic, ok := types.Unalias(typ).Underlying().(*types.Basic); ok && basic.Kind() == types.Complex64 {
return runtimeOwner.QualifiedHelper(RuntimeHelperComplex64) + "(" + call + ")"
}
return call
}

// lowerPointerEqualityExpr compares two pointers, also unsafe pointers.
// Pointers to the same storage are the same object, except references to a
// slice or array element, which each &s[i] makes anew, and views that an
Expand Down Expand Up @@ -10216,6 +10259,9 @@ func (o *LoweringOwner) lowerExpr(ctx lowerFileContext, expr ast.Expr) (string,
if _, ok := typed.Y.(*ast.BinaryExpr); ok {
right = "(" + right + ")"
}
if value, ok := o.lowerComplexArithmeticExpr(ctx, typed, left, right); ok {
return value, append(leftDiagnostics, rightDiagnostics...)
}
if typed.Op == token.AND_NOT {
// Wide (int64/uint64) operands lower to bigint, and a Go constant like
// 1<<63 lowers to a JS number whose raw ~ collapses to -1, so the raw
Expand Down Expand Up @@ -10302,6 +10348,11 @@ func (o *LoweringOwner) lowerExpr(ctx lowerFileContext, expr ast.Expr) (string,
return "await " + o.runtimeOwner.QualifiedHelper(RuntimeHelperChanRecv) + "(" + value + ")", diagnostics
}
value, diagnostics := o.lowerExpr(ctx, typed.X)
if typed.Op == token.SUB {
if resultType := ctx.semPkg.source.TypesInfo.TypeOf(typed); isComplexType(resultType) {
return complexHelperCall(o.runtimeOwner, resultType, RuntimeHelperComplexNeg, value), diagnostics
}
}
if typed.Op == token.NOT || typed.Op == token.SUB || typed.Op == token.ADD {
return lowerPrefixUnaryExpr(typed.Op, value), diagnostics
}
Expand Down
10 changes: 10 additions & 0 deletions compiler/runtime-contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ const (
RuntimeHelperMax RuntimeHelper = "builtin.max"
RuntimeHelperComplex RuntimeHelper = "builtin.complex"
RuntimeHelperComplex64 RuntimeHelper = "builtin.complex64"
RuntimeHelperComplexAdd RuntimeHelper = "builtin.complexAdd"
RuntimeHelperComplexSub RuntimeHelper = "builtin.complexSub"
RuntimeHelperComplexMul RuntimeHelper = "builtin.complexMul"
RuntimeHelperComplexDiv RuntimeHelper = "builtin.complexDiv"
RuntimeHelperComplexNeg RuntimeHelper = "builtin.complexNeg"
RuntimeHelperReal RuntimeHelper = "builtin.real"
RuntimeHelperImag RuntimeHelper = "builtin.imag"
RuntimeHelperUint64Shl RuntimeHelper = "builtin.uint64Shl"
Expand Down Expand Up @@ -393,6 +398,11 @@ func runtimeHelperContracts() []RuntimeHelperContract {
runtimeHelper(RuntimeHelperMax, "max", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperComplex, "complex", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperComplex64, "complex64", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperComplexAdd, "complexAdd", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperComplexSub, "complexSub", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperComplexMul, "complexMul", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperComplexDiv, "complexDiv", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperComplexNeg, "complexNeg", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperReal, "real", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperImag, "imag", RuntimeHelperCategoryBuiltin),
runtimeHelper(RuntimeHelperUint64Shl, "uint64Shl", RuntimeHelperCategoryBuiltin),
Expand Down
106 changes: 106 additions & 0 deletions gs/builtin/builtin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,112 @@ export function complex64(
return { real: float32(valueOrReal), imag: float32(imag) }
}

// complexOperand returns the complex number of an operand of a complex
// operation, which is a number for a constant of an integer or float value.
function complexOperand(value: Complex | number | bigint): Complex {
return typeof value === 'object' ? value : { real: Number(value), imag: 0 }
}

// complexAdd returns a + b.
export function complexAdd(
a: Complex | number | bigint,
b: Complex | number | bigint,
): Complex {
const x = complexOperand(a)
const y = complexOperand(b)
return { real: x.real + y.real, imag: x.imag + y.imag }
}

// complexSub returns a - b.
export function complexSub(
a: Complex | number | bigint,
b: Complex | number | bigint,
): Complex {
const x = complexOperand(a)
const y = complexOperand(b)
return { real: x.real - y.real, imag: x.imag - y.imag }
}

// complexMul returns a * b.
export function complexMul(
a: Complex | number | bigint,
b: Complex | number | bigint,
): Complex {
const x = complexOperand(a)
const y = complexOperand(b)
return {
real: x.real * y.real - x.imag * y.imag,
imag: x.real * y.imag + x.imag * y.real,
}
}

// complexDiv returns a / b as the Go runtime computes it (complex128div):
// by Smith's algorithm, with the infinities and zeros of C99 Annex G when
// both parts of the quotient are NaN. A division by zero does not panic.
export function complexDiv(
a: Complex | number | bigint,
b: Complex | number | bigint,
): Complex {
const n = complexOperand(a)
const m = complexOperand(b)
let e: number
let f: number
if (Math.abs(m.real) >= Math.abs(m.imag)) {
const ratio = m.imag / m.real
const denom = m.real + ratio * m.imag
e = (n.real + n.imag * ratio) / denom
f = (n.imag - n.real * ratio) / denom
} else {
const ratio = m.real / m.imag
const denom = m.imag + ratio * m.real
e = (n.real * ratio + n.imag) / denom
f = (n.imag * ratio - n.real) / denom
}
if (!Number.isNaN(e) || !Number.isNaN(f)) {
return { real: e, imag: f }
}

// Correct the result to infinities and zeros where they apply.
let [x, y, c, d] = [n.real, n.imag, m.real, m.imag]
const isInf = (v: number): boolean => v === Infinity || v === -Infinity
if (c === 0 && d === 0 && (!Number.isNaN(x) || !Number.isNaN(y))) {
e = copySign(Infinity, c) * x
f = copySign(Infinity, c) * y
} else if (
(isInf(x) || isInf(y)) &&
Number.isFinite(c) &&
Number.isFinite(d)
) {
x = copySign(isInf(x) ? 1 : 0, x)
y = copySign(isInf(y) ? 1 : 0, y)
e = Infinity * (x * c + y * d)
f = Infinity * (y * c - x * d)
} else if (
(isInf(c) || isInf(d)) &&
Number.isFinite(x) &&
Number.isFinite(y)
) {
c = copySign(isInf(c) ? 1 : 0, c)
d = copySign(isInf(d) ? 1 : 0, d)
e = 0 * (x * c + y * d)
f = 0 * (y * c - x * d)
}
return { real: e, imag: f }
}

// copySign returns a value with the magnitude of v and the sign of sign,
// a negative zero counting as negative.
function copySign(v: number, sign: number): number {
const negative = sign < 0 || Object.is(sign, -0)
return negative ? -Math.abs(v) : Math.abs(v)
}

// complexNeg returns -a.
export function complexNeg(a: Complex | number | bigint): Complex {
const x = complexOperand(a)
return { real: -x.real, imag: -x.imag }
}

export function real(value: number | Complex | null | undefined): number {
value = unwrapGoValue(value)
if (typeof value === 'number') {
Expand Down
66 changes: 62 additions & 4 deletions gs/fmt/fmt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,19 +309,73 @@ function floatBits(value: unknown, typeInfo?: $.TypeInfo | string): number {
return 0
}

// formatFloatVerb formats a float for the e, E, f, F, g and G verbs, with an
// optional precision, as in Go.
// formatFloatVerb formats a float or complex number for the e, E, f, F, g
// and G verbs, with an optional precision, as in Go.
function formatFloatVerb(
value: unknown,
verb: string,
precision: string,
): string {
const bits = floatBits(value) || 64
let prec = precision === '' ? -1 : parseInt(precision)
if (prec < 0 && verb !== 'g' && verb !== 'G') {
prec = 6
}
return $.formatFloat(Number(value), verb === 'F' ? 'f' : verb, prec, bits)
const fmtVerb = verb === 'F' ? 'f' : verb
const cbits = complexBits(value)
if (cbits !== 0) {
return formatComplex(basicValue(value) as $.Complex, fmtVerb, prec, cbits)
}
const bits = floatBits(value) || 64
return $.formatFloat(Number(value), fmtVerb, prec, bits)
}

// complexBits returns 64 or 128 if value is a complex64 or complex128, as
// the given type, the type it is boxed with or else its form tells, or 0
// otherwise.
function complexBits(value: unknown, typeInfo?: $.TypeInfo | string): number {
const info =
typeInfo ??
(value !== null && typeof value === 'object' ?
(value as { __goTypeInfo?: $.TypeInfo | string }).__goTypeInfo
: undefined)
const name =
typeof info === 'string' ? info
: info?.kind === $.TypeKind.Basic ? info.name
: undefined
if (name === 'complex64') return 64
if (name === 'complex128') return 128
if (name !== undefined) return 0
const kind = typeof info === 'object' ? info.kind : undefined
if (kind !== undefined && kind !== $.TypeKind.Interface) return 0

// A complex number is a plain object of its two parts.
const v = basicValue(value) as { real?: unknown; imag?: unknown } | null
return (
v !== null &&
typeof v === 'object' &&
Object.getPrototypeOf(v) === Object.prototype &&
typeof v.real === 'number' &&
typeof v.imag === 'number'
) ?
128
: 0
}

// formatComplex formats the complex number value of bits bits as Go does:
// its parts formatted as floats of half its size, the imaginary one always
// signed, in parentheses.
function formatComplex(
value: $.Complex,
verb: string,
prec: number,
bits: number,
): string {
const re = $.formatFloat(value.real, verb, prec, bits / 2)
let im = $.formatFloat(value.imag, verb, prec, bits / 2)
if (!im.startsWith('-') && !im.startsWith('+')) {
im = '+' + im
}
return '(' + re + im + 'i)'
}

function hasGoTypeName(value: unknown): value is { __goType: string } {
Expand Down Expand Up @@ -621,6 +675,10 @@ function defaultFormatMaybe(
allowMethods,
)
}
const cbits = complexBits(value, typeInfo)
if (cbits !== 0) {
return formatComplex(value, 'g', -1, cbits)
}
if (
info?.kind === $.TypeKind.Struct ||
$.isStructObject(value) ||
Expand Down
37 changes: 37 additions & 0 deletions tests/tests/complex_arithmetic/complex_arithmetic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"fmt"
"math"
)

// pair returns two complex128 values that the compiler cannot fold.
func pair() (complex128, complex128) { return 1 + 2i, 3 - 1i }

func main() {
// The binary operators, with variable and constant operands.
a, b := pair()
fmt.Println(a+b, a-b, a*b, a/b, -a, +a)
fmt.Println(a*2, 2*a, a/2, a+1i, 1-a)
fmt.Println(a == b, a != b, a == 1+2i)
fmt.Printf("%v %.2f %e %g %+v\n", a, a, b, -b, struct{ C complex64 }{2i})

// The assignment operators.
c := a
c += b
c -= 1
c *= a
c /= 2i
fmt.Println(c)

// complex64 rounds to the precision of float32 parts.
var x, y complex64 = complex64(a), complex64(b)
fmt.Println(x+y, x*y, x/y, -x)
x *= y
fmt.Println(x, real(x), imag(x))

// Division by zero and by infinities follows the Go runtime.
var zero complex128
inf := complex(math.Inf(1), 0)
fmt.Println(a/zero, zero/zero, a/inf, inf/a)
}
43 changes: 43 additions & 0 deletions tests/tests/complex_arithmetic/complex_arithmetic.gs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Generated file based on complex_arithmetic.go
// Updated when compliance tests are re-run, DO NOT EDIT!

import * as $ from "@goscript/builtin/index.js"
import * as fmt from "@goscript/fmt/index.js"
import * as math from "@goscript/math/index.js"

export function pair(): [$.Complex, $.Complex] {
return [$.complex(1, 2), $.complex(3, -1)]
}

export async function main(): globalThis.Promise<void> {
// The binary operators, with variable and constant operands.
let [a, b] = pair()
fmt.Println($.basicInterfaceValue($.complexAdd(a, b), "complex128"), $.basicInterfaceValue($.complexSub(a, b), "complex128"), $.basicInterfaceValue($.complexMul(a, b), "complex128"), $.basicInterfaceValue($.complexDiv(a, b), "complex128"), $.basicInterfaceValue($.complexNeg(a), "complex128"), $.basicInterfaceValue(a, "complex128"))
fmt.Println($.basicInterfaceValue($.complexMul(a, $.complex(2, 0)), "complex128"), $.basicInterfaceValue($.complexMul($.complex(2, 0), a), "complex128"), $.basicInterfaceValue($.complexDiv(a, $.complex(2, 0)), "complex128"), $.basicInterfaceValue($.complexAdd(a, $.complex(0, 1)), "complex128"), $.basicInterfaceValue($.complexSub($.complex(1, 0), a), "complex128"))
fmt.Println($.arrayEqual(a, b), !$.arrayEqual(a, b), $.arrayEqual(a, ($.complex(1, 2))))
fmt.Printf("%v %.2f %e %g %+v\n", $.basicInterfaceValue(a, "complex128"), $.basicInterfaceValue(a, "complex128"), $.basicInterfaceValue(b, "complex128"), $.basicInterfaceValue($.complexNeg(b), "complex128"), $.interfaceValue($.anonymousStructValue({C: $.complex64($.complex(0, 2))}, { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("C", /* @__PURE__ */ $.basicType("complex64"), [0], 0, true)] }), "struct{C complex64}", { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("C", /* @__PURE__ */ $.basicType("complex64"), [0], 0, true)] }))

// The assignment operators.
let c = a
c = $.complexAdd(c, b)
c = $.complexSub(c, $.complex(1, 0))
c = $.complexMul(c, a)
c = $.complexDiv(c, $.complex(0, 2))
fmt.Println($.basicInterfaceValue(c, "complex128"))

// complex64 rounds to the precision of float32 parts.
let x: $.Complex = $.complex64($.complex64(a))
let y: $.Complex = $.complex64($.complex64(b))
fmt.Println($.basicInterfaceValue($.complex64($.complexAdd(x, y)), "complex64"), $.basicInterfaceValue($.complex64($.complexMul(x, y)), "complex64"), $.basicInterfaceValue($.complex64($.complexDiv(x, y)), "complex64"), $.basicInterfaceValue($.complex64($.complexNeg(x)), "complex64"))
x = $.complex64($.complexMul(x, $.complex64(y)))
fmt.Println($.basicInterfaceValue(x, "complex64"), $.basicInterfaceValue($.real($.complex64(x)), "float32"), $.basicInterfaceValue($.imag($.complex64(x)), "float32"))

// Division by zero and by infinities follows the Go runtime.
let zero: $.Complex = $.complex(0, 0)
let inf = $.complex(math.Inf(1), 0)
fmt.Println($.basicInterfaceValue($.complexDiv(a, zero), "complex128"), $.basicInterfaceValue($.complexDiv(zero, zero), "complex128"), $.basicInterfaceValue($.complexDiv(a, inf), "complex128"), $.basicInterfaceValue($.complexDiv(inf, a), "complex128"))
}

if ($.isMainScript(import.meta)) {
await main()
}
8 changes: 8 additions & 0 deletions tests/tests/complex_arithmetic/expected.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
(4+1i) (-2+3i) (5+5i) (0.1+0.7000000000000001i) (-1-2i) (1+2i)
(2+4i) (2+4i) (0.5+1i) (1+3i) (0-2i)
false true true
(1+2i) (1.00+2.00i) (3.000000e+00-1.000000e+00i) (-3+1i) {C:(0+2i)}
(3.5-0.5i)
(4+1i) (5+5i) (0.1+0.7i) (-1-2i)
(5+5i) 5 5
(+Inf+Infi) (NaN+NaNi) (0+0i) (+Inf-Infi)
Empty file.
Loading
Loading