import { REVISION } from 'three'; import { VariableDeclaration, Accessor } from './AST.js'; import * as Nodes from '../nodes/Nodes.js'; const opLib = { '=': 'assign', '+': 'add', '-': 'sub', '*': 'mul', '/': 'div', '%': 'remainder', '<': 'lessThan', '>': 'greaterThan', '<=': 'lessThanEqual', '>=': 'greaterThanEqual', '==': 'equal', '&&': 'and', '||': 'or', '^^': 'xor', '&': 'bitAnd', '|': 'bitOr', '^': 'bitXor', '<<': 'shiftLeft', '>>': 'shiftRight', '+=': 'addAssign', '-=': 'subAssign', '*=': 'mulAssign', '/=': 'divAssign', '%=': 'remainderAssign', '^=': 'bitXorAssign', '&=': 'bitAndAssign', '|=': 'bitOrAssign', '<<=': 'shiftLeftAssign', '>>=': 'shiftRightAssign' }; const unaryLib = { '+': '', // positive '-': 'negate', '~': 'bitNot', '!': 'not', '++': 'increment', // incrementBefore '--': 'decrement' // decrementBefore }; const isPrimitive = ( value ) => /^(true|false|-?\d)/.test( value ); class TSLEncoder { constructor() { this.tab = ''; this.imports = new Set(); this.global = new Set(); this.overloadings = new Map(); this.layoutsCode = ''; this.iife = false; this.uniqueNames = false; this.reference = false; this._currentProperties = {}; this._lastStatement = null; } addImport( name ) { // import only if it's a node name = name.split( '.' )[ 0 ]; if ( Nodes[ name ] !== undefined && this.global.has( name ) === false && this._currentProperties[ name ] === undefined ) { this.imports.add( name ); } } emitUniform( node ) { let code = `const ${ node.name } = `; if ( this.reference === true ) { this.addImport( 'reference' ); this.global.add( node.name ); //code += `reference( '${ node.name }', '${ node.type }', uniforms )`; // legacy code += `reference( 'value', '${ node.type }', uniforms[ '${ node.name }' ] )`; } else { this.addImport( 'uniform' ); this.global.add( node.name ); code += `uniform( '${ node.type }' )`; } return code; } emitExpression( node ) { let code; /*@TODO: else if ( node.isVarying ) { code = this.emitVarying( node ); }*/ if ( node.isAccessor ) { this.addImport( node.property ); code = node.property; } else if ( node.isNumber ) { if ( node.type === 'int' || node.type === 'uint' ) { code = node.type + '( ' + node.value + ' )'; this.addImport( node.type ); } else { code = node.value; } } else if ( node.isString ) { code = '\'' + node.value + '\''; } else if ( node.isOperator ) { const opFn = opLib[ node.type ] || node.type; const left = this.emitExpression( node.left ); const right = this.emitExpression( node.right ); if ( isPrimitive( left ) && isPrimitive( right ) ) { return left + ' ' + node.type + ' ' + right; } if ( isPrimitive( left ) ) { code = opFn + '( ' + left + ', ' + right + ' )'; this.addImport( opFn ); } else { code = left + '.' + opFn + '( ' + right + ' )'; } } else if ( node.isFunctionCall ) { const params = []; for ( const parameter of node.params ) { params.push( this.emitExpression( parameter ) ); } this.addImport( node.name ); const paramsStr = params.length > 0 ? ' ' + params.join( ', ' ) + ' ' : ''; code = `${ node.name }(${ paramsStr })`; } else if ( node.isReturn ) { code = 'return'; if ( node.value ) { code += ' ' + this.emitExpression( node.value ); } } else if ( node.isAccessorElements ) { code = node.property; for ( const element of node.elements ) { if ( element.isStaticElement ) { code += '.' + this.emitExpression( element.value ); } else if ( element.isDynamicElement ) { const value = this.emitExpression( element.value ); if ( isPrimitive( value ) ) { code += `[ ${ value } ]`; } else { code += `.element( ${ value } )`; } } } } else if ( node.isDynamicElement ) { code = this.emitExpression( node.value ); } else if ( node.isStaticElement ) { code = this.emitExpression( node.value ); } else if ( node.isFor ) { code = this.emitFor( node ); } else if ( node.isVariableDeclaration ) { code = this.emitVariables( node ); } else if ( node.isUniform ) { code = this.emitUniform( node ); } else if ( node.isTernary ) { code = this.emitTernary( node ); } else if ( node.isConditional ) { code = this.emitConditional( node ); } else if ( node.isUnary && node.expression.isNumber ) { code = node.type + ' ' + node.expression.value; } else if ( node.isUnary ) { let type = unaryLib[ node.type ]; if ( node.after === false && ( node.type === '++' || node.type === '--' ) ) { type += 'Before'; } const exp = this.emitExpression( node.expression ); if ( isPrimitive( exp ) ) { this.addImport( type ); code = type + '( ' + exp + ' )'; } else { code = exp + '.' + type + '()'; } } else { console.warn( 'Unknown node type', node ); } if ( ! code ) code = '/* unknown statement */'; return code; } emitBody( body ) { this.setLastStatement( null ); let code = ''; this.tab += '\t'; for ( const statement of body ) { code += this.emitExtraLine( statement ); code += this.tab + this.emitExpression( statement ); if ( code.slice( - 1 ) !== '}' ) code += ';'; code += '\n'; this.setLastStatement( statement ); } code = code.slice( 0, - 1 ); // remove the last extra line this.tab = this.tab.slice( 0, - 1 ); return code; } emitTernary( node ) { const condStr = this.emitExpression( node.cond ); const leftStr = this.emitExpression( node.left ); const rightStr = this.emitExpression( node.right ); this.addImport( 'cond' ); return `cond( ${ condStr }, ${ leftStr }, ${ rightStr } )`; } emitConditional( node ) { const condStr = this.emitExpression( node.cond ); const bodyStr = this.emitBody( node.body ); let ifStr = `If( ${ condStr }, () => { ${ bodyStr } ${ this.tab }} )`; let current = node; while ( current.elseConditional ) { const elseBodyStr = this.emitBody( current.elseConditional.body ); if ( current.elseConditional.cond ) { const elseCondStr = this.emitExpression( current.elseConditional.cond ); ifStr += `.elseif( ${ elseCondStr }, () => { ${ elseBodyStr } ${ this.tab }} )`; } else { ifStr += `.else( () => { ${ elseBodyStr } ${ this.tab }} )`; } current = current.elseConditional; } this.imports.add( 'If' ); return ifStr; } emitLoop( node ) { const start = this.emitExpression( node.initialization.value ); const end = this.emitExpression( node.condition.right ); const name = node.initialization.name; const type = node.initialization.type; const condition = node.condition.type; const update = node.afterthought.type; const nameParam = name !== 'i' ? `, name: '${ name }'` : ''; const typeParam = type !== 'int' ? `, type: '${ type }'` : ''; const conditionParam = condition !== '<' ? `, condition: '${ condition }'` : ''; const updateParam = update !== '++' ? `, update: '${ update }'` : ''; let loopStr = `loop( { start: ${ start }, end: ${ end + nameParam + typeParam + conditionParam + updateParam } }, ( { ${ name } } ) => {\n\n`; loopStr += this.emitBody( node.body ) + '\n\n'; loopStr += this.tab + '} )'; this.imports.add( 'loop' ); return loopStr; } emitFor( node ) { const { initialization, condition, afterthought } = node; if ( ( initialization && initialization.isVariableDeclaration && initialization.next === null ) && ( condition && condition.left.isAccessor && condition.left.property === initialization.name ) && ( afterthought && afterthought.isUnary ) && ( initialization.name === afterthought.expression.property ) ) { return this.emitLoop( node ); } return this.emitForWhile( node ); } emitForWhile( node ) { const initialization = this.emitExpression( node.initialization ); const condition = this.emitExpression( node.condition ); const afterthought = this.emitExpression( node.afterthought ); this.tab += '\t'; let forStr = '{\n\n' + this.tab + initialization + ';\n\n'; forStr += `${ this.tab }While( ${ condition }, () => {\n\n`; forStr += this.emitBody( node.body ) + '\n\n'; forStr += this.tab + '\t' + afterthought + ';\n\n'; forStr += this.tab + '} )\n\n'; this.tab = this.tab.slice( 0, - 1 ); forStr += this.tab + '}'; this.imports.add( 'While' ); return forStr; } emitVariables( node, isRoot = true ) { const { name, type, value, next } = node; const valueStr = value ? this.emitExpression( value ) : ''; let varStr = isRoot ? 'const ' : ''; varStr += name; if ( value ) { if ( value.isFunctionCall && value.name === type ) { varStr += ' = ' + valueStr; } else { varStr += ` = ${ type }( ${ valueStr } )`; } } else { varStr += ` = ${ type }()`; } if ( node.immutable === false ) { varStr += '.toVar()'; } if ( next ) { varStr += ', ' + this.emitVariables( next, false ); } this.addImport( type ); return varStr; } /*emitVarying( node ) { }*/ emitOverloadingFunction( nodes ) { const { name } = nodes[ 0 ]; this.addImport( 'overloadingFn' ); return `const ${ name } = overloadingFn( [ ${ nodes.map( node => node.name + '_' + nodes.indexOf( node ) ).join( ', ' ) } ] );\n`; } emitFunction( node ) { const { name, type } = node; this._currentProperties = { name: node }; const params = []; const inputs = []; const mutableParams = []; let hasPointer = false; for ( const param of node.params ) { let str = `{ name: '${ param.name }', type: '${ param.type }'`; let name = param.name; if ( param.immutable === false && ( param.qualifier !== 'inout' && param.qualifier !== 'out' ) ) { name = name + '_immutable'; mutableParams.push( param ); } if ( param.qualifier ) { if ( param.qualifier === 'inout' || param.qualifier === 'out' ) { hasPointer = true; } str += ', qualifier: \'' + param.qualifier + '\''; } inputs.push( str + ' }' ); params.push( name ); this._currentProperties[ name ] = param; } for ( const param of mutableParams ) { node.body.unshift( new VariableDeclaration( param.type, param.name, new Accessor( param.name + '_immutable' ) ) ); } const paramsStr = params.length > 0 ? ' [ ' + params.join( ', ' ) + ' ] ' : ''; const bodyStr = this.emitBody( node.body ); let fnName = name; let overloadingNodes = null; if ( this.overloadings.has( name ) ) { const overloadings = this.overloadings.get( name ); if ( overloadings.length > 1 ) { const index = overloadings.indexOf( node ); fnName += '_' + index; if ( index === overloadings.length - 1 ) { overloadingNodes = overloadings; } } } let funcStr = `const ${ fnName } = tslFn( (${ paramsStr }) => { ${ bodyStr } ${ this.tab }} );\n`; const layoutInput = inputs.length > 0 ? '\n\t\t' + this.tab + inputs.join( ',\n\t\t' + this.tab ) + '\n\t' + this.tab : ''; if ( node.layout !== false && hasPointer === false ) { const uniqueName = this.uniqueNames ? fnName + '_' + Math.random().toString( 36 ).slice( 2 ) : fnName; this.layoutsCode += `${ this.tab + fnName }.setLayout( { ${ this.tab }\tname: '${ uniqueName }', ${ this.tab }\ttype: '${ type }', ${ this.tab }\tinputs: [${ layoutInput }] ${ this.tab }} );\n\n`; } this.imports.add( 'tslFn' ); this.global.add( node.name ); if ( overloadingNodes !== null ) { funcStr += '\n' + this.emitOverloadingFunction( overloadingNodes ); } return funcStr; } setLastStatement( statement ) { this._lastStatement = statement; } emitExtraLine( statement ) { const last = this._lastStatement; if ( last === null ) return ''; if ( statement.isReturn ) return '\n'; const isExpression = ( st ) => st.isFunctionDeclaration !== true && st.isFor !== true && st.isConditional !== true; const lastExp = isExpression( last ); const currExp = isExpression( statement ); if ( lastExp !== currExp || ( ! lastExp && ! currExp ) ) return '\n'; return ''; } emit( ast ) { let code = '\n'; if ( this.iife ) this.tab += '\t'; const overloadings = this.overloadings; for ( const statement of ast.body ) { if ( statement.isFunctionDeclaration ) { if ( overloadings.has( statement.name ) === false ) { overloadings.set( statement.name, [] ); } overloadings.get( statement.name ).push( statement ); } } for ( const statement of ast.body ) { code += this.emitExtraLine( statement ); if ( statement.isFunctionDeclaration ) { code += this.tab + this.emitFunction( statement ); } else { code += this.tab + this.emitExpression( statement ) + ';\n'; } this.setLastStatement( statement ); } const imports = [ ...this.imports ]; const exports = [ ...this.global ]; const layouts = this.layoutsCode.length > 0 ? `\n${ this.tab }// layouts\n\n` + this.layoutsCode : ''; let header = '// Three.js Transpiler r' + REVISION + '\n\n'; let footer = ''; if ( this.iife ) { header += '( function ( TSL, uniforms ) {\n\n'; header += imports.length > 0 ? '\tconst { ' + imports.join( ', ' ) + ' } = TSL;\n' : ''; footer += exports.length > 0 ? '\treturn { ' + exports.join( ', ' ) + ' };\n' : ''; footer += '\n} );'; } else { header += imports.length > 0 ? 'import { ' + imports.join( ', ' ) + ' } from \'three/nodes\';\n' : ''; footer += exports.length > 0 ? 'export { ' + exports.join( ', ' ) + ' };\n' : ''; } return header + code + layouts + footer; } } export default TSLEncoder;