Harbor

branch main
showing the latest snapshot on main
common.odin 7.0 KB · Plain text
shader/common.odin 0644 Raw
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