import { parse } from '../../vendor/glsl-parser/glsl-parser.mjs'
/**
* GLSL parser — walks the @shaderfrog/glsl-parser AST to extract:
* - Functions (void main, helpers)
* - Uniforms (regular uniform vars + uniform blocks)
* - Buffers (readonly/writeonly storage buffers)
* - Push constants (uniform blocks with push_constant layout)
* - Structs → classes
* - #defines → constants
*
* GLSL has no import system in the general case (dc-terrain shaders are monolithic),
* so imports[] is always empty. Godot .gdshader files may use `#include` but those
* are engine-specific and not modeled here yet.
*/
export function parseFile (source, filePath) {
// Always return these empty arrays so downstream consumers can `.length` safely even
// on parse failure.
const result = {
filePath,
imports: [],
exports: [],
classes: [],
functions: [],
constants: [],
uniforms: [],
buffers: [],
pushConstants: []
}
// Godot .gdshader files are a GLSL superset with editor hints that standard GLSL
// doesn't accept (e.g. `uniform sampler3D foo : filter_linear, repeat_enable;`).
// Strip these hints — they're purely presentational, not part of the shader logic.
const isGdShader = filePath && filePath.endsWith('.gdshader')
const processed = isGdShader ? stripGodotShaderHints(source) : source
// The parser emits warnings to console.warn for unknown identifiers (e.g. GLSL builtins
// like gl_GlobalInvocationID). Silence them during structural extraction.
const origWarn = console.warn
console.warn = () => {}
let ast
try { ast = parse(processed) }
catch (err) {
console.warn = origWarn
result.error = err.message
return result
}
console.warn = origWarn
if (!ast || !ast.program) return result
for (const node of ast.program) {
switch (node.type) {
case 'preprocessor':
handlePreprocessor(node, result)
break
case 'declaration_statement':
handleDeclarationStatement(node, result)
break
case 'function':
handleFunction(node, result)
break
}
}
return result
}
// --- Preprocessor (#define, #include) ---
function handlePreprocessor (node, result) {
const line = node.line || ''
// #define NAME value — extract as constant
const defMatch = line.match(/^\s*#\s*define\s+(\w+)\b/)
if (defMatch) {
result.constants.push({ name: defMatch[1], kind: 'define' })
return
}
// #include "path" (Godot .gdshader style) — capture as import
const incMatch = line.match(/^\s*#\s*include\s+["<]([^">]+)[">]/)
if (incMatch) {
result.imports.push({ specifier: incMatch[1], names: [] })
}
}
// --- Declaration statements (uniforms, buffers, structs, layout) ---
function handleDeclarationStatement (node, result) {
const decl = node.declaration
if (!decl) return
switch (decl.type) {
case 'interface_declarator':
handleInterfaceDeclarator(decl, result)
return
case 'declarator_list':
handleDeclaratorList(decl, result)
return
case 'qualifier_declarator':
// layout-only decls like `layout(local_size_x=256) in;` — no extraction needed
return
}
}
function handleInterfaceDeclarator (decl, result) {
// Used for: buffer blocks, uniform blocks, push constants
const qualTokens = extractQualifierTokens(decl.qualifiers || [])
const hasLayout = (decl.qualifiers || []).some(q => q.type === 'layout_qualifier')
const layoutIds = extractLayoutIds(decl.qualifiers || [])
const blockName = decl.interface_type && decl.interface_type.identifier
? decl.interface_type.identifier
: null
const instanceName = decl.identifier && decl.identifier.identifier
? decl.identifier.identifier
: null
if (!blockName) return
const isBuffer = qualTokens.includes('buffer')
const isUniform = qualTokens.includes('uniform')
const isPushConstant = layoutIds.some(id => id === 'push_constant')
const isReadonly = qualTokens.includes('readonly')
const isWriteonly = qualTokens.includes('writeonly')
const entry = {
name: blockName,
instance: instanceName,
binding: layoutIdValue(layoutIds, 'binding'),
set: layoutIdValue(layoutIds, 'set'),
std: layoutIds.find(id => id === 'std140' || id === 'std430') || null
}
if (isReadonly) entry.readonly = true
if (isWriteonly) entry.writeonly = true
if (isPushConstant) {
result.pushConstants.push(entry)
} else if (isBuffer) {
result.buffers.push(entry)
} else if (isUniform) {
result.uniforms.push(entry)
}
}
function handleDeclaratorList (decl, result) {
// Could be:
// - struct declaration: specifier.type === 'struct'
// - uniform sampler2D u_tex;
// - in/out variables
// - regular global variables
const specType = decl.specified_type && decl.specified_type.specifier
const quals = (decl.specified_type && decl.specified_type.qualifiers) || []
const qualTokens = extractQualifierTokens(quals)
// Struct declaration
if (specType && specType.specifier && specType.specifier.type === 'struct') {
const structNode = specType.specifier
const structName = structNode.typeName && structNode.typeName.identifier
? structNode.typeName.identifier
: null
if (structName) {
const cls = {
name: structName,
kind: 'struct',
extends: null,
line: null,
methods: [],
getters: [],
staticMethods: [],
constructor: null,
fields: extractStructFields(structNode)
}
result.classes.push(cls)
result.exports.push({ name: structName, type: 'struct' })
}
return
}
// Regular variable declaration (uniform float u_time, in vec3 pos, etc.)
const declarations = decl.declarations || []
for (const d of declarations) {
if (d.type === 'declaration' && d.identifier && d.identifier.identifier) {
const name = d.identifier.identifier
const typeToken = specType && specType.specifier && specType.specifier.token
? specType.specifier.token
: null
if (qualTokens.includes('uniform')) {
result.uniforms.push({ name, type: typeToken, binding: null, set: null })
} else if (qualTokens.includes('in') || qualTokens.includes('out')) {
result.constants.push({ name, kind: qualTokens.includes('out') ? 'out' : 'in' })
} else {
result.constants.push({ name, kind: 'var' })
}
}
}
}
function extractStructFields (structNode) {
const fields = []
for (const d of structNode.declarations || []) {
if (d.type !== 'struct_declaration') continue
const sd = d.declaration
if (!sd || !sd.declarations) continue
for (const q of sd.declarations) {
if (q.type === 'quantified_identifier' && q.identifier) {
fields.push(q.identifier.identifier)
}
}
}
return fields
}
// --- Function definitions ---
function handleFunction (node, result) {
const proto = node.prototype
if (!proto || !proto.header) return
const header = proto.header
const name = header.name && header.name.identifier ? header.name.identifier : null
if (!name) return
const params = []
const pl = proto.parameters || []
for (const p of pl) {
if (p.type === 'parameter_declaration' && p.declaration && p.declaration.identifier) {
params.push(p.declaration.identifier.identifier)
}
}
result.functions.push({ name, params })
// GLSL `void main()` is the shader's entry point — export it so downstream tools
// can tell entry shaders from helpers.
if (name === 'main') result.exports.push({ name: 'main', type: 'function' })
}
// --- Helpers ---
function extractQualifierTokens (quals) {
const tokens = []
for (const q of quals) {
if (q.type === 'keyword' && q.token) tokens.push(q.token)
}
return tokens
}
function extractLayoutIds (quals) {
const ids = []
for (const q of quals) {
if (q.type !== 'layout_qualifier') continue
for (const lq of q.qualifiers || []) {
if (lq.type === 'layout_qualifier_id' && lq.identifier && lq.identifier.identifier) {
const key = lq.identifier.identifier
const value = lq.expression && lq.expression.token ? lq.expression.token : null
ids.push(value ? key + '=' + value : key)
}
}
}
return ids
}
function layoutIdValue (ids, key) {
const match = ids.find(id => id.startsWith(key + '='))
if (!match) return null
return match.slice(key.length + 1)
}
// --- Godot .gdshader preprocessing ---
function stripGodotShaderHints (source) {
// Remove:
// shader_type spatial; / shader_type canvas_item; / etc. (top-of-file directive)
// render_mode foo, bar; (mode modifiers)
// uniform T name : hint_name, other_hint; (editor hints)
// uniform T name : hint_name, other_hint = default; (same with default)
let s = source
// shader_type line
s = s.replace(/^\s*shader_type\s+\w+\s*;/gm, '')
// render_mode line (may span across commas)
s = s.replace(/^\s*render_mode\s+[^;]+;/gm, '')
// Uniform hints: `uniform T name : hint, hint_range(0.0, 1.0) = 0.5;`
// Strip the `: <hints>` portion while preserving any `= default` value.
// Process line by line to avoid greedy matches spanning multiple uniforms.
s = s.split('\n').map(line => {
const m = line.match(/^(\s*uniform\s+[^;=:]+?)\s*:\s*([\s\S]+?)(\s*(?:=[^;]*)?;)\s*$/)
if (!m) return line
return m[1] + m[3]
}).join('\n')
return s
}