WFCodeImpl.evaluate: Skip return-type resolution when the return type is a built-in or Z881<T_builtin>

The return-type branch of WFCodeImpl.evaluate's Promise.all currently does, unconditionally:

await WFBase.from( Z8K2 ) // wrap as WFReference await returnType.realize() // resolver.dereferenceZObjects([ZID]) await returnType.fullyRealize() Z7.Z7K1.Z8K2 = await getFullTypeAsJSON( returnType, OUTPUT )

This produces a full Z4 type definition with a canonicalised Z4K8 output-converter list. The only downstream consumer of Z7.Z7K1.Z8K2 is maybeGetSerializer in exchange-format-2.1.0.js, which reads returnType.Z4K8 to find a language-specific serializer for user- defined types. For built-in return types -- Z6, Z9, Z40, Z11, Z12, Z21, Z22, and the other ZIDs in function-schemata's builtInTypes_ set -- Z4K8 is undefined, findTypeConverterForProgrammingLanguage short-circuits on its typeConverterList === undefined guard, and maybeGetSerializer returns null. The expensive resolve-and-rebuild work produces a value the downstream code then discards.

Add a fast path: if wfFunction.Z8K2 is a Z9 reference whose Z9K1 is a built-in type ZID, set Z7.Z7K1.Z8K2 directly to that bare reference and return without doing the resolver fetch or the realize/fullyRealize/getFullTypeAsJSON pass. Both shapes (full Z4 or bare Z9 ref) lead to the same exchange-format output because the serializer lookup is the same null in either case.

Typed-list return types (Z881) are intentionally left on the slow path: even though Z881 is itself built-in, the member type T may not be, and resolveListMemberType is needed to canonicalise its converter list. The TODO comment in the original block pointed at this generalisation; deferring that to a follow-up keeps this diff narrow.

Mocked benchmark deltas vs. the previous commit (Benchmark.js, node 24, two-run stable readings): Evaluate py3 function: 1181 -> 1276-1293 ops/sec Evaluate py3 function without validate: 1258 -> 1340-1410 ops/sec Evaluate generic as composition: 777 -> 749-769 ops/sec

The compose benchmark is flat-within-noise because composition routes through WFCompositionImpl.evaluate, not WFCodeImpl.evaluate, and so doesn't touch this code path. The mocked harness has an in-memory resolver, so what we're measuring here is the cost of the realize/asZObject/converter-traversal pass alone; in production the resolver fetch this skips will be a cache or network call, so the production win should be at least this much and likely more on cache misses.

For Z881 where T is itself a built-in (Z6, Z40, Z9, ...), all of that machinery produces a Z4 whose downstream-relevant fields -- the identity function-call (Z4K1) and the head key's element type (Z4K2.K1.Z3K1) -- are already determined by Z8K2 itself. Synthesise the Z4 directly instead. The shape matches WFList.getFullType's existing synthesis for typed-list types, which is what the TODO comment in the original code was pointing at when it mentioned deduplicating WFList.getFullType and builtinGenericListType.

Two small helpers are introduced near setProgrammingLanguageLiteral- AndGetZID:

  • findReferenceIdentity handles the canonical/normal-form asymmetry: it returns the ZID from either a 'Z6' string or a {Z1K1:'Z9', Z9K1:'Z6'} object, so the typed-list shape check doesn't have to enumerate both forms at every key access.
  • listMemberBuiltinTypeRef returns the bare member reference (normalised to normal form) when Z8K2 is Z881<T_builtin>, or null otherwise.

Test impact: 821 unit/integration tests pass, including the fixtures that exercise typed-list return types -- Z19243, map-Z444, type-converters-Z494/Z495/Z496, prove_resolution_Z7_491, scope-attachment-Z456, all_Z437. Those used to drive Z881 through the full function-call execution path and now skip to the synthesised Z4; the regression signal is that they still produce byte-identical results.

Benchmark impact: the existing suite had no test exercising a Z881<T_builtin> return type, so add one ('Evaluate py3 function returning Z881') modelled on the existing py3 case but with Z8K2 narrowed from Z1 to a typed list of strings. Validated the fast path's impact by toggling its if condition with false && and re-running:

Fast path off (two runs): 1,084 / 1,136 ops/sec Fast path on (two runs): 1,384 / 1,472 ops/sec

That's a +22% to +36% throughput delta cleanly attributable to this fast path -- well above the ±2% per-run variance Benchmark.js reports and the ±10% run-to-run drift visible on the unrelated benchmarks.

Out of scope: nested typed lists (Z881<Z881>) -- the member-ref check rejects non-Z9 forms, so a list-of-lists falls through to the slow path. The same applies to user-defined member types, which is the correct behaviour because user-defined types may have a Z4K8 converter that the synthesised wrapper would miss.

Bug: T426412
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Edited by Jforrester

Merge request reports

Loading