import { identifierOrOperator } from './lexer' ; import { assertUnreachable } from './util' ; type TerminalType = 'keyword' | 'operator' | 'identifier' | 'value' ; export type AST = { type : 'error' ; message : string ; } | { type : 'literal' ; value : string ; } | { type : 'terminal' ; value : TerminalType ; } | { type : 'identifier' ; value : string ; } | { type : 'structure' ; startToken : string ; endToken : string ; expr : AST ; } | { type : 'maybe' ; expr : AST ; } | { type : 'repeat0' ; expr : AST ; } | { type : 'repeat1' ; expr : AST ; } | { type : 'sepBy0' ; expr : AST ; separator : AST ; } | { type : 'sepBy1' ; expr : AST ; separator : AST ; } | { type : 'seq' ; exprs : AST [ ] ; fn : SeqFn ; } | { type : 'alt' ; exprs : AST [ ] ; } | { type : 'ruleset' ; rules : Array < { name : string ; expr : AST ; } > ; } ; const spaces = ( n ) => Array ( n ) . fill ( ' ' ) . join ( '' ) ; const wrapIf = ( cond , str ) => ( cond ? `(${ str })` : str ) ; export function print ( node , indent = 0 , prec = 0 ) { switch ( node . type ) { case 'error' : return `<error: ${ node . message }>` ; case 'literal' : return `"${ node . value }"` ; case 'terminal' : return node . value ; case 'identifier' : return node . value ; case 'structure' : return `#${ node . startToken } ${ print ( node . expr , indent ) } ${ node . endToken }` ; case 'maybe' : return wrapIf ( prec > 3 , `${ print ( node . expr , indent , 4 ) }?` ) ; case 'repeat0' : return wrapIf ( prec > 3 , `${ print ( node . expr , indent , 4 ) }*` ) ; case 'repeat1' : return wrapIf ( prec > 3 , `${ print ( node . expr , indent , 4 ) }+` ) ; case 'sepBy0' : return wrapIf ( prec > 2 , `${ print ( node . expr , indent , 3 ) } ** ${ print ( node . separator , indent , 3 ) }` ) ; case 'sepBy1' : return wrapIf ( prec > 2 , `${ print ( node . expr , indent , 3 ) } ++ ${ print ( node . separator , indent , 3 ) }` ) ; case 'seq' : return ( wrapIf ( prec > 1 , node . exprs . map ( expr => print ( expr , indent , 2 ) ) . join ( ' ' ) ) || 'nil' ) ; case 'alt' : return wrapIf ( prec > 0 , node . exprs . map ( expr => print ( expr , indent , 1 ) ) . join ( ` | ` ) ) ; case 'ruleset' : return node . rules . map ( rule => rule . expr . type === 'alt' ? `\n${ spaces ( indent ) }${ rule . name } = ${ rule . expr . exprs . map ( expr => print ( expr , indent ) ) . join ( `\n${ spaces ( indent + rule . name . length ) } | ` ) }` : `\n${ spaces ( indent ) }${ rule . name } = ${ print ( rule . expr , indent ) }` ) . join ( '' ) ; default : assertUnreachable ( node ) ; } } export function checkLit ( x ) { if ( typeof x !== 'string' || ! identifierOrOperator . test ( x ) ) { return { type : 'error' , message : 'invalid pattern' } ; } return { type : 'literal' , value : x } ; } type SeqFn = ( ... xs : any [ ] ) => unknown ; const error = ( message ) : AST => ( { type : 'error' , message } ) ; const ident = ( value ) : AST => ( { type : 'identifier' , value } ) ; const lit = ( value ) : AST => ( { type : 'literal' , value } ) ; const terminal = ( value ) : AST => ( { type : 'terminal' , value } ) ; const alt = ( ... exprs ) : AST => ( { type : 'alt' , exprs } ) ; const seq = ( fn , ... exprs ) : AST => ( { type : 'seq' , fn , exprs , } ) ; const rule = ( name , expr ) : { name : string ; expr : AST ; } => ( { name , expr , } ) ; const ruleset = ( ... rules ) : AST => ( { type : 'ruleset' , rules , } ) ; const repeat0 = ( expr ) : AST => ( { type : 'repeat0' , expr } ) ; const repeat1 = ( expr ) : AST => ( { type : 'repeat1' , expr } ) ; const maybe = ( expr ) : AST => ( { type : 'maybe' , expr } ) ; const sepBy0 = ( expr , separator ) : AST => ( { type : 'sepBy0' , expr , separator , } ) ; const sepBy1 = ( expr , separator ) : AST => ( { type : 'sepBy1' , expr , separator , } ) ; const structure = ( startToken , expr , endToken ) : AST => ( { type : 'structure' , expr , startToken , endToken , } ) ; export const builders = { error , ident , lit , terminal , alt , seq , repeat0 , repeat1 , maybe , sepBy0 , sepBy1 , structure , rule , ruleset , } ; export const coreAST = ruleset ( rule ( "Grammar" , seq ( ( rules ) => ruleset ( ... rules ) , sepBy1 ( ident ( "Rule" ) , lit ( ";" ) ) ) ) , rule ( "Rule" , seq ( ( name , _ , expr ) => rule ( name , expr ) , terminal ( 'identifier' ) , lit ( '=' ) , ident ( 'AltExpr' ) ) ) , rule ( 'AltExpr' , seq ( ( _ , first , rest ) => rest . length ? alt ( first , ... rest ) : first , maybe ( lit ( '|' ) ) , ident ( 'SeqExpr' ) , repeat0 ( seq ( ( _ , x ) => x , lit ( '|' ) , ident ( 'SeqExpr' ) ) ) ) ) , rule ( 'SeqExpr' , seq ( ( first , getSeq ) => getSeq ? getSeq ( first ) : first , ident ( 'SepExpr' ) , maybe ( seq ( ( rest , _ , fn ) => ( first ) => seq ( fn , first , ... rest ) , repeat0 ( ident ( 'SepExpr' ) ) , lit ( ":" ) , terminal ( 'value' ) ) ) ) ) , rule ( 'SepExpr' , alt ( seq ( ( expr , _ , sep ) => sepBy0 ( expr , sep ) , ident ( 'SepExpr' ) , lit ( '**' ) , ident ( 'RepExpr' ) ) , seq ( ( expr , _ , sep ) => sepBy1 ( expr , sep ) , ident ( 'SepExpr' ) , lit ( '++' ) , ident ( 'RepExpr' ) ) , ident ( 'RepExpr' ) ) ) , rule ( 'RepExpr' , alt ( seq ( repeat0 , ident ( 'Expr' ) , lit ( '*' ) ) , seq ( repeat1 , ident ( 'Expr' ) , lit ( '+' ) ) , seq ( maybe , ident ( 'Expr' ) , lit ( '?' ) ) , ident ( 'Expr' ) ) ) , rule ( 'Expr' , alt ( structure ( '(' , ident ( 'AltExpr' ) , ')' ) , seq ( ( _ , expr ) => structure ( '(' , expr , ')' ) , lit ( '#' ) , structure ( '(' , ident ( 'AltExpr' ) , ')' ) ) , seq ( ( _ , expr ) => structure ( '[' , expr , ']' ) , lit ( '#' ) , structure ( '[' , ident ( 'AltExpr' ) , ']' ) ) , seq ( ( _ , expr ) => structure ( '{' , expr , '}' ) , lit ( '#' ) , structure ( '{' , ident ( 'AltExpr' ) , '}' ) ) , seq ( ( _ , lang ) => lang && lang . ast || error ( 'expected a language or AST' ) , lit ( 'include' ) , terminal ( 'value' ) ) , seq ( ident , terminal ( 'identifier' ) ) , seq ( ( ) => terminal ( 'identifier' ) , lit ( 'identifier' ) ) , seq ( ( ) => terminal ( 'operator' ) , lit ( 'operator' ) ) , seq ( ( ) => terminal ( 'keyword' ) , lit ( 'keyword' ) ) , seq ( ( ) => terminal ( 'value' ) , lit ( 'value' ) ) , seq ( ( ) => seq ( ( ) => null ) , lit ( 'nil' ) ) , seq ( checkLit , terminal ( 'value' ) ) ) ) ) ;