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
29 changes: 29 additions & 0 deletions gs/builtin/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2947,10 +2947,39 @@ export function typeInfoGoString(
prefixTypeString(`map[${key}]`, info.elemType)
)
}
case TypeKind.Function:
return functionTypeString(info)
case TypeKind.Interface:
return info.methods.length === 0 ? 'interface {}' : undefined
}
return undefined
}

// functionTypeString returns the Go type string of the function type info,
// such as func(int, ...string) (bool, error), or undefined if one of its
// types cannot be spelled.
function functionTypeString(info: FunctionTypeInfo): string | undefined {
const params = (info.params ?? []).map(typeInfoGoString)
const results = (info.results ?? []).map(typeInfoGoString)
if ([...params, ...results].some((t) => t === undefined)) {
return undefined
}

// The last parameter of a variadic function is spelled ...T, not []T.
const last = params.length - 1
if (info.isVariadic && last >= 0 && params[last]!.startsWith('[]')) {
params[last] = '...' + params[last]!.substring(2)
}
const text = 'func(' + params.join(', ') + ')'
if (results.length === 0) {
return text
}
if (results.length === 1) {
return text + ' ' + results[0]
}
return text + ' (' + results.join(', ') + ')'
}

// dynamicTypeName returns the Go type string of info as the dynamic type of
// a value, or undefined when info is an interface type, whose values have
// dynamic types of their own, or cannot be spelled.
Expand Down
9 changes: 3 additions & 6 deletions gs/fmt/fmt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,9 @@ describe('fmt basic value formatting', () => {
const m = new Map<any, any>()
m.set('a', 1)
m.set('b', 2)
const out = await fmt.Sprintf('%v', m)
// Order in Map iteration is insertion order; verify shape
expect(out.startsWith('{')).toBe(true)
expect(out.includes('a:1')).toBe(true)
expect(out.includes('b:2')).toBe(true)
expect(out.endsWith('}')).toBe(true)
m.set('', 0)
// A map formats as in Go, sorted by key.
expect(await fmt.Sprintf('%v', m)).toBe('map[:0 a:1 b:2]')

const s = new Set<any>([1, 2, 3])
expect(await fmt.Sprintf('%v', s)).toBe('[1 2 3]')
Expand Down
112 changes: 99 additions & 13 deletions gs/fmt/fmt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,12 @@ function formatValue(value: any, verb: string, flags = ''): string {
return value ? 'true' : 'false'
case 'T': // type (approximate Go names for primitives we need)
if (hasGoTypeName(value)) return $.goTypeString(value.__goType)
if (typeof value === 'function') {
// A function value has the type information of its signature.
const info = (value as { __typeInfo?: $.TypeInfo }).__typeInfo
const name = $.typeInfoGoString(info)
if (name !== undefined) return name
}
if (typeof value === 'number') {
return Number.isInteger(value) ? 'int' : 'float64'
}
Expand Down Expand Up @@ -482,6 +488,71 @@ function nilString(
return '<nil>'
}

// compareMapKeys orders two map keys as Go's fmt sorts them: numbers by
// value, with NaN first, strings lexically, false before true, and arrays
// and structs by their elements in order. Keys of different kinds, such as
// those of an interface map, are ordered by kind.
function compareMapKeys(a: unknown, b: unknown): number {
a = basicValue(a)
b = basicValue(b)
const ka = typeof a
const kb = typeof b
if (
(ka === 'number' || ka === 'bigint') &&
(kb === 'number' || kb === 'bigint')
) {
if (ka === 'bigint' && kb === 'bigint') {
return (
(a as bigint) < (b as bigint) ? -1
: (a as bigint) > (b as bigint) ? 1
: 0
)
}
const x = Number(a)
const y = Number(b)
if (Number.isNaN(x) || Number.isNaN(y)) {
return (
Number.isNaN(x) && Number.isNaN(y) ? 0
: Number.isNaN(x) ? -1
: 1
)
}
return (
x < y ? -1
: x > y ? 1
: 0
)
}
if (ka !== kb) {
return ka < kb ? -1 : 1
}
if (ka === 'string' || ka === 'boolean') {
return (
(a as string) < (b as string) ? -1
: (a as string) > (b as string) ? 1
: 0
)
}
if (a === null || b === null || ka !== 'object') {
return (
a === b ? 0
: a === null ? -1
: 1
)
}

// Arrays compare by element, and structs by field, in order.
const xs = $.isArrayOrGoSlice(a) ? sliceItems(a) : Object.values(a as object)
const ys = $.isArrayOrGoSlice(b) ? sliceItems(b) : Object.values(b as object)
for (let i = 0; i < Math.min(xs.length, ys.length); i++) {
const c = compareMapKeys(xs[i], ys[i])
if (c !== 0) {
return c
}
}
return xs.length - ys.length
}

/** defaultFormatMaybe renders Go values without exposing their runtime storage. */
function defaultFormatMaybe(
value: any,
Expand Down Expand Up @@ -655,23 +726,38 @@ function defaultFormatMaybe(
formattingStructs.delete(storage)
}
}
// Basic Map/Set rendering
// A map formats as map[k:v ...], with its entries sorted by key as Go
// sorts them, or in Go syntax for %#v.
if (value instanceof Map) {
const parts: MaybeString[] = []
for (const [k, v] of (value as Map<any, any>).entries()) {
const key = defaultFormatMaybe(k, flags, depth + 1)
const elem = defaultFormatMaybe(v, flags, depth + 1)
const mapInfo = info?.kind === $.TypeKind.Map ? info : undefined
const entries = [...(value as Map<unknown, unknown>).entries()].sort(
([a], [b]) => compareMapKeys(a, b),
)
const parts = entries.map(([k, v]): MaybeString => {
const key = defaultFormatMaybe(
k,
flags,
depth + 1,
mapInfo?.keyType,
allowMethods,
)
const elem = defaultFormatMaybe(
v,
flags,
depth + 1,
mapInfo?.elemType,
allowMethods,
)
if (isPromiseLike(key) || isPromiseLike(elem)) {
parts.push(
Promise.all([Promise.resolve(key), Promise.resolve(elem)]).then(
([resolvedKey, resolvedElem]) => `${resolvedKey}:${resolvedElem}`,
),
)
} else {
parts.push(`${key}:${elem}`)
return Promise.all([key, elem]).then(([rk, re]) => `${rk}:${re}`)
}
return `${key}:${elem}`
})
if (flags.includes('#')) {
const name = $.typeInfoGoString(mapInfo) ?? 'map'
return joinMaybe(parts, ', ', name + '{', '}')
}
return joinMaybe(parts, ' ', '{', '}')
return joinMaybe(parts, ' ', 'map[', ']')
}
if (value instanceof Set) {
const parts: MaybeString[] = []
Expand Down
9 changes: 9 additions & 0 deletions tests/tests/fmt_map_sorted/expected.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
map[-1:a 2:b 3:c] map[NaN:false -1:true 2.5:true]
map[-1:a 2:b 3:c] map[-1:a 2:b 3:c] map[int]string{-1:"a", 2:"b", 3:"c"}
map[:0 a:1 b:2]
map[false:0 true:1]
map[[1 9]:x [2 1]:y]
map[{1 a}:1 {1 b}:2 {2 a}:3]
map[x:1 y:s] map[1:map[2:3]]
map[] map[string]int(nil) map[]
map[k:1] map[k:1]
32 changes: 32 additions & 0 deletions tests/tests/fmt_map_sorted/fmt_map_sorted.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package main

import (
"fmt"
"math"
)

type key struct {
A int
B string
}

func main() {
// Keys are sorted, numbers by value with NaN first.
ints := map[int]string{3: "c", -1: "a", 2: "b"}
floats := map[float64]bool{2.5: true, math.NaN(): false, -1: true}
fmt.Println(ints, floats)
fmt.Printf("%v %+v %#v\n", ints, ints, ints)

// Strings, bools, arrays and structs sort by value, field by field.
fmt.Println(map[string]int{"b": 2, "a": 1, "": 0})
fmt.Println(map[bool]int{true: 1, false: 0})
fmt.Println(map[[2]int]string{{2, 1}: "y", {1, 9}: "x"})
fmt.Println(map[key]int{{2, "a"}: 3, {1, "b"}: 2, {1, "a"}: 1})

// Interface elements, nested maps and nil and empty maps.
var nilMap map[string]int
fmt.Println(map[string]any{"x": 1, "y": "s"}, map[int]map[int]int{1: {2: 3}})
fmt.Printf("%v %#v %v\n", nilMap, nilMap, map[string]int{})
var x any = map[string]int{"k": 1}
fmt.Println(x, fmt.Sprint(x))
}
61 changes: 61 additions & 0 deletions tests/tests/fmt_map_sorted/fmt_map_sorted.gs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Generated file based on fmt_map_sorted.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 class key {
public declare A: number

public declare B: string

constructor(__goscriptValue?: boolean, A$?: number, B$?: string) {
this.A = A$ ?? 0
this.B = B$ ?? ""
;(this as any)[$.structValueMarker] = __goscriptValue === true
}

public clone(): key {
return new key(true, this.A, this.B)
}

public __goscriptAssign(source: key): this {
this.A = source.A
this.B = source.B
return this
}

static __typeInfo = $.registerStructType(
"main.key",
() => new key(),
() => [],
key,
() => [{ name: "A", key: "A", type: /* @__PURE__ */ $.basicType("int") }, { name: "B", key: "B", type: /* @__PURE__ */ $.basicType("string") }]
)
}

export async function main(): globalThis.Promise<void> {
// Keys are sorted, numbers by value with NaN first.
let ints = $.makeMap<number, string>([[3, "c"], [-1, "a"], [2, "b"]])
let floats = $.makeMap<number, boolean>([[2.5, true], [math.NaN(), false], [-1, true]])
fmt.Println($.interfaceValue(ints, "map[int]string", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.basicType("string"))), $.interfaceValue(floats, "map[float64]bool", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("float64"), /* @__PURE__ */ $.basicType("bool"))))
fmt.Printf("%v %+v %#v\n", $.interfaceValue(ints, "map[int]string", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.basicType("string"))), $.interfaceValue(ints, "map[int]string", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.basicType("string"))), $.interfaceValue(ints, "map[int]string", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.basicType("string"))))

// Strings, bools, arrays and structs sort by value, field by field.
fmt.Println($.interfaceValue($.makeMap<string, number>([["b", 2], ["a", 1], ["", 0]]), "map[string]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), /* @__PURE__ */ $.basicType("int"))))
fmt.Println($.interfaceValue($.makeMap<boolean, number>([[true, 1], [false, 0]]), "map[bool]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("bool"), /* @__PURE__ */ $.basicType("int"))))
fmt.Println($.interfaceValue($.makeMap<number[], string>([[$.arrayValue([2, 1]), "y"], [$.arrayValue([1, 9]), "x"]]), "map[[2]int]string", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.arrayType(/* @__PURE__ */ $.basicType("int"), 2), /* @__PURE__ */ $.basicType("string"))))
fmt.Println($.interfaceValue($.makeMap<key, number>([[new key(true, 2, "a"), 3], [new key(true, 1, "b"), 2], [new key(true, 1, "a"), 1]]), "map[main.key]int", /* @__PURE__ */ $.mapType("main.key", /* @__PURE__ */ $.basicType("int"))))

// Interface elements, nested maps and nil and empty maps.
let nilMap: globalThis.Map<string, number> | null = null! as globalThis.Map<string, number> | null
fmt.Println($.interfaceValue($.makeMap<string, any>([["x", $.basicInterfaceValue(1, "int")], ["y", "s"]]), "map[string]interface{}", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), { kind: $.TypeKind.Interface, methods: [] })), $.interfaceValue($.makeMap<number, globalThis.Map<number, number> | null>([[1, $.makeMap<number, number>([[2, 3]])]]), "map[int]map[int]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.basicType("int")))))
fmt.Printf("%v %#v %v\n", $.interfaceValue(nilMap, "map[string]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), /* @__PURE__ */ $.basicType("int"))), $.interfaceValue(nilMap, "map[string]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), /* @__PURE__ */ $.basicType("int"))), $.interfaceValue($.makeMap<string, number>([]), "map[string]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), /* @__PURE__ */ $.basicType("int"))))
let x: any = $.interfaceValue($.makeMap<string, number>([["k", 1]]), "map[string]int", /* @__PURE__ */ $.mapType(/* @__PURE__ */ $.basicType("string"), /* @__PURE__ */ $.basicType("int")))
fmt.Println(x, (fmt.Sprint(x) as string))
}

if ($.isMainScript(import.meta)) {
await main()
}
1 change: 1 addition & 0 deletions tests/tests/fmt_map_sorted/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { key } from "./fmt_map_sorted.gs.ts"
25 changes: 25 additions & 0 deletions tests/tests/fmt_map_sorted/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"paths": {
"*": [
"./*"
],
"@go/github.com/s4wave/goscript/*": [
"../../../*"
],
"@goscript/*": [
"../../../tests/tests/fmt_map_sorted/run/output/@goscript/*",
"../../../gs/*",
"../../../tests/deps/*"
],
"@goscript/github.com/s4wave/goscript/tests/tests/fmt_map_sorted/*": [
"./*"
]
}
},
"extends": "../../../tests/tsconfig.base.json",
"include": [
"fmt_map_sorted.gs.ts",
"index.ts"
]
}
3 changes: 3 additions & 0 deletions tests/tests/fmt_type_func/expected.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
func(int) string|func(int, int) int|main.handler|func(...interface {}) (int, error)
func()
func(int) string
17 changes: 17 additions & 0 deletions tests/tests/fmt_type_func/fmt_type_func.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import "fmt"

type handler func(string) error

func add(a, b int) int { return a + b }

func main() {
f := func(a int) string { return "" }
var g func()
var h handler = func(string) error { return nil }
fmt.Printf("%T|%T|%T|%T\n", f, add, h, func(...any) (int, error) { return 0, nil })
fmt.Printf("%T\n", g)
var x any = f
fmt.Println(fmt.Sprintf("%T", x))
}
31 changes: 31 additions & 0 deletions tests/tests/fmt_type_func/fmt_type_func.gs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Generated file based on fmt_type_func.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"

export type handler = ((_p0: string) => $.GoError | globalThis.Promise<$.GoError>) | null

export function add(a: number, b: number): number {
return a + b
}

export async function main(): globalThis.Promise<void> {
let f = $.functionValue((a: number): string => {
return ""
}, ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("int")], results: [/* @__PURE__ */ $.basicType("string")] } as $.FunctionTypeInfo))
let g: (() => void) | null = null! as (() => void) | null
let h: handler | null = $.functionValue((_p0: string): $.GoError => {
return null
}, ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("string")], results: ["error"] } as $.FunctionTypeInfo))
fmt.Printf("%T|%T|%T|%T\n", $.interfaceValue(f, "func(a int) string", ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("int")], results: [/* @__PURE__ */ $.basicType("string")] } as $.FunctionTypeInfo)), $.interfaceValue($.goFunc(add, "main.add"), "func(a int, b int) int", ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("int"), /* @__PURE__ */ $.basicType("int")], results: [/* @__PURE__ */ $.basicType("int")] } as $.FunctionTypeInfo)), $.interfaceValue(h, "main.handler", ({ kind: $.TypeKind.Function, name: "main.handler", params: [/* @__PURE__ */ $.basicType("string")], results: ["error"] } as $.FunctionTypeInfo)), $.interfaceValue($.functionValue((_p0: $.Slice<any>): [number, $.GoError] => {
return [0, null]
}, ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.sliceType({ kind: $.TypeKind.Interface, methods: [] })], results: [/* @__PURE__ */ $.basicType("int"), "error"], isVariadic: true } as $.FunctionTypeInfo)), "func(...any) (int, error)", ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.sliceType({ kind: $.TypeKind.Interface, methods: [] })], results: [/* @__PURE__ */ $.basicType("int"), "error"], isVariadic: true } as $.FunctionTypeInfo)))
fmt.Printf("%T\n", $.interfaceValue(g, "func()", ({ kind: $.TypeKind.Function, params: [], results: [] } as $.FunctionTypeInfo)))
let x: any = $.interfaceValue(f, "func(a int) string", ({ kind: $.TypeKind.Function, params: [/* @__PURE__ */ $.basicType("int")], results: [/* @__PURE__ */ $.basicType("string")] } as $.FunctionTypeInfo))
fmt.Println((fmt.Sprintf("%T", x) as string))
}

if ($.isMainScript(import.meta)) {
await main()
}
1 change: 1 addition & 0 deletions tests/tests/fmt_type_func/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type { handler } from "./fmt_type_func.gs.ts"
Loading
Loading