Skip to content

Commit 4141a3d

Browse files
authored
Optimize LSP discriminated union decoding (#63934)
1 parent 4000050 commit 4141a3d

5 files changed

Lines changed: 416 additions & 85 deletions

File tree

tsc/internal/lsp/lsproto/_generate/generate.mts

Lines changed: 87 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2085,6 +2085,7 @@ function findDiscriminatorField(entries: { fieldName: string; typeName: string;
20852085
if (!fieldCandidates.has(prop.name)) {
20862086
fieldCandidates.set(prop.name, new Map());
20872087
}
2088+
20882089
const mapping = fieldCandidates.get(prop.name)!;
20892090
if (!mapping.has(prop.type.value)) {
20902091
mapping.set(prop.type.value, entry);
@@ -2120,6 +2121,10 @@ function findDiscriminatorField(entries: { fieldName: string; typeName: string;
21202121
return { fieldName: bestField, mapping: bestMapping, unmapped };
21212122
}
21222123

2124+
function hasCustomStructureCodec(name: string): boolean {
2125+
return name === "Registration";
2126+
}
2127+
21232128
/**
21242129
* For a group of union entries that share the same JSON kind, find fields whose
21252130
* presence/absence in the JSON uniquely identifies a variant. A "presence discriminator"
@@ -2207,6 +2212,51 @@ function generateCode() {
22072212
return exhaustive;
22082213
}
22092214

2215+
/**
2216+
* Generate streaming discriminator dispatch for unions with at most one
2217+
* unmapped fallback arm. Fields after the discriminator decode directly
2218+
* from the active decoder; fields before it are replayed by the helper.
2219+
*/
2220+
function generateStreamingDiscriminatorDispatch(
2221+
name: string,
2222+
disc: NonNullable<ReturnType<typeof findDiscriminatorField>>,
2223+
indent: string,
2224+
) {
2225+
writeLine(`${indent}state, err := scanDiscriminatedStruct(dec, "${name}", ${JSON.stringify(disc.fieldName)})`);
2226+
writeLine(`${indent}if err != nil {`);
2227+
writeLine(`${indent}\treturn err`);
2228+
writeLine(`${indent}}`);
2229+
writeLine(`${indent}switch string(state.discriminatorValue) {`);
2230+
for (const [value, entry] of disc.mapping) {
2231+
writeLine(`${indent}case \`"${value}"\`:`);
2232+
writeLine(`${indent}\treturn unmarshalDiscriminatedArm(state, &o.${entry.fieldName})`);
2233+
}
2234+
writeLine(`${indent}default:`);
2235+
if (disc.unmapped.length === 1) {
2236+
writeLine(`${indent}\treturn unmarshalDiscriminatedArm(state, &o.${disc.unmapped[0].fieldName})`);
2237+
}
2238+
else {
2239+
writeLine(`${indent}\treturn state.invalidDiscriminator()`);
2240+
}
2241+
writeLine(`${indent}}`);
2242+
}
2243+
2244+
function canStreamDiscriminator(
2245+
disc: NonNullable<ReturnType<typeof findDiscriminatorField>>,
2246+
): boolean {
2247+
if (disc.unmapped.length > 1) {
2248+
return false;
2249+
}
2250+
const entries = [...disc.mapping.values(), ...disc.unmapped];
2251+
return entries.every(entry => {
2252+
if (entry.originalType.kind !== "reference") {
2253+
return false;
2254+
}
2255+
const name = entry.originalType.name;
2256+
return !hasCustomStructureCodec(name) && model.structures.some(structure => structure.name === name);
2257+
});
2258+
}
2259+
22102260
/**
22112261
* Generate try-each fallback code for unmapped entries, chaining into
22122262
* presence dispatch if possible before falling back to raw try-each.
@@ -2547,8 +2597,8 @@ function generateCode() {
25472597
writeLine("");
25482598
}
25492599

2550-
// Generate UnmarshalJSONFrom method for structure validation
2551-
// Skip Registration (has custom marshal/unmarshal generated separately)
2600+
// Generate UnmarshalJSONFrom method for structure validation.
2601+
// Structures with custom codecs are generated separately.
25522602
// Skip properties marked with omitzeroValue since they're optional by nature
25532603
const requiredProps = structure.properties?.filter(p => {
25542604
if (p.optional) return false;
@@ -2562,7 +2612,7 @@ function generateCode() {
25622612
const resolved = resolveType(p.type);
25632613
return p.optional || resolved.needsPointer || resolved.name.startsWith("[]") || resolved.name.startsWith("map[");
25642614
}) || false;
2565-
if ((requiredProps.length > 0 || hasNullRejectableFields) && structure.name !== "Registration") {
2615+
if ((requiredProps.length > 0 || hasNullRejectableFields) && !hasCustomStructureCodec(structure.name)) {
25662616
writeLine(`\tvar _ json.UnmarshalerFrom = (*${structure.name})(nil)`);
25672617
writeLine("");
25682618
writeLine(`func (s *${structure.name}) UnmarshalJSONFrom(dec *json.Decoder) error {`);
@@ -3392,17 +3442,24 @@ function generateCode() {
33923442
}
33933443
}
33943444
else {
3395-
// Ambiguous: buffer and dispatch
3396-
writeLine(`\t\tdata, err := dec.ReadValue()`);
3397-
writeLine(`\t\tif err != nil {`);
3398-
writeLine(`\t\t\treturn err`);
3399-
writeLine(`\t\t}`);
34003445
let exhaustive = false;
34013446
const disc = findDiscriminatorField(entries);
3402-
if (disc) {
3403-
exhaustive = generateDiscriminatorDispatch(disc, "\t\t");
3447+
if (disc && canStreamDiscriminator(disc)) {
3448+
generateStreamingDiscriminatorDispatch(name, disc, "\t\t");
3449+
exhaustive = true;
34043450
}
34053451
else {
3452+
// Ambiguous non-discriminated objects need the complete
3453+
// value for presence checks or speculative decoding.
3454+
writeLine(`\t\tdata, err := dec.ReadValue()`);
3455+
writeLine(`\t\tif err != nil {`);
3456+
writeLine(`\t\t\treturn err`);
3457+
writeLine(`\t\t}`);
3458+
}
3459+
if (disc && !canStreamDiscriminator(disc)) {
3460+
exhaustive = generateDiscriminatorDispatch(disc, "\t\t");
3461+
}
3462+
else if (!disc) {
34063463
const pres = findPresenceDiscriminator(entries);
34073464
if (pres) {
34083465
exhaustive = generatePresenceDispatch(pres, "\t\t");
@@ -3428,25 +3485,30 @@ function generateCode() {
34283485
writeLine(`\t}`);
34293486
}
34303487
else {
3431-
// Fallback: unknown kinds present (e.g. `any`), use ReadValue + try-each.
3432-
writeLine("\tdata, err := dec.ReadValue()");
3433-
writeLine("\tif err != nil {");
3434-
writeLine("\t\treturn err");
3435-
writeLine("\t}");
3436-
3437-
if (unionContainedNull) {
3438-
writeLine(`\tif string(data) == "null" {`);
3439-
writeLine(`\t\treturn nil`);
3440-
writeLine(`\t}`);
3441-
writeLine("");
3442-
}
3443-
3488+
// Fallback for unknown kinds (e.g. `any`). Discriminated object
3489+
// unions can still stream; other unions use ReadValue + try-each.
34443490
let exhaustive = false;
34453491
const disc = findDiscriminatorField(fieldEntries);
3446-
if (disc) {
3447-
exhaustive = generateDiscriminatorDispatch(disc, "\t");
3492+
if (disc && canStreamDiscriminator(disc)) {
3493+
generateStreamingDiscriminatorDispatch(name, disc, "\t");
3494+
exhaustive = true;
34483495
}
34493496
else {
3497+
writeLine("\tdata, err := dec.ReadValue()");
3498+
writeLine("\tif err != nil {");
3499+
writeLine("\t\treturn err");
3500+
writeLine("\t}");
3501+
if (unionContainedNull) {
3502+
writeLine(`\tif string(data) == "null" {`);
3503+
writeLine(`\t\treturn nil`);
3504+
writeLine(`\t}`);
3505+
writeLine("");
3506+
}
3507+
}
3508+
if (disc && !canStreamDiscriminator(disc)) {
3509+
exhaustive = generateDiscriminatorDispatch(disc, "\t");
3510+
}
3511+
else if (!disc) {
34503512
const pres = findPresenceDiscriminator(fieldEntries);
34513513
if (pres) {
34523514
exhaustive = generatePresenceDispatch(pres, "\t");

tsc/internal/lsp/lsproto/lsp.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ func jsonObjectRawField(data []byte, field string) json.Value {
142142
return nil
143143
}
144144
if jsonKeyCheck(name, field) {
145-
val, err := dec.ReadValue()
145+
value, err := dec.ReadValue()
146146
if err != nil {
147147
return nil
148148
}
149-
return val
149+
return value
150150
}
151151
if err := dec.SkipValue(); err != nil {
152152
return nil

tsc/internal/lsp/lsproto/lsp_generated.go

Lines changed: 45 additions & 58 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)