@@ -36,6 +36,8 @@ interface GenericRenderContext {
3636 emittedParamSpecs : Set < string > ;
3737}
3838
39+ type ReturnDefinitionNames = ReadonlySet < string > ;
40+
3941export interface CodeGeneratorOptions {
4042 /** Reports a generated annotation that cannot be represented by emitted declarations. */
4143 onTypeDegrade ?: ( typeName : string ) => void ;
@@ -397,11 +399,164 @@ export class CodeGenerator {
397399 } ;
398400 }
399401
402+ /**
403+ * Preserve Python provenance in generated return validators instead of
404+ * reconstructing a checker from the final TypeScript spelling. In particular
405+ * pandas/NumPy values retain their marker contract after Arrow decoding.
406+ */
407+ private returnSchema ( type : PythonType , definitions : ReturnDefinitionNames = new Set ( ) ) : unknown {
408+ // A bare `-> None` return maps to TS void and validates nothing, but a
409+ // NESTED None (union member, tuple element) is a real wire value — it
410+ // decodes to null and must be checked as null, or a union containing it
411+ // would carry an any-member and accept everything.
412+ if ( type . kind === 'primitive' && type . name === 'None' ) {
413+ return { kind : 'any' } ;
414+ }
415+ const schema = ( current : PythonType ) : unknown => {
416+ switch ( current . kind ) {
417+ case 'primitive' :
418+ return current . name === 'int' || current . name === 'float'
419+ ? { kind : 'primitive' , type : 'number' }
420+ : current . name === 'str'
421+ ? { kind : 'primitive' , type : 'string' }
422+ : current . name === 'bool'
423+ ? { kind : 'primitive' , type : 'boolean' }
424+ : current . name === 'bytes'
425+ ? { kind : 'primitive' , type : 'Uint8Array' }
426+ : current . name === 'None'
427+ ? { kind : 'primitive' , type : 'null' }
428+ : { kind : 'any' } ;
429+ case 'literal' :
430+ return { kind : 'literal' , value : current . value } ;
431+ case 'annotated' :
432+ return schema ( current . base ) ;
433+ case 'final' :
434+ case 'classvar' :
435+ return schema ( current . type ) ;
436+ case 'optional' :
437+ return {
438+ kind : 'union' ,
439+ options : [ schema ( current . type ) , { kind : 'primitive' , type : 'null' } ] ,
440+ } ;
441+ case 'union' :
442+ return { kind : 'union' , options : current . types . map ( schema ) } ;
443+ case 'collection' : {
444+ if ( current . name === 'tuple' ) {
445+ if ( current . itemTypes [ 1 ] ?. kind === 'custom' && current . itemTypes [ 1 ] . name === '...' ) {
446+ return {
447+ kind : 'array' ,
448+ element : schema ( current . itemTypes [ 0 ] ?? { kind : 'custom' , name : 'Any' } ) ,
449+ } ;
450+ }
451+ return { kind : 'tuple' , elements : current . itemTypes . map ( schema ) } ;
452+ }
453+ if ( current . name === 'dict' ) {
454+ return {
455+ kind : 'record' ,
456+ values : schema ( current . itemTypes [ 1 ] ?? { kind : 'custom' , name : 'Any' } ) ,
457+ } ;
458+ }
459+ return {
460+ kind : 'array' ,
461+ element : schema ( current . itemTypes [ 0 ] ?? { kind : 'custom' , name : 'Any' } ) ,
462+ } ;
463+ }
464+ case 'generic' : {
465+ const leaf = current . name . split ( '.' ) . at ( - 1 ) ?? current . name ;
466+ if ( leaf === 'NDArray' || leaf === 'ndarray' ) {
467+ return { kind : 'marker' , marker : 'ndarray' } ;
468+ }
469+ if (
470+ [
471+ 'list' ,
472+ 'List' ,
473+ 'Sequence' ,
474+ 'Iterable' ,
475+ 'Iterator' ,
476+ 'Generator' ,
477+ 'set' ,
478+ 'frozenset' ,
479+ ] . includes ( leaf )
480+ ) {
481+ return {
482+ kind : 'array' ,
483+ element : schema ( current . typeArgs [ 0 ] ?? { kind : 'custom' , name : 'Any' } ) ,
484+ } ;
485+ }
486+ if ( [ 'dict' , 'Dict' , 'Mapping' , 'MutableMapping' ] . includes ( leaf ) ) {
487+ return {
488+ kind : 'record' ,
489+ values : schema ( current . typeArgs [ 1 ] ?? { kind : 'custom' , name : 'Any' } ) ,
490+ } ;
491+ }
492+ return definitions . has ( leaf ) ? { kind : 'ref' , name : leaf } : { kind : 'any' } ;
493+ }
494+ case 'custom' : {
495+ const leaf = current . name . split ( '.' ) . at ( - 1 ) ?? current . name ;
496+ const full = `${ current . module ?? '' } .${ leaf } ` ;
497+ if ( leaf === 'None' ) {
498+ return { kind : 'primitive' , type : 'null' } ;
499+ }
500+ if ( leaf === 'Any' || leaf === 'object' || leaf === 'NoReturn' || leaf === 'Never' ) {
501+ return { kind : 'any' } ;
502+ }
503+ if ( leaf === 'DataFrame' && ( ! current . module || current . module . startsWith ( 'pandas' ) ) ) {
504+ return { kind : 'marker' , marker : 'dataframe' } ;
505+ }
506+ if ( leaf === 'Series' && ( ! current . module || current . module . startsWith ( 'pandas' ) ) ) {
507+ return { kind : 'marker' , marker : 'series' } ;
508+ }
509+ if ( leaf === 'ndarray' || leaf === 'NDArray' || full . includes ( 'numpy' ) ) {
510+ return { kind : 'marker' , marker : 'ndarray' } ;
511+ }
512+ return definitions . has ( leaf ) ? { kind : 'ref' , name : leaf } : { kind : 'any' } ;
513+ }
514+ case 'typevar' :
515+ case 'paramspec' :
516+ case 'paramspec_args' :
517+ case 'paramspec_kwargs' :
518+ case 'typevartuple' :
519+ case 'unpack' :
520+ case 'callable' :
521+ return { kind : 'any' } ;
522+ }
523+ } ;
524+ return schema ( type ) ;
525+ }
526+
527+ private returnDefinitions ( module : PythonModule ) : ReturnDefinitionNames {
528+ return new Set (
529+ module . classes
530+ . filter ( cls => cls . kind === 'typed_dict' || cls . decorators . includes ( '__typed_dict__' ) )
531+ . map ( cls => cls . name )
532+ ) ;
533+ }
534+
535+ private emitReturnDefinitions ( module : PythonModule ) : string {
536+ const definitions = this . returnDefinitions ( module ) ;
537+ const entries = module . classes
538+ . filter ( cls => cls . kind === 'typed_dict' || cls . decorators . includes ( '__typed_dict__' ) )
539+ . map ( cls => {
540+ const fields = Object . fromEntries (
541+ cls . properties . map ( property => [
542+ property . name ,
543+ {
544+ schema : this . returnSchema ( property . type , definitions ) ,
545+ optional : property . optional === true ,
546+ } ,
547+ ] )
548+ ) ;
549+ return [ cls . name , { kind : 'record' , fields } ] ;
550+ } ) ;
551+ return `const __tywrapReturnDefinitions: Record<string, ReturnSchema> = ${ JSON . stringify ( Object . fromEntries ( entries ) ) } ;\n\n` ;
552+ }
553+
400554 generateFunctionWrapper (
401555 func : PythonFunction ,
402556 moduleName ?: string ,
403557 annotatedJSDoc = false ,
404- localDeclaredNames : Set < string > = new Set ( )
558+ localDeclaredNames : Set < string > = new Set ( ) ,
559+ returnDefinitions : ReturnDefinitionNames = new Set ( )
405560 ) : GeneratedCode {
406561 const jsdoc = this . generateJsDoc (
407562 func . docstring ,
@@ -489,6 +644,8 @@ export class CodeGenerator {
489644 const returnType = this . typeToTsFromPython ( func . returnType , genericContext , 'return' ) ;
490645 const fname = this . escapeIdentifier ( func . name ) ;
491646 const moduleId = moduleName ?? '__main__' ;
647+ const validatorName = `__validate${ this . escapeIdentifier ( func . name , { preserveCase : true } ) } Result` ;
648+ const returnValidator = `const ${ validatorName } = createReturnValidator(${ JSON . stringify ( this . returnSchema ( func . returnType , returnDefinitions ) ) } , ${ JSON . stringify ( `${ moduleId } .${ func . name } ` ) } , __tywrapReturnDefinitions);\n\n` ;
492649
493650 // Overloads: generate trailing optional parameter drop variants (exclude *args/**kwargs).
494651 // Why: Python APIs frequently have many optional tail params. TypeScript callers expect
@@ -577,10 +734,8 @@ export class CodeGenerator {
577734 const callPreludeLines = emitCallPrelude ( callDescriptor , this . callEmitHelpers ( ) ) ;
578735 const callPrelude = callPreludeLines . length > 0 ? `${ callPreludeLines . join ( '\n' ) } \n` : '' ;
579736
580- const ts = `${ jsdoc } ${ overloadDecl } export async function ${ fname } ${ typeParamDecl } (${ paramDecl } ): Promise<${ returnType } > {
581- ${ callPrelude } ${ guards } return getRuntimeBridge().call('${ moduleId } ', '${ func . name } ', __args${
582- hasKwArgs ? ', __kwargs' : ''
583- } );
737+ const ts = `${ jsdoc } ${ returnValidator } ${ overloadDecl } export async function ${ fname } ${ typeParamDecl } (${ paramDecl } ): Promise<${ returnType } > {
738+ ${ callPrelude } ${ guards } return getRuntimeBridge().call<${ returnType } >('${ moduleId } ', '${ func . name } ', __args, ${ hasKwArgs ? '__kwargs' : 'undefined' } , ${ validatorName } );
584739}
585740` ;
586741
@@ -596,7 +751,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
596751 cls : PythonClass ,
597752 moduleName ?: string ,
598753 _annotatedJSDoc = false ,
599- localDeclaredNames : Set < string > = new Set ( )
754+ localDeclaredNames : Set < string > = new Set ( ) ,
755+ returnDefinitions : ReturnDefinitionNames = new Set ( )
600756 ) : GeneratedCode {
601757 const moduleDeclaredNames = new Set ( localDeclaredNames ) ;
602758 moduleDeclaredNames . add ( cls . name ) ;
@@ -782,6 +938,8 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
782938 'return'
783939 ) ;
784940 const mname = this . escapeIdentifier ( method . name ) ;
941+ const validatorName = `__validate${ this . escapeIdentifier ( cls . name , { preserveCase : true } ) } ${ this . escapeIdentifier ( method . name , { preserveCase : true } ) } Result` ;
942+ const returnValidator = `const ${ validatorName } = createReturnValidator(${ JSON . stringify ( this . returnSchema ( method . returnType , returnDefinitions ) ) } , ${ JSON . stringify ( `${ moduleId } .${ cls . name } .${ method . name } ` ) } , __tywrapReturnDefinitions);\n\n` ;
785943
786944 const overloads : string [ ] = [ ] ;
787945 if ( needsKwargsParam && requiredKwOnlyNames . length > 0 ) {
@@ -829,11 +987,9 @@ ${callPrelude}${guards} return getRuntimeBridge().call('${moduleId}', '${func.n
829987 const guardLines = emitArgGuards ( callDescriptor ) ;
830988 const guards = guardLines . length > 0 ? `${ guardLines . join ( '\n' ) } \n` : '' ;
831989
832- const callExpr = `getRuntimeBridge().call('${ moduleId } ', '${ cls . name } .${ method . name } ', __args${
833- needsKwargsParam ? ', __kwargs' : ''
834- } )`;
990+ const callExpr = `getRuntimeBridge().call<${ returnType } >('${ moduleId } ', '${ cls . name } .${ method . name } ', __args, ${ needsKwargsParam ? '__kwargs' : 'undefined' } , ${ validatorName } )` ;
835991 methodBodies . push ( `${ overloadDecl } ${ staticPrefix } async ${ mname } ${ methodTypeParamDecl } (${ paramsDecl } ): Promise<${ returnType } > {
836- ${ callPrelude } ${ guards } return ${ callExpr } ;
992+ ${ returnValidator } ${ callPrelude } ${ guards } return ${ callExpr } ;
837993 }` ) ;
838994 methodDeclarations . push (
839995 `${ overloadDecl } ${ overloads . length > 0 ? '' : ` ${ staticPrefix } ${ mname } ${ methodTypeParamDecl } (${ paramsDecl } ): Promise<${ returnType } >;\n` } `
@@ -913,10 +1069,26 @@ ${migrationNote}${declarationMethodsSection}
9131069 ] ) ;
9141070 const functionResults = [ ...module . functions ]
9151071 . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
916- . map ( f => this . generateFunctionWrapper ( f , module . name , annotatedJSDoc , localDeclaredNames ) ) ;
1072+ . map ( f =>
1073+ this . generateFunctionWrapper (
1074+ f ,
1075+ module . name ,
1076+ annotatedJSDoc ,
1077+ localDeclaredNames ,
1078+ this . returnDefinitions ( module )
1079+ )
1080+ ) ;
9171081 const classResults = [ ...module . classes ]
9181082 . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
919- . map ( c => this . generateClassWrapper ( c , module . name , annotatedJSDoc , localDeclaredNames ) ) ;
1083+ . map ( c =>
1084+ this . generateClassWrapper (
1085+ c ,
1086+ module . name ,
1087+ annotatedJSDoc ,
1088+ localDeclaredNames ,
1089+ this . returnDefinitions ( module )
1090+ )
1091+ ) ;
9201092 const typeAliasResults = [ ...( module . typeAliases ?? [ ] ) ]
9211093 . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
9221094 . map ( alias => this . generateTypeAlias ( alias , module . name , localDeclaredNames ) ) ;
@@ -932,7 +1104,9 @@ ${migrationNote}${declarationMethodsSection}
9321104 return kind === 'class' && ! c . decorators . includes ( '__typed_dict__' ) ;
9331105 } ) ;
9341106 const needsRuntime = module . functions . length > 0 || hasRuntimeClasses ;
935- const bridgeDecl = needsRuntime ? `import { getRuntimeBridge } from 'tywrap/runtime';\n\n` : '' ;
1107+ const bridgeDecl = needsRuntime
1108+ ? `import { createReturnValidator, getRuntimeBridge, type ReturnSchema } from 'tywrap/runtime';\n\n${ this . emitReturnDefinitions ( module ) } `
1109+ : '' ;
9361110
9371111 const ts = `${ `${ header } ${ bridgeDecl } ${ functionCodes } \n${ classCodes } \n${ typeAliasCodes } ` . trimEnd ( ) } \n` ;
9381112 const declaration = `${ `${ declarationHeader } ${ functionResults
0 commit comments