Skip to content

⬅️ Back to Table of Contents

📄 WGSLEncoder.js

📊 Analysis Summary

Metric Count
🔧 Functions 13
🧱 Classes 1
📦 Imports 4
📊 Variables & Constants 39

📚 Table of Contents

🛠️ File Location:

📂 examples/jsm/transpiler/WGSLEncoder.js

📦 Imports

Name Source
REVISION three/webgpu
VariableDeclaration ./AST.js
Accessor ./AST.js
isExpression ./TranspilerUtils.js

Variables & Constants

Name Type Kind Value Exported
typeMap { float: string; int: string; uint: s... let/var { 'float': 'f32', 'int': 'i32', 'uint': 'u32', 'bool': 'bool', 'vec2': 'vec2f...
wgslLib { abs: string; acos: string; asin: st... let/var { 'abs': 'abs', 'acos': 'acos', 'asin': 'asin', 'atan': 'atan', 'atan2': 'ata...
code any let/var *not shown*
fnName any let/var wgslLib[ node.name ] \|\| node.name
modFnName string let/var 'mod_' + types.join( '_' )
op "-" \| "+" let/var node.type === '++' ? '+' : '-'
wgslFn any let/var wgslLib[ node.name ]
samplerName string let/var ${textureName}_sampler
code any let/var *not shown*
lodFetch any let/var node.params.length > 2 ? this.emitExpression( node.params[ 2 ] ) : '0'
code string let/var ''
ifStr any let/var if ( ${ condStr } ) {\n\n${ bodyStr }\n\n${ this.tab }}
current any let/var node
switchStr any let/var switch ( ${ discriminant } ) {\n\n
declarations any[] let/var []
current any let/var node
valueStr string let/var ''
keyword any let/var *not shown*
name any let/var node.name
params any[] let/var []
body any[] let/var [ ...node.body ]
paramName any let/var param.name
immutableParamName string let/var ${paramName}_in
immutableAccessor Accessor let/var new Accessor( immutableParamName )
mutableVar VariableDeclaration let/var new VariableDeclaration( param.type, param.name, immutableAccessor )
paramsStr string let/var params.length > 0 ? ' ' + params.join( ', ' ) + ' ' : ''
returnStr string let/var ( returnType && returnType !== 'void' ) ? -> ${returnType}: ''
previous any let/var body[ index - 1 ]
next any let/var body[ index + 1 ]
output string let/var ''
previous any let/var body[ index - 1 ]
header string let/var '// Three.js Transpiler r' + REVISION + '\n\n'
globals string let/var ''
functions string let/var ''
dependencies string let/var ''
bindingIndex number let/var 0
uniformStructMembers any[] let/var []
textureGlobals any[] let/var []
location number let/var 0

Functions

WGSLEncoder.getWgslType(type: any): any

Parameters:

  • type any

Returns: any

Code
getWgslType( type ) {

        return typeMap[ type ] || type;

    }

WGSLEncoder.emitExpression(node: any): any

Parameters:

  • node any

Returns: any

Calls:

  • this.uniforms.find
  • uniform.type.includes
  • code.includes
  • this.emitExpression
  • node.params.map
  • p.getType
  • types.join
  • this.polyfills.has
  • this.polyfills.set
  • this.getWgslType
  • snippets.join
  • fnName.startsWith
  • this.emitTextureAccess
  • params.join
  • this.emitFor
  • this.emitWhile
  • this.emitSwitch
  • this.emitVariables
  • this.uniforms.push
  • this.varyings.push
  • this.emitConditional
  • console.warn

Internal Comments:

// Check if this accessor is part of a uniform struct (x2)
// WGSL requires floating point numbers to have a decimal
// Handle texture functions separately due to sampler handling (x3)
// Handle type constructors like vec3(...) (x3)
// WGSL's equivalent to the ternary operator is select(false_val, true_val, condition) (x3)

Code
emitExpression( node ) {

        if ( ! node ) return '';

        let code;

        if ( node.isAccessor ) {

            // Check if this accessor is part of a uniform struct
            const uniform = this.uniforms.find( u => u.name === node.property );

            if ( uniform && ! uniform.type.includes( 'texture' ) ) {

                return `uniforms.${node.property}`;

            }

            code = node.property;

        } else if ( node.isNumber ) {

            code = node.value;

            // WGSL requires floating point numbers to have a decimal
            if ( node.type === 'float' && ! code.includes( '.' ) ) {

                code += '.0';

            }

        } else if ( node.isOperator ) {

            const left = this.emitExpression( node.left );
            const right = this.emitExpression( node.right );

            code = `${ left } ${ node.type } ${ right }`;

            if ( node.parent.isAssignment !== true && node.parent.isOperator ) {

                code = `( ${ code } )`;

            }

        } else if ( node.isFunctionCall ) {

            const fnName = wgslLib[ node.name ] || node.name;

            if ( fnName === 'mod' ) {

                const snippets = node.params.map( p => this.emitExpression( p ) );
                const types = node.params.map( p => p.getType() );

                const modFnName = 'mod_' + types.join( '_' );

                if ( this.polyfills.has( modFnName ) === false ) {

                    this.polyfills.set( modFnName, `fn ${ modFnName }( x: ${ this.getWgslType( types[ 0 ] ) }, y: ${ this.getWgslType( types[ 1 ] ) } ) -> ${ this.getWgslType( types[ 0 ] ) } {

    return x - y * floor( x / y );

}` );

                }

                code = `${ modFnName }( ${ snippets.join( ', ' ) } )`;

            } else if ( fnName.startsWith( 'texture' ) ) {

                // Handle texture functions separately due to sampler handling

                code = this.emitTextureAccess( node );

            } else {

                const params = node.params.map( p => this.emitExpression( p ) );

                if ( typeMap[ fnName ] ) {

                    // Handle type constructors like vec3(...)

                    code = this.getWgslType( fnName );

                } else {

                    code = fnName;

                }

                if ( params.length > 0 ) {

                    code += '( ' + params.join( ', ' ) + ' )';

                } else {

                    code += '()';

                }

            }

        } else if ( node.isReturn ) {

            code = 'return';

            if ( node.value ) {

                code += ' ' + this.emitExpression( node.value );

            }

        } else if ( node.isDiscard ) {

            code = 'discard';

        } else if ( node.isBreak ) {

            if ( node.parent.isSwitchCase !== true ) {

                code = 'break';

            }

        } else if ( node.isContinue ) {

            code = 'continue';

        } else if ( node.isAccessorElements ) {

            code = this.emitExpression( node.object );

            for ( const element of node.elements ) {

                const value = this.emitExpression( element.value );

                if ( element.isStaticElement ) {

                    code += '.' + value;

                } else if ( element.isDynamicElement ) {

                    code += `[${value}]`;

                }

            }

        } else if ( node.isFor ) {

            code = this.emitFor( node );

        } else if ( node.isWhile ) {

            code = this.emitWhile( node );

        } else if ( node.isSwitch ) {

            code = this.emitSwitch( node );

        } else if ( node.isVariableDeclaration ) {

            code = this.emitVariables( node );

        } else if ( node.isUniform ) {

            this.uniforms.push( node );
            return ''; // Defer emission to the header

        } else if ( node.isVarying ) {

            this.varyings.push( node );
            return ''; // Defer emission to the header

        } else if ( node.isTernary ) {

            const cond = this.emitExpression( node.cond );
            const left = this.emitExpression( node.left );
            const right = this.emitExpression( node.right );

            // WGSL's equivalent to the ternary operator is select(false_val, true_val, condition)
            code = `select( ${ right }, ${ left }, ${ cond } )`;

        } else if ( node.isConditional ) {

            code = this.emitConditional( node );

        } else if ( node.isUnary ) {

            const expr = this.emitExpression( node.expression );

            if ( node.type === '++' || node.type === '--' ) {

                const op = node.type === '++' ? '+' : '-';

                code = `${ expr } = ${ expr } ${ op } 1`;

            } else {

                code = `${ node.type }${ expr }`;

            }

        } else {

            console.warn( 'Unknown node type in WGSL Encoder:', node );

            code = `/* unknown node: ${ node.constructor.name } */`;

        }

        return code;

    }

WGSLEncoder.emitTextureAccess(node: any): any

Parameters:

  • node any

Returns: any

Calls:

  • this.emitExpression

Internal Comments:

// WGSL requires explicit samplers. We assume a naming convention. (x2)
// format: textureSample(texture, sampler, coords, [offset]) (x3)
// Handle optional bias parameter (note: WGSL uses textureSampleBias)
// format: textureSampleLevel(texture, sampler, coords, level) (x2)
// format: textureSampleGrad(texture, sampler, coords, ddx, ddy) (x2)
// format: textureLoad(texture, coords, [level]) (x2)

Code
emitTextureAccess( node ) {

        const wgslFn = wgslLib[ node.name ];
        const textureName = this.emitExpression( node.params[ 0 ] );
        const uv = this.emitExpression( node.params[ 1 ] );

        // WGSL requires explicit samplers. We assume a naming convention.
        const samplerName = `${textureName}_sampler`;

        let code;

        switch ( node.name ) {

            case 'texture':
            case 'texture2D':
            case 'texture3D':
            case 'textureCube':
                // format: textureSample(texture, sampler, coords, [offset])
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}`;
                // Handle optional bias parameter (note: WGSL uses textureSampleBias)
                if ( node.params.length === 3 ) {

                    const bias = this.emitExpression( node.params[ 2 ] );
                    code = `textureSampleBias(${textureName}, ${samplerName}, ${uv}, ${bias})`;

                } else {

                    code += ')';

                }

                break;

            case 'textureLod':
                // format: textureSampleLevel(texture, sampler, coords, level)
                const lod = this.emitExpression( node.params[ 2 ] );
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}, ${lod})`;
                break;

            case 'textureGrad':
                // format: textureSampleGrad(texture, sampler, coords, ddx, ddy)
                const ddx = this.emitExpression( node.params[ 2 ] );
                const ddy = this.emitExpression( node.params[ 3 ] );
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}, ${ddx}, ${ddy})`;
                break;

            case 'texelFetch':
                // format: textureLoad(texture, coords, [level])
                const coords = this.emitExpression( node.params[ 1 ] ); // should be ivec
                const lodFetch = node.params.length > 2 ? this.emitExpression( node.params[ 2 ] ) : '0';
                code = `${wgslFn}(${textureName}, ${coords}, ${lodFetch})`;
                break;

            default:
                code = `/* unsupported texture op: ${node.name} */`;

        }

        return code;

    }

WGSLEncoder.emitBody(body: any): string

Parameters:

  • body any

Returns: string

Calls:

  • this.emitExtraLine
  • this.emitComment
  • this.emitExpression
  • statementCode.endsWith
  • this.tab.slice
  • code.slice
Code
emitBody( body ) {

        let code = '';
        this.tab += '\t';

        for ( const statement of body ) {

            code += this.emitExtraLine( statement, body );

            if ( statement.isComment ) {

                code += this.emitComment( statement, body );
                continue;

            }

            const statementCode = this.emitExpression( statement );

            if ( statementCode ) {

                code += this.tab + statementCode;

                if ( ! statementCode.endsWith( '}' ) && ! statementCode.endsWith( '{' ) ) {

                    code += ';';

                }

                code += '\n';

            }

        }

        this.tab = this.tab.slice( 0, - 1 );
        return code.slice( 0, - 1 ); // remove the last extra line

    }

WGSLEncoder.emitConditional(node: any): any

Parameters:

  • node any

Returns: any

Calls:

  • this.emitExpression
  • this.emitBody
Code
emitConditional( node ) {

        const condStr = this.emitExpression( node.cond );
        const bodyStr = this.emitBody( node.body );

        let ifStr = `if ( ${ condStr } ) {\n\n${ bodyStr }\n\n${ this.tab }}`;

        let current = node;

        while ( current.elseConditional ) {

            current = current.elseConditional;
            const elseBodyStr = this.emitBody( current.body );

            if ( current.cond ) { // This is an 'else if'

                const elseCondStr = this.emitExpression( current.cond );

                ifStr += ` else if ( ${ elseCondStr } ) {\n\n${ elseBodyStr }\n\n${ this.tab }}`;

            } else { // This is an 'else'

                ifStr += ` else {\n\n${ elseBodyStr }\n\n${ this.tab }}`;

            }

        }

        return ifStr;

    }

WGSLEncoder.emitFor(node: any): any

Parameters:

  • node any

Returns: any

Calls:

  • this.emitExpression
  • this.emitBody
Code
emitFor( node ) {

        const init = this.emitExpression( node.initialization );
        const cond = this.emitExpression( node.condition );
        const after = this.emitExpression( node.afterthought );
        const body = this.emitBody( node.body );

        return `for ( ${ init }; ${ cond }; ${ after } ) {\n\n${ body }\n\n${ this.tab }}`;

    }

WGSLEncoder.emitWhile(node: any): any

Parameters:

  • node any

Returns: any

Calls:

  • this.emitExpression
  • this.emitBody
Code
emitWhile( node ) {

        const cond = this.emitExpression( node.condition );
        const body = this.emitBody( node.body );

        return `while ( ${ cond } ) {\n\n${ body }\n\n${ this.tab }}`;

    }

WGSLEncoder.emitSwitch(node: any): any

Parameters:

  • node any

Returns: any

Calls:

  • this.emitExpression
  • this.emitBody
  • switchCase.conditions.map( c => this.emitExpression( c ) ).join
  • this.tab.slice
Code
emitSwitch( node ) {

        const discriminant = this.emitExpression( node.discriminant );

        let switchStr = `switch ( ${ discriminant } ) {\n\n`;

        this.tab += '\t';

        for ( const switchCase of node.cases ) {

            const body = this.emitBody( switchCase.body );

            if ( switchCase.isDefault ) {

                switchStr += `${ this.tab }default: {\n\n${ body }\n\n${ this.tab }}\n\n`;

            } else {

                const cases = switchCase.conditions.map( c => this.emitExpression( c ) ).join( ', ' );

                switchStr += `${ this.tab }case ${ cases }: {\n\n${ body }\n\n${ this.tab }}\n\n`;

            }

        }

        this.tab = this.tab.slice( 0, - 1 );

        switchStr += `${this.tab}}`;

        return switchStr;

    }

WGSLEncoder.emitVariables(node: any): string

Parameters:

  • node any

Returns: string

Calls:

  • this.getWgslType
  • this.emitExpression
  • declarations.push
  • declarations.join

Internal Comments:

// The AST linker tracks if a variable is ever reassigned. (x2)
// If so, use 'var'; otherwise, use 'let'. (x2)
// In WGSL, multiple declarations in one line are not supported, so join with semicolons.

Code
emitVariables( node ) {

        const declarations = [];

        let current = node;

        while ( current ) {

            const type = this.getWgslType( current.type );

            let valueStr = '';

            if ( current.value ) {

                valueStr = ` = ${this.emitExpression( current.value )}`;

            }

            // The AST linker tracks if a variable is ever reassigned.
            // If so, use 'var'; otherwise, use 'let'.

            let keyword;

            if ( current.linker ) {

                if ( current.linker.assignments.length > 0 ) {

                    keyword = 'var'; // Reassigned variable

                } else {

                    if ( current.value && current.value.isNumericExpression ) {

                        keyword = 'const'; // Immutable numeric expression

                    } else {

                        keyword = 'let'; // Immutable variable

                    }

                }

            }

            declarations.push( `${ keyword } ${ current.name }: ${ type }${ valueStr }` );

            current = current.next;

        }

        // In WGSL, multiple declarations in one line are not supported, so join with semicolons.
        return declarations.join( ';\n' + this.tab );

    }

WGSLEncoder.emitFunction(node: any): string

Parameters:

  • node any

Returns: string

Calls:

  • this.getWgslType
  • params.push
  • body.unshift
  • params.join
  • this.emitBody

Internal Comments:

// We will prepend to a copy of the body, not the original AST node. (x2)
// Handle 'inout' and 'out' qualifiers using pointers. They are already mutable.
// If the parameter is reassigned within the function, we need to
// create a local, mutable variable that shadows the parameter's name.
// 1. Rename the incoming parameter to avoid name collision. (x2)
// 2. Create a new Accessor node for the renamed immutable parameter. (x2)
// 3. Create a new VariableDeclaration node for the mutable local variable. (x2)
// This new variable will have the original parameter's name. (x2)
// 4. Mark this new variable as mutable so `emitVariables` uses `var`. (x4)
// 5. Prepend this new declaration to the function's body. (x4)
// This parameter is not reassigned, so treat it as a normal immutable parameter. (x4)
// Emit the function body, which now includes our injected variable declarations. (x2)

Code
emitFunction( node ) {

        const name = node.name;
        const returnType = this.getWgslType( node.type );

        const params = [];
        // We will prepend to a copy of the body, not the original AST node.
        const body = [ ...node.body ];

        for ( const param of node.params ) {

            const paramName = param.name;
            let paramType = this.getWgslType( param.type );

            // Handle 'inout' and 'out' qualifiers using pointers. They are already mutable.
            if ( param.qualifier === 'inout' || param.qualifier === 'out' ) {

                paramType = `ptr<function, ${paramType}>`;
                params.push( `${paramName}: ${paramType}` );
                continue;

            }

            // If the parameter is reassigned within the function, we need to
            // create a local, mutable variable that shadows the parameter's name.
            if ( param.linker && param.linker.assignments.length > 0 ) {

                // 1. Rename the incoming parameter to avoid name collision.
                const immutableParamName = `${paramName}_in`;
                params.push( `${immutableParamName}: ${paramType}` );

                // 2. Create a new Accessor node for the renamed immutable parameter.
                const immutableAccessor = new Accessor( immutableParamName );
                immutableAccessor.isAccessor = true;
                immutableAccessor.property = immutableParamName;

                // 3. Create a new VariableDeclaration node for the mutable local variable.
                // This new variable will have the original parameter's name.
                const mutableVar = new VariableDeclaration( param.type, param.name, immutableAccessor );

                // 4. Mark this new variable as mutable so `emitVariables` uses `var`.
                mutableVar.linker = { assignments: [ true ] };

                // 5. Prepend this new declaration to the function's body.
                body.unshift( mutableVar );

            } else {

                // This parameter is not reassigned, so treat it as a normal immutable parameter.
                params.push( `${paramName}: ${paramType}` );

            }

        }

        const paramsStr = params.length > 0 ? ' ' + params.join( ', ' ) + ' ' : '';
        const returnStr = ( returnType && returnType !== 'void' ) ? ` -> ${returnType}` : '';

        // Emit the function body, which now includes our injected variable declarations.
        const bodyStr = this.emitBody( body );

        return `fn ${name}(${paramsStr})${returnStr} {\n\n${bodyStr}\n\n${this.tab}}`;

    }

WGSLEncoder.emitComment(statement: any, body: any): string

Parameters:

  • statement any
  • body any

Returns: string

Calls:

  • body.indexOf
  • isExpression (from ./TranspilerUtils.js)
  • statement.comment.replace
Code
emitComment( statement, body ) {

        const index = body.indexOf( statement );
        const previous = body[ index - 1 ];
        const next = body[ index + 1 ];

        let output = '';

        if ( previous && isExpression( previous ) ) {

            output += '\n';

        }

        output += this.tab + statement.comment.replace( /\n/g, '\n' + this.tab ) + '\n';

        if ( next && isExpression( next ) ) {

            output += '\n';

        }

        return output;

    }

WGSLEncoder.emitExtraLine(statement: any, body: any): "" | "\n"

Parameters:

  • statement any
  • body any

Returns: "" | "\n"

Calls:

  • body.indexOf
  • isExpression (from ./TranspilerUtils.js)
Code
emitExtraLine( statement, body ) {

        const index = body.indexOf( statement );
        const previous = body[ index - 1 ];

        if ( previous === undefined ) return '';

        if ( statement.isReturn ) return '\n';

        const lastExp = isExpression( previous );
        const currExp = isExpression( statement );

        if ( lastExp !== currExp || ( ! lastExp && ! currExp ) ) return '\n';

        return '';

    }

WGSLEncoder.emit(ast: any): string

Parameters:

  • ast any

Returns: string

Calls:

  • this.functions.set
  • this.uniforms.push
  • this.varyings.push
  • uniform.type.includes
  • textureGlobals.push
  • this.getWgslType
  • uniformStructMembers.push
  • uniformStructMembers.join
  • textureGlobals.join
  • this.emitExtraLine
  • this.emitFunction
  • this.emitComment
  • this.emitExpression
  • this.polyfills.values
  • functions.trimEnd

Internal Comments:

// 1. Pre-process to find all global declarations
// 2. Build resource bindings (uniforms, textures, samplers)
// Textures are declared as separate global variables, not in the UBO
// Create a UBO struct if there are any non-texture uniforms
// Add the texture and sampler globals (x3)
// 3. Build varying structs for stage I/O
// This is a simplification; a full implementation would need to know the shader stage.
// 4. Emit all functions and other global statements
// Handle other top-level statements like 'const' (x3)
// 4. Build dependencies

Code
emit( ast ) {

        const header = '// Three.js Transpiler r' + REVISION + '\n\n';

        let globals = '';
        let functions = '';
        let dependencies = '';

        // 1. Pre-process to find all global declarations
        for ( const statement of ast.body ) {

            if ( statement.isFunctionDeclaration ) {

                this.functions.set( statement.name, statement );

            } else if ( statement.isUniform ) {

                this.uniforms.push( statement );

            } else if ( statement.isVarying ) {

                this.varyings.push( statement );

            }

        }

        // 2. Build resource bindings (uniforms, textures, samplers)
        if ( this.uniforms.length > 0 ) {

            let bindingIndex = 0;
            const uniformStructMembers = [];
            const textureGlobals = [];

            for ( const uniform of this.uniforms ) {

                // Textures are declared as separate global variables, not in the UBO
                if ( uniform.type.includes( 'texture' ) ) {

                    textureGlobals.push( `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var ${uniform.name}: ${this.getWgslType( uniform.type )};` );
                    textureGlobals.push( `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var ${uniform.name}_sampler: sampler;` );

                } else {

                    uniformStructMembers.push( `\t${uniform.name}: ${this.getWgslType( uniform.type )},` );

                }

            }

            // Create a UBO struct if there are any non-texture uniforms
            if ( uniformStructMembers.length > 0 ) {

                globals += 'struct Uniforms {\n';
                globals += uniformStructMembers.join( '\n' );
                globals += '\n};\n';
                globals += `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var<uniform> uniforms: Uniforms;\n\n`;

            }

            // Add the texture and sampler globals
            globals += textureGlobals.join( '\n' ) + '\n\n';

        }

        // 3. Build varying structs for stage I/O
        // This is a simplification; a full implementation would need to know the shader stage.
        if ( this.varyings.length > 0 ) {

            globals += 'struct Varyings {\n';
            let location = 0;
            for ( const varying of this.varyings ) {

                globals += `\t@location(${location ++}) ${varying.name}: ${this.getWgslType( varying.type )},\n`;

            }

            globals += '};\n\n';

        }

        // 4. Emit all functions and other global statements
        for ( const statement of ast.body ) {

            functions += this.emitExtraLine( statement, ast.body );

            if ( statement.isFunctionDeclaration ) {

                functions += this.emitFunction( statement ) + '\n';

            } else if ( statement.isComment ) {

                functions += this.emitComment( statement, ast.body );

            } else if ( ! statement.isUniform && ! statement.isVarying ) {

                // Handle other top-level statements like 'const'
                functions += this.emitExpression( statement ) + ';\n';

            }

        }

        // 4. Build dependencies
        for ( const value of this.polyfills.values() ) {

            dependencies = `${ value }\n\n`;

        }

        return header + dependencies + globals + functions.trimEnd() + '\n';

    }

Classes

WGSLEncoder

Class Code
class WGSLEncoder {

    constructor() {

        this.tab = '';
        this.functions = new Map();
        this.uniforms = [];
        this.varyings = [];
        this.structs = new Map();
        this.polyfills = new Map();

        // Assume a single group for simplicity
        this.groupIndex = 0;

    }

    getWgslType( type ) {

        return typeMap[ type ] || type;

    }

    emitExpression( node ) {

        if ( ! node ) return '';

        let code;

        if ( node.isAccessor ) {

            // Check if this accessor is part of a uniform struct
            const uniform = this.uniforms.find( u => u.name === node.property );

            if ( uniform && ! uniform.type.includes( 'texture' ) ) {

                return `uniforms.${node.property}`;

            }

            code = node.property;

        } else if ( node.isNumber ) {

            code = node.value;

            // WGSL requires floating point numbers to have a decimal
            if ( node.type === 'float' && ! code.includes( '.' ) ) {

                code += '.0';

            }

        } else if ( node.isOperator ) {

            const left = this.emitExpression( node.left );
            const right = this.emitExpression( node.right );

            code = `${ left } ${ node.type } ${ right }`;

            if ( node.parent.isAssignment !== true && node.parent.isOperator ) {

                code = `( ${ code } )`;

            }

        } else if ( node.isFunctionCall ) {

            const fnName = wgslLib[ node.name ] || node.name;

            if ( fnName === 'mod' ) {

                const snippets = node.params.map( p => this.emitExpression( p ) );
                const types = node.params.map( p => p.getType() );

                const modFnName = 'mod_' + types.join( '_' );

                if ( this.polyfills.has( modFnName ) === false ) {

                    this.polyfills.set( modFnName, `fn ${ modFnName }( x: ${ this.getWgslType( types[ 0 ] ) }, y: ${ this.getWgslType( types[ 1 ] ) } ) -> ${ this.getWgslType( types[ 0 ] ) } {

    return x - y * floor( x / y );

}` );

                }

                code = `${ modFnName }( ${ snippets.join( ', ' ) } )`;

            } else if ( fnName.startsWith( 'texture' ) ) {

                // Handle texture functions separately due to sampler handling

                code = this.emitTextureAccess( node );

            } else {

                const params = node.params.map( p => this.emitExpression( p ) );

                if ( typeMap[ fnName ] ) {

                    // Handle type constructors like vec3(...)

                    code = this.getWgslType( fnName );

                } else {

                    code = fnName;

                }

                if ( params.length > 0 ) {

                    code += '( ' + params.join( ', ' ) + ' )';

                } else {

                    code += '()';

                }

            }

        } else if ( node.isReturn ) {

            code = 'return';

            if ( node.value ) {

                code += ' ' + this.emitExpression( node.value );

            }

        } else if ( node.isDiscard ) {

            code = 'discard';

        } else if ( node.isBreak ) {

            if ( node.parent.isSwitchCase !== true ) {

                code = 'break';

            }

        } else if ( node.isContinue ) {

            code = 'continue';

        } else if ( node.isAccessorElements ) {

            code = this.emitExpression( node.object );

            for ( const element of node.elements ) {

                const value = this.emitExpression( element.value );

                if ( element.isStaticElement ) {

                    code += '.' + value;

                } else if ( element.isDynamicElement ) {

                    code += `[${value}]`;

                }

            }

        } else if ( node.isFor ) {

            code = this.emitFor( node );

        } else if ( node.isWhile ) {

            code = this.emitWhile( node );

        } else if ( node.isSwitch ) {

            code = this.emitSwitch( node );

        } else if ( node.isVariableDeclaration ) {

            code = this.emitVariables( node );

        } else if ( node.isUniform ) {

            this.uniforms.push( node );
            return ''; // Defer emission to the header

        } else if ( node.isVarying ) {

            this.varyings.push( node );
            return ''; // Defer emission to the header

        } else if ( node.isTernary ) {

            const cond = this.emitExpression( node.cond );
            const left = this.emitExpression( node.left );
            const right = this.emitExpression( node.right );

            // WGSL's equivalent to the ternary operator is select(false_val, true_val, condition)
            code = `select( ${ right }, ${ left }, ${ cond } )`;

        } else if ( node.isConditional ) {

            code = this.emitConditional( node );

        } else if ( node.isUnary ) {

            const expr = this.emitExpression( node.expression );

            if ( node.type === '++' || node.type === '--' ) {

                const op = node.type === '++' ? '+' : '-';

                code = `${ expr } = ${ expr } ${ op } 1`;

            } else {

                code = `${ node.type }${ expr }`;

            }

        } else {

            console.warn( 'Unknown node type in WGSL Encoder:', node );

            code = `/* unknown node: ${ node.constructor.name } */`;

        }

        return code;

    }

    emitTextureAccess( node ) {

        const wgslFn = wgslLib[ node.name ];
        const textureName = this.emitExpression( node.params[ 0 ] );
        const uv = this.emitExpression( node.params[ 1 ] );

        // WGSL requires explicit samplers. We assume a naming convention.
        const samplerName = `${textureName}_sampler`;

        let code;

        switch ( node.name ) {

            case 'texture':
            case 'texture2D':
            case 'texture3D':
            case 'textureCube':
                // format: textureSample(texture, sampler, coords, [offset])
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}`;
                // Handle optional bias parameter (note: WGSL uses textureSampleBias)
                if ( node.params.length === 3 ) {

                    const bias = this.emitExpression( node.params[ 2 ] );
                    code = `textureSampleBias(${textureName}, ${samplerName}, ${uv}, ${bias})`;

                } else {

                    code += ')';

                }

                break;

            case 'textureLod':
                // format: textureSampleLevel(texture, sampler, coords, level)
                const lod = this.emitExpression( node.params[ 2 ] );
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}, ${lod})`;
                break;

            case 'textureGrad':
                // format: textureSampleGrad(texture, sampler, coords, ddx, ddy)
                const ddx = this.emitExpression( node.params[ 2 ] );
                const ddy = this.emitExpression( node.params[ 3 ] );
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}, ${ddx}, ${ddy})`;
                break;

            case 'texelFetch':
                // format: textureLoad(texture, coords, [level])
                const coords = this.emitExpression( node.params[ 1 ] ); // should be ivec
                const lodFetch = node.params.length > 2 ? this.emitExpression( node.params[ 2 ] ) : '0';
                code = `${wgslFn}(${textureName}, ${coords}, ${lodFetch})`;
                break;

            default:
                code = `/* unsupported texture op: ${node.name} */`;

        }

        return code;

    }

    emitBody( body ) {

        let code = '';
        this.tab += '\t';

        for ( const statement of body ) {

            code += this.emitExtraLine( statement, body );

            if ( statement.isComment ) {

                code += this.emitComment( statement, body );
                continue;

            }

            const statementCode = this.emitExpression( statement );

            if ( statementCode ) {

                code += this.tab + statementCode;

                if ( ! statementCode.endsWith( '}' ) && ! statementCode.endsWith( '{' ) ) {

                    code += ';';

                }

                code += '\n';

            }

        }

        this.tab = this.tab.slice( 0, - 1 );
        return code.slice( 0, - 1 ); // remove the last extra line

    }

    emitConditional( node ) {

        const condStr = this.emitExpression( node.cond );
        const bodyStr = this.emitBody( node.body );

        let ifStr = `if ( ${ condStr } ) {\n\n${ bodyStr }\n\n${ this.tab }}`;

        let current = node;

        while ( current.elseConditional ) {

            current = current.elseConditional;
            const elseBodyStr = this.emitBody( current.body );

            if ( current.cond ) { // This is an 'else if'

                const elseCondStr = this.emitExpression( current.cond );

                ifStr += ` else if ( ${ elseCondStr } ) {\n\n${ elseBodyStr }\n\n${ this.tab }}`;

            } else { // This is an 'else'

                ifStr += ` else {\n\n${ elseBodyStr }\n\n${ this.tab }}`;

            }

        }

        return ifStr;

    }

    emitFor( node ) {

        const init = this.emitExpression( node.initialization );
        const cond = this.emitExpression( node.condition );
        const after = this.emitExpression( node.afterthought );
        const body = this.emitBody( node.body );

        return `for ( ${ init }; ${ cond }; ${ after } ) {\n\n${ body }\n\n${ this.tab }}`;

    }

    emitWhile( node ) {

        const cond = this.emitExpression( node.condition );
        const body = this.emitBody( node.body );

        return `while ( ${ cond } ) {\n\n${ body }\n\n${ this.tab }}`;

    }

    emitSwitch( node ) {

        const discriminant = this.emitExpression( node.discriminant );

        let switchStr = `switch ( ${ discriminant } ) {\n\n`;

        this.tab += '\t';

        for ( const switchCase of node.cases ) {

            const body = this.emitBody( switchCase.body );

            if ( switchCase.isDefault ) {

                switchStr += `${ this.tab }default: {\n\n${ body }\n\n${ this.tab }}\n\n`;

            } else {

                const cases = switchCase.conditions.map( c => this.emitExpression( c ) ).join( ', ' );

                switchStr += `${ this.tab }case ${ cases }: {\n\n${ body }\n\n${ this.tab }}\n\n`;

            }

        }

        this.tab = this.tab.slice( 0, - 1 );

        switchStr += `${this.tab}}`;

        return switchStr;

    }

    emitVariables( node ) {

        const declarations = [];

        let current = node;

        while ( current ) {

            const type = this.getWgslType( current.type );

            let valueStr = '';

            if ( current.value ) {

                valueStr = ` = ${this.emitExpression( current.value )}`;

            }

            // The AST linker tracks if a variable is ever reassigned.
            // If so, use 'var'; otherwise, use 'let'.

            let keyword;

            if ( current.linker ) {

                if ( current.linker.assignments.length > 0 ) {

                    keyword = 'var'; // Reassigned variable

                } else {

                    if ( current.value && current.value.isNumericExpression ) {

                        keyword = 'const'; // Immutable numeric expression

                    } else {

                        keyword = 'let'; // Immutable variable

                    }

                }

            }

            declarations.push( `${ keyword } ${ current.name }: ${ type }${ valueStr }` );

            current = current.next;

        }

        // In WGSL, multiple declarations in one line are not supported, so join with semicolons.
        return declarations.join( ';\n' + this.tab );

    }

    emitFunction( node ) {

        const name = node.name;
        const returnType = this.getWgslType( node.type );

        const params = [];
        // We will prepend to a copy of the body, not the original AST node.
        const body = [ ...node.body ];

        for ( const param of node.params ) {

            const paramName = param.name;
            let paramType = this.getWgslType( param.type );

            // Handle 'inout' and 'out' qualifiers using pointers. They are already mutable.
            if ( param.qualifier === 'inout' || param.qualifier === 'out' ) {

                paramType = `ptr<function, ${paramType}>`;
                params.push( `${paramName}: ${paramType}` );
                continue;

            }

            // If the parameter is reassigned within the function, we need to
            // create a local, mutable variable that shadows the parameter's name.
            if ( param.linker && param.linker.assignments.length > 0 ) {

                // 1. Rename the incoming parameter to avoid name collision.
                const immutableParamName = `${paramName}_in`;
                params.push( `${immutableParamName}: ${paramType}` );

                // 2. Create a new Accessor node for the renamed immutable parameter.
                const immutableAccessor = new Accessor( immutableParamName );
                immutableAccessor.isAccessor = true;
                immutableAccessor.property = immutableParamName;

                // 3. Create a new VariableDeclaration node for the mutable local variable.
                // This new variable will have the original parameter's name.
                const mutableVar = new VariableDeclaration( param.type, param.name, immutableAccessor );

                // 4. Mark this new variable as mutable so `emitVariables` uses `var`.
                mutableVar.linker = { assignments: [ true ] };

                // 5. Prepend this new declaration to the function's body.
                body.unshift( mutableVar );

            } else {

                // This parameter is not reassigned, so treat it as a normal immutable parameter.
                params.push( `${paramName}: ${paramType}` );

            }

        }

        const paramsStr = params.length > 0 ? ' ' + params.join( ', ' ) + ' ' : '';
        const returnStr = ( returnType && returnType !== 'void' ) ? ` -> ${returnType}` : '';

        // Emit the function body, which now includes our injected variable declarations.
        const bodyStr = this.emitBody( body );

        return `fn ${name}(${paramsStr})${returnStr} {\n\n${bodyStr}\n\n${this.tab}}`;

    }

    emitComment( statement, body ) {

        const index = body.indexOf( statement );
        const previous = body[ index - 1 ];
        const next = body[ index + 1 ];

        let output = '';

        if ( previous && isExpression( previous ) ) {

            output += '\n';

        }

        output += this.tab + statement.comment.replace( /\n/g, '\n' + this.tab ) + '\n';

        if ( next && isExpression( next ) ) {

            output += '\n';

        }

        return output;

    }

    emitExtraLine( statement, body ) {

        const index = body.indexOf( statement );
        const previous = body[ index - 1 ];

        if ( previous === undefined ) return '';

        if ( statement.isReturn ) return '\n';

        const lastExp = isExpression( previous );
        const currExp = isExpression( statement );

        if ( lastExp !== currExp || ( ! lastExp && ! currExp ) ) return '\n';

        return '';

    }

    emit( ast ) {

        const header = '// Three.js Transpiler r' + REVISION + '\n\n';

        let globals = '';
        let functions = '';
        let dependencies = '';

        // 1. Pre-process to find all global declarations
        for ( const statement of ast.body ) {

            if ( statement.isFunctionDeclaration ) {

                this.functions.set( statement.name, statement );

            } else if ( statement.isUniform ) {

                this.uniforms.push( statement );

            } else if ( statement.isVarying ) {

                this.varyings.push( statement );

            }

        }

        // 2. Build resource bindings (uniforms, textures, samplers)
        if ( this.uniforms.length > 0 ) {

            let bindingIndex = 0;
            const uniformStructMembers = [];
            const textureGlobals = [];

            for ( const uniform of this.uniforms ) {

                // Textures are declared as separate global variables, not in the UBO
                if ( uniform.type.includes( 'texture' ) ) {

                    textureGlobals.push( `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var ${uniform.name}: ${this.getWgslType( uniform.type )};` );
                    textureGlobals.push( `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var ${uniform.name}_sampler: sampler;` );

                } else {

                    uniformStructMembers.push( `\t${uniform.name}: ${this.getWgslType( uniform.type )},` );

                }

            }

            // Create a UBO struct if there are any non-texture uniforms
            if ( uniformStructMembers.length > 0 ) {

                globals += 'struct Uniforms {\n';
                globals += uniformStructMembers.join( '\n' );
                globals += '\n};\n';
                globals += `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var<uniform> uniforms: Uniforms;\n\n`;

            }

            // Add the texture and sampler globals
            globals += textureGlobals.join( '\n' ) + '\n\n';

        }

        // 3. Build varying structs for stage I/O
        // This is a simplification; a full implementation would need to know the shader stage.
        if ( this.varyings.length > 0 ) {

            globals += 'struct Varyings {\n';
            let location = 0;
            for ( const varying of this.varyings ) {

                globals += `\t@location(${location ++}) ${varying.name}: ${this.getWgslType( varying.type )},\n`;

            }

            globals += '};\n\n';

        }

        // 4. Emit all functions and other global statements
        for ( const statement of ast.body ) {

            functions += this.emitExtraLine( statement, ast.body );

            if ( statement.isFunctionDeclaration ) {

                functions += this.emitFunction( statement ) + '\n';

            } else if ( statement.isComment ) {

                functions += this.emitComment( statement, ast.body );

            } else if ( ! statement.isUniform && ! statement.isVarying ) {

                // Handle other top-level statements like 'const'
                functions += this.emitExpression( statement ) + ';\n';

            }

        }

        // 4. Build dependencies
        for ( const value of this.polyfills.values() ) {

            dependencies = `${ value }\n\n`;

        }

        return header + dependencies + globals + functions.trimEnd() + '\n';

    }

}

