| // Copyright 2026 The IREE Authors | |
| // | |
| // Licensed under the Apache License v2.0 with LLVM Exceptions. | |
| // See https://llvm.org/LICENSE.txt for license information. | |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception | |
| import {readFileSync} from 'node:fs'; | |
| import {createRequire} from 'node:module'; | |
| import {pathToFileURL} from 'node:url'; | |
| const require = createRequire(import.meta.url); | |
| const wgslPaths = process.argv.slice(2); | |
| if (wgslPaths.length === 0) { | |
| console.error('usage: node validate_wgsl.mjs <shader.wgsl>...'); | |
| process.exit(1); | |
| } | |
| async function importWebGpuPackage() { | |
| try { | |
| return await import('webgpu'); | |
| } catch (primaryError) { | |
| const packageRoot = process.env.IREE_WEBGPU_PACKAGE_ROOT; | |
| if (packageRoot) { | |
| try { | |
| const packagePath = require.resolve('webgpu', {paths: [packageRoot]}); | |
| return await import(pathToFileURL(packagePath)); | |
| } catch (rootError) { | |
| throw new Error( | |
| primaryError.message + '\n' + | |
| `Failed to import webgpu from IREE_WEBGPU_PACKAGE_ROOT=${ | |
| packageRoot}: ${rootError.message}`); | |
| } | |
| } | |
| throw primaryError; | |
| } | |
| } | |
| let gpu; | |
| try { | |
| const {create, globals} = await importWebGpuPackage(); | |
| Object.assign(globalThis, globals); | |
| gpu = create([]); | |
| } catch (error) { | |
| console.error( | |
| 'Failed to import webgpu package: ' + error.message + '\n' + | |
| 'Install outside the checkout with:\n' + | |
| ' npm install --prefix /tmp/iree-webgpu-validator webgpu\n' + | |
| ' IREE_WEBGPU_PACKAGE_ROOT=/tmp/iree-webgpu-validator node ' + | |
| 'samples/webgpu/hello_world/validate_wgsl.mjs <shader.wgsl>...'); | |
| process.exit(1); | |
| } | |
| const adapter = await gpu.requestAdapter(); | |
| if (!adapter) { | |
| console.error('WebGPU adapter not available.'); | |
| process.exit(1); | |
| } | |
| const device = await adapter.requestDevice(); | |
| if (!device) { | |
| console.error('WebGPU device creation failed.'); | |
| process.exit(1); | |
| } | |
| function getComputeEntryPoints(source) { | |
| const entryPoints = []; | |
| const entryPointExpression = | |
| /@compute[\s\S]*?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g; | |
| for (let match = entryPointExpression.exec(source); match; | |
| match = entryPointExpression.exec(source)) { | |
| entryPoints.push(match[1]); | |
| } | |
| return entryPoints; | |
| } | |
| let failed = false; | |
| for (const wgslPath of wgslPaths) { | |
| const source = readFileSync(wgslPath, 'utf8'); | |
| const entryPoints = getComputeEntryPoints(source); | |
| if (entryPoints.length === 0) { | |
| console.error(`${wgslPath}: no @compute entry point found.`); | |
| failed = true; | |
| continue; | |
| } | |
| device.pushErrorScope('validation'); | |
| const shaderModule = device.createShaderModule({ | |
| label: wgslPath, | |
| code: source, | |
| }); | |
| if (shaderModule.getCompilationInfo) { | |
| const compilationInfo = await shaderModule.getCompilationInfo(); | |
| for (const message of compilationInfo.messages) { | |
| const location = `${message.lineNum}:${message.linePos}`; | |
| const text = | |
| `${wgslPath}:${location}: ${message.type}: ${message.message}`; | |
| if (message.type === 'error') { | |
| console.error(text); | |
| failed = true; | |
| } else { | |
| console.warn(text); | |
| } | |
| } | |
| } | |
| for (const entryPoint of entryPoints) { | |
| device.createComputePipeline({ | |
| label: `${wgslPath}:${entryPoint}`, | |
| layout: 'auto', | |
| compute: { | |
| module: shaderModule, | |
| entryPoint, | |
| }, | |
| }); | |
| } | |
| const validationError = await device.popErrorScope(); | |
| if (validationError) { | |
| console.error(`${wgslPath}: ${validationError.message}`); | |
| failed = true; | |
| } else { | |
| console.log(`${wgslPath}: validated ${entryPoints.join(', ')}`); | |
| } | |
| } | |
| device.destroy(); | |
| process.exit(failed ? 1 : 0); |