Harbor

branch main
showing the latest snapshot on main
preprocessor.odin 6.2 KB · Plain text
gpu/shader/preprocessor.odin 0644 Raw
package shader

import "core:os"
import "core:strings"
import "core:path/filepath"

MAX_INCLUDE_DEPTH :: 32

// Source map entry: maps a range in the flattened source back to original file + line
Source_Map_Entry :: struct {
	flat_line_start: int,    // line in flattened source
	flat_line_count: int,    // number of lines from this file segment
	orig_file:       string, // original file path
	orig_line_start: int,    // starting line in original file
}

Source_Map :: struct {
	entries: [dynamic]Source_Map_Entry,
}

Preprocessor :: struct {
	include_dirs: []string,
	file_reader:  File_Reader,
	included:     map[string]bool,  // for #pragma once (by resolved path)
	diagnostics:  [dynamic]Diagnostic,
	source_map:   Source_Map,
}

File_Reader :: #type proc(path: string) -> (content: string, ok: bool)

default_file_reader :: proc(path: string) -> (content: string, ok: bool) {
	data, err := os.read_entire_file(path, context.allocator)
	if err != nil {
		return "", false
	}
	return string(data), true
}

preprocessor_init :: proc(include_dirs: []string = nil, reader: File_Reader = nil, allocator := context.allocator) -> Preprocessor {
	return Preprocessor{
		include_dirs = include_dirs,
		file_reader  = reader != nil ? reader : default_file_reader,
		included     = make(map[string]bool, allocator = allocator),
		diagnostics  = make([dynamic]Diagnostic, allocator),
		source_map   = Source_Map{entries = make([dynamic]Source_Map_Entry, allocator)},
	}
}

// Preprocess expands #include directives and returns flattened source
preprocess :: proc(pp: ^Preprocessor, source: string, file: string) -> (string, []Diagnostic) {
	result := preprocess_recursive(pp, source, file, 0)
	return result, pp.diagnostics[:]
}

@(private = "file")
preprocess_recursive :: proc(pp: ^Preprocessor, source: string, file: string, depth: int) -> string {
	if depth > MAX_INCLUDE_DEPTH {
		append(&pp.diagnostics, Diagnostic{
			level   = .Error,
			message = "maximum include depth exceeded",
			span    = Source_Span{file = file, line_start = 1, col_start = 1},
		})
		return source
	}

	buf := strings.builder_make()
	lines := strings.split(source, "\n")
	current_flat_line := 1
	segment_start_line := 1

	for i := 0; i < len(lines); i += 1 {
		line := strings.trim_space(lines[i])

		if strings.has_prefix(line, "#pragma once") {
			abs_path := resolve_path(file)
			if abs_path in pp.included {
				return "" // already included
			}
			pp.included[abs_path] = true
			continue
		}

		if strings.has_prefix(line, "#include") {
			// Flush source map entry for lines before this include
			if i > segment_start_line - 1 {
				append(&pp.source_map.entries, Source_Map_Entry{
					flat_line_start = current_flat_line,
					flat_line_count = i - (segment_start_line - 1),
					orig_file       = file,
					orig_line_start = segment_start_line,
				})
				current_flat_line += i - (segment_start_line - 1)
			}

			include_path, ok := parse_include_directive(line)
			if !ok {
				append(&pp.diagnostics, Diagnostic{
					level   = .Error,
					message = "malformed #include directive",
					span    = Source_Span{file = file, line_start = i + 1, col_start = 1},
				})
				continue
			}

			resolved, is_angle := resolve_include_path(pp, include_path, file)

			// If filesystem resolution failed, try the file_reader directly
			// (supports embedded/virtual includes)
			if resolved == "" {
				if _, reader_ok := pp.file_reader(include_path); reader_ok {
					resolved = include_path
				}
			}

			if resolved == "" {
				append(&pp.diagnostics, Diagnostic{
					level   = .Error,
					message = strings.concatenate({"could not find included file: ", include_path}),
					span    = Source_Span{file = file, line_start = i + 1, col_start = 1},
				})
				continue
			}

			// Check circular include
			abs_resolved := resolve_path(resolved)
			if abs_resolved in pp.included {
				// Already included via #pragma once — skip silently
				segment_start_line = i + 2
				continue
			}

			content, read_ok := pp.file_reader(resolved)
			if !read_ok {
				append(&pp.diagnostics, Diagnostic{
					level   = .Error,
					message = strings.concatenate({"could not read included file: ", resolved}),
					span    = Source_Span{file = file, line_start = i + 1, col_start = 1},
				})
				continue
			}

			expanded := preprocess_recursive(pp, content, resolved, depth + 1)
			strings.write_string(&buf, expanded)
			if len(expanded) > 0 && expanded[len(expanded) - 1] != '\n' {
				strings.write_string(&buf, "\n")
			}

			segment_start_line = i + 2
			continue
		}

		strings.write_string(&buf, lines[i])
		if i < len(lines) - 1 {
			strings.write_string(&buf, "\n")
		}
	}

	return strings.to_string(buf)
}

@(private = "file")
parse_include_directive :: proc(line: string) -> (path: string, ok: bool) {
	rest := strings.trim_space(line[len("#include"):])
	if len(rest) < 2 do return "", false

	if rest[0] == '"' {
		end := strings.index_byte(rest[1:], '"')
		if end < 0 do return "", false
		return rest[1:1 + end], true
	}
	if rest[0] == '<' {
		end := strings.index_byte(rest[1:], '>')
		if end < 0 do return "", false
		return rest[1:1 + end], true
	}
	return "", false
}

@(private = "file")
resolve_include_path :: proc(pp: ^Preprocessor, path: string, current_file: string) -> (resolved: string, is_angle: bool) {
	// Try relative to current file first
	dir := filepath.dir(current_file)
	candidate, join_err := filepath.join({dir, path}, context.allocator)
	if join_err == nil && os.exists(candidate) {
		return candidate, false
	}

	// Try include dirs
	for inc_dir in pp.include_dirs {
		c, jerr := filepath.join({inc_dir, path}, context.allocator)
		if jerr == nil && os.exists(c) {
			return c, true
		}
	}

	return "", false
}

@(private = "file")
resolve_path :: proc(path: string) -> string {
	abs, err := filepath.abs(path, context.allocator)
	if err == nil {
		return abs
	}
	return path
}

// Look up original file and line from a flattened source position
source_map_lookup :: proc(sm: ^Source_Map, flat_line: int) -> (file: string, orig_line: int) {
	for entry in sm.entries {
		if flat_line >= entry.flat_line_start && flat_line < entry.flat_line_start + entry.flat_line_count {
			offset := flat_line - entry.flat_line_start
			return entry.orig_file, entry.orig_line_start + offset
		}
	}
	return "", flat_line
}