Methods

getWgslType(type: any): any
Code
getWgslType( type ) {

        return typeMap[ type ] || type;

    }
emitExpression(node: any): any
Code
emitExpression( node ) {

        if ( ! node ) return '';

        let code;

        if ( node.isAccessor ) {

            // Check if this accessor is part of a uniform struct
            const uniform = this.uniforms.find( u => u.name === node.property );

            if ( uniform && ! uniform.type.includes( 'texture' ) ) {

                return `uniforms.${node.property}`;

            }

            code = node.property;

        } else if ( node.isNumber ) {

            code = node.value;

            // WGSL requires floating point numbers to have a decimal
            if ( node.type === 'float' && ! code.includes( '.' ) ) {

                code += '.0';

            }

        } else if ( node.isOperator ) {

            const left = this.emitExpression( node.left );
            const right = this.emitExpression( node.right );

            code = `${ left } ${ node.type } ${ right }`;

            if ( node.parent.isAssignment !== true && node.parent.isOperator ) {

                code = `( ${ code } )`;

            }

        } else if ( node.isFunctionCall ) {

            const fnName = wgslLib[ node.name ] || node.name;

            if ( fnName === 'mod' ) {

                const snippets = node.params.map( p => this.emitExpression( p ) );
                const types = node.params.map( p => p.getType() );

                const modFnName = 'mod_' + types.join( '_' );

                if ( this.polyfills.has( modFnName ) === false ) {

                    this.polyfills.set( modFnName, `fn ${ modFnName }( x: ${ this.getWgslType( types[ 0 ] ) }, y: ${ this.getWgslType( types[ 1 ] ) } ) -> ${ this.getWgslType( types[ 0 ] ) } {

    return x - y * floor( x / y );

}` );

                }

                code = `${ modFnName }( ${ snippets.join( ', ' ) } )`;

            } else if ( fnName.startsWith( 'texture' ) ) {

                // Handle texture functions separately due to sampler handling

                code = this.emitTextureAccess( node );

            } else {

                const params = node.params.map( p => this.emitExpression( p ) );

                if ( typeMap[ fnName ] ) {

                    // Handle type constructors like vec3(...)

                    code = this.getWgslType( fnName );

                } else {

                    code = fnName;

                }

                if ( params.length > 0 ) {

                    code += '( ' + params.join( ', ' ) + ' )';

                } else {

                    code += '()';

                }

            }

        } else if ( node.isReturn ) {

            code = 'return';

            if ( node.value ) {

                code += ' ' + this.emitExpression( node.value );

            }

        } else if ( node.isDiscard ) {

            code = 'discard';

        } else if ( node.isBreak ) {

            if ( node.parent.isSwitchCase !== true ) {

                code = 'break';

            }

        } else if ( node.isContinue ) {

            code = 'continue';

        } else if ( node.isAccessorElements ) {

            code = this.emitExpression( node.object );

            for ( const element of node.elements ) {

                const value = this.emitExpression( element.value );

                if ( element.isStaticElement ) {

                    code += '.' + value;

                } else if ( element.isDynamicElement ) {

                    code += `[${value}]`;

                }

            }

        } else if ( node.isFor ) {

            code = this.emitFor( node );

        } else if ( node.isWhile ) {

            code = this.emitWhile( node );

        } else if ( node.isSwitch ) {

            code = this.emitSwitch( node );

        } else if ( node.isVariableDeclaration ) {

            code = this.emitVariables( node );

        } else if ( node.isUniform ) {

            this.uniforms.push( node );
            return ''; // Defer emission to the header

        } else if ( node.isVarying ) {

            this.varyings.push( node );
            return ''; // Defer emission to the header

        } else if ( node.isTernary ) {

            const cond = this.emitExpression( node.cond );
            const left = this.emitExpression( node.left );
            const right = this.emitExpression( node.right );

            // WGSL's equivalent to the ternary operator is select(false_val, true_val, condition)
            code = `select( ${ right }, ${ left }, ${ cond } )`;

        } else if ( node.isConditional ) {

            code = this.emitConditional( node );

        } else if ( node.isUnary ) {

            const expr = this.emitExpression( node.expression );

            if ( node.type === '++' || node.type === '--' ) {

                const op = node.type === '++' ? '+' : '-';

                code = `${ expr } = ${ expr } ${ op } 1`;

            } else {

                code = `${ node.type }${ expr }`;

            }

        } else {

            console.warn( 'Unknown node type in WGSL Encoder:', node );

            code = `/* unknown node: ${ node.constructor.name } */`;

        }

        return code;

    }
