@hydranium/core 1.0.0-next.316 → 1.0.0-next.317

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
  import type { AstNode, AstReflection } from '@hydranium/langium';
10
+ import type { ConcreteTypeName, SingleTypeName } from '@hydranium/protocol';
10
11
  /**
11
12
  * Shape of the per-type constants Langium emits alongside each
12
13
  * grammar-generated interface — a `const` of the same name whose `$type` field
@@ -50,13 +51,17 @@ export type AstNodeInit<T extends AstNode> = Omit<T, keyof AstNode | ArrayKeys<T
50
51
  * the AST interface from a constant's `$type` literal alone, so call sites do
51
52
  * not have to write the type name twice.
52
53
  *
54
+ * The constant must name one type its own `$type` lists: a union's or an
55
+ * abstract type's constant, or one typed by a union of type names, would let
56
+ * an incomplete node through.
57
+ *
53
58
  * @param typeConstant the per-type constant Langium emits (drives `$type`).
54
59
  * @param init typed fields — TS enforces mandatory non-array fields here.
55
60
  * @param extras explicitly-unchecked escape hatch for properties not declared
56
61
  * on the AST interface. Use sparingly; lives in a separate
57
62
  * argument so typos can't silently pass into the typed slot.
58
63
  */
59
- export type AstNodeBuilder<TMap> = <TType extends string & keyof TMap>(typeConstant: AstTypeConstant<TType>, init: AstNodeInit<Extract<TMap[TType], AstNode>>, extras?: Record<string, unknown>) => Extract<TMap[TType], AstNode>;
64
+ export type AstNodeBuilder<TMap> = <TType extends ConcreteTypeName<TMap>>(typeConstant: AstTypeConstant<TType> & SingleTypeName<TType>, init: AstNodeInit<Extract<TMap[TType], AstNode>>, extras?: Record<string, unknown>) => Extract<TMap[TType], AstNode>;
60
65
  /**
61
66
  * Build an {@link AstNodeBuilder} pre-bound to a language's `AstReflection`
62
67
  * and its grammar `AstType` registry.
@@ -101,12 +106,12 @@ export declare function makeAstNodeBuilder<TMap>(reflection: AstReflection): Ast
101
106
  * permissive lookup into a throw at a call site that can only guess.
102
107
  *
103
108
  * **The `type` argument is authoritative: `init` cannot rebind `$type`.** The
104
- * typed builder gets this for free — `AstNodeInit` omits `$type`, so a call site
105
- * cannot reach it — but here `init` is loosely typed, and the case that needs
106
- * the guarantee is RETYPING: a caller spreading an existing node to produce one
107
- * of a different type (`{ ...source }`) carries the SOURCE's `$type` in, which
108
- * would silently win over the type actually requested and hand back a node of
109
- * the wrong type with no error anywhere.
109
+ * typed builder holds the same rule, since its init type keeps out only a
110
+ * `$type` written in the call. The case that needs the guarantee is RETYPING:
111
+ * a caller spreading an existing node to produce one of a different type
112
+ * (`{ ...source }`) carries the SOURCE's `$type` in, which would silently win
113
+ * over the type actually requested and hand back a node of the wrong type with
114
+ * no error anywhere.
110
115
  */
111
116
  export declare function buildAstNode<TAst extends AstNode = AstNode>(reflection: AstReflection, type: string, init?: Partial<AstNode> & Record<string, unknown>): TAst;
