gcanti / io-ts-codegen

Code generation for io-ts
https://gcanti.github.io/io-ts-codegen/
MIT License
156 stars 14 forks source link

Recursive expressions #21

Closed SAnDAnGE closed 6 years ago

SAnDAnGE commented 6 years ago

I am trying to create a small but recursive AST with boolean logic:

const declarations = [
  gen.typeDeclaration(
    'Lit_bV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_b')),
      gen.property('litValue', gen.booleanType)
    ])
  ),
  gen.typeDeclaration(
    'NotV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Not')),
      gen.property(
        'exp',
        gen.recursiveCombinator(gen.identifier('BExpr'), 'BooleanExpr', gen.identifier('BExpr'))
      )
    ])
  ),
  gen.typeDeclaration(
    'AndV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('And')),
      gen.property(
        'a',
        gen.recursiveCombinator(gen.identifier('BExpr'), 'BooleanExpr', gen.identifier('BExpr'))
      ),
      gen.property(
        'b',
        gen.recursiveCombinator(gen.identifier('BExpr'), 'BooleanExpr', gen.identifier('BExpr'))
      )
    ])
  ),
  gen.typeDeclaration(
    'BExpr',
    gen.taggedUnionCombinator('t', [
      gen.identifier('Lit_bV'),
      gen.identifier('NotV'),
      gen.identifier('AndV')
    ])
  )
];

const sorted = gen.sort(declarations);
console.log(sorted.map(d => gen.printRuntime(d)).join('\n'));
console.log(sorted.map(d => gen.printStatic(d)).join('\n'));

The output is:


const Lit_bV = t.interface({
  t: t.literal('Lit_b'),
  litValue: t.boolean
})
const BExpr: t.RecursiveType<t.Any, BExpr> = t.recursion<BExpr>('BExpr', (_: t.Any) => t.taggedUnion('t', [
  Lit_bV,
  NotV,
  AndV
]))
const NotV: t.RecursiveType<t.Any, NotV> = t.recursion<NotV>('NotV', (_: t.Any) => t.interface({
  t: t.literal('Not'),
  exp: t.recursion<BExpr>('BooleanExpr', (_: t.Any) => BExpr)
}))
const AndV: t.RecursiveType<t.Any, AndV> = t.recursion<AndV>('AndV', (_: t.Any) => t.interface({
  t: t.literal('And'),
  a: t.recursion<BExpr>('BooleanExpr', (_: t.Any) => BExpr),
  b: t.recursion<BExpr>('BooleanExpr', (_: t.Any) => BExpr)
}))
//------------
interface Lit_bV {
  t: 'Lit_b',
  litValue: boolean
}
interface BExpr 
  | Lit_bV
  | NotV
  | AndV
interface NotV {
  t: 'Not',
  exp: BExpr
}
interface AndV {
  t: 'And',
  a: BExpr,
  b: BExpr
}

The interfaces explain pretty well the desired result, but not sure how to create the logic in io-ts to obtain the needed recursivity

gcanti commented 6 years ago

@SAnDAnGE thanks for opening this issue.

So there are several problems:

first of all there's a bug in the static emitter, this

interface BExpr 
  | Lit_bV
  | NotV
  | AndV

should be

type BExpr =
  | Lit_bV
  | NotV
  | AndV

I released a new version (io-ts-codegen@0.1.10) with a fix.

Also currently (io-ts@1.2.0) recursive runtime types are not allowed in tagged unions, I released a related patch (io-ts@1.2.1).

Finally this is the source I'd use to implement your use case

const declarations = [
  gen.typeDeclaration(
    'Lit_bV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_b')),
      gen.property('litValue', gen.booleanType)
    ])
  ),
  gen.typeDeclaration(
    'NotV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Not')),
      gen.property('exp', gen.identifier('BExpr'))
    ])
  ),
  gen.typeDeclaration(
    'AndV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('And')),
      gen.property('a', gen.identifier('BExpr')),
      gen.property('b', gen.identifier('BExpr'))
    ])
  ),
  gen.typeDeclaration(
    'BExpr',
    gen.taggedUnionCombinator('t', [gen.identifier('Lit_bV'), gen.identifier('NotV'), gen.identifier('AndV')])
  )
]

const sorted = gen.sort(declarations)
console.log(sorted.map(d => gen.printStatic(d)).join('\n'))
console.log(sorted.map(d => gen.printRuntime(d)).join('\n'))

and this is its output (produced with latest version)

interface Lit_bV {
  t: 'Lit_b'
  litValue: boolean
}
type BExpr = Lit_bV | NotV | AndV
interface NotV {
  t: 'Not'
  exp: BExpr
}
interface AndV {
  t: 'And'
  a: BExpr
  b: BExpr
}
const Lit_bV = t.interface({
  t: t.literal('Lit_b'),
  litValue: t.boolean
})
const BExpr: t.RecursiveType<t.Type<BExpr>, BExpr> = t.recursion<BExpr>('BExpr', _ =>
  t.taggedUnion('t', [Lit_bV, NotV, AndV])
)
const NotV: t.RecursiveType<t.Type<NotV>, NotV> = t.recursion<NotV>('NotV', _ =>
  t.interface({
    t: t.literal('Not'),
    exp: BExpr
  })
)
const AndV: t.RecursiveType<t.Type<AndV>, AndV> = t.recursion<AndV>('AndV', _ =>
  t.interface({
    t: t.literal('And'),
    a: BExpr,
    b: BExpr
  })
)
SAnDAnGE commented 6 years ago