emitTextureAccess(node: any): any
Code
emitTextureAccess( node ) {

        const wgslFn = wgslLib[ node.name ];
        const textureName = this.emitExpression( node.params[ 0 ] );
        const uv = this.emitExpression( node.params[ 1 ] );

        // WGSL requires explicit samplers. We assume a naming convention.
        const samplerName = `${textureName}_sampler`;

        let code;

        switch ( node.name ) {

            case 'texture':
            case 'texture2D':
            case 'texture3D':
            case 'textureCube':
                // format: textureSample(texture, sampler, coords, [offset])
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}`;
                // Handle optional bias parameter (note: WGSL uses textureSampleBias)
                if ( node.params.length === 3 ) {

                    const bias = this.emitExpression( node.params[ 2 ] );
                    code = `textureSampleBias(${textureName}, ${samplerName}, ${uv}, ${bias})`;

                } else {

                    code += ')';

                }

                break;

            case 'textureLod':
                // format: textureSampleLevel(texture, sampler, coords, level)
                const lod = this.emitExpression( node.params[ 2 ] );
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}, ${lod})`;
                break;

            case 'textureGrad':
                // format: textureSampleGrad(texture, sampler, coords, ddx, ddy)
                const ddx = this.emitExpression( node.params[ 2 ] );
                const ddy = this.emitExpression( node.params[ 3 ] );
                code = `${wgslFn}(${textureName}, ${samplerName}, ${uv}, ${ddx}, ${ddy})`;
                break;

            case 'texelFetch':
                // format: textureLoad(texture, coords, [level])
                const coords = this.emitExpression( node.params[ 1 ] ); // should be ivec
                const lodFetch = node.params.length > 2 ? this.emitExpression( node.params[ 2 ] ) : '0';
                code = `${wgslFn}(${textureName}, ${coords}, ${lodFetch})`;
                break;

            default:
                code = `/* unsupported texture op: ${node.name} */`;

        }

        return code;

    }
