66 NodeFunction ,
77 SubFlowValue ,
88 NodeParameter ,
9- ReferenceValue , Maybe
9+ ReferenceValue , Maybe ,
10+ InlineReferenceValue
1011} from "@code0-tech/sagittarius-graphql-types" ;
1112import ts from "typescript" ;
1213import { createSystem , createVirtualTypeScriptEnvironment , VirtualTypeScriptEnvironment } from "@typescript/vfs"
@@ -160,6 +161,75 @@ export function getSharedTypeDeclarations(dataTypes?: DataType[], genericType: s
160161 */
161162export const sanitizeId = ( id : string ) => id ?. replace ( / [ ^ a - z A - Z 0 - 9 ] / g, '_' ) ;
162163
164+ /**
165+ * Escapes text for safe inclusion inside a template literal, neutralising
166+ * backslashes, backticks and any literal `${` that is not a resolved reference.
167+ */
168+ const escapeTemplateText = ( text : string ) : string =>
169+ text . replace ( / \\ / g, "\\\\" ) . replace ( / ` / g, "\\`" ) . replace ( / \$ \{ / g, "\\${" ) ;
170+
171+ /**
172+ * Emits a string leaf of a literal value. If the string contains `${signature}`
173+ * tokens that match known inline references, it is emitted as a template literal
174+ * with those tokens spliced as the referenced expressions; unknown tokens are
175+ * preserved literally. Strings without any known reference are emitted as plain
176+ * JSON string literals, identical to the prior behaviour.
177+ */
178+ const emitStringLeaf = ( value : string , refExpr : Map < string , string > ) : string => {
179+ // Whole-string standalone reference: the leaf is exactly `${signature}` with
180+ // no surrounding text. Emit the raw referenced expression so its actual type
181+ // is preserved (a NUMBER reference stays a NUMBER rather than being coerced
182+ // to string). This matters for references sitting directly on an array
183+ // element or object value, e.g. `[1, "${x}", 3]` or `{count: "${x}"}`.
184+ const standalone = value . match ( / ^ \$ \{ ( [ ^ } ] + ) \} $ / ) ;
185+ if ( standalone && refExpr . has ( standalone [ 1 ] ) ) {
186+ return refExpr . get ( standalone [ 1 ] ) ! ;
187+ }
188+
189+ const tokenRe = / \$ \{ ( [ ^ } ] + ) \} / g;
190+
191+ let hasKnown = false ;
192+ for ( const match of value . matchAll ( tokenRe ) ) {
193+ if ( refExpr . has ( match [ 1 ] ) ) {
194+ hasKnown = true ;
195+ break ;
196+ }
197+ }
198+ if ( ! hasKnown ) return stringify ( value ) ?? '""' ;
199+
200+ let result = "" ;
201+ let last = 0 ;
202+ for ( const match of value . matchAll ( tokenRe ) ) {
203+ const [ full , signature ] = match ;
204+ const start = match . index ! ;
205+ result += escapeTemplateText ( value . slice ( last , start ) ) ;
206+ result += refExpr . has ( signature )
207+ ? "${" + refExpr . get ( signature ) + "}"
208+ : escapeTemplateText ( full ) ;
209+ last = start + full . length ;
210+ }
211+ result += escapeTemplateText ( value . slice ( last ) ) ;
212+ return "`" + result + "`" ;
213+ } ;
214+
215+ /**
216+ * Recursively emits a literal JSON value as a TypeScript expression, splicing
217+ * inline references into string leaves. Non-string primitives keep their
218+ * lossless-JSON representation so number precision is preserved.
219+ */
220+ const emitLiteralNode = ( value : unknown , refExpr : Map < string , string > ) : string => {
221+ if ( typeof value === "string" ) return emitStringLeaf ( value , refExpr ) ;
222+ if ( Array . isArray ( value ) ) {
223+ return `[${ value . map ( item => emitLiteralNode ( item , refExpr ) ) . join ( ", " ) } ]` ;
224+ }
225+ if ( value !== null && typeof value === "object" ) {
226+ const entries = Object . entries ( value as Record < string , unknown > )
227+ . map ( ( [ key , val ] ) => `${ stringify ( key ) } : ${ emitLiteralNode ( val , refExpr ) } ` ) ;
228+ return `{${ entries . join ( ", " ) } }` ;
229+ }
230+ return stringify ( value ) ?? "undefined" ;
231+ } ;
232+
163233/**
164234 * Generates TypeScript source code for a flow, suitable for validation and type inference.
165235 */
@@ -174,6 +244,87 @@ export function generateFlowSourceCode(
174244 const funcMap = new Map ( functions ?. map ( f => [ f . identifier , f ] ) ) ;
175245 const visited = new Set < NodeFunction [ 'id' ] > ( ) ;
176246
247+ // Emits the TypeScript expression for a reference value (without the leading
248+ // `@pos` marker). A reference may be nullable (`string | null`) or reach
249+ // through optional properties (an optional chain like `node_X?.text`). When
250+ // `assertNonNullReferences` is set (inference/schema), the nullish part is
251+ // waived with a `!` so the base type flows cleanly. During validation it is
252+ // left in place so a possibly-null reference feeding a non-null parameter
253+ // surfaces a diagnostic — which is then downgraded to a warning rather than
254+ // being silently erased. Base type mismatches still fail validation in both
255+ // modes. Reused both for top-level parameters and for inline references
256+ // spliced into literal values.
257+ const renderReference = ( ref : ReferenceValue ) : string => {
258+ let refCode = typeof ref . inputIndex === "number"
259+ ? `p_${ sanitizeId ( ref . nodeFunctionId ?? "undefined" ) } _${ ref . parameterIndex } [${ ref . inputIndex } ]`
260+ : ref . nodeFunctionId ? `node_${ sanitizeId ( ref . nodeFunctionId ) } ` : `flow_${ sanitizeId ( flow ?. id ?? "undefined" ) } ` ;
261+ ref . referencePath ?. forEach ( pathObj => {
262+ refCode += `?.${ pathObj . path } ` ;
263+ } ) ;
264+ const nonNull = assertNonNullReferences ? "!" : "" ;
265+ return `(${ refCode } )${ nonNull } ` ;
266+ } ;
267+
268+ // Emits the TypeScript expression for a sub-flow value (without the leading
269+ // `@pos` marker). A sub-flow that directly maps an existing function emits
270+ // that function reference so its signature drives the sub-flow's I/O;
271+ // otherwise a lambda wrapping the sub-tree is generated. The lambda parameter
272+ // name deliberately stays `p_${id}_${index}` (the parent node/param slot):
273+ // references to the sub-flow's own inputs resolve through that exact name
274+ // (see `renderReference`). Multiple sub-flows spliced into the same literal
275+ // (e.g. an array) are sibling expressions in separate function scopes, so an
276+ // identical parameter name across them does not collide.
277+ const renderSubFlow = (
278+ wrapper : SubFlowValue ,
279+ id : NodeFunction [ 'id' ] | FunctionDefinition [ 'identifier' ] ,
280+ index : number ,
281+ indent : string
282+ ) : string => {
283+ if ( ! wrapper . startingNodeId && wrapper . functionDefinition ?. identifier ) {
284+ return `fn_${ wrapper . functionDefinition . identifier . replace ( / : : / g, '_' ) } ` ;
285+ }
286+ const lambdaArgName = `p_${ sanitizeId ( id as string ) } _${ index } ` ;
287+ const subTreeCode = generateNodeCode ( wrapper . startingNodeId || wrapper . functionDefinition ?. id ! , indent + " " ) ;
288+ return `(...${ lambdaArgName } ) => {\n${ subTreeCode } ${ indent } }` ;
289+ } ;
290+
291+ // Emits the TypeScript expression for a literal value. When the literal
292+ // carries inline references (addressable via `${signature}` inside its
293+ // string leaves), each occurrence is spliced with the generated expression
294+ // of the referenced value so the value is type-checked. A string leaf that
295+ // consists solely of a single `${signature}` preserves the referenced type
296+ // verbatim (a NUMBER reference stays a NUMBER); a leaf with surrounding text
297+ // or multiple tokens becomes a template literal (a string). References are
298+ // resolved recursively through nested literals, arrays and objects. Without
299+ // references, the original lossless-JSON stringification is preserved.
300+ const renderLiteral = (
301+ value : unknown ,
302+ references : Maybe < InlineReferenceValue [ ] > | undefined ,
303+ ctx : { id : NodeFunction [ 'id' ] | FunctionDefinition [ 'identifier' ] ; index : number ; indent : string }
304+ ) : string | undefined => {
305+ if ( value === null || value === undefined ) return undefined ;
306+
307+ const refExpr = new Map < string , string > ( ) ;
308+ for ( const r of references ?? [ ] ) {
309+ if ( ! r ?. signature ) continue ;
310+ const inner = r . value ;
311+ if ( inner ?. __typename === "ReferenceValue" ) {
312+ refExpr . set ( r . signature , renderReference ( inner as ReferenceValue ) ) ;
313+ } else if ( inner ?. __typename === "LiteralValue" ) {
314+ const nested = renderLiteral ( inner . value , inner . references , ctx ) ;
315+ if ( nested !== undefined ) refExpr . set ( r . signature , nested ) ;
316+ } else if ( inner ?. __typename === "SubFlowValue" ) {
317+ refExpr . set ( r . signature , renderSubFlow ( inner as SubFlowValue , ctx . id , ctx . index , ctx . indent ) ) ;
318+ }
319+ }
320+
321+ // No resolvable inline references: keep the exact prior behaviour so
322+ // existing generated code (and number precision) is unchanged.
323+ if ( refExpr . size === 0 ) return stringify ( value ) ;
324+
325+ return emitLiteralNode ( value , refExpr ) ;
326+ } ;
327+
177328 const generateNodeCode = ( id : NodeFunction [ 'id' ] | FunctionDefinition [ 'identifier' ] , indent : string = "" ) : string => {
178329 const node = nodes . find ( n => n ?. id === id ) ;
179330 if ( ! node || ! node . functionDefinition ) return "" ;
@@ -187,41 +338,19 @@ export function generateFlowSourceCode(
187338 const val = p . value ;
188339 if ( ! val ) return isForInference ? `/* @pos ${ id } ${ index } */ {}` : `/* @pos ${ id } ${ index } */ undefined` ;
189340 if ( val . __typename === "ReferenceValue" ) {
190- const ref = val as ReferenceValue ;
191- let refCode = typeof ref . inputIndex === "number"
192- ? `p_${ sanitizeId ( ref . nodeFunctionId ?? "undefined" ) } _${ ref . parameterIndex } [${ ref . inputIndex } ]`
193- : ref . nodeFunctionId ? `node_${ sanitizeId ( ref . nodeFunctionId ) } ` : `flow_${ sanitizeId ( flow ?. id ?? "undefined" ) } ` ;
194- ref . referencePath ?. forEach ( pathObj => {
195- refCode += `?.${ pathObj . path } ` ;
196- } ) ;
197- // A reference may be nullable (`string | null`) or reach through optional
198- // properties (an optional chain like `node_X?.text`). When `assertNonNullReferences`
199- // is set (inference/schema), the nullish part is waived with a `!` so the base
200- // type flows cleanly. During validation it is left in place so a possibly-null
201- // reference feeding a non-null parameter surfaces a diagnostic — which is then
202- // downgraded to a warning rather than being silently erased. Base type
203- // mismatches still fail validation in both modes.
204- const nonNull = assertNonNullReferences ? "!" : "" ;
205- return `/* @pos ${ id } ${ index } */ (${ refCode } )${ nonNull } ` ;
341+ return `/* @pos ${ id } ${ index } */ ${ renderReference ( val as ReferenceValue ) } ` ;
206342 }
207343 if ( val . __typename === "LiteralValue" ) {
208- const jsonString = val ? .value !== null && val ?. value !== undefined ? stringify ( val ?. value ) : undefined
344+ const jsonString = renderLiteral ( val . value , val . references , { id , index , indent } ) ;
209345 return `/* @pos ${ id } ${ index } */ ${ jsonString } ` ;
210346 }
211347 if ( val . __typename === "SubFlowValue" ) {
212- const wrapper = val as SubFlowValue ;
213348 // Direct mapping: the sub-flow *is* an existing function, with no
214349 // node tree of its own (no startingNodeId). Emit the function
215350 // reference itself as the value so its own signature drives the
216351 // sub-flow's I/O — e.g. mapping `std::math::add` yields
217352 // `(a, b) => NUMBER` rather than an empty `(...p) => {}` lambda.
218- if ( ! wrapper . startingNodeId && wrapper . functionDefinition ?. identifier ) {
219- const funcName = `fn_${ wrapper . functionDefinition . identifier . replace ( / : : / g, '_' ) } ` ;
220- return `/* @pos ${ id } ${ index } */ ${ funcName } ` ;
221- }
222- const lambdaArgName = `p_${ sanitizeId ( id as string ) } _${ index } ` ;
223- const subTreeCode = generateNodeCode ( wrapper . startingNodeId || wrapper . functionDefinition ?. id ! , indent + " " ) ;
224- return `/* @pos ${ id } ${ index } */ (...${ lambdaArgName } ) => {\n${ subTreeCode } ${ indent } }` ;
353+ return `/* @pos ${ id } ${ index } */ ${ renderSubFlow ( val as SubFlowValue , id , index , indent ) } ` ;
225354 }
226355 return isForInference ? `/* @pos ${ id } ${ index } */ {}` : `/* @pos ${ id } ${ index } */ undefined` ;
227356 } ) ;
0 commit comments