Impressive work and response time, thank you very much for this.

3 more issues left :)

import * as gen from 'io-ts-codegen';

const declarations = [
  gen.typeDeclaration(
    'LiteralBooleanV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_b')),
      gen.property('litValue', gen.booleanType)
    ])
  ),
  gen.typeDeclaration(
    'NotV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Not')),
      gen.property('exp', gen.identifier('BExpr'))
    ])
  ),
  gen.typeDeclaration(
    'LiteralIntegerV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_i')),
      gen.property('litValue', gen.integerType)
    ])
  ),
  gen.typeDeclaration(
    'LiteralFloatV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_f')),
      gen.property('litValue', gen.numberType)
    ])
  ),

  gen.typeDeclaration(
    'AExpr',
    gen.taggedUnionCombinator('t', [
      gen.identifier('LiteralIntegerV'),
      gen.identifier('LiteralFloatV')
    ])
  ),

  gen.typeDeclaration(
    'GtV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Gt')),
      gen.property('a', gen.identifier('AExpr')),
      gen.property('b', gen.identifier('AExpr'))
    ])
  ),
  gen.typeDeclaration(
    'EqV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Eq')),
      gen.property(
        'a',
        gen.unionCombinator([
            gen.identifier('AExpr'),
            gen.identifier('BExpr')
          ])
      ),
      gen.property(
        'b',
        gen.unionCombinator([
            gen.identifier('AExpr'),
            gen.identifier('BExpr')
          ])
      )
    ])
  ),

  gen.typeDeclaration(
    'BExpr',
    gen.taggedUnionCombinator('t', [
      gen.identifier('LiteralBooleanV'),
      gen.identifier('NotV'),
      gen.identifier('GtV')
    ])
  )
];

const sorted = gen.sort(declarations);
console.log(sorted.map(d => gen.printStatic(d)).join('\n'));
console.log(sorted.map(d => gen.printRuntime(d)).join('\n'));

The generated output is:

interface LiteralBooleanV {                                                                                  
  t: 'Lit_b',          // 1) [tslint] Properties should be separated by semicolons (semicolon)                                                                                      
  litValue: boolean                                                                                          
}                                                                                                            
interface LiteralIntegerV {                                                                                  
  t: 'Lit_i',                                                                                                
  litValue: number                                                                                           
}                                                                                                            
interface LiteralFloatV {                                                                                    
  t: 'Lit_f',                                                                                                
  litValue: number                                                                                           
}                                                                                                            
type AExpr =                                                                                                 
  | LiteralIntegerV                                                                                          
  | LiteralFloatV                                                                                            
interface GtV {                                                                                              
  t: 'Gt',                                                                                                   
  a: AExpr,                                                                                                  
  b: AExpr                                                                                                   
}                                                                                                            
interface EqV {                                                                                              
  t: 'Eq',                                                                                                   
  a:                                                                                                         
    | AExpr                                                                                                  
    | BExpr,                                                                                                 
  b:                                                                                                         
    | AExpr                                                                                                  
    | BExpr                                                                                                  
}                                                                                                            
type BExpr =                                                                                                 
  | LiteralBooleanV                                                                                          
  | NotV                                                                                                     
  | GtV                                                                                                      
interface NotV {                                                                                             
  t: 'Not',                                                                                                  
  exp: BExpr                                                                                                 
}                                                                                                            
const LiteralBooleanV = t.interface({                                                                        
  t: t.literal('Lit_b'),                               
  litValue: t.boolean                                                                                        
})                                                                                                           
const LiteralIntegerV = t.interface({                                                                        
  t: t.literal('Lit_i'),                                                                                     
  litValue: t.Integer                                                                                        
})                                                                                                           
const LiteralFloatV = t.interface({                                                                          
  t: t.literal('Lit_f'),                                                                                     
  litValue: t.number                                                                                         
})                                                                                                           
const AExpr = t.taggedUnion('t', [                                                                           
  LiteralIntegerV,                                                                                           
  LiteralFloatV                                                                                              
])                                                                                                           
const GtV = t.interface({                                                                                    
  t: t.literal('Gt'),                                                                                        
  a: AExpr,                                                                                                  
  b: AExpr                                                                                                   
})                                                                                                           
const EqV = t.interface({                                                                                    
  t: t.literal('Eq'),                                                                                        
  a: t.union([              // 2) Declaring this as taggedUnion generates Maximum call stack exceeded when decoding                                                                                
    AExpr,                                                                                                   
    BExpr              // 3) [ts] Block-scoped variable 'BExpr' used before its declaration. [ts] Variable 'BExpr' is used before being assigned.                                                                                      
  ]),                                                                                                        
  b: t.union([                                                                                               
    AExpr,                                                                                                   
    BExpr                                                                                                    
  ])                                                                                                         
})                                                                                                           
const BExpr: t.RecursiveType<t.Type<BExpr>, BExpr> = t.recursion<BExpr>('BExpr', _ => t.taggedUnion('t', [   
  LiteralBooleanV,                                                                                           
  NotV,                                                                                                      
  GtV                                                                                                        
]))                                                                                                          
const NotV: t.RecursiveType<t.Type<NotV>, NotV> = t.recursion<NotV>('NotV', _ => t.interface({               
  t: t.literal('Not'),                                                                                       
  exp: BExpr                                                                                                 
}))                                                                                                          
gcanti commented 6 years ago