emitBody(body: any): string
Code
emitBody( body ) {

        let code = '';
        this.tab += '\t';

        for ( const statement of body ) {

            code += this.emitExtraLine( statement, body );

            if ( statement.isComment ) {

                code += this.emitComment( statement, body );
                continue;

            }

            const statementCode = this.emitExpression( statement );

            if ( statementCode ) {

                code += this.tab + statementCode;

                if ( ! statementCode.endsWith( '}' ) && ! statementCode.endsWith( '{' ) ) {

                    code += ';';

                }

                code += '\n';

            }

        }

        this.tab = this.tab.slice( 0, - 1 );
        return code.slice( 0, - 1 ); // remove the last extra line

    }
emitConditional(node: any): any
Code
emitConditional( node ) {

        const condStr = this.emitExpression( node.cond );
        const bodyStr = this.emitBody( node.body );

        let ifStr = `if ( ${ condStr } ) {\n\n${ bodyStr }\n\n${ this.tab }}`;

        let current = node;

        while ( current.elseConditional ) {

            current = current.elseConditional;
            const elseBodyStr = this.emitBody( current.body );

            if ( current.cond ) { // This is an 'else if'

                const elseCondStr = this.emitExpression( current.cond );

                ifStr += ` else if ( ${ elseCondStr } ) {\n\n${ elseBodyStr }\n\n${ this.tab }}`;

            } else { // This is an 'else'

                ifStr += ` else {\n\n${ elseBodyStr }\n\n${ this.tab }}`;

            }

        }

        return ifStr;

    }
