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
28 changes: 25 additions & 3 deletions compiler/lowering.go
Original file line number Diff line number Diff line change
Expand Up @@ -14675,11 +14675,33 @@ func (o *LoweringOwner) lowerIndexExpr(ctx lowerFileContext, expr *ast.IndexExpr
return o.lowerMapGetValue(ctx, expr, target, index), diagnostics
}

// Array elements are read directly; slices and the rest need arrayIndex.
// Array elements are read directly; slices and the rest need arrayIndex,
// or a version of it for the representations of a kind of slice.
if array, ok := types.Unalias(targetType).Underlying().(*types.Array); ok {
return o.lowerArrayElement(ctx, expr, target, index, array), diagnostics
}
return o.runtimeOwner.QualifiedHelper(RuntimeHelperArrayIndex) + "(" + o.lowerIndexTarget(ctx, target, targetType) + ", " + o.lowerNumberIndexValue(ctx, expr.Index, index) + ")", diagnostics
helper := sliceIndexHelper(targetType)
return o.runtimeOwner.QualifiedHelper(helper) + "(" + o.lowerIndexTarget(ctx, target, targetType) + ", " + o.lowerNumberIndexValue(ctx, expr.Index, index) + ")", diagnostics
}

// sliceIndexHelper returns the runtime helper that reads an element of a
// value of the given type at an index. A slice of a concrete type is a typed
// array if its element type is held in one (see typedSliceArray), and is
// otherwise a plain array or a window object, so typedSliceIndex or
// plainSliceIndex test that representation first, which is much faster than
// the general arrayIndex when the call is not inlined. Each of them is
// arrayIndex for the other representations, so the choice only affects the
// speed. A slice whose element type has a type parameter, and every other
// type, uses arrayIndex.
func sliceIndexHelper(typ types.Type) RuntimeHelper {
slice, ok := types.Unalias(typ).Underlying().(*types.Slice)
if !ok || typeContainsTypeParam(slice.Elem()) {
return RuntimeHelperArrayIndex
}
if array, _ := typedSliceArray(slice.Elem()); array != "" {
return RuntimeHelperTypedSliceIndex
}
return RuntimeHelperPlainSliceIndex
}

