270 lines
7.5 KiB
Odin
270 lines
7.5 KiB
Odin
package loader
|
|
|
|
import "../ast"
|
|
import "../lexer"
|
|
import "../parser"
|
|
import "../source"
|
|
import "../symbol"
|
|
import "core:mem"
|
|
import "core:os"
|
|
import "core:path/filepath"
|
|
import "core:slice"
|
|
import "core:strings"
|
|
|
|
State :: struct {
|
|
module: ^ast.Module,
|
|
sources: ^source.Store,
|
|
diagnostics: ^source.Diagnostics,
|
|
symbols: ^symbol.Table,
|
|
token_allocator: mem.Allocator,
|
|
allocator: mem.Allocator,
|
|
root_failed: bool,
|
|
}
|
|
|
|
is_identifier :: proc(value: string) -> bool {
|
|
if len(value) == 0 {
|
|
return false
|
|
}
|
|
is_start := proc(value: byte) -> bool {
|
|
return value == '_' || value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z'
|
|
}
|
|
if !is_start(value[0]) {
|
|
return false
|
|
}
|
|
for byte_value in transmute([]byte)value[1:] {
|
|
if !is_start(byte_value) && !(byte_value >= '0' && byte_value <= '9') {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
find_package :: proc(state: ^State, path: string) -> int {
|
|
for pkg, id in state.module.packages {
|
|
if pkg.path == path {
|
|
return id
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
add_placeholder :: proc(state: ^State, path: string) -> int {
|
|
if existing := find_package(state, path); existing >= 0 {
|
|
return existing
|
|
}
|
|
id := len(state.module.packages)
|
|
append(&state.module.packages, ast.Package{
|
|
path=strings.clone(path, state.allocator),
|
|
name=symbol.intern(state.symbols, filepath.base(path)),
|
|
available=false,
|
|
})
|
|
return id
|
|
}
|
|
|
|
read_package_files :: proc(state: ^State, path: string) -> ([]os.File_Info, bool) {
|
|
handle, open_error := os.open(path, os.O_RDONLY)
|
|
if open_error != nil {
|
|
return nil, false
|
|
}
|
|
defer os.close(handle)
|
|
entries, read_error := os.read_dir(handle, -1, state.allocator)
|
|
if read_error != nil {
|
|
return nil, false
|
|
}
|
|
slice.sort_by(entries, proc(a, b: os.File_Info) -> bool {
|
|
return a.name < b.name
|
|
})
|
|
files: [dynamic]os.File_Info
|
|
files.allocator = state.allocator
|
|
for entry in entries {
|
|
if !entry.is_dir && filepath.ext(entry.name) == ".bro" {
|
|
append(&files, entry)
|
|
} else {
|
|
os.file_info_delete(entry, state.allocator)
|
|
}
|
|
}
|
|
delete(entries, state.allocator)
|
|
return files[:], true
|
|
}
|
|
|
|
resolve_import_path :: proc(state: ^State, importing_path, import_path: string) -> (string, bool) {
|
|
if filepath.is_abs(import_path) {
|
|
return "", false
|
|
}
|
|
joined, join_error := filepath.join({importing_path, import_path}, state.allocator)
|
|
if join_error != nil {
|
|
return "", false
|
|
}
|
|
canonical, ok := filepath.abs(joined, state.allocator)
|
|
if ok {
|
|
delete(joined, state.allocator)
|
|
return canonical, true
|
|
}
|
|
return joined, false
|
|
}
|
|
|
|
load_package :: proc(state: ^State, path: string, import_span: source.Span, is_root := false) -> int {
|
|
canonical, ok := filepath.abs(path, state.allocator)
|
|
if !ok || !os.is_dir(path) {
|
|
if is_root {
|
|
state.root_failed = true
|
|
if len(canonical) > 0 {
|
|
delete(canonical, state.allocator)
|
|
}
|
|
return -1
|
|
}
|
|
placeholder := path
|
|
if len(canonical) > 0 {
|
|
placeholder = canonical
|
|
}
|
|
id := add_placeholder(state, placeholder)
|
|
source.addf(state.diagnostics, import_span, "could not import package directory '%s'", path)
|
|
if len(canonical) > 0 {
|
|
delete(canonical, state.allocator)
|
|
}
|
|
return id
|
|
}
|
|
if existing := find_package(state, canonical); existing >= 0 {
|
|
delete(canonical, state.allocator)
|
|
return existing
|
|
}
|
|
|
|
pkg_id := len(state.module.packages)
|
|
append(&state.module.packages, ast.Package{
|
|
path=canonical,
|
|
name=symbol.intern(state.symbols, filepath.base(canonical)),
|
|
available=true,
|
|
})
|
|
files, files_ok := read_package_files(state, canonical)
|
|
if !files_ok {
|
|
state.root_failed = true
|
|
return pkg_id
|
|
}
|
|
if len(files) == 0 {
|
|
if is_root {
|
|
state.root_failed = true
|
|
} else {
|
|
source.addf(state.diagnostics, import_span, "package '%s' contains no readable .bro files", canonical)
|
|
state.module.packages[pkg_id].available = false
|
|
}
|
|
os.file_info_slice_delete(files, state.allocator)
|
|
return pkg_id
|
|
}
|
|
|
|
for file_info in files {
|
|
bytes, read_ok := os.read_entire_file(file_info.fullpath)
|
|
if !read_ok {
|
|
state.root_failed = true
|
|
continue
|
|
}
|
|
source_id := source.add_source(state.sources, file_info.fullpath, string(bytes))
|
|
delete(bytes)
|
|
file_id := len(state.module.files)
|
|
append(&state.module.files, ast.File{source=source_id, pkg=pkg_id})
|
|
stream := lexer.lex(&state.sources.items[source_id], state.diagnostics, state.symbols, state.token_allocator)
|
|
parser.parse_into(&stream, &state.sources.items[source_id], state.diagnostics, state.module, pkg_id, file_id)
|
|
delete(stream.items)
|
|
}
|
|
os.file_info_slice_delete(files, state.allocator)
|
|
|
|
import_count := len(state.module.imports)
|
|
for import_id in 0..<import_count {
|
|
import_item := state.module.imports[import_id]
|
|
if import_item.pkg != pkg_id || import_item.target >= 0 {
|
|
continue
|
|
}
|
|
if filepath.is_abs(import_item.path) {
|
|
state.module.imports[import_id].diagnostic = source.add(state.diagnostics, import_item.span, "absolute import paths are invalid")
|
|
state.module.imports[import_id].valid = false
|
|
state.module.imports[import_id].target = add_placeholder(state, import_item.path)
|
|
continue
|
|
}
|
|
target_path, target_ok := resolve_import_path(state, canonical, import_item.path)
|
|
target := load_package(state, target_path, import_item.span)
|
|
state.module.imports[import_id].target = target
|
|
if !target_ok || target < 0 || !state.module.packages[target].available {
|
|
state.module.imports[import_id].valid = false
|
|
}
|
|
delete(target_path, state.allocator)
|
|
}
|
|
return pkg_id
|
|
}
|
|
|
|
declaration_conflicts :: proc(module: ^ast.Module, pkg: int, name: symbol.Id) -> bool {
|
|
for function in module.functions {
|
|
if function.pkg == pkg && function.name == name {
|
|
return true
|
|
}
|
|
}
|
|
for global in module.globals {
|
|
if global.pkg == pkg && global.name == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
validate_imports :: proc(state: ^State) {
|
|
for import_item, import_id in state.module.imports {
|
|
if !symbol.is_valid(import_item.alias) && import_item.target >= 0 {
|
|
state.module.imports[import_id].alias = state.module.packages[import_item.target].name
|
|
}
|
|
alias := state.module.imports[import_id].alias
|
|
alias_text := symbol.resolve(state.symbols, alias)
|
|
if !is_identifier(alias_text) {
|
|
state.module.imports[import_id].diagnostic = source.add(
|
|
state.diagnostics,
|
|
import_item.span,
|
|
"import requires an explicit valid identifier alias",
|
|
)
|
|
state.module.imports[import_id].valid = false
|
|
}
|
|
if declaration_conflicts(state.module, import_item.pkg, alias) {
|
|
state.module.imports[import_id].diagnostic = source.addf(
|
|
state.diagnostics,
|
|
import_item.span,
|
|
"import alias '%s' conflicts with a package declaration",
|
|
alias_text,
|
|
)
|
|
state.module.imports[import_id].valid = false
|
|
}
|
|
for previous in state.module.imports[:import_id] {
|
|
if previous.file == import_item.file && previous.alias == alias {
|
|
state.module.imports[import_id].diagnostic = source.addf(
|
|
state.diagnostics,
|
|
import_item.span,
|
|
"duplicate import alias '%s' in the same file",
|
|
alias_text,
|
|
)
|
|
state.module.imports[import_id].valid = false
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
load :: proc(
|
|
root_path: string,
|
|
sources: ^source.Store,
|
|
diagnostics: ^source.Diagnostics,
|
|
symbols: ^symbol.Table,
|
|
token_allocator := context.allocator,
|
|
allocator := context.allocator,
|
|
) -> (ast.Module, bool) {
|
|
module := ast.init_module(allocator)
|
|
state := State{
|
|
module=&module,
|
|
sources=sources,
|
|
diagnostics=diagnostics,
|
|
symbols=symbols,
|
|
token_allocator=token_allocator,
|
|
allocator=allocator,
|
|
}
|
|
root := load_package(&state, root_path, source.Span{}, true)
|
|
if root != 0 && root >= 0 {
|
|
state.root_failed = true
|
|
}
|
|
validate_imports(&state)
|
|
return module, !state.root_failed
|
|
}
|