import { op , VAL_TYPE , BLOCK , IS_ZEROISH , IS_NOT_ZEROISH } from "./encoding" ; import shims from "./shims" ; import { createUserError , createCompilerError } from "./errorUtils" ; import { Ast , CompilerContext , AssignmentExpressionAstNode } from "./types" ; import { localFuncMap } from "./wasmFunctions" ; import { flatten , arrayJoin } from "./utils" ; import { BUFFER_SIZE , MAX_LOOP_COUNT } from "./constants" ; export function emit ( ast , context ) { switch ( ast . type ) { case "SCRIPT" : { const body = ast . body . map ( ( statement , i ) => { return [ ... emit ( statement , context ) , op . drop ] ; } ) ; return flatten ( body ) ; } case "EXPRESSION_BLOCK" : { return emitExpressionBlock ( ast . body , context ) ; } case "BINARY_EXPRESSION" : { const left = emit ( ast . left , context ) ; const right = emit ( ast . right , context ) ; const operatorToOps = { "+" : [ op . f64_add ] , "-" : [ op . f64_sub ] , "*" : [ op . f64_mul ] , "/" : context . resolveFunc ( "div" ) , "%" : context . resolveFunc ( "mod" ) , "|" : context . resolveFunc ( "bitwiseOr" ) , "&" : context . resolveFunc ( "bitwiseAnd" ) , "^" : context . resolveFunc ( "pow" ) , "==" : [ op . f64_sub , ... IS_ZEROISH , op . f64_convert_i32_s ] , "!=" : [ op . f64_sub , ... IS_NOT_ZEROISH , op . f64_convert_i32_s ] , "<" : [ op . f64_lt , op . f64_convert_i32_s ] , ">" : [ op . f64_gt , op . f64_convert_i32_s ] , "<=" : [ op . f64_le , op . f64_convert_i32_s ] , ">=" : [ op . f64_ge , op . f64_convert_i32_s ] , } ; const code = operatorToOps [ ast . operator ] ; if ( code == null ) { throw createCompilerError ( `Unknown binary expression operator ${ ast . operator }` , ast . loc , context . rawSource ) ; } return [ ... left , ... right , ... code ] ; } case "CALL_EXPRESSION" : { const functionName = ast . callee . value ; const argList = ast . arguments ; const assertArity = ( arity ) => { if ( argList . length < arity ) { throw createUserError ( `Too few arguments passed to \`${ functionName }()\`. Expected ${ arity } but only got ${ argList . length }.` , ast . loc , context . rawSource ) ; } if ( argList . length > arity ) { throw createUserError ( `Too many arguments passed to \`${ functionName }()\`. Expected ${ arity } but got ${ argList . length }.` , argList [ arity ] . loc , context . rawSource ) ; } } ; switch ( functionName ) { case "exec2" : assertArity ( 2 ) ; return emitExpressionBlock ( ast . arguments , context ) ; case "exec3" : assertArity ( 3 ) ; return emitExpressionBlock ( ast . arguments , context ) ; case "if" : assertArity ( 3 ) ; const [ test , consequent , alternate ] = ast . arguments ; return emitConditional ( test , consequent , alternate , context ) ; case "while" : assertArity ( 1 ) ; return emitWhile ( ast . arguments [ 0 ] , context ) ; case "loop" : assertArity ( 2 ) ; return emitLoop ( ast . arguments [ 0 ] , ast . arguments [ 1 ] , context ) ; case "megabuf" : case "gmegabuf" : assertArity ( 1 ) ; const index = context . resolveLocal ( VAL_TYPE . i32 ) ; return [ ... emit ( ast . arguments [ 0 ] , context ) , ... ( context . resolveFunc ( "_getBufferIndex" ) ?? [ ] ) , ... op . local_tee ( index ) , ... op . i32_const ( - 1 ) , op . i32_ne , ... op . if ( BLOCK . f64 ) , ... op . local_get ( index ) , ... op . f64_load ( 3 , emitAddMemoryOffset ( functionName ) ) , op . else , ... op . f64_const ( 0 ) , op . end , ] ; case "assign" : assertArity ( 2 ) ; const variableIdentifier = ast . arguments [ 0 ] ; if ( variableIdentifier . type != "IDENTIFIER" ) { throw createUserError ( "Expected the first argument of `assign()` to be an identifier." , variableIdentifier . loc , context . rawSource ) ; } const resolvedName = context . resolveVar ( variableIdentifier . value ) ; return [ ... emit ( ast . arguments [ 1 ] , context ) , ... op . global_set ( resolvedName ) , ... op . global_get ( resolvedName ) , ] ; } const args = flatten ( ast . arguments . map ( node => emit ( node , context ) ) ) ; switch ( functionName ) { case "abs" : assertArity ( 1 ) ; return [ ... args , op . f64_abs ] ; case "sqrt" : assertArity ( 1 ) ; return [ ... args , op . f64_abs , op . f64_sqrt ] ; case "int" : assertArity ( 1 ) ; return [ ... args , op . f64_floor ] ; case "min" : assertArity ( 2 ) ; return [ ... args , op . f64_min ] ; case "max" : assertArity ( 2 ) ; return [ ... args , op . f64_max ] ; case "above" : assertArity ( 2 ) ; return [ ... args , op . f64_gt , op . f64_convert_i32_s ] ; case "below" : assertArity ( 2 ) ; return [ ... args , op . f64_lt , op . f64_convert_i32_s ] ; case "equal" : assertArity ( 2 ) ; return [ ... args , op . f64_sub , ... IS_ZEROISH , op . f64_convert_i32_s ] ; case "bnot" : assertArity ( 1 ) ; return [ ... args , ... IS_ZEROISH , op . f64_convert_i32_s ] ; case "floor" : assertArity ( 1 ) ; return [ ... args , op . f64_floor ] ; case "ceil" : assertArity ( 1 ) ; return [ ... args , op . f64_ceil ] ; } const invocation = context . resolveFunc ( functionName ) ; if ( invocation == null || functionName . startsWith ( "_" ) ) { throw createUserError ( `"${ functionName }" is not defined.` , ast . callee . loc , context . rawSource ) ; } if ( shims [ functionName ] != null ) { assertArity ( shims [ functionName ] . length ) ; } else if ( localFuncMap [ functionName ] != null ) { assertArity ( localFuncMap [ functionName ] . args . length ) ; } else { throw createCompilerError ( `Missing arity information for the function \`${ functionName }()\`` , ast . callee . loc , context . rawSource ) ; } return [ ... args , ... invocation ] ; } case "ASSIGNMENT_EXPRESSION" : { const { left } = ast ; const rightCode = emit ( ast . right , context ) ; const mutationCode = getAssignmentOperatorMutation ( ast , context ) ; if ( left . type === "IDENTIFIER" ) { const resolvedName = context . resolveVar ( left . value ) ; const get = op . global_get ( resolvedName ) ; const set = op . global_set ( resolvedName ) ; if ( mutationCode === null ) { return [ ... rightCode , ... set , ... get ] ; } return [ ... get , ... rightCode , ... mutationCode , ... set , ... get ] ; } if ( left . type !== "CALL_EXPRESSION" ) { throw createCompilerError ( `Unexpected left hand side type for assignment: ${ left . type }` , ast . loc , context . rawSource ) ; } const localIndex = context . resolveLocal ( VAL_TYPE . i32 ) ; if ( left . arguments . length !== 1 ) { throw createUserError ( `Expected 1 argument when assigning to a buffer but got ${ left . arguments . length }.` , left . arguments . length === 0 ? left . loc : left . arguments [ 1 ] . loc , context . rawSource ) ; } const bufferName = left . callee . value ; if ( bufferName !== "gmegabuf" && bufferName !== "megabuf" ) { throw createUserError ( "The only function calls which may be assigned to are `gmegabuf()` and `megabuf()`." , left . callee . loc , context . rawSource ) ; } const addOffset = emitAddMemoryOffset ( bufferName ) ; if ( mutationCode === null ) { const unnormalizedIndex = context . resolveLocal ( VAL_TYPE . i32 ) ; const rightValue = context . resolveLocal ( VAL_TYPE . f64 ) ; return [ ... rightCode , ... op . local_set ( rightValue ) , ... emit ( left . arguments [ 0 ] , context ) , ... ( context . resolveFunc ( "_getBufferIndex" ) ?? [ ] ) , ... op . local_tee ( unnormalizedIndex ) , ... op . i32_const ( 0 ) , op . i32_lt_s , ... op . if ( BLOCK . f64 ) , ... op . f64_const ( 0 ) , op . else , ... op . local_get ( unnormalizedIndex ) , ... op . local_tee ( localIndex ) , ... op . local_get ( rightValue ) , ... op . f64_store ( 3 , addOffset ) , ... op . local_get ( rightValue ) , op . end , ] ; } const index = context . resolveLocal ( VAL_TYPE . i32 ) ; const inBounds = context . resolveLocal ( VAL_TYPE . i32 ) ; const rightValue = context . resolveLocal ( VAL_TYPE . f64 ) ; const result = context . resolveLocal ( VAL_TYPE . f64 ) ; return [ ... rightCode , ... op . local_set ( rightValue ) , ... emit ( left . arguments [ 0 ] , context ) , ... ( context . resolveFunc ( "_getBufferIndex" ) ?? [ ] ) , ... op . local_tee ( index ) , ... op . i32_const ( - 1 ) , op . i32_ne , ... op . local_tee ( inBounds ) , ... op . if ( BLOCK . f64 ) , ... op . local_get ( index ) , ... op . f64_load ( 3 , addOffset ) , op . else , ... op . f64_const ( 0 ) , op . end , ... op . local_get ( rightValue ) , ... mutationCode , ... op . local_tee ( result ) , ... op . local_get ( inBounds ) , ... op . if ( BLOCK . void ) , ... op . local_get ( index ) , ... op . local_get ( result ) , ... op . f64_store ( 3 , addOffset ) , op . end , ] ; } case "LOGICAL_EXPRESSION" : { const left = emit ( ast . left , context ) ; const right = emit ( ast . right , context ) ; const behaviorMap = { "&&" : { comparison : IS_ZEROISH , shortCircuitValue : 0 , } , "||" : { comparison : IS_NOT_ZEROISH , shortCircuitValue : 1 , } , } ; const behavior = behaviorMap [ ast . operator ] ; if ( behavior == null ) { throw createCompilerError ( `Unknown logical expression operator ${ ast . operator }` , ast . loc , context . rawSource ) ; } const { comparison , shortCircuitValue } = behavior ; return [ ... left , ... comparison , ... op . if ( BLOCK . f64 ) , ... op . f64_const ( shortCircuitValue ) , op . else , ... right , ... IS_NOT_ZEROISH , op . f64_convert_i32_s , op . end , ] ; } case "UNARY_EXPRESSION" : { const value = emit ( ast . value , context ) ; const operatorToCode = { "-" : [ op . f64_neg ] , "+" : [ ] as number [ ] , "!" : [ ... IS_ZEROISH , op . f64_convert_i32_s ] , } ; const code = operatorToCode [ ast . operator ] ; if ( code == null ) { throw createCompilerError ( `Unknown logical unary operator ${ ast . operator }` , ast . loc , context . rawSource ) ; } return [ ... value , ... code ] ; } case "IDENTIFIER" : const variableName = ast . value ; return op . global_get ( context . resolveVar ( variableName ) ) ; case "NUMBER_LITERAL" : return op . f64_const ( ast . value ) ; default : throw createCompilerError ( `Unknown AST node type ${ ast . type }` , ast . loc , context . rawSource ) ; } } function emitExpressionBlock ( body , context ) { const statements = body . map ( ( statement , i ) => { return emit ( statement , context ) ; } ) ; return flatten ( arrayJoin ( statements , [ op . drop ] ) ) ; } function emitWhile ( expression , context ) { const body = emit ( expression , context ) ; const iterationCount = context . resolveLocal ( VAL_TYPE . i32 ) ; return [ ... op . i32_const ( 0 ) , ... op . local_set ( iterationCount ) , ... op . loop ( BLOCK . void ) , ... op . local_get ( iterationCount ) , ... op . i32_const ( 1 ) , op . i32_add , ... op . local_tee ( iterationCount ) , ... op . i32_const ( MAX_LOOP_COUNT ) , op . i32_lt_u , ... body , ... IS_NOT_ZEROISH , op . i32_and , ... op . br_if ( 0 ) , op . end , ... op . f64_const ( 0 ) , ] ; } function emitLoop ( count , expression , context ) { const body = emit ( expression , context ) ; const localIndex = context . resolveLocal ( VAL_TYPE . i32 ) ; return [ ... op . block ( BLOCK . void ) , ... emit ( count , context ) , op . i32_trunc_f64_s , ... op . local_tee ( localIndex ) , ... op . i32_const ( 0 ) , op . i32_le_s , ... op . br_if ( 1 ) , ... op . loop ( BLOCK . void ) , ... body , op . drop , ... op . local_get ( localIndex ) , ... op . i32_const ( 1 ) , op . i32_sub , ... op . local_tee ( localIndex ) , ... op . i32_const ( 0 ) , op . i32_ne , ... op . br_if ( 0 ) , op . end , op . end , ... op . f64_const ( 0 ) , ] ; } function emitConditional ( test , consequent , alternate , context ) { return [ ... emit ( test , context ) , ... IS_NOT_ZEROISH , ... op . if ( BLOCK . f64 ) , ... emit ( consequent , context ) , op . else , ... emit ( alternate , context ) , op . end , ] ; } function emitAddMemoryOffset ( name ) { switch ( name ) { case "gmegabuf" : return BUFFER_SIZE * 8 ; case "megabuf" : return 0 ; } } function getAssignmentOperatorMutation ( ast , context ) { const operatorToCode = { "+=" : [ op . f64_add ] , "-=" : [ op . f64_sub ] , "*=" : [ op . f64_mul ] , "/=" : [ op . f64_div ] , "%=" : context . resolveFunc ( "mod" ) , "=" : null , } ; const operatorCode = operatorToCode [ ast . operator ] ; if ( operatorCode === undefined ) { throw createCompilerError ( `Unknown assignment operator "${ ast . operator }"` , ast . loc , context . rawSource ) ; } return operatorCode ; }