diff --git a/gs/builtin/runtime-contract.test.ts b/gs/builtin/runtime-contract.test.ts index 4665edbfd..08cb57a3b 100644 --- a/gs/builtin/runtime-contract.test.ts +++ b/gs/builtin/runtime-contract.test.ts @@ -558,6 +558,25 @@ describe('builtin runtime contract helpers', () => { ) }) + it('returns the same field reference across interleaved fieldRef calls', () => { + // The last reference is cached, which must not change which reference + // each target and field gets. + const a = { x: 1, y: 2 } + const b = { x: 3, y: 4 } + const ax = fieldRef(a, 'x') + const bx = fieldRef(b, 'x') + const ay = fieldRef(a, 'y') + expect(fieldRef(a, 'x')).toBe(ax) + expect(fieldRef(a, 'x')).toBe(ax) + expect(fieldRef(b, 'x')).toBe(bx) + expect(fieldRef(a, 'y')).toBe(ay) + expect(ax).not.toBe(bx) + expect(ax).not.toBe(ay) + ax.value = 5 + expect(a.x).toBe(5) + expect(fieldRef(a, 'x').value).toBe(5) + }) + it('matches struct map keys by Go comparable value', () => { class Key { declare public name: string diff --git a/gs/builtin/varRef.ts b/gs/builtin/varRef.ts index 4ee848f4e..17472fe6f 100644 --- a/gs/builtin/varRef.ts +++ b/gs/builtin/varRef.ts @@ -123,6 +123,31 @@ export function varRef(v: T): VarRef { export function fieldRef( target: T, key: K, +): VarRef { + // A loop usually takes the address of the same field again and again, as + // a method returning &s.field does, so the last reference is remembered, + // which avoids the two map lookups. References are never replaced, so it + // is always the one that the maps hold. + if (target === lastFieldTarget && key === lastFieldKey) { + return lastFieldRef as VarRef + } + const ref = fieldRefLookup(target, key) + lastFieldTarget = target + lastFieldKey = key + lastFieldRef = ref + return ref +} + +// lastFieldTarget, lastFieldKey and lastFieldRef are the arguments and result +// of the last fieldRef call. Only this one target is kept alive by them. +let lastFieldTarget: object | undefined +let lastFieldKey: PropertyKey | undefined +let lastFieldRef: VarRef | undefined + +// fieldRefLookup finds or makes the reference of fieldRef in fieldReferences. +function fieldRefLookup( + target: T, + key: K, ): VarRef { let references = fieldReferences.get(target) if (references === undefined) {