// lowerArrayElement lowers reading the element of an array value at an index.
Expand Down Expand Up @@ -19753,7 +19775,7 @@ func isSimpleLoweredOperand(value string) bool {
// The only calls are of runtime readers without side effects.
for _, call := range simpleOperandCalls.FindAllStringSubmatch(value, -1) {
switch call[1] {
case "$.pointerValue", "$.pointerTarget", "$.arrayIndex":
case "$.pointerValue", "$.pointerTarget", "$.arrayIndex", "$.plainSliceIndex", "$.typedSliceIndex":
default:
return false
}
Expand Down
4 changes: 4 additions & 0 deletions compiler/runtime-contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ const (
RuntimeHelperIndexString RuntimeHelper = "slice.indexString"
RuntimeHelperArrayIndex RuntimeHelper = "slice.arrayIndex"
RuntimeHelperCheckArrayIndex RuntimeHelper = "slice.checkArrayIndex"
RuntimeHelperTypedSliceIndex RuntimeHelper = "slice.typedSliceIndex"
RuntimeHelperPlainSliceIndex RuntimeHelper = "slice.plainSliceIndex"
RuntimeHelperSliceStringOrBytes RuntimeHelper = "slice.sliceStringOrBytes"
RuntimeHelperSliceStringEqual RuntimeHelper = "slice.sliceStringEqual"
RuntimeHelperIndexRef RuntimeHelper = "slice.indexRef"
Expand Down Expand Up @@ -509,6 +511,8 @@ func runtimeHelperContracts() []RuntimeHelperContract {
runtimeHelper(RuntimeHelperIndexString, "indexString", RuntimeHelperCategorySlice),
runtimeHelper(RuntimeHelperArrayIndex, "arrayIndex", RuntimeHelperCategorySlice),
runtimeHelper(RuntimeHelperCheckArrayIndex, "checkArrayIndex", RuntimeHelperCategorySlice),
runtimeHelper(RuntimeHelperTypedSliceIndex, "typedSliceIndex", RuntimeHelperCategorySlice),
runtimeHelper(RuntimeHelperPlainSliceIndex, "plainSliceIndex", RuntimeHelperCategorySlice),
runtimeHelper(RuntimeHelperSliceStringOrBytes, "sliceStringOrBytes", RuntimeHelperCategorySlice),
runtimeHelper(RuntimeHelperSliceStringEqual, "sliceStringEqual", RuntimeHelperCategorySlice),
runtimeHelper(RuntimeHelperIndexRef, "indexRef", RuntimeHelperCategorySlice),
Expand Down
2 changes: 1 addition & 1 deletion compiler/skeleton_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2556,7 +2556,7 @@ func TestCompilePackagesEmitsArraySliceMapStringAndNamedMethods(t *testing.T) {
"$.mapSet(m, \"one\", 1)",
"let [value, ok] = $.mapGet<string, number, number>(m, \"missing\", 0)",
"$.sliceSlot(slice!, 0)",
"$.arrayIndex(literal!, 2)",
"$.plainSliceIndex(literal!, 2)",
"let list: $.VarRef<MySlice> = $.varRef(null! as MySlice)",
"MySlice_Add(list, 7)",
"$.indexString(text, 0)",
Expand Down
51 changes: 50 additions & 1 deletion gs/builtin/slice.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { arrayIndex, sliceElems, sliceSlot, windowSlot } from './slice.js'
import {
arrayIndex,
plainSliceIndex,
sliceElems,
sliceSlot,
typedSliceIndex,
windowSlot,
} from './slice.js'
import { describe, expect, it, vi } from 'vitest'

import { makeMap, mapGet, mapSet } from './map.js'
Expand Down Expand Up @@ -720,3 +727,45 @@ describe('typed slices', () => {
expect(typedSliceHintOf([1])).toBe(undefined)
})
})

describe('type-directed slice indexing', () => {
// typedSliceIndex and plainSliceIndex only test some representations
// first, so they read every slice as arrayIndex does.
const slices: [string, any][] = [
['plain array', [1, 2, 3]],
['window', goSlice([0, 1, 2, 3, 4], 1, 4)],
['typed array', new Float32Array([1, 2, 3])],
['typed window', goSlice(new Int32Array([0, 1, 2, 3, 4]), 1, 4)],
['proxy', arrayToSlice([[1], [2], [3]], 2)],
]
for (const [name, slice] of slices) {
it(`reads a ${name} as arrayIndex does`, () => {
for (let i = 0; i < 3; i++) {
expect(typedSliceIndex(slice, i)).toEqual(arrayIndex(slice, i))
expect(plainSliceIndex(slice, i)).toEqual(arrayIndex(slice, i))
}
})
}

it('panics on nil slices and indexes out of range as arrayIndex does', () => {
// Each case panics with the same message as arrayIndex.
const cases: [any, number][] = [
[null, 0],
[[1, 2], 2],
[[1, 2], -1],
[new Float32Array(2), 2],
[goSlice([0, 1, 2, 3], 1, 3), 2],
]
for (const [slice, i] of cases) {
let want = ''
try {
arrayIndex(slice, i)
} catch (e) {
want = String(e)
}
expect(want).not.toBe('')
expect(() => typedSliceIndex(slice, i)).toThrow(want.replace(/^.*?: /, ''))
expect(() => plainSliceIndex(slice, i)).toThrow(want.replace(/^.*?: /, ''))
}
})
})
60 changes: 60 additions & 0 deletions gs/builtin/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2372,6 +2372,66 @@ export function arrayIndex<
return arrayIndexSlow(collection, index)
}

/**
* typedSliceIndex is arrayIndex for a slice whose element type is held in a
* typed array (see TypedSlice), which the compiler knows from its type. It
* tests that representation first, in one condition that engines optimize
* well even where they do not inline it, and is otherwise arrayIndex.
*/
export function typedSliceIndex<
C extends
| Slice<unknown>
| SliceProxy<unknown>
| unknown[]
| TypedSlice
| null
| undefined,
>(collection: C, index: number): ArrayIndexValue<C> {
if (
ArrayBuffer.isView(collection) &&
index >= 0 &&
index < (collection as TypedSlice).length
) {
return (collection as TypedSlice)[index] as ArrayIndexValue<C>
}
return arrayIndex(collection, index)
}

/**
* plainSliceIndex is arrayIndex for a slice whose element type is not held in
* a typed array, which the compiler knows from its type, and which is then
* usually a plain array, or else a window object. It tests those
* representations first, a plain array in one condition that engines
* optimize well even where they do not inline it, and is otherwise
* arrayIndex.
*/
export function plainSliceIndex<
C extends
| Slice<unknown>
| SliceProxy<unknown>
| unknown[]
| TypedSlice
| null
| undefined,
>(collection: C, index: number): ArrayIndexValue<C> {
if (
Array.isArray(collection) &&
index >= 0 &&
index < collection.length &&
(collection as any).__meta__ === undefined
) {
return collection[index] as ArrayIndexValue<C>
}
if (
collection instanceof GoSlice &&
index >= 0 &&
index < collection.length
) {
return collection.backing[collection.offset + index] as ArrayIndexValue<C>
}
return arrayIndex(collection, index)
}

// arrayIndexSlow is arrayIndex for the representations and indexes that its
// fast path does not handle.
function arrayIndexSlow<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function main(): globalThis.Promise<void> {

let x = $.anonymousStructValue({Name: "third", Offsets: (null as $.Slice<number>), Count: 3}, { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("Name", /* @__PURE__ */ $.basicType("string"), [0], 0, true), /* @__PURE__ */ $.structField("Offsets", /* @__PURE__ */ $.sliceType(/* @__PURE__ */ $.basicType("int")), [1], 16, true), /* @__PURE__ */ $.structField("Count", /* @__PURE__ */ $.basicType("int"), [2], 40, true)] })
x.Offsets = $.appendOne(x.Offsets, 5)
await $.println(x.Name, $.arrayIndex(x.Offsets!, 0), x.Count)
await $.println(x.Name, $.plainSliceIndex(x.Offsets!, 0), x.Count)

let __goscriptTuple0: any = buildNamedItems()
let items: $.Slice<namedItem> = __goscriptTuple0[0]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ export async function main(): globalThis.Promise<void> {
await $.println("param:", a.N)

let s = $.arrayToSlice<{"N": number}>([$.anonymousStructValue({N: 1}, { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("N", /* @__PURE__ */ $.basicType("int"), [0], 0, true)] })])
let e = $.markAsStructValue($.cloneStructValue($.arrayIndex(s!, 0)))
let e = $.markAsStructValue($.cloneStructValue($.plainSliceIndex(s!, 0)))
e.N = 9
await $.println("index:", $.arrayIndex(s!, 0).N)
await $.println("index:", $.plainSliceIndex(s!, 0).N)

let t = $.makeSlice<{"N": number}>(1, undefined, undefined, () => $.anonymousStructValue({"N": 0}, { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("N", /* @__PURE__ */ $.basicType("int"), [0], 0, true)] }))
$.copy(t, s, true)
$.arrayIndex(t!, 0).N = 9
await $.println("copy:", $.arrayIndex(s!, 0).N)
$.plainSliceIndex(t!, 0).N = 9
await $.println("copy:", $.plainSliceIndex(s!, 0).N)

let c = (slices.Clone(s) as $.Slice<{"N": number}>)
$.arrayIndex(c!, 0).N = 9
await $.println("clone:", $.arrayIndex(s!, 0).N)
$.plainSliceIndex(c!, 0).N = 9
await $.println("clone:", $.plainSliceIndex(s!, 0).N)

let x: any = $.interfaceValue($.anonymousStructValue({N: 1}, { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("N", /* @__PURE__ */ $.basicType("int"), [0], 0, true)] }), "struct{N int}", { kind: $.TypeKind.Struct, methods: [], fields: [/* @__PURE__ */ $.structField("N", /* @__PURE__ */ $.basicType("int"), [0], 0, true)] })
{
Expand All @@ -54,7 +54,7 @@ export async function main(): globalThis.Promise<void> {
let r = $.markAsStructValue($.cloneStructValue((__goscriptRangeWindow0.backing[__goscriptRangeWindow0.offset + __rangeIndex] as {"N": number})))
r.N = 9
}
await $.println("range:", $.arrayIndex(s!, 0).N)
await $.println("range:", $.plainSliceIndex(s!, 0).N)
}