emitFor(node: any): any
Code
emitFor( node ) {

        const init = this.emitExpression( node.initialization );
        const cond = this.emitExpression( node.condition );
        const after = this.emitExpression( node.afterthought );
        const body = this.emitBody( node.body );

        return `for ( ${ init }; ${ cond }; ${ after } ) {\n\n${ body }\n\n${ this.tab }}`;

    }
emitWhile(node: any): any
Code
emitWhile( node ) {

        const cond = this.emitExpression( node.condition );
        const body = this.emitBody( node.body );

        return `while ( ${ cond } ) {\n\n${ body }\n\n${ this.tab }}`;

    }
emitSwitch(node: any): any
Code
emitSwitch( node ) {

        const discriminant = this.emitExpression( node.discriminant );

        let switchStr = `switch ( ${ discriminant } ) {\n\n`;

        this.tab += '\t';

        for ( const switchCase of node.cases ) {

            const body = this.emitBody( switchCase.body );

            if ( switchCase.isDefault ) {

                switchStr += `${ this.tab }default: {\n\n${ body }\n\n${ this.tab }}\n\n`;

            } else {

                const cases = switchCase.conditions.map( c => this.emitExpression( c ) ).join( ', ' );

                switchStr += `${ this.tab }case ${ cases }: {\n\n${ body }\n\n${ this.tab }}\n\n`;

            }

        }

        this.tab = this.tab.slice( 0, - 1 );

        switchStr += `${this.tab}}`;

        return switchStr;

    }
