package shader import "core:fmt" import "core:strconv" import "core:strings" // Source location tracking Source_Span :: struct { file: string, line_start: int, col_start: int, line_end: int, col_end: int, } // Diagnostics Diagnostic_Level :: enum { Error, Warning, Note, } Diagnostic :: struct { level: Diagnostic_Level, message: string, span: Source_Span, } // Shader stages Shader_Stage :: enum { None, Vertex, Fragment, Compute, Geometry, Tessellation_Control, Tessellation_Eval, } // Diagnostic helpers format_span :: proc(span: Source_Span, allocator := context.allocator) -> string { if span.file == "" { return fmt.aprintf("%v:%v", span.line_start, span.col_start, allocator = allocator) } return fmt.aprintf("%v:%v:%v", span.file, span.line_start, span.col_start, allocator = allocator) } format_diagnostic :: proc(d: Diagnostic, allocator := context.allocator) -> string { level_str: string switch d.level { case .Error: level_str = "error" case .Warning: level_str = "warning" case .Note: level_str = "note" } loc := format_span(d.span, allocator) return fmt.aprintf("%v: %v: %v", loc, level_str, d.message, allocator = allocator) } // Format diagnostic with source line context and caret underline format_diagnostic_with_source :: proc(d: Diagnostic, source: string, allocator := context.allocator) -> string { base := format_diagnostic(d, allocator) if source == "" || d.span.line_start <= 0 do return base // Find the source line line_num := 1 line_start_idx := 0 for i := 0; i < len(source); i += 1 { if line_num == d.span.line_start { line_start_idx = i break } if source[i] == '\n' { line_num += 1 } } if line_num != d.span.line_start do return base // Extract the line line_end_idx := line_start_idx for line_end_idx < len(source) && source[line_end_idx] != '\n' { line_end_idx += 1 } line_text := source[line_start_idx:line_end_idx] // Build caret underline col_start := max(d.span.col_start - 1, 0) col_end := max(d.span.col_end, d.span.col_start) underline_len := max(col_end - col_start, 1) if col_start + underline_len > len(line_text) { underline_len = max(len(line_text) - col_start, 1) } buf := strings.builder_make(allocator) strings.write_string(&buf, base) strings.write_string(&buf, "\n") fmt.sbprintf(&buf, " %4d | %s\n", d.span.line_start, line_text) strings.write_string(&buf, " | ") for _ in 0 ..< col_start { strings.write_byte(&buf, ' ') } for _ in 0 ..< underline_len { strings.write_byte(&buf, '^') } return strings.to_string(buf) } has_errors :: proc(diagnostics: [dynamic]Diagnostic) -> bool { for d in diagnostics { if d.level == .Error { return true } } return false } has_errors_slice :: proc(diagnostics: []Diagnostic) -> bool { for d in diagnostics { if d.level == .Error { return true } } return false } // String builder helper for code emission Writer :: struct { buf: strings.Builder, indent: int, } writer_init :: proc(allocator := context.allocator) -> Writer { return Writer{buf = strings.builder_make(allocator)} } writer_destroy :: proc(w: ^Writer) { strings.builder_destroy(&w.buf) } writer_to_string :: proc(w: Writer) -> string { return strings.to_string(w.buf) } write :: proc(w: ^Writer, args: ..any) { fmt.sbprint(&w.buf, args = args, sep = "") } write_line :: proc(w: ^Writer, args: ..any) { for _ in 0 ..< w.indent { strings.write_string(&w.buf, "\t") } fmt.sbprint(&w.buf, args = args, sep = "") strings.write_string(&w.buf, "\n") } write_fmt :: proc(w: ^Writer, format: string, args: ..any) { for _ in 0 ..< w.indent { strings.write_string(&w.buf, "\t") } fmt.sbprintf(&w.buf, format, ..args) strings.write_string(&w.buf, "\n") } indent :: proc(w: ^Writer) { w.indent += 1 } dedent :: proc(w: ^Writer) { w.indent = max(0, w.indent - 1) } // Shared attribute/type helpers used by IR builder and backends type_expr_name :: proc(te: Type_Expr) -> string { switch t in te { case Type_Named: return t.name case Type_Array: return "" case Type_Tuple: return "" } return "" } is_sampler_type_name :: proc(name: string) -> bool { switch name { case "sampler2D", "sampler3D", "samplerCube", "sampler2DArray", "sampler2DShadow": return true } return false } // Returns (-1, -1) when attributes are absent to distinguish from explicit (0, 0). get_group_binding :: proc(attrs: []Ast_Attribute) -> (group: int, binding: int) { group = -1 binding = -1 for attr in attrs { if attr.name == "group" && len(attr.args) > 0 { group = parse_int(attr.args[0]) } if attr.name == "binding" && len(attr.args) > 0 { binding = parse_int(attr.args[0]) } } return } get_location :: proc(attrs: []Ast_Attribute) -> int { for attr in attrs { if attr.name == "location" && len(attr.args) > 0 { return parse_int(attr.args[0]) } } return -1 } get_builtin_name :: proc(attrs: []Ast_Attribute) -> string { for attr in attrs { if attr.name == "builtin" && len(attr.args) > 0 { return attr.args[0] } } return "" } // Find the split texture+sampler binding names for a combined sampler. // The combined_name is the original AST name (e.g. "material_tex"). find_split_bindings :: proc(module: ^IR_Module, combined_name: string) -> (tex_name: string, samp_name: string) { for &b in module.bindings { if b.combined_name == combined_name { switch b.kind { case .Texture: tex_name = b.name case .Sampler: samp_name = b.name case .Uniform, .Buffer, .Push_Constant: // skip } } } return } // Convert integer swizzle indices to component string (e.g. [0,1,2] -> "xyz") swizzle_indices_to_string :: proc(indices: []int) -> string { components := "xyzw" buf: [4]u8 for idx, i in indices { if i >= 4 do break buf[i] = idx < 4 ? components[idx] : 'x' } return strings.clone_from_bytes(buf[:len(indices)]) } has_attribute :: proc(attrs: []Ast_Attribute, name: string) -> bool { for attr in attrs { if attr.name == name do return true } return false } ir_const_value_to_string :: proc(v: IR_Const_Value) -> string { switch val in v { case i64: return fmt.aprintf("%d", val) case f64: s := fmt.aprintf("%v", val) if !strings.contains(s, ".") && !strings.contains(s, "e") { result := fmt.aprintf("%s.0", s) delete(s) return result } return s case bool: return val ? "true" : "false" } return "0" } parse_int :: proc(s: string) -> int { val, ok := strconv.parse_int(s) if ok { return val } return 0 } // Levenshtein edit distance for "did you mean?" suggestions levenshtein_distance :: proc(a, b: string) -> int { if len(a) == 0 do return len(b) if len(b) == 0 do return len(a) // Use single-row DP prev := make([]int, len(b) + 1) curr := make([]int, len(b) + 1) defer delete(prev) defer delete(curr) for j in 0 ..= len(b) { prev[j] = j } for i in 1 ..= len(a) { curr[0] = i for j in 1 ..= len(b) { cost := a[i - 1] == b[j - 1] ? 0 : 1 curr[j] = min( prev[j] + 1, // deletion curr[j - 1] + 1, // insertion prev[j - 1] + cost, // substitution ) } prev, curr = curr, prev } return prev[len(b)] } MAX_ERRORS :: 20