var __goscriptStatic0$value: any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ export async function main(): globalThis.Promise<void> {
local.value.T.N = 4
for (let __goscriptRangeTarget0 = registry, __goscriptRangeWindow0 = $.sliceRange(__goscriptRangeTarget0), __goscriptSliceWindow0 = $.sliceRange(all), __goscriptSliceWindow1 = $.sliceRange(allPtrs), i = 0; i < __goscriptRangeWindow0.length; i++) {
let g = (__goscriptRangeWindow0.backing[__goscriptRangeWindow0.offset + i] as Getter | null)
await $.println(i, (__goscriptAwait = (g !== null && (g as any).__isVarRef !== true && ((g as any).__goValue === undefined || (g as any).__goType?.charCodeAt(0) !== 42) ? (g as Exclude<Getter, null>) : $.pointerValue<Exclude<Getter, null>>(g)).Get(), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait), $.pointerValue<T>($.arrayIndex(ptrs!, i)).N, (__goscriptAwait = $.pointerValue<Exclude<Getter, null>>(((__goscriptSliceWindow0.backing as any[])[$.windowSlot(__goscriptSliceWindow0, i)] as Getter | null)).Get(), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait), $.pointerValue<T>(((__goscriptSliceWindow1.backing as any[])[$.windowSlot(__goscriptSliceWindow1, i)] as T | $.VarRef<T> | null)).N)
await $.println(i, (__goscriptAwait = (g !== null && (g as any).__isVarRef !== true && ((g as any).__goValue === undefined || (g as any).__goType?.charCodeAt(0) !== 42) ? (g as Exclude<Getter, null>) : $.pointerValue<Exclude<Getter, null>>(g)).Get(), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait), $.pointerValue<T>($.plainSliceIndex(ptrs!, i)).N, (__goscriptAwait = $.pointerValue<Exclude<Getter, null>>(((__goscriptSliceWindow0.backing as any[])[$.windowSlot(__goscriptSliceWindow0, i)] as Getter | null)).Get(), __goscriptAwait instanceof Promise ? await __goscriptAwait : __goscriptAwait), $.pointerValue<T>(((__goscriptSliceWindow1.backing as any[])[$.windowSlot(__goscriptSliceWindow1, i)] as T | $.VarRef<T> | null)).N)
}
}