emitVariables(node: any): string
Code
emitVariables( node ) {

        const declarations = [];

        let current = node;

        while ( current ) {

            const type = this.getWgslType( current.type );

            let valueStr = '';

            if ( current.value ) {

                valueStr = ` = ${this.emitExpression( current.value )}`;

            }

            // The AST linker tracks if a variable is ever reassigned.
            // If so, use 'var'; otherwise, use 'let'.

            let keyword;

            if ( current.linker ) {

                if ( current.linker.assignments.length > 0 ) {

                    keyword = 'var'; // Reassigned variable

                } else {

                    if ( current.value && current.value.isNumericExpression ) {

                        keyword = 'const'; // Immutable numeric expression

                    } else {

                        keyword = 'let'; // Immutable variable

                    }

                }

            }

            declarations.push( `${ keyword } ${ current.name }: ${ type }${ valueStr }` );

            current = current.next;

        }

        // In WGSL, multiple declarations in one line are not supported, so join with semicolons.
        return declarations.join( ';\n' + this.tab );

    }
emitFunction(node: any): string
Code
emitFunction( node ) {

        const name = node.name;
        const returnType = this.getWgslType( node.type );

        const params = [];
        // We will prepend to a copy of the body, not the original AST node.
        const body = [ ...node.body ];

        for ( const param of node.params ) {

            const paramName = param.name;
            let paramType = this.getWgslType( param.type );

            // Handle 'inout' and 'out' qualifiers using pointers. They are already mutable.
            if ( param.qualifier === 'inout' || param.qualifier === 'out' ) {

                paramType = `ptr<function, ${paramType}>`;
                params.push( `${paramName}: ${paramType}` );
                continue;

            }

            // If the parameter is reassigned within the function, we need to
            // create a local, mutable variable that shadows the parameter's name.
            if ( param.linker && param.linker.assignments.length > 0 ) {

                // 1. Rename the incoming parameter to avoid name collision.
                const immutableParamName = `${paramName}_in`;
                params.push( `${immutableParamName}: ${paramType}` );

                // 2. Create a new Accessor node for the renamed immutable parameter.
                const immutableAccessor = new Accessor( immutableParamName );
                immutableAccessor.isAccessor = true;
                immutableAccessor.property = immutableParamName;

                // 3. Create a new VariableDeclaration node for the mutable local variable.
                // This new variable will have the original parameter's name.
                const mutableVar = new VariableDeclaration( param.type, param.name, immutableAccessor );

                // 4. Mark this new variable as mutable so `emitVariables` uses `var`.
                mutableVar.linker = { assignments: [ true ] };

                // 5. Prepend this new declaration to the function's body.
                body.unshift( mutableVar );

            } else {

                // This parameter is not reassigned, so treat it as a normal immutable parameter.
                params.push( `${paramName}: ${paramType}` );

            }

        }

        const paramsStr = params.length > 0 ? ' ' + params.join( ', ' ) + ' ' : '';
        const returnStr = ( returnType && returnType !== 'void' ) ? ` -> ${returnType}` : '';

        // Emit the function body, which now includes our injected variable declarations.
        const bodyStr = this.emitBody( body );

        return `fn ${name}(${paramsStr})${returnStr} {\n\n${bodyStr}\n\n${this.tab}}`;

    }
