You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

716 lines
14 KiB

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;