Expand Down
28 changes: 14 additions & 14 deletions tests/tests/append_spare_capacity/append_spare_capacity.gs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,13 @@ export async function main(): globalThis.Promise<void> {
ps = $.appendOne(ps, new pos(true, i + 1, i))
}
let all = $.goSlice(ps, undefined, $.cap(ps))
await $.println($.sliceLen(ps), $.cap(ps), $.arrayIndex(all!, 4).Line, $.arrayIndex(all!, 5).Line, $.arrayIndex(all!, 7).Char)
$.arrayIndex(all!, 5).Line = 9
await $.println($.arrayIndex(all!, 5).Line, $.arrayIndex(all!, 6).Line)
await $.println($.sliceLen(ps), $.cap(ps), $.plainSliceIndex(all!, 4).Line, $.plainSliceIndex(all!, 5).Line, $.plainSliceIndex(all!, 7).Char)
$.plainSliceIndex(all!, 5).Line = 9
await $.println($.plainSliceIndex(all!, 5).Line, $.plainSliceIndex(all!, 6).Line)

// Appending after a reslice writes the slot the reslice zeroed.
let more = $.appendOne(ps, new pos(true, 42))
await $.println($.arrayIndex(more!, 5).Line, $.arrayIndex(all!, 5).Line)
await $.println($.plainSliceIndex(more!, 5).Line, $.plainSliceIndex(all!, 5).Line)