112
117
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"ast-node-builder.d.ts","sourceRoot":"","sources":["../../../src/langium/ast-extension/ast-node-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEjE;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IAC3D,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACxB;AAED;;;;;GAKG;AACH,KAAK,SAAS,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK;CAAE,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnG;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,OAAO,IAAI,IAAI,CAAC,CAAC,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,oBAAoB,GAAG,iBAAiB,CAAC,CAAC,GAAG;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEL;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,CAAC,IAAI,IAAI,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM,IAAI,EAClE,YAAY,EAAE,eAAe,CAAC,KAAK,CAAC,EACpC,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,EAChD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC9B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,CAexF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,IAAI,SAAS,OAAO,GAAG,OAAO,EACxD,UAAU,EAAE,aAAa,EACzB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjD,IAAI,CAON"}
1
+ {"version":3,"file":"ast-node-builder.d.ts","sourceRoot":"","sources":["../../../src/langium/ast-extension/ast-node-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE5E;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe,CAAC,KAAK,SAAS,MAAM,GAAG,MAAM;IAC3D,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;CACxB;AAED;;;;;GAKG;AACH,KAAK,SAAS,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,aAAa,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK;CAAE,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnG;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,OAAO,IAAI,IAAI,CAAC,CAAC,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC,GAC/E,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAC9B,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,GAAG,oBAAoB,GAAG,iBAAiB,CAAC,CAAC,GAAG;IAC/E,UAAU,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEL;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,cAAc,CAAC,IAAI,IAAI,CAAC,KAAK,SAAS,gBAAgB,CAAC,IAAI,CAAC,EACrE,YAAY,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,cAAc,CAAC,KAAK,CAAC,EAC5D,IAAI,EAAE,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,EAChD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC9B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,aAAa,GAAG,cAAc,CAAC,IAAI,CAAC,CAkBxF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,YAAY,CAAC,IAAI,SAAS,OAAO,GAAG,OAAO,EACxD,UAAU,EAAE,aAAa,EACzB,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjD,IAAI,CAON"}
@@ -35,12 +35,15 @@
35
35
  export function makeAstNodeBuilder(reflection) {
36
36
  return function astNode(typeConstant, init, extras) {
37
37
  const result = withTypeDefaults(reflection, typeConstant.$type);
38
- Object.assign(result, init);
38
+ assignOverDefaults(result, init);
39
39
  if (extras) {
40
40
  // Extras win over init: this is the explicit escape hatch, so when a
41
41
  // call site uses it, treat that as deliberate override intent.
42
- Object.assign(result, extras);
42
+ assignOverDefaults(result, extras);
43
43
  }
44
+ // Last, so neither can rebind it: `AstNodeInit` keeps out only a `$type`
45
+ // written in the call, and a spread of another node carries its own.
46
+ result.$type = typeConstant.$type;
44
47
  return result;
45
48
  };
46
49
  }
@@ -61,17 +64,17 @@ export function makeAstNodeBuilder(reflection) {
61
64
  * permissive lookup into a throw at a call site that can only guess.
62
65
  *
63
66
  * **The `type` argument is authoritative: `init` cannot rebind `$type`.** The
64
- * typed builder gets this for free — `AstNodeInit` omits `$type`, so a call site
65
- * cannot reach it — but here `init` is loosely typed, and the case that needs
66
- * the guarantee is RETYPING: a caller spreading an existing node to produce one
67
- * of a different type (`{ ...source }`) carries the SOURCE's `$type` in, which
68
- * would silently win over the type actually requested and hand back a node of
69
- * the wrong type with no error anywhere.
67
+ * typed builder holds the same rule, since its init type keeps out only a
68
+ * `$type` written in the call. The case that needs the guarantee is RETYPING:
69
+ * a caller spreading an existing node to produce one of a different type
70
+ * (`{ ...source }`) carries the SOURCE's `$type` in, which would silently win
71
+ * over the type actually requested and hand back a node of the wrong type with
72
+ * no error anywhere.
70
73
  */
71
74
  export function buildAstNode(reflection, type, init) {
72
75
  const result = withTypeDefaults(reflection, type);
73
76
  if (init) {
74
- Object.assign(result, init);
77
+ assignOverDefaults(result, init);
75
78
  result.$type = type;
76
79
  }
77
80
  return result;
@@ -96,4 +99,17 @@ function withTypeDefaults(reflection, type) {
96
99
  }
97
100
  return result;
98
101
  }
102
+ /**
103
+ * `Object.assign`, except that an `undefined` does not replace a value already
104
+ * there: an optional field the caller names as `undefined` keeps its grammar
105
+ * default, where a plain assign would leave a node typed complete without it.
106
+ * Any other `undefined` is written, so the key stays present.
107
+ */
108
+ function assignOverDefaults(target, source) {
109
+ for (const [key, value] of Object.entries(source)) {
110
+ if (value !== undefined || !Object.hasOwn(target, key)) {
111
+ target[key] = value;
112
+ }
113
+ }
114
+ }
99
115
  //# sourceMappingURL=ast-node-builder.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ast-node-builder.js","sourceRoot":"","sources":["../../../src/langium/ast-extension/ast-node-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AA8DlF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,kBAAkB,CAAO,UAAyB;IAC/D,OAAO,SAAS,OAAO,CACpB,YAAoC,EACpC,IAAgD,EAChD,MAAgC;QAEhC,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAChE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5B,IAAI,MAAM,EAAE,CAAC;YACV,qEAAqE;YACrE,+DAA+D;YAC/D,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,MAAkD,CAAC;IAC7D,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CACzB,UAAyB,EACzB,IAAY,EACZ,IAAiD;IAEjD,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAClD,IAAI,IAAI,EAAE,CAAC;QACR,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAC5B,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;IACvB,CAAC;IACD,OAAO,MAAyB,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,UAAyB,EAAE,IAAY;IAC9D,MAAM,MAAM,GAA4B,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACxD,MAAM,IAAI,GAAG,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClE,MAAM,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC3C,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAC9B,SAAS;YACZ,CAAC;YACD,MAAM,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;QACrF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"ast-node-builder.js","sourceRoot":"","sources":["../../../src/langium/ast-extension/ast-node-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAmElF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,kBAAkB,CAAO,UAAyB;IAC/D,OAAO,SAAS,OAAO,CACpB,YAA4D,EAC5D,IAAgD,EAChD,MAAgC;QAEhC,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,EAAE,YAAY,CAAC,KAAK,CAAC,CAAC;QAChE,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjC,IAAI,MAAM,EAAE,CAAC;YACV,qEAAqE;YACrE,+DAA+D;YAC/D,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACtC,CAAC;QACD,yEAAyE;QACzE,qEAAqE;QACrE,MAAM,CAAC,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC;QAClC,OAAO,MAAkD,CAAC;IAC7D,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,YAAY,CACzB,UAAyB,EACzB,IAAY,EACZ,IAAiD;IAEjD,MAAM,MAAM,GAAG,gBAAgB,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;IAClD,IAAI,IAAI,EAAE,CAAC;QACR,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACjC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC;IACvB,CAAC;IACD,OAAO,MAAyB,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,SAAS,gBAAgB,CAAC,UAAyB,EAAE,IAAY;IAC9D,MAAM,MAAM,GAA4B,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACxD,MAAM,IAAI,GAAG,UAAU,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IAC9C,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;QACpB,KAAK,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClE,MAAM,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC;YAC3C,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAC9B,SAAS;YACZ,CAAC;YACD,MAAM,CAAC,QAAQ,CAAC,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC;QACrF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,SAAS,kBAAkB,CAAC,MAA+B,EAAE,MAAc;IACxE,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QACjD,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,CAAC;YACtD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACvB,CAAC;IACJ,CAAC;AACJ,CAAC"}
@@ -125,8 +125,8 @@ export interface TransferEncoder<TDiagnostic extends TransferDiagnostic = Transf
125
125
  * resolving each AST type to its declared wire shape. Default is
126
126
  * `Record<string, TransferElement>`, which collapses every encoded node
127
127
  * to the structural base — adequate for adopters without a generated
128
- * overlay; richer adopters declare typed maps for autocomplete on the
129
- * encoder's return types.
128
+ * overlay; an adopter passes the generated `<LanguageId>TransferTypes` to
129
+ * type the encoder's returns by the grammar.
130
130
  * - `TDiagnostic` — wire-diagnostic shape produced by
131
131
  * {@link toTransferDiagnostic}. Constrained to extend
132
132
  * {@link TransferDiagnostic} so the framework default (which constructs a
@@ -26,8 +26,8 @@ import { Diagnostic, DiagnosticSeverity } from 'vscode-languageserver-protocol';
26
26
  * resolving each AST type to its declared wire shape. Default is
27
27
  * `Record<string, TransferElement>`, which collapses every encoded node
28
28
  * to the structural base — adequate for adopters without a generated
29
- * overlay; richer adopters declare typed maps for autocomplete on the
30
- * encoder's return types.
29
+ * overlay; an adopter passes the generated `<LanguageId>TransferTypes` to
30
+ * type the encoder's returns by the grammar.
31
31
  * - `TDiagnostic` — wire-diagnostic shape produced by
32
32
  * {@link toTransferDiagnostic}. Constrained to extend
33
33
  * {@link TransferDiagnostic} so the framework default (which constructs a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/core",
3
- "version": "1.0.0-next.316",
3
+ "version": "1.0.0-next.317",
4
4
  "description": "Foundational runtime of the hydranium framework: AST coordination layer (services, integrity, AST extension, serialization, multi-client documents) plus the LSP textual head at the /lsp subpath. Consumed by protocol-head packages (@hydranium/data-server, @hydranium/glsp-server).",
5
5
  "keywords": [
6
6
  "ast",
@@ -87,8 +87,8 @@
87
87
  "diff": "^5.2.0"
88
88
  },
89
89
  "devDependencies": {
90
- "@hydranium/langium": "1.0.0-next.316",
91
- "@hydranium/protocol": "1.0.0-next.316",
90
+ "@hydranium/langium": "1.0.0-next.317",
91
+ "@hydranium/protocol": "1.0.0-next.317",
92
92
  "@playwright/test": "^1.55.0",
93
93
  "@types/diff": "^5.2.0",
94
94
  "fake-indexeddb": "^6.2.5",
@@ -101,8 +101,8 @@
101
101
  "vscode-languageserver-types": "^3.17.5"
102
102
  },
103
103
  "peerDependencies": {
104
- "@hydranium/langium": "1.0.0-next.316",
105
- "@hydranium/protocol": "1.0.0-next.316",
104
+ "@hydranium/langium": "1.0.0-next.317",
105
+ "@hydranium/protocol": "1.0.0-next.317",
106
106
  "@playwright/test": "^1.40.0",
107
107
  "vscode-jsonrpc": "^9.0.0",
108
108
  "vscode-languageserver": "~10.0.1",
@@ -8,6 +8,7 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import type { AstNode, AstReflection } from '@hydranium/langium';
11
+ import type { ConcreteTypeName, SingleTypeName } from '@hydranium/protocol';
11
12
 
12
13
  /**
13
14
  * Shape of the per-type constants Langium emits alongside each
@@ -55,14 +56,18 @@ export type AstNodeInit<T extends AstNode> = Omit<T, keyof AstNode | ArrayKeys<T
55
56
  * the AST interface from a constant's `$type` literal alone, so call sites do
56
57
  * not have to write the type name twice.
57
58
  *
59
+ * The constant must name one type its own `$type` lists: a union's or an
60
+ * abstract type's constant, or one typed by a union of type names, would let
61
+ * an incomplete node through.
62
+ *
58
63
  * @param typeConstant the per-type constant Langium emits (drives `$type`).
59
64
  * @param init typed fields — TS enforces mandatory non-array fields here.
60
65
  * @param extras explicitly-unchecked escape hatch for properties not declared
61
66
  * on the AST interface. Use sparingly; lives in a separate
62
67
  * argument so typos can't silently pass into the typed slot.
63
68
  */
64
- export type AstNodeBuilder<TMap> = <TType extends string & keyof TMap>(
65
- typeConstant: AstTypeConstant<TType>,
69
+ export type AstNodeBuilder<TMap> = <TType extends ConcreteTypeName<TMap>>(
70
+ typeConstant: AstTypeConstant<TType> & SingleTypeName<TType>,
66
71
  init: AstNodeInit<Extract<TMap[TType], AstNode>>,
67
72
  extras?: Record<string, unknown>
68
73
  ) => Extract<TMap[TType], AstNode>;
@@ -94,18 +99,21 @@ export type AstNodeBuilder<TMap> = <TType extends string & keyof TMap>(
94
99
  * rather than a way to drop fields from the typed call site.
95
100
  */
96
101
  export function makeAstNodeBuilder<TMap>(reflection: AstReflection): AstNodeBuilder<TMap> {
97
- return function astNode<TType extends string & keyof TMap>(
98
- typeConstant: AstTypeConstant<TType>,
102
+ return function astNode<TType extends ConcreteTypeName<TMap>>(
103
+ typeConstant: AstTypeConstant<TType> & SingleTypeName<TType>,
99
104
  init: AstNodeInit<Extract<TMap[TType], AstNode>>,
100
105
  extras?: Record<string, unknown>
101
106
  ): Extract<TMap[TType], AstNode> {
102
107
  const result = withTypeDefaults(reflection, typeConstant.$type);
103
- Object.assign(result, init);
108
+ assignOverDefaults(result, init);
104
109
  if (extras) {
105
110
  // Extras win over init: this is the explicit escape hatch, so when a
106
111
  // call site uses it, treat that as deliberate override intent.
107
- Object.assign(result, extras);
112
+ assignOverDefaults(result, extras);
108
113
  }
114
+ // Last, so neither can rebind it: `AstNodeInit` keeps out only a `$type`
115
+ // written in the call, and a spread of another node carries its own.
116
+ result.$type = typeConstant.$type;
109
117
  return result as unknown as Extract<TMap[TType], AstNode>;
110
118
  };
111
119
  }
@@ -127,12 +135,12 @@ export function makeAstNodeBuilder<TMap>(reflection: AstReflection): AstNodeBuil
127
135
  * permissive lookup into a throw at a call site that can only guess.
128
136
  *
129
137
  * **The `type` argument is authoritative: `init` cannot rebind `$type`.** The
130
- * typed builder gets this for free — `AstNodeInit` omits `$type`, so a call site
131
- * cannot reach it — but here `init` is loosely typed, and the case that needs
132
- * the guarantee is RETYPING: a caller spreading an existing node to produce one
133
- * of a different type (`{ ...source }`) carries the SOURCE's `$type` in, which
134
- * would silently win over the type actually requested and hand back a node of
135
- * the wrong type with no error anywhere.
138
+ * typed builder holds the same rule, since its init type keeps out only a
139
+ * `$type` written in the call. The case that needs the guarantee is RETYPING:
140
+ * a caller spreading an existing node to produce one of a different type
141
+ * (`{ ...source }`) carries the SOURCE's `$type` in, which would silently win
142
+ * over the type actually requested and hand back a node of the wrong type with
143
+ * no error anywhere.
136
144
  */
137
145
  export function buildAstNode<TAst extends AstNode = AstNode>(
138
146
  reflection: AstReflection,
@@ -141,7 +149,7 @@ export function buildAstNode<TAst extends AstNode = AstNode>(
141
149
  ): TAst {
142
150
  const result = withTypeDefaults(reflection, type);
143
151
  if (init) {
144
- Object.assign(result, init);
152
+ assignOverDefaults(result, init);
145
153
  result.$type = type;
146
154
  }
147
155
  return result as unknown as TAst;
@@ -167,3 +175,17 @@ function withTypeDefaults(reflection: AstReflection, type: string): Record<strin
167
175
  }
168
176
  return result;
169
177
  }
178
+
179
+ /**
180
+ * `Object.assign`, except that an `undefined` does not replace a value already
181
+ * there: an optional field the caller names as `undefined` keeps its grammar
182
+ * default, where a plain assign would leave a node typed complete without it.
183
+ * Any other `undefined` is written, so the key stays present.
184
+ */
185
+ function assignOverDefaults(target: Record<string, unknown>, source: object): void {
186
+ for (const [key, value] of Object.entries(source)) {
187
+ if (value !== undefined || !Object.hasOwn(target, key)) {
188
+ target[key] = value;
189
+ }
190
+ }
191
+ }
@@ -142,8 +142,8 @@ export interface TransferEncoder<TDiagnostic extends TransferDiagnostic = Transf
142
142
  * resolving each AST type to its declared wire shape. Default is
143
143
  * `Record<string, TransferElement>`, which collapses every encoded node
144
144
  * to the structural base — adequate for adopters without a generated
145
- * overlay; richer adopters declare typed maps for autocomplete on the
146
- * encoder's return types.
145
+ * overlay; an adopter passes the generated `<LanguageId>TransferTypes` to
146
+ * type the encoder's returns by the grammar.
147
147
  * - `TDiagnostic` — wire-diagnostic shape produced by
148
148
  * {@link toTransferDiagnostic}. Constrained to extend
149
149
  * {@link TransferDiagnostic} so the framework default (which constructs a