415 lines
17 KiB
Plaintext
415 lines
17 KiB
Plaintext
import "@std/mem"
|
|
import "@std/debug"
|
|
import "@std/arraylist"
|
|
import "@std/hashmap"
|
|
import "@std/io"
|
|
import "@std/fs"
|
|
import "@source/strpool"
|
|
import "@source/diagnostics"
|
|
import "@source/lexer"
|
|
import "@source/parser"
|
|
import "@source/ast"
|
|
import "@source/sema"
|
|
import "@source/lower"
|
|
import "@source/loader"
|
|
import "@source/ir"
|
|
typecheck :: import "@source/sema/check"
|
|
import "@source/sema/specialize"
|
|
|
|
#! identities are valid only within their owning compilation.
|
|
SourceId :: alias diagnostics.SourceId
|
|
PackageId :: distinct usize
|
|
|
|
#! node IDs belong to the compilation arena; source identifies their originating file.
|
|
DeclarationId :: struct {
|
|
source SourceId
|
|
node ast.NodeId
|
|
}
|
|
|
|
Source :: struct {
|
|
package PackageId
|
|
path []u8
|
|
bytes arraylist.ArrayList(u8)
|
|
root ast.NodeId
|
|
}
|
|
|
|
Package :: struct {
|
|
sources arraylist.ArrayList(SourceId)
|
|
path []u8 = &[]
|
|
root ?ast.NodeId = null
|
|
loading bool = false
|
|
}
|
|
|
|
Import :: struct {
|
|
node ast.NodeId
|
|
target ?PackageId
|
|
}
|
|
|
|
#! owns source buffers, paths, the AST arena, package graph and one identifier pool.
|
|
#! metadata borrows are invalidated by registration; IDs remain stable until deinit.
|
|
Compilation :: struct {
|
|
allocator mem.Allocator
|
|
strings strpool.StringPool
|
|
builder ast.Builder
|
|
sources arraylist.ArrayList(Source)
|
|
packages arraylist.ArrayList(Package)
|
|
imports arraylist.ArrayList(Import)
|
|
paths hashmap.StringHashMap(PackageId)
|
|
module_root ?ast.NodeId = null
|
|
}
|
|
|
|
LoadError :: enum { empty_package }
|
|
Error :: alias mem.AllocError | diagnostics.CapacityError | lexer.SourceError | strpool.InternError | loader.Error | LoadError
|
|
|
|
#! retains `allocator`; release all compilation storage with deinit.
|
|
init proc(allocator mem.Allocator) Compilation {
|
|
return Compilation{
|
|
allocator = allocator,
|
|
strings = strpool.init(allocator),
|
|
builder = ast.init_builder(allocator, &[]),
|
|
sources = arraylist.init(allocator),
|
|
packages = arraylist.init(allocator),
|
|
imports = arraylist.init(allocator),
|
|
paths = hashmap.init(allocator),
|
|
}
|
|
}
|
|
|
|
deinit proc(compilation @mut Compilation) void {
|
|
ast.deinit_builder(&compilation.builder)
|
|
hashmap.deinit(&compilation.paths)
|
|
for (compilation.sources.items) |@file| {
|
|
arraylist.deinit(&file.bytes)
|
|
mem.free(compilation.allocator, file.path)
|
|
}
|
|
for (compilation.packages.items) |@package| {
|
|
arraylist.deinit(&package.sources)
|
|
mem.free(compilation.allocator, package.path)
|
|
}
|
|
arraylist.deinit(&compilation.sources)
|
|
arraylist.deinit(&compilation.packages)
|
|
arraylist.deinit(&compilation.imports)
|
|
strpool.deinit(&compilation.strings)
|
|
}
|
|
|
|
#! creates an in-memory package; load registers filesystem packages by canonical directory.
|
|
add_package proc(compilation @mut Compilation) PackageId ! mem.AllocError {
|
|
id :: PackageId(compilation.packages.items.len)
|
|
try arraylist.append(&compilation.packages, Package{
|
|
sources = arraylist.init(compilation.allocator),
|
|
})
|
|
compilation.module_root = null
|
|
return id
|
|
}
|
|
|
|
#! copies `path`, scans/parses into the shared arena and takes `bytes` only on success.
|
|
#! failure leaves `bytes` caller-owned and rolls back source diagnostics; interned names may remain.
|
|
add_source proc(
|
|
compilation @mut Compilation,
|
|
package PackageId,
|
|
path []u8,
|
|
bytes @mut arraylist.ArrayList(u8),
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) SourceId ! Error {
|
|
debug.assert(usize(package) < compilation.packages.items.len)
|
|
try arraylist.reserve(&compilation.sources, compilation.sources.items.len + 1)
|
|
members := &compilation.packages.items[usize(package)].sources
|
|
try arraylist.reserve(members, members.items.len + 1)
|
|
owned_path :: try mem.alloc(u8, compilation.allocator, path.len)
|
|
errdefer mem.free(compilation.allocator, owned_path)
|
|
memcopy!(owned_path, path)
|
|
id :: try ast.set_source(&compilation.builder, owned_path, bytes.items)
|
|
errdefer ast.remove_source(&compilation.builder)
|
|
diagnostic_count :: diagnostics.count(diagnostic_store)
|
|
errdefer diagnostic_store.list.items = diagnostic_store.list.items[..diagnostic_count]
|
|
debug.assert(usize(id) == compilation.sources.items.len)
|
|
scan_state := lexer.init(compilation.allocator)
|
|
defer lexer.deinit(&scan_state)
|
|
scan_state.source_id = id
|
|
try lexer.scan(&scan_state, bytes.items, &compilation.strings, diagnostic_store)
|
|
root :: try parser.parse_source(&compilation.builder, scan_state.tokens.items, diagnostic_store)
|
|
try arraylist.append(&compilation.sources, Source{
|
|
package = package,
|
|
path = owned_path,
|
|
bytes = bytes^,
|
|
root = root,
|
|
})
|
|
try arraylist.append(members, id)
|
|
bytes^ = arraylist.init(bytes.allocator)
|
|
compilation.packages.items[usize(package)].root = null
|
|
compilation.module_root = null
|
|
return id
|
|
}
|
|
|
|
#! borrows source metadata until registration or deinit.
|
|
source proc(compilation @Compilation, id SourceId) @Source {
|
|
debug.assert(usize(id) < compilation.sources.items.len)
|
|
return &compilation.sources.items[usize(id)]
|
|
}
|
|
|
|
declaration proc(compilation @Compilation, id SourceId, index usize) DeclarationId {
|
|
return DeclarationId{
|
|
source = id,
|
|
node = ast.builder_package_decl(&compilation.builder, source(compilation, id).root, index),
|
|
}
|
|
}
|
|
|
|
#! borrows source paths and bytes until registration or deinit; valid after loading a source.
|
|
source_files proc(compilation @Compilation) []diagnostics.Source {
|
|
tree :: ast.view(&compilation.builder, compilation.sources.items[0].root)
|
|
return ast.source_files(&tree)
|
|
}
|
|
|
|
#! loads a file alone or every production source in a directory, then its dependency graph.
|
|
#! directory identities resolve symlinks; file commands do not implicitly include siblings.
|
|
load proc(
|
|
compilation @mut Compilation,
|
|
runtime io.Io,
|
|
path []u8,
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) PackageId ! Error {
|
|
canonical :: try fs.canonical(compilation.allocator, path)
|
|
defer mem.free(compilation.allocator, canonical)
|
|
directory :: try loader.directory(compilation.allocator, canonical)
|
|
transferred := false
|
|
defer { if (!transferred) mem.free(compilation.allocator, directory) }
|
|
project_root :: try loader.project_root(compilation.allocator, directory)
|
|
defer mem.free(compilation.allocator, project_root)
|
|
entry_file ?[]u8 := null
|
|
if (try fs.kind(compilation.allocator, canonical) != .directory) entry_file = canonical
|
|
transferred = true
|
|
return try load_package(compilation, runtime, project_root, directory, entry_file, null, diagnostic_store)
|
|
}
|
|
|
|
@hide:file
|
|
load_package proc(
|
|
compilation @mut Compilation,
|
|
runtime io.Io,
|
|
project_root []u8,
|
|
owned_directory []mut u8,
|
|
entry_file ?[]u8,
|
|
import_span ?diagnostics.Span,
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) PackageId ! Error {
|
|
registered := false
|
|
defer { if (!registered) mem.free(compilation.allocator, owned_directory) }
|
|
if (hashmap.get(&compilation.paths, owned_directory)) |existing| {
|
|
if (compilation.packages.items[usize(existing)].loading) {
|
|
if (import_span) |span| _ = try diagnostics.add(diagnostic_store, .import_cycle, span)
|
|
}
|
|
return existing
|
|
}
|
|
id :: try add_package(compilation)
|
|
compilation.packages.items[usize(id)].path = owned_directory
|
|
compilation.packages.items[usize(id)].loading = true
|
|
registered = true
|
|
hashmap.put(&compilation.paths, owned_directory, id) catch |err| {
|
|
match err {
|
|
.key_exists: unreachable
|
|
else: return err
|
|
}
|
|
}
|
|
read_failed := false
|
|
load_sources(compilation, runtime, id, entry_file, diagnostic_store) catch |err| {
|
|
match err {
|
|
.out_of_memory, .diagnostic_limit_reached, .storage_limit_reached, .out_of_space: return err
|
|
else: {
|
|
if (import_span) |span| {
|
|
_ = try diagnostics.add(diagnostic_store, .package_unavailable, span)
|
|
read_failed = true
|
|
} else return err
|
|
}
|
|
}
|
|
}
|
|
if (compilation.packages.items[usize(id)].sources.items.len == 0) {
|
|
if (import_span) |span| {
|
|
if (!read_failed) _ = try diagnostics.add(diagnostic_store, .package_unavailable, span)
|
|
} else return .empty_package
|
|
}
|
|
# Source and package arrays may grow during recursion; retain IDs, never their element addresses.
|
|
for (0..(compilation.packages.items[usize(id)].sources.items.len)) |index| {
|
|
file_id :: compilation.packages.items[usize(id)].sources.items[index]
|
|
root :: source(compilation, file_id).root
|
|
count :: ast.builder_package_decl_count(&compilation.builder, root)
|
|
for (0..count) |declaration_index| {
|
|
node :: ast.builder_package_decl(&compilation.builder, root, declaration_index)
|
|
tree :: ast.view(&compilation.builder, root)
|
|
if (ast.decl_kind(&tree, node) != .import) continue
|
|
imported :: ast.import_decl(&tree, node)
|
|
if (imported.test_only) continue
|
|
path :: ast.source_text(&tree, imported.path)
|
|
target ?PackageId := null
|
|
if (path.len == 0 or path[0] == '/' or (path.len == 1 and path[0] == '@')) {
|
|
_ = try diagnostics.add(diagnostic_store, .invalid_import_path, imported.path)
|
|
} else {
|
|
resolved := loader.resolve(
|
|
compilation.allocator,
|
|
project_root,
|
|
compilation.packages.items[usize(id)].path,
|
|
path,
|
|
) catch |err| {
|
|
match err {
|
|
.out_of_memory: return err
|
|
.invalid_path: {
|
|
_ = try diagnostics.add(diagnostic_store, .invalid_import_path, imported.path)
|
|
try arraylist.append(&compilation.imports, Import{ node = node, target = null })
|
|
continue
|
|
}
|
|
else: {
|
|
_ = try diagnostics.add(diagnostic_store, .package_unavailable, imported.path)
|
|
try arraylist.append(&compilation.imports, Import{ node = node, target = null })
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
owned := true
|
|
defer { if (owned) mem.free(compilation.allocator, resolved) }
|
|
if (try fs.kind(compilation.allocator, resolved) == .directory) {
|
|
owned = false
|
|
target = try load_package(compilation, runtime, project_root, resolved, null, imported.path, diagnostic_store)
|
|
} else {
|
|
_ = try diagnostics.add(diagnostic_store, .package_unavailable, imported.path)
|
|
}
|
|
}
|
|
try arraylist.append(&compilation.imports, Import{ node = node, target = target })
|
|
}
|
|
}
|
|
compilation.packages.items[usize(id)].loading = false
|
|
return id
|
|
}
|
|
|
|
@hide:file
|
|
load_sources proc(
|
|
compilation @mut Compilation,
|
|
runtime io.Io,
|
|
package PackageId,
|
|
entry_file ?[]u8,
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) void ! Error {
|
|
if (entry_file) |path| {
|
|
try load_source(compilation, runtime, package, path, diagnostic_store)
|
|
return
|
|
}
|
|
files := try loader.files(compilation.allocator, compilation.packages.items[usize(package)].path)
|
|
defer {
|
|
for (files.items) |path| mem.free(compilation.allocator, path)
|
|
arraylist.deinit(&files)
|
|
}
|
|
for (files.items) |path| try load_source(compilation, runtime, package, path, diagnostic_store)
|
|
}
|
|
|
|
@hide:file
|
|
load_source proc(
|
|
compilation @mut Compilation,
|
|
runtime io.Io,
|
|
package PackageId,
|
|
path []u8,
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) void ! Error {
|
|
bytes := try loader.read_source(compilation.allocator, runtime, path)
|
|
defer arraylist.deinit(&bytes)
|
|
_ = try add_source(compilation, package, path, &bytes, diagnostic_store)
|
|
}
|
|
|
|
@hide:file
|
|
package_root proc(compilation @mut Compilation, package PackageId) ast.NodeId ! mem.AllocError {
|
|
if (compilation.packages.items[usize(package)].root) |root| return root
|
|
declarations := arraylist.init(ast.NodeId, compilation.allocator)
|
|
defer arraylist.deinit(&declarations)
|
|
span := diagnostics.Span{ start = 0, end = 0 }
|
|
for (compilation.packages.items[usize(package)].sources.items) |id, index| {
|
|
root :: source(compilation, id).root
|
|
tree :: ast.view(&compilation.builder, root)
|
|
if (index == 0) span = ast.node_span(&tree, root)
|
|
for (0..ast.package_decl_count(&tree)) |i| try arraylist.append(&declarations, ast.package_decl(&tree, i))
|
|
}
|
|
root :: try ast.add_package(&compilation.builder, span, declarations.items)
|
|
compilation.packages.items[usize(package)].root = root
|
|
return root
|
|
}
|
|
|
|
#! checks every package in a separate scope and lowers one module with root-only entry selection.
|
|
#! returned IR borrows no frontend data; release it with ir.deinit.
|
|
#! temporary compilation storage uses the retained allocator and is released before returning.
|
|
compile proc(
|
|
compilation @mut Compilation,
|
|
root_package PackageId,
|
|
diagnostic_store @mut diagnostics.Store,
|
|
) ir.Module ! Error {
|
|
roots := arraylist.init(ast.NodeId, compilation.allocator)
|
|
defer arraylist.deinit(&roots)
|
|
declarations := arraylist.init(ast.NodeId, compilation.allocator)
|
|
defer arraylist.deinit(&declarations)
|
|
for (0..(compilation.packages.items.len)) |index| {
|
|
root :: try package_root(compilation, PackageId(index))
|
|
try arraylist.append(&roots, root)
|
|
if (compilation.module_root == null) {
|
|
tree :: ast.view(&compilation.builder, root)
|
|
for (0..ast.package_decl_count(&tree)) |i| try arraylist.append(&declarations, ast.package_decl(&tree, i))
|
|
}
|
|
}
|
|
if (compilation.module_root == null) compilation.module_root = try ast.add_package(&compilation.builder, diagnostics.Span{ start = 0, end = 0 }, declarations.items)
|
|
imports := arraylist.init(sema.Import, compilation.allocator)
|
|
defer arraylist.deinit(&imports)
|
|
tree := ast.view(&compilation.builder, compilation.module_root?)
|
|
for (compilation.imports.items) |link| {
|
|
imported :: ast.import_decl(&tree, link.node)
|
|
name := strpool.no_str
|
|
failure ?diagnostics.DiagnosticId := null
|
|
for (diagnostic_store.list.items) |diagnostic, index| {
|
|
if (
|
|
diagnostic.span.source == imported.path.source
|
|
and diagnostic.span.start == imported.path.start
|
|
and diagnostic.span.end == imported.path.end
|
|
and (diagnostic.code == .package_unavailable
|
|
or diagnostic.code == .invalid_import_path
|
|
or diagnostic.code == .import_cycle)
|
|
) {
|
|
failure = diagnostics.DiagnosticId(index)
|
|
break
|
|
}
|
|
}
|
|
if (imported.namespace) |explicit| {
|
|
name = explicit
|
|
} else {
|
|
path :: if (link.target) |target|
|
|
compilation.packages.items[usize(target)].path
|
|
else
|
|
ast.source_text(&tree, imported.path)
|
|
start := path.len
|
|
while (start > 0 and path[start - 1] != '/') start -= 1
|
|
candidate :: path[start..]
|
|
valid := candidate.len > 0
|
|
for (candidate) |byte, index| {
|
|
if (
|
|
!(byte >= 'a' and byte <= 'z')
|
|
and !(byte >= 'A' and byte <= 'Z')
|
|
and byte != '_'
|
|
and !(index > 0 and byte >= '0' and byte <= '9')
|
|
) valid = false
|
|
}
|
|
if (valid) {
|
|
name = try strpool.intern(&compilation.strings, candidate)
|
|
} else if (failure == null) {
|
|
failure = try diagnostics.add(diagnostic_store, .invalid_import_alias, imported.path)
|
|
}
|
|
}
|
|
target ?ast.NodeId :: if (link.target) |package| roots.items[usize(package)] else null
|
|
try arraylist.append(&imports, sema.Import{ node = link.node, name = name, target = target, diagnostic = failure })
|
|
}
|
|
resolution := try sema.resolve_packages(compilation.allocator, &tree, roots.items, imports.items, &compilation.strings, diagnostic_store)
|
|
defer sema.deinit(&resolution)
|
|
elaborated := try specialize.elaborate(
|
|
compilation.allocator,
|
|
&compilation.builder,
|
|
compilation.module_root?,
|
|
&resolution,
|
|
&compilation.strings,
|
|
diagnostic_store,
|
|
)
|
|
defer specialize.deinit(&elaborated)
|
|
tree = ast.view(&compilation.builder, compilation.module_root?)
|
|
checked := try typecheck.check_elaborated(compilation.allocator, &tree, &resolution, elaborated.constraints.items, diagnostic_store)
|
|
defer typecheck.deinit(&checked)
|
|
return try lower.lower_package(compilation.allocator, &tree, &resolution, &checked, roots.items[usize(root_package)], diagnostic_store)
|
|
}
|