// Grown slices of other element types have their zero values too.
let ns: $.Slice<number> = null! as $.Slice<number>
Expand All @@ -61,43 +61,43 @@ export async function main(): globalThis.Promise<void> {
let nall = $.goSlice(ns, undefined, $.cap(ns))
let sall = $.goSlice(ss, undefined, $.cap(ss))
let pall = $.goSlice(ptrs, undefined, $.cap(ptrs))
await $.println($.arrayIndex(nall!, 3), $.stringLen($.arrayIndex(sall!, 3)), $.arrayIndex(pall!, 3) == null, $.pointerValue<pos>($.arrayIndex(ptrs!, 2)).Line)
await $.println($.plainSliceIndex(nall!, 3), $.stringLen($.plainSliceIndex(sall!, 3)), $.plainSliceIndex(pall!, 3) == null, $.pointerValue<pos>($.plainSliceIndex(ptrs!, 2)).Line)

// A grown copy holds copies of the struct elements.
let small = $.makeSlice<pos>(1, 1, undefined, () => new pos(true))
$.arrayIndex(small!, 0).Line = 7
$.plainSliceIndex(small!, 0).Line = 7
let grown = $.appendOne(small, new pos(true, 8))
$.arrayIndex(grown!, 0).Line = 70
await $.println($.arrayIndex(small!, 0).Line, $.arrayIndex(grown!, 0).Line, $.arrayIndex(grown!, 1).Line)
$.plainSliceIndex(grown!, 0).Line = 70
await $.println($.plainSliceIndex(small!, 0).Line, $.plainSliceIndex(grown!, 0).Line, $.plainSliceIndex(grown!, 1).Line)

// Appending a slice grows the same way.
let ext = $.appendSlice($.arrayToSlice<pos>([new pos(true, 1)]), $.arrayToSlice<pos>([new pos(true, 2), new pos(true, 3)]))
let full = $.goSlice(ext, undefined, $.cap(ext))
await $.println($.sliceLen(ext), $.cap(ext) >= 3, $.arrayIndex(full!, $.sliceLen(full) - 1).Line)
await $.println($.sliceLen(ext), $.cap(ext) >= 3, $.plainSliceIndex(full!, $.sliceLen(full) - 1).Line)

// Several values append in place into spare capacity, each a copy.
let p = new pos(true, 5)
let pair: $.Slice<pos> = $.appendValues($.makeSlice<pos>(0, 4, undefined, () => new pos(true)), undefined, (p).clone(), (p).clone())
$.arrayIndex(pair!, 0).Line = 50
$.plainSliceIndex(pair!, 0).Line = 50
p.Line = 500
let shared = $.goSlice(pair, undefined, 3)
pair = $.appendValues(pair, undefined, new pos(true, 6), new pos(true, 7))
await $.println($.sliceLen(pair), $.arrayIndex(pair!, 0).Line, $.arrayIndex(pair!, 1).Line, $.arrayIndex(pair!, 2).Line, $.arrayIndex(shared!, 2).Line, $.arrayIndex(pair!, 3).Line)
await $.println($.sliceLen(pair), $.plainSliceIndex(pair!, 0).Line, $.plainSliceIndex(pair!, 1).Line, $.plainSliceIndex(pair!, 2).Line, $.plainSliceIndex(shared!, 2).Line, $.plainSliceIndex(pair!, 3).Line)

// Typed and zero-hinted elements append several values in place and
// when they grow.
let fs: $.Slice<number> = $.makeSlice<number>(0, 4, "float32")
let fview = $.goSlice(fs, undefined, 4)
fs = $.appendValues(fs, $.typedSliceHints.float32, 0.10000000149011612, 0.5, 2)
await $.println($.sliceLen(fs), $.cap(fs), $.arrayIndex(fview!, 0) == $.arrayIndex(fs!, 0), $.arrayIndex(fview!, 2))
await $.println($.sliceLen(fs), $.cap(fs), $.typedSliceIndex(fview!, 0) == $.typedSliceIndex(fs!, 0), $.typedSliceIndex(fview!, 2))
fs = $.appendValues(fs, $.typedSliceHints.float32, 3, 4, 5)
$.sliceElems(fview!)[$.sliceSlot(fview!, 1)] = 9
await $.println($.sliceLen(fs), $.arrayIndex(fs!, 1), $.arrayIndex(fs!, 5), $.arrayIndex(fs!, 0) == 0.10000000149011612)
await $.println($.sliceLen(fs), $.typedSliceIndex(fs!, 1), $.typedSliceIndex(fs!, 5), $.typedSliceIndex(fs!, 0) == 0.10000000149011612)
let cs: $.Slice<$.Complex> = null! as $.Slice<$.Complex>
cs = $.appendValues(cs, $.appendZeros.complex, $.complex(1, 2), $.complex(3, 0))
let nps: $.Slice<pos | $.VarRef<pos> | null> = null! as $.Slice<pos | $.VarRef<pos> | null>
nps = $.appendValues(nps, $.appendZeros.nil, null, new pos(false, 8))
await $.println($.sliceLen(cs), $.real($.arrayIndex(cs!, 0)), $.imag($.arrayIndex(cs!, 1)), $.sliceLen(nps), $.arrayIndex(nps!, 0) == null, $.pointerValue<pos>($.arrayIndex(nps!, 1)).Line)
await $.println($.sliceLen(cs), $.real($.plainSliceIndex(cs!, 0)), $.imag($.plainSliceIndex(cs!, 1)), $.sliceLen(nps), $.plainSliceIndex(nps!, 0) == null, $.pointerValue<pos>($.plainSliceIndex(nps!, 1)).Line)
}

