diff --git a/compiler/lowering.go b/compiler/lowering.go index 7fc30e21a..4aeee6686 100644 --- a/compiler/lowering.go +++ b/compiler/lowering.go @@ -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) } @@ -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 @@ -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 @@ -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 } diff --git a/compiler/runtime-contract.go b/compiler/runtime-contract.go index ca3e9ece0..dd46b594d 100644 --- a/compiler/runtime-contract.go +++ b/compiler/runtime-contract.go @@ -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" @@ -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), diff --git a/gs/builtin/builtin.ts b/gs/builtin/builtin.ts index 228bfc72d..72db840db 100644 --- a/gs/builtin/builtin.ts +++ b/gs/builtin/builtin.ts @@ -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') { diff --git a/gs/fmt/fmt.ts b/gs/fmt/fmt.ts index 5c0100a70..af0b0f367 100644 --- a/gs/fmt/fmt.ts +++ b/gs/fmt/fmt.ts @@ -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 } { @@ -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) || diff --git a/tests/tests/complex_arithmetic/complex_arithmetic.go b/tests/tests/complex_arithmetic/complex_arithmetic.go new file mode 100644 index 000000000..dc6e3869c --- /dev/null +++ b/tests/tests/complex_arithmetic/complex_arithmetic.go @@ -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) +} diff --git a/tests/tests/complex_arithmetic/complex_arithmetic.gs.ts b/tests/tests/complex_arithmetic/complex_arithmetic.gs.ts new file mode 100644 index 000000000..9f8eeceec --- /dev/null +++ b/tests/tests/complex_arithmetic/complex_arithmetic.gs.ts @@ -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 { + // 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() +} diff --git a/tests/tests/complex_arithmetic/expected.log b/tests/tests/complex_arithmetic/expected.log new file mode 100644 index 000000000..ae949d538 --- /dev/null +++ b/tests/tests/complex_arithmetic/expected.log @@ -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) diff --git a/tests/tests/complex_arithmetic/index.ts b/tests/tests/complex_arithmetic/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/tests/tests/complex_arithmetic/tsconfig.json b/tests/tests/complex_arithmetic/tsconfig.json new file mode 100644 index 000000000..005935f57 --- /dev/null +++ b/tests/tests/complex_arithmetic/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "paths": { + "*": [ + "./*" + ], + "@go/github.com/s4wave/goscript/*": [ + "../../../*" + ], + "@goscript/*": [ + "../../../tests/tests/complex_arithmetic/run/output/@goscript/*", + "../../../gs/*", + "../../../tests/deps/*" + ], + "@goscript/github.com/s4wave/goscript/tests/tests/complex_arithmetic/*": [ + "./*" + ] + } + }, + "extends": "../../../tests/tsconfig.base.json", + "include": [ + "complex_arithmetic.gs.ts", + "index.ts" + ] +}