1) this is a styling issue, my suggestion is passing the generated code through prettier so you can apply your styling settings 2) could you please provide a repro? 3) Ah right, I guess recursive types should be emitted before "normal" types

gcanti commented 6 years ago

Declaring this as taggedUnion generates Maximum call stack exceeded when decoding

@SAnDAnGE I'm not able to repro:

this is the source

const declarations = [
  gen.typeDeclaration(
    'LiteralBooleanV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_b')),
      gen.property('litValue', gen.booleanType)
    ])
  ),
  gen.typeDeclaration(
    'NotV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Not')),
      gen.property('exp', gen.identifier('BExpr'))
    ])
  ),
  gen.typeDeclaration(
    'LiteralIntegerV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_i')),
      gen.property('litValue', gen.integerType)
    ])
  ),
  gen.typeDeclaration(
    'LiteralFloatV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Lit_f')),
      gen.property('litValue', gen.numberType)
    ])
  ),

  gen.typeDeclaration(
    'AExpr',
    gen.taggedUnionCombinator('t', [gen.identifier('LiteralIntegerV'), gen.identifier('LiteralFloatV')])
  ),

  gen.typeDeclaration(
    'GtV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Gt')),
      gen.property('a', gen.identifier('AExpr')),
      gen.property('b', gen.identifier('AExpr'))
    ])
  ),
  gen.typeDeclaration(
    'EqV',
    gen.interfaceCombinator([
      gen.property('t', gen.literalCombinator('Eq')),
      gen.property('a', gen.taggedUnionCombinator('t', [gen.identifier('AExpr'), gen.identifier('BExpr')])),
      gen.property('b', gen.taggedUnionCombinator('t', [gen.identifier('AExpr'), gen.identifier('BExpr')]))
    ])
  ),

  gen.typeDeclaration(
    'BExpr',
    gen.taggedUnionCombinator('t', [gen.identifier('LiteralBooleanV'), gen.identifier('NotV'), gen.identifier('GtV')])
  )
]

const sorted = gen.sort(declarations)
console.log(sorted.map(d => gen.printStatic(d)).join('\n'))
console.log(sorted.map(d => gen.printRuntime(d)).join('\n'))

this is the output (with the fix above applied)

type BExpr = LiteralBooleanV | NotV | GtV
interface NotV {
  t: 'Not'
  exp: BExpr
}
interface LiteralBooleanV {
  t: 'Lit_b'
  litValue: boolean
}
interface LiteralIntegerV {
  t: 'Lit_i'
  litValue: number
}
interface LiteralFloatV {
  t: 'Lit_f'
  litValue: number
}
type AExpr = LiteralIntegerV | LiteralFloatV
interface GtV {
  t: 'Gt'
  a: AExpr
  b: AExpr
}
interface EqV {
  t: 'Eq'
  a: AExpr | BExpr
  b: AExpr | BExpr
}
const BExpr: t.RecursiveType<t.Type<BExpr>, BExpr> = t.recursion<BExpr>('BExpr', _ =>
  t.taggedUnion('t', [LiteralBooleanV, NotV, GtV])
)
const NotV: t.RecursiveType<t.Type<NotV>, NotV> = t.recursion<NotV>('NotV', _ =>
  t.interface({
    t: t.literal('Not'),
    exp: BExpr
  })
)
const LiteralBooleanV = t.interface({
  t: t.literal('Lit_b'),
  litValue: t.boolean
})
const LiteralIntegerV = t.interface({
  t: t.literal('Lit_i'),
  litValue: t.Integer
})
const LiteralFloatV = t.interface({
  t: t.literal('Lit_f'),
  litValue: t.number
})
const AExpr = t.taggedUnion('t', [LiteralIntegerV, LiteralFloatV])
const GtV = t.interface({
  t: t.literal('Gt'),
  a: AExpr,
  b: AExpr
})
const EqV = t.interface({
  t: t.literal('Eq'),
  a: t.taggedUnion('t', [AExpr, BExpr]),
  b: t.taggedUnion('t', [AExpr, BExpr])
})

And this is the payload (which doesn't show any issue)

console.log(
  EqV.decode({
    t: 'Eq',
    a: { t: 'Lit_i', litValue: 1 },
    b: { t: 'Lit_i', litValue: 1 }
  }).isRight()
)
// true

Have you got a payload which blows the stack?