import { keywordFor , assocBang , MalError , symbolFor as S , MalNode , MalNodeMap , isMap , MalMap , MalVal , M_OUTER , isNode , M_ELMSTRS , M_DELIMITERS , M_ISSUGAR , M_ISLIST , M_OUTER_INDEX , isSeq , getName , createList as L , MalSeq , isSymbol , } from './types' ; import printExp from './printer' ; const S_QUOTE = S ( 'quote' ) ; const S_QUASIQUOTE = S ( 'quasiquote' ) ; const S_UNQUOTE = S ( 'unquote' ) ; const S_SPLICE_UNQUOTE = S ( 'splice-unquote' ) ; const S_FN_SUGAR = S ( 'fn-sugar' ) ; const S_WITH_META_SUGAR = S ( 'with-meta-sugar' ) ; const S_UI_ANNOTATE = S ( 'ui-annotate' ) ; const S_DEREF = S ( 'deref' ) ; class Reader { private tokens : string [ ] | [ string , number ] [ ] ; private str : string ; private strlen : number ; private _index : number ; constructor ( tokens , str ) { this . tokens = [ ... tokens ] ; this . str = str ; this . strlen = str . length ; this . _index = 0 ; } public next ( ) { const token = this . tokens [ this . _index ++ ] ; return Array . isArray ( token ) ? token [ 0 ] : token ; } public peek ( pos = this . _index ) { const token = this . tokens [ pos ] ; return Array . isArray ( token ) ? token [ 0 ] : token ; } public get index ( ) { return this . _index ; } public getStr ( start , end ) { return this . str . slice ( start , end ) ; } public offset ( pos = this . _index ) { const token = this . tokens [ pos ] ; return ( token !== undefined ? token [ 1 ] : this . strlen ) as number ; } public endOffset ( pos = this . _index ) { const token = this . tokens [ pos ] ; return ( token !== undefined ? ( token [ 1 ] as number ) + token [ 0 ] . length : this . strlen ) as number ; } public prevEndOffset ( ) { return this . endOffset ( this . _index - 1 ) ; } } function tokenize ( str , saveStr = false ) { const re = /[\s,]*(~@|[\[\]{}()'`~^@#]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/g ; let match = null ; const spaceRe = /^[\s,]*/ ; let spaceMatch = null , spaceOffset = null ; const results = [ ] ; while ( ( match = re . exec ( str ) ) && match [ 1 ] != '' ) { if ( match [ 1 ] [ 0 ] === ';' ) { continue ; } if ( saveStr ) { spaceMatch = spaceRe . exec ( match [ 0 ] ) ; spaceOffset = spaceMatch ? spaceMatch [ 0 ] . length : 0 ; results . push ( [ match [ 1 ] , match . index + spaceOffset ] as [ string , number ] ) ; } else { results . push ( match [ 1 ] ) ; } } return results ; } function readAtom ( reader ) { const token = reader . next ( ) ; if ( typeof token === 'string' ) { if ( token . match ( /^[-+]?[0-9]+$/ ) ) { return parseInt ( token , 10 ) ; } else if ( token . match ( /^[-+]?([0-9]*\.[0-9]+|[0-9]+)$/ ) ) { return parseFloat ( token ) ; } else if ( token . match ( /^"(?:\\.|[^\\"])*"$/ ) ) { return token . slice ( 1 , token . length - 1 ) . replace ( /\\(.)/g , ( _ , c ) => ( c === 'n' ? '\n' : c ) ) ; } else if ( token [ 0 ] === '"' ) { throw new MalError ( "Expected '\"', got EOF" ) ; } else if ( token [ 0 ] === ':' ) { return keywordFor ( token . slice ( 1 ) ) ; } else if ( token === 'nil' ) { return null ; } else if ( token === 'true' ) { return true ; } else if ( token === 'false' ) { return false ; } else if ( /^NaN$|^-?Infinity$/ . test ( token ) ) { return parseFloat ( token ) ; } else { return S ( token as string ) ; } } else { return token ; } } function readVector ( reader , saveStr , start = '[' , end = ']' ) { const exp = [ ] ; let elmStrs = null , delimiters = null ; let token = reader . next ( ) ; if ( token !== start ) { throw new MalError ( `Expected '${ start }'` ) ; } if ( saveStr ) { elmStrs = [ ] ; delimiters = [ ] ; } let elmStart = 0 ; while ( ( token = reader . peek ( ) ) !== end ) { if ( ! token ) { throw new MalError ( `Expected '${ end }', got EOF` ) ; } if ( saveStr ) { const delimiter = reader . getStr ( reader . prevEndOffset ( ) , reader . offset ( ) ) ; delimiters ?. push ( delimiter ) ; elmStart = reader . offset ( ) ; } exp . push ( readForm ( reader , saveStr ) ) ; if ( saveStr ) { const elm = reader . getStr ( elmStart , reader . prevEndOffset ( ) ) ; elmStrs ?. push ( elm ) ; } } if ( saveStr ) { const delimiter = reader . getStr ( reader . prevEndOffset ( ) , reader . offset ( ) ) ; delimiters ?. push ( delimiter ) ; exp [ M_DELIMITERS ] = delimiters ; exp [ M_ELMSTRS ] = elmStrs ; } reader . next ( ) ; return exp ; } function readList ( reader , saveStr ) { const exp = readVector ( reader , saveStr , '(' , ')' ) ; ( exp as MalSeq ) [ M_ISLIST ] = true ; return exp ; } function readHashMap ( reader , saveStr ) { const lst = readVector ( reader , saveStr , '{' , '}' ) ; const map = assocBang ( { } , ... lst ) as MalNodeMap ; if ( saveStr ) { const elmStrs = [ ] ; for ( let i = 0 ; i < lst . length ; i += 2 ) { elmStrs . push ( lst [ M_ELMSTRS ] [ i ] , lst [ M_ELMSTRS ] [ i + 1 ] ) ; } map [ M_ELMSTRS ] = elmStrs ; map [ M_DELIMITERS ] = lst [ M_DELIMITERS ] ; } return map ; } function readForm ( reader , saveStr ) { let val ; const startIdx = reader . index ; let sugar = null ; switch ( reader . peek ( ) ) { case ';' : val = null ; break ; case "'" : reader . next ( ) ; if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; val = L ( S_QUOTE , readForm ( reader , saveStr ) ) ; break ; case '`' : reader . next ( ) ; if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; val = L ( S_QUASIQUOTE , readForm ( reader , saveStr ) ) ; break ; case '~' : reader . next ( ) ; if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; val = L ( S_UNQUOTE , readForm ( reader , saveStr ) ) ; break ; case '~@' : reader . next ( ) ; if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; val = L ( S_SPLICE_UNQUOTE , readForm ( reader , saveStr ) ) ; break ; case '#' : { reader . next ( ) ; const type = reader . peek ( ) ; if ( type === '(' ) { if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; val = L ( S_FN_SUGAR , readForm ( reader , saveStr ) ) ; } else if ( type === '@' ) { reader . next ( ) ; if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; const annotation = readForm ( reader , saveStr ) ; if ( sugar ) sugar . push ( reader . prevEndOffset ( ) , reader . offset ( ) ) ; const expr = readForm ( reader , saveStr ) ; val = L ( S_UI_ANNOTATE , annotation , expr ) ; } else if ( type [ 0 ] === '"' ) { if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; const meta = readForm ( reader , saveStr ) ; if ( sugar ) sugar . push ( reader . prevEndOffset ( ) , reader . offset ( ) ) ; const expr = readForm ( reader , saveStr ) ; val = L ( S ( 'set-id' ) , meta , expr ) ; } break ; } case '^' : { reader . next ( ) ; if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; const meta = readForm ( reader , saveStr ) ; if ( sugar ) sugar . push ( reader . prevEndOffset ( ) , reader . offset ( ) ) ; const expr = readForm ( reader , saveStr ) ; val = L ( S_WITH_META_SUGAR , meta , expr ) ; break ; } case '@' : reader . next ( ) ; if ( saveStr ) sugar = [ reader . prevEndOffset ( ) , reader . offset ( ) ] ; val = L ( S_DEREF , readForm ( reader , saveStr ) ) ; break ; case ')' : throw new MalError ( "unexpected ')'" ) ; case '(' : val = readList ( reader , saveStr ) ; break ; case ']' : throw new Error ( "unexpected ']'" ) ; case '[' : val = readVector ( reader , saveStr ) ; break ; case '}' : throw new Error ( "unexpected '}'" ) ; case '{' : val = readHashMap ( reader , saveStr ) ; break ; default : val = readAtom ( reader ) ; } if ( sugar ) { const annotator = reader . peek ( startIdx ) ; const formEnd = reader . prevEndOffset ( ) ; val [ M_ISSUGAR ] = true ; const delimiters = [ '' ] ; const elmStrs = [ annotator ] ; sugar . push ( formEnd ) ; for ( let i = 0 ; i < sugar . length - 1 ; i += 2 ) { delimiters . push ( reader . getStr ( sugar [ i ] , sugar [ i + 1 ] ) ) ; elmStrs . push ( reader . getStr ( sugar [ i + 1 ] , sugar [ i + 2 ] ) ) ; } delimiters . push ( '' ) ; val [ M_DELIMITERS ] = delimiters ; val [ M_ELMSTRS ] = elmStrs ; } return val ; } export function getRangeOfExp ( exp , root ? ) { function isParent ( parent , child ) { if ( parent === child ) { return true ; } const outer = child [ M_OUTER ] ; if ( ! outer ) { return false ; } else if ( outer === parent ) { return true ; } else { return isParent ( parent , outer ) ; } } function calcOffset ( exp ) { if ( ! exp [ M_OUTER ] || exp === root ) { return 0 ; } const outer = exp [ M_OUTER ] ; let offset = calcOffset ( outer ) ; printExp ( outer ) ; if ( isSeq ( outer ) ) { const index = exp [ M_OUTER_INDEX ] ; offset += ( outer [ M_ISSUGAR ] ? 0 : 1 ) + outer [ M_DELIMITERS ] . slice ( 0 , index + 1 ) . join ( '' ) . length + outer [ M_ELMSTRS ] . slice ( 0 , index ) . join ( '' ) . length ; } else if ( isMap ( outer ) ) { const index = exp [ M_OUTER_INDEX ] ; offset += 1 + outer [ M_DELIMITERS ] . slice ( 0 , ( index + 1 ) * 2 ) . join ( '' ) . length + outer [ M_ELMSTRS ] . slice ( 0 , index * 2 + 1 ) . join ( '' ) . length ; } return offset ; } const isExpOutsideOfParent = root && ! isParent ( root , exp ) ; if ( ! isNode ( exp ) || isExpOutsideOfParent ) { return null ; } const expLength = printExp ( exp , true ) . length ; const offset = calcOffset ( exp ) ; return [ offset , offset + expLength ] ; } export function findExpByRange ( exp , start , end ) { if ( ! isNode ( exp ) ) { return null ; } const expLen = printExp ( exp , true ) . length ; if ( ! ( 0 <= start && end <= expLen ) ) { return null ; } if ( isSeq ( exp ) ) { let offset = exp [ M_ISSUGAR ] ? 0 : 1 ; for ( let i = 0 ; i < exp . length ; i ++ ) { const child = exp [ i ] ; offset += exp [ M_DELIMITERS ] [ i ] . length ; const ret = findExpByRange ( child , start - offset , end - offset ) ; if ( ret !== null ) { return ret ; } if ( i < exp [ M_ELMSTRS ] . length ) { offset += exp [ M_ELMSTRS ] [ i ] . length ; } } } else if ( isMap ( exp ) ) { let offset = 1 ; const keys = Object . keys ( exp ) ; const elmStrs = exp [ M_ELMSTRS ] ; const delimiters = exp [ M_DELIMITERS ] ; for ( let i = 0 ; i < keys . length ; i ++ ) { const child = exp [ keys [ i ] ] ; offset += delimiters [ i * 2 ] . length + elmStrs [ i * 2 ] . length + delimiters [ i * 2 + 1 ] . length ; const ret = findExpByRange ( child , start - offset , end - offset ) ; if ( ret !== null ) { return ret ; } offset += elmStrs [ i * 2 + 1 ] . length ; } } return exp ; } export function convertJSObjectToMalMap ( obj ) { if ( Array . isArray ( obj ) ) { const ret = obj . map ( v => convertJSObjectToMalMap ( v ) ) ; return ret ; } else if ( isSymbol ( obj ) || obj instanceof Function ) { return obj ; } else if ( obj instanceof Object ) { const ret = { } ; for ( const [ key , value ] of Object . entries ( obj ) ) { ret [ keywordFor ( key ) ] = convertJSObjectToMalMap ( value ) ; } return ret ; } else { return obj ; } } export function convertMalNodeToJSObject ( exp ) { if ( isMap ( exp ) ) { const ret = { } ; for ( const [ key , value ] of Object . entries ( exp ) ) { const jsKey = getName ( key ) ; ret [ jsKey ] = convertMalNodeToJSObject ( value ) ; } return ret ; } else if ( isSeq ( exp ) ) { return ( exp as MalVal [ ] ) . map ( e => convertMalNodeToJSObject ( e ) ) ; } else { return exp ; } } export class BlankException extends Error { } export function reconstructTree ( exp ) { if ( ! isNode ( exp ) ) { return ; } else { if ( isMap ( exp ) ) { const keys = Object . keys ( exp ) ; keys . forEach ( ( key , i ) => { const e = exp [ key ] ; if ( isNode ( e ) ) { e [ M_OUTER ] = exp ; e [ M_OUTER_INDEX ] = i ; reconstructTree ( e ) ; } } ) ; } else { exp . forEach ( ( e , i ) => { if ( isNode ( e ) ) { e [ M_OUTER ] = exp ; e [ M_OUTER_INDEX ] = i ; reconstructTree ( e ) ; } } ) ; } } } export default function readStr ( str , saveStr = true ) { const tokens = tokenize ( str , saveStr ) as string [ ] ; if ( tokens . length === 0 ) { throw new BlankException ( ) ; } const reader = new Reader ( tokens , str ) ; const exp = readForm ( reader , saveStr ) ; if ( reader . index < tokens . length - 1 ) { throw new MalError ( 'Invalid end of file' ) ; } if ( saveStr ) { saveOuter ( exp , null ) ; } return exp ; function saveOuter ( exp , outer , index ? ) { if ( isNode ( exp ) && ! ( M_OUTER in exp ) ) { if ( isNode ( outer ) && index !== undefined ) { exp [ M_OUTER ] = outer ; exp [ M_OUTER_INDEX ] = index ; } const children = Array . isArray ( exp ) ? exp : isMap ( exp ) ? Object . keys ( exp ) . map ( k => exp [ k ] ) : null ; if ( children ) { children . forEach ( ( child , index ) => saveOuter ( child , exp , index ) ) ; } } } }