if ($.isMainScript(import.meta)) {
Expand Down
6 changes: 3 additions & 3 deletions tests/tests/append_value_copy/append_value_copy.gs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,18 @@ export async function main(): globalThis.Promise<void> {
let current = $.varRef(new cell(true, 7))
let cells = $.appendOne<cell>(null, (current.value).clone())
current.value = current.value.__goscriptAssign(new cell(true))
await $.println("cell:", $.arrayIndex(cells!, 0).width, current.value.width)
await $.println("cell:", $.plainSliceIndex(cells!, 0).width, current.value.width)

// Arrays also copy at the argument boundary.
let row = $.arrayValue([3, 4])
let rows = $.appendOne<number[]>(null, $.cloneArrayValue(row, /* @__PURE__ */ $.arrayType(/* @__PURE__ */ $.basicType("int"), 2)))
row[0] = 9
await $.println("array:", ($.arrayIndex(rows!, 0))[0], row[0])
await $.println("array:", ($.plainSliceIndex(rows!, 0))[0], row[0])

// Pointer elements retain their identity.
let pointers = $.appendOne<cell | $.VarRef<cell> | null>(null, current, $.appendZeros.nil)
current.value.width = 11
await $.println("pointer:", $.pointerValue<cell>($.arrayIndex(pointers!, 0)).width)
await $.println("pointer:", $.pointerValue<cell>($.plainSliceIndex(pointers!, 0)).width)
}

if ($.isMainScript(import.meta)) {
Expand Down
Loading
Loading