emitComment(statement: any, body: any): string
Code
emitComment( statement, body ) {

        const index = body.indexOf( statement );
        const previous = body[ index - 1 ];
        const next = body[ index + 1 ];

        let output = '';

        if ( previous && isExpression( previous ) ) {

            output += '\n';

        }

        output += this.tab + statement.comment.replace( /\n/g, '\n' + this.tab ) + '\n';

        if ( next && isExpression( next ) ) {

            output += '\n';

        }

        return output;

    }
emitExtraLine(statement: any, body: any): "" | "\n"
Code
emitExtraLine( statement, body ) {

        const index = body.indexOf( statement );
        const previous = body[ index - 1 ];

        if ( previous === undefined ) return '';

        if ( statement.isReturn ) return '\n';

        const lastExp = isExpression( previous );
        const currExp = isExpression( statement );

        if ( lastExp !== currExp || ( ! lastExp && ! currExp ) ) return '\n';

        return '';

    }
emit(ast: any): string
Code
emit( ast ) {

        const header = '// Three.js Transpiler r' + REVISION + '\n\n';

        let globals = '';
        let functions = '';
        let dependencies = '';

        // 1. Pre-process to find all global declarations
        for ( const statement of ast.body ) {

            if ( statement.isFunctionDeclaration ) {

                this.functions.set( statement.name, statement );

            } else if ( statement.isUniform ) {

                this.uniforms.push( statement );

            } else if ( statement.isVarying ) {

                this.varyings.push( statement );

            }

        }

        // 2. Build resource bindings (uniforms, textures, samplers)
        if ( this.uniforms.length > 0 ) {

            let bindingIndex = 0;
            const uniformStructMembers = [];
            const textureGlobals = [];

            for ( const uniform of this.uniforms ) {

                // Textures are declared as separate global variables, not in the UBO
                if ( uniform.type.includes( 'texture' ) ) {

                    textureGlobals.push( `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var ${uniform.name}: ${this.getWgslType( uniform.type )};` );
                    textureGlobals.push( `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var ${uniform.name}_sampler: sampler;` );

                } else {

                    uniformStructMembers.push( `\t${uniform.name}: ${this.getWgslType( uniform.type )},` );

                }

            }

            // Create a UBO struct if there are any non-texture uniforms
            if ( uniformStructMembers.length > 0 ) {

                globals += 'struct Uniforms {\n';
                globals += uniformStructMembers.join( '\n' );
                globals += '\n};\n';
                globals += `@group(${this.groupIndex}) @binding(${bindingIndex ++}) var<uniform> uniforms: Uniforms;\n\n`;

            }

            // Add the texture and sampler globals
            globals += textureGlobals.join( '\n' ) + '\n\n';

        }

        // 3. Build varying structs for stage I/O
        // This is a simplification; a full implementation would need to know the shader stage.
        if ( this.varyings.length > 0 ) {

            globals += 'struct Varyings {\n';
            let location = 0;
            for ( const varying of this.varyings ) {

                globals += `\t@location(${location ++}) ${varying.name}: ${this.getWgslType( varying.type )},\n`;

            }

            globals += '};\n\n';

        }

        // 4. Emit all functions and other global statements
        for ( const statement of ast.body ) {

            functions += this.emitExtraLine( statement, ast.body );

            if ( statement.isFunctionDeclaration ) {

                functions += this.emitFunction( statement ) + '\n';

            } else if ( statement.isComment ) {

                functions += this.emitComment( statement, ast.body );

            } else if ( ! statement.isUniform && ! statement.isVarying ) {

                // Handle other top-level statements like 'const'
                functions += this.emitExpression( statement ) + ';\n';

            }

        }

        // 4. Build dependencies
        for ( const value of this.polyfills.values() ) {

            dependencies = `${ value }\n\n`;

        }

        return header + dependencies + globals + functions.trimEnd() + '\n';

    }