From 1910d8fa9803f2bc1bb07d92505d012e67820d12 Mon Sep 17 00:00:00 2001 From: "Randall C. O'Reilly" Date: Sun, 25 Oct 2026 13:32:07 +0100 Subject: [PATCH] feat(compiler): compute and format complex numbers Complex numbers are objects of their two parts, but the arithmetic operators lowered to the JavaScript ones, which concatenate or give NaN for objects, and fmt formatted them as structs. The compiler now lowers +, -, *, / and unary - of complex operands, and their assignment forms, to runtime helpers, rounding complex64 results to float32 parts. Division follows the Go runtime's complex128div, so division by zero and by infinities gives the same infinities and NaNs. fmt formats a complex number as Go does, for %v and the float verbs. Signed-off-by: Randall C. O'Reilly --- compiler/lowering.go | 55 ++++++++- compiler/runtime-contract.go | 10 ++ gs/builtin/builtin.ts | 106 ++++++++++++++++++ gs/fmt/fmt.ts | 66 ++++++++++- .../complex_arithmetic/complex_arithmetic.go | 37 ++++++ .../complex_arithmetic.gs.ts | 43 +++++++ tests/tests/complex_arithmetic/expected.log | 8 ++ tests/tests/complex_arithmetic/index.ts | 0 tests/tests/complex_arithmetic/tsconfig.json | 25 +++++ 9 files changed, 344 insertions(+), 6 deletions(-) create mode 100644 tests/tests/complex_arithmetic/complex_arithmetic.go create mode 100644 tests/tests/complex_arithmetic/complex_arithmetic.gs.ts create mode 100644 tests/tests/complex_arithmetic/expected.log create mode 100644 tests/tests/complex_arithmetic/index.ts create mode 100644 tests/tests/complex_arithmetic/tsconfig.json 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" + ] +}