Skip to content

Commit 0089517

Browse files
committed
fix(compiler): preserve append and string-data semantics
Signed-off-by: Christian Stewart <christian@aperture.us>
1 parent d3a259f commit 0089517

49 files changed

Lines changed: 572 additions & 155 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

compiler/lowering.go

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8266,12 +8266,27 @@ func (o *LoweringOwner) lowerCallExpr(ctx lowerFileContext, expr *ast.CallExpr)
82668266
appendHelper += "<" + o.tsTypeFor(ctx, slice.Elem()) + ">"
82678267
}
82688268
}
8269-
// Byte specialization is destination-independent: emit a trailing
8270-
// byte element-type hint (mirroring make([]byte,...)) so the runtime
8271-
// keeps a Uint8Array representation even when the append destination
8272-
// is nil, empty, or a generically-backed array.
8273-
if slice, ok := types.Unalias(ctx.semPkg.source.TypesInfo.TypeOf(expr)).Underlying().(*types.Slice); ok && isByteType(slice.Elem()) {
8274-
args = append(args, o.runtimeOwner.QualifiedHelper(RuntimeHelperByteSliceHint))
8269+
// Most zeros are inferred from appended values. Carry static
8270+
// metadata only when the dynamic value can differ from the Go
8271+
// slice element zero.
8272+
if slice, ok := types.Unalias(ctx.semPkg.source.TypesInfo.TypeOf(expr)).Underlying().(*types.Slice); ok {
8273+
if isByteType(slice.Elem()) {
8274+
args = append(args, o.runtimeOwner.QualifiedHelper(RuntimeHelperByteSliceHint))
8275+
} else if appendZeroNeedsStaticHint(slice.Elem()) {
8276+
zero := o.lowerDeclarationZeroValueExpr(ctx, slice.Elem())
8277+
zeroHint := ""
8278+
switch {
8279+
case zero == "null" || strings.HasPrefix(zero, "null as "):
8280+
zeroHint = o.runtimeOwner.QualifiedHelper(RuntimeHelperAppendZeros) + ".nil"
8281+
case isComplexType(slice.Elem()):
8282+
zeroHint = o.runtimeOwner.QualifiedHelper(RuntimeHelperAppendZeros) + ".complex"
8283+
default:
8284+
zeroFactory := "() => (" + zero + " as " + o.tsSliceElemTypeFor(ctx, slice.Elem()) + ")"
8285+
zeroHint = o.runtimeOwner.QualifiedHelper(RuntimeHelperAppendZero) +
8286+
"(" + zeroFactory + ")"
8287+
}
8288+
args = append(args, zeroHint)
8289+
}
82758290
}
82768291
return appendHelper + "(" + strings.Join(args, ", ") + ")", diagnostics
82778292
case "cap":
@@ -12500,6 +12515,20 @@ func (o *LoweringOwner) tsStructFieldTypeFor(ctx lowerFileContext, typ types.Typ
1250012515
asyncCompatibleResultType(o.tsSignatureResultFor(ctx, signature)) + ") | null"
1250112516
}
1250212517

12518+
func appendZeroNeedsStaticHint(typ types.Type) bool {
12519+
if namedStructType(typ) != nil && isStructValueType(typ) {
12520+
return false
12521+
}
12522+
switch typed := types.Unalias(typ).Underlying().(type) {
12523+
case *types.Basic:
12524+
return typed.Kind() == types.UnsafePointer || typed.Info()&types.IsComplex != 0
12525+
case *types.Array:
12526+
return appendZeroNeedsStaticHint(typed.Elem())
12527+
default:
12528+
return true
12529+
}
12530+
}
12531+
1250312532
func zeroValueExpr(typ types.Type) string {
1250412533
if typ == nil {
1250512534
return "undefined"

compiler/runtime-contract.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ const (
100100
RuntimeHelperAppend RuntimeHelper = "slice.append"
101101
RuntimeHelperAppendSlice RuntimeHelper = "slice.appendSlice"
102102
RuntimeHelperByteSliceHint RuntimeHelper = "slice.byteSliceHint"
103+
RuntimeHelperAppendZero RuntimeHelper = "slice.appendZero"
104+
RuntimeHelperAppendZeros RuntimeHelper = "slice.appendZeros"
103105
RuntimeHelperCopy RuntimeHelper = "slice.copy"
104106
RuntimeHelperAsArray RuntimeHelper = "slice.asArray"
105107
RuntimeHelperStringToRunes RuntimeHelper = "slice.stringToRunes"
@@ -143,7 +145,7 @@ const (
143145
RuntimeHelperTypedNil RuntimeHelper = "type.typedNil"
144146
RuntimeHelperInterfaceValue RuntimeHelper = "type.interfaceValue"
145147
RuntimeHelperNamedValueInterfaceValue RuntimeHelper = "type.namedValueInterfaceValue"
146-
RuntimeHelperCallInterfaceMethod RuntimeHelper = "type.callInterfaceMethod"
148+
RuntimeHelperCallInterfaceMethod RuntimeHelper = "type.callInterfaceMethod"
147149
RuntimeHelperFunctionValue RuntimeHelper = "type.functionValue"
148150
RuntimeHelperNamedFunction RuntimeHelper = "type.namedFunction"
149151
RuntimeHelperGenericZero RuntimeHelper = "type.genericZero"
@@ -346,6 +348,8 @@ func runtimeHelperContracts() []RuntimeHelperContract {
346348
runtimeHelper(RuntimeHelperAppend, "append", RuntimeHelperCategorySlice),
347349
runtimeHelper(RuntimeHelperAppendSlice, "appendSlice", RuntimeHelperCategorySlice),
348350
runtimeHelper(RuntimeHelperByteSliceHint, "byteSliceHint", RuntimeHelperCategorySlice),
351+
runtimeHelper(RuntimeHelperAppendZero, "appendZero", RuntimeHelperCategorySlice),
352+
runtimeHelper(RuntimeHelperAppendZeros, "appendZeros", RuntimeHelperCategorySlice),
349353
runtimeHelper(RuntimeHelperCopy, "copy", RuntimeHelperCategorySlice),
350354
runtimeHelper(RuntimeHelperAsArray, "asArray", RuntimeHelperCategorySlice),
351355
runtimeHelper(RuntimeHelperStringToRunes, "stringToRunes", RuntimeHelperCategorySlice),

compiler/skeleton_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3202,7 +3202,7 @@ func TestCompilePackagesEmitsGenericMethodsAliasesAndDictionaries(t *testing.T)
32023202
"export async function CallString(__typeArgs: $.GenericTypeArgs | undefined, v: any): globalThis.Promise<string>",
32033203
"export function Sum<T>(__typeArgs: $.GenericTypeArgs | undefined, vals: $.Slice<T>): any",
32043204
"export function Copy<T>(__typeArgs: $.GenericTypeArgs | undefined, vals: $.Slice<T>): $.Slice<T>",
3205-
"return $.appendSlice($.arrayToSlice<T>([]), vals)",
3205+
"return $.appendSlice($.arrayToSlice<T>([]), vals, $.appendZero(() => ($.genericZero(__typeArgs, \"T\", null) as T)))",
32063206
"let seen: Set = $.makeMap<number, {}>()",
32073207
"$.mapSet(seen, 1, {})",
32083208
"$.genericZero(__typeArgs, \"T\", null)",

gs/builtin/slice.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { describe, expect, it } from 'vitest'
2-
32
import { makeMap, mapGet, mapSet } from './map.js'
43
import {
54
append,
65
appendSlice,
6+
appendZero,
7+
appendZeros,
78
arrayToSlice,
89
byteSliceHint,
910
bytesToString,
1011
copy,
12+
goSlice,
1113
indexString,
1214
len,
1315
makeSlice,
@@ -18,6 +20,7 @@ import {
1820
stringEqual,
1921
stringToBytes,
2022
} from './slice.js'
23+
import { markAsStructValue } from './type.js'
2124

2225
describe('rune to string encoding (Go string(rune) semantics)', () => {
2326
it('preserves astral-plane runes above U+FFFF', () => {
@@ -97,6 +100,63 @@ describe('destination-independent byte specialization', () => {
97100
})
98101
})
99102

103+
describe('append spare capacity', () => {
104+
class item {
105+
value = 0
106+
}
107+
108+
it('zero-initializes primitive elements exposed by reslicing', () => {
109+
let values = append<number>(null, 1)
110+
values = append(values, 2)
111+
values = append(values, 3)
112+
113+
expect(goSlice(values, undefined, 4)[3]).toBe(0)
114+
})
115+
116+
it('zero-initializes struct elements exposed by reslicing', () => {
117+
const zeroHint = appendZero(() => markAsStructValue(new item()))
118+
let values = append<item>(null, markAsStructValue(new item()), zeroHint)
119+
values = append(values, markAsStructValue(new item()), zeroHint)
120+
values = append(values, markAsStructValue(new item()), zeroHint)
121+
122+
const zero = goSlice(values, undefined, 4)[3]
123+
expect(zero).toBeInstanceOf(item)
124+
expect(zero.value).toBe(0)
125+
})
126+
127+
it('creates independent struct zeros for every spare slot', () => {
128+
const zeroHint = appendZero(() => markAsStructValue(new item()))
129+
let values = append<item>(null, markAsStructValue(new item()), zeroHint)
130+
for (let i = 0; i < 4; i++) {
131+
values = append(values, markAsStructValue(new item()), zeroHint)
132+
}
133+
134+
const expanded = goSlice(values, undefined, 8)
135+
expanded[5].value = 7
136+
expect(expanded[6].value).toBe(0)
137+
expect(expanded[5]).not.toBe(expanded[6])
138+
})
139+
140+
it('zero-initializes appendSlice spare capacity from a static hint', () => {
141+
const dynamic = markAsStructValue(new item())
142+
const source: (item | null)[] = [dynamic]
143+
let values = appendSlice<item | null>(null, source, appendZeros.nil)
144+
values = appendSlice(values, source, appendZeros.nil)
145+
values = appendSlice(values, source, appendZeros.nil)
146+
147+
expect(goSlice(values, undefined, 4)[3]).toBeNull()
148+
})
149+
150+
it('uses the static interface zero instead of the dynamic element type', () => {
151+
const dynamic = markAsStructValue(new item())
152+
let values = append<item | null>(null, dynamic, appendZeros.nil)
153+
values = append(values, dynamic, appendZeros.nil)
154+
values = append(values, dynamic, appendZeros.nil)
155+
156+
expect(goSlice(values, undefined, 4)[3]).toBeNull()
157+
})
158+
})
159+
100160
describe('builtin string byte representation', () => {
101161
it('appends large byte slices without JavaScript argument spreading', () => {
102162
const dst = new Uint8Array(0)

0 commit comments

Comments
 (0)