feat: resolve file-local import namespaces
This commit is contained in:
@@ -20,7 +20,9 @@ run a source file with the interpreter:
|
||||
|
||||
`run`, `emit-llvm` and `build` also accept a directory. directory inputs compile its `.hon` and `.bro` files in bytewise filename order, excluding `.test.hon`/`.test.bro` and non-file entries; a file input still compiles only that file, not its siblings. declarations share a scope within each package, while dependencies have separate scopes and cannot supply the root package's `main`.
|
||||
|
||||
bare `import "../library"` and aliased `library :: import "@library"` declarations discover dependencies. relative paths start at the importing package directory; `@` starts at the nearest ancestor containing `build.hon` or `build.bro`, falling back to the entry package directory. package directories are canonicalized through symlinks and loaded once. cycles, missing/empty dependencies and invalid import paths produce source diagnostics. absolute paths and empty, escaped or control-containing import strings are rejected. production loading ignores test-only imports. namespace aliases are not bound yet, and qualified calls such as `library.add(...)` remain unsupported.
|
||||
bare `import "../library"` and aliased `library :: import "@library"` declarations load dependencies. relative paths start at the importing package directory; `@` starts at the nearest ancestor containing `build.hon` or `build.bro`, falling back to the entry package directory. package directories are canonicalized through symlinks and loaded once. cycles, missing/empty dependencies and invalid import paths produce source diagnostics. absolute paths and empty, escaped or control-containing import strings are rejected. production loading ignores test-only imports.
|
||||
|
||||
imports bind file-local namespaces, never dependency declarations in the importing package. bare imports use the canonical package directory's final name; directories without a valid identifier name require an explicit alias. sibling files can use the same alias for different packages, but an alias cannot conflict with a package declaration, builtin type or another import in its own file. local shadowing remains forbidden. `library.add(...)` selects a public declaration and uses ordinary signature checking, function values and IR calls without a runtime namespace value. imported namespaces are not re-exported; missing members and non-namespace selections are diagnosed. general record fields, imported types and visibility attributes are not implemented; unsupported attributes produce syntax diagnostics.
|
||||
|
||||
```hon
|
||||
add proc(left, right int) int {
|
||||
@@ -105,7 +107,7 @@ identifier interning is compilation-owned: initialize a `strpool.StringPool`, pa
|
||||
|
||||
resolver scopes use `@std/hashmap.AutoHashMap(K, V)`, which shares `init`, `put`, `get` and `deinit` with custom `HashMap` and `StringHashMap`. automatic keys support integers, booleans, enums, distinct types, fixed arrays and records recursively; record padding is ignored. slices, floating-point values, pointers, optionals and unions require explicit hashing and equality through `HashMap` (`StringHashMap` supplies content-based byte-string operations). keys and values are stored by value; referenced resources remain caller-owned. the builtin declaration count is inferred, and builtin ID storage derives its fixed stack-array length from that declaration array rather than repeating a count or initializer.
|
||||
|
||||
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax retains its existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
|
||||
`sema.resolve` returns an owned binding table indexed by AST `NodeId`, without modifying the AST. `sema.binding` identifies a declaration, package namespace, builtin type, or invalid binding carrying its diagnostic ID; nodes with no binding return `.none`. `sema.resolve_packages` additionally borrows package roots and import links, predeclares every package before resolving qualified uses, and keeps import aliases in their originating source file. selected declarations have the same binding identity as unqualified declarations. release the result with `sema.deinit`. unknown names and duplicate declarations add source-spanned diagnostics; invalid syntax and failed imports retain their existing diagnostic identity. same-scope duplicates retain the first declaration for recovery, while forbidden inner declarations invalidate that name's uses rather than silently binding them to an outer declaration.
|
||||
|
||||
type checking (`@source/sema/check`) runs after name resolution. `check.check` borrows the AST and binding table and returns owned per-node types and interned array descriptors; query with `check.type_of` and `check.array_type`, and release with `check.deinit`. declarations infer their initializer types, including forward references and function aliases; function types identify the declaration supplying the callable signature. annotations support builtin types and fixed arrays; type names are not ordinary values. `void` is valid as a function result, not as a parameter or stored value.
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
## current boundary
|
||||
|
||||
directory loading and import declaration discovery are implemented; qualified
|
||||
namespace selection and the richer types/control flow used by the compiler
|
||||
remain unsupported. the driver retains frontend storage and source provenance
|
||||
through `@source/compilation`, with compilation-wide AST node identities.
|
||||
directory packages support file-local import namespaces and postfix declaration
|
||||
selection through the interpreter and native backend. the richer types/control
|
||||
flow used by the compiler remain unsupported. the driver retains frontend storage
|
||||
and source provenance through `@source/compilation`, with compilation-wide AST node identities.
|
||||
file commands retain single-file behavior; directory commands compile the
|
||||
package's production files and load its dependencies in separate scopes.
|
||||
|
||||
@@ -20,8 +20,8 @@ needs records, pointers, generics and C calls.
|
||||
|
||||
- [x] retain source/package identity and source ownership across the compilation; keep one identifier pool and unambiguous declaration IDs.
|
||||
- [x] load directory packages once per canonical path, with deterministic file discovery, package scopes and root-only entry selection. follow Bro's existing relative and project-root `@` import rules; diagnose cycles rather than recurse indefinitely.
|
||||
- [ ] finish postfix namespace selection and resolve file-local import aliases without injecting dependency names into the importing package; bare and aliased import declarations are already parsed for dependency discovery.
|
||||
- [ ] reuse ordinary scalar signature checking, function values and IR calls for selected declarations; namespaces have no runtime representation.
|
||||
- [x] finish postfix namespace selection and resolve file-local import aliases without injecting dependency names into the importing package; bare and aliased import declarations are already parsed for dependency discovery.
|
||||
- [x] reuse ordinary scalar signature checking, function values and IR calls for selected declarations; namespaces have no runtime representation.
|
||||
- [ ] preserve originating filenames and spans in dependency diagnostics and native runtime traps; retain demand-driven diagnostic recovery.
|
||||
- [ ] verify sibling source files, transitive/shared imports, same-spelled declarations in different packages, alias conflicts, missing packages/members, argument errors and dependency-file diagnostics. preserve existing single-file commands.
|
||||
|
||||
|
||||
+33
-2
@@ -3,13 +3,13 @@
|
||||
#! * binding ... -> :: | :=
|
||||
#! * literal .. -> int | float | string | 'true' | 'false'
|
||||
#! * op ....... -> '+' | '-' | '*' | '/' | '==' | '!=' | '<' | '<=' | '>' | '>=' | 'and' | 'or'
|
||||
#! * expr ..... -> identifier | literal | type | '[' [expr {',' expr} [',']] ']' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr ('..' | '..=') expr
|
||||
#! * expr ..... -> identifier | literal | type | '[' [expr {',' expr} [',']] ']' | '(' expr ')' | ('-' | '!') expr | expr op expr | expr '(' [expr {',' expr} [',']] ')' | expr '[' expr ']' | expr '.' identifier | expr ('..' | '..=') expr
|
||||
#! * decl ..... -> identifier [type] binding expr | ['test'] [identifier '::'] 'import' string
|
||||
#! * block .... -> '{' {stmt} '}'
|
||||
#! * arm ...... -> block | stmt (single statements become canonical blocks)
|
||||
#! * stmt ..... -> decl | expr | expr '=' expr | 'return' [expr] | 'break' | 'continue' | 'if' expr arm ['else' arm] | 'while' expr arm | 'for' expr '|' ['@'] identifier [',' identifier] '|' arm
|
||||
#! braceless guards/iterables require parentheses unless they are calls; else binds to the nearest if.
|
||||
#! precedence (tightest first): calls/indices, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses.
|
||||
#! precedence (tightest first): calls/indices/selections, unary -/!, multiplication/division, addition/subtraction, relational comparisons, equality, and, or, ranges. compound range endpoints require their own parentheses.
|
||||
#! binary operators associate left; unary operators associate right.
|
||||
|
||||
import "@std/debug"
|
||||
@@ -44,6 +44,7 @@ ExprKind :: enum {
|
||||
array_type
|
||||
array_literal
|
||||
index
|
||||
selection
|
||||
invalid
|
||||
}
|
||||
|
||||
@@ -96,6 +97,7 @@ NodeKind :: enum {
|
||||
expr_array_type
|
||||
expr_array_literal
|
||||
expr_index
|
||||
expr_selection
|
||||
stmt_expr
|
||||
stmt_decl
|
||||
stmt_return
|
||||
@@ -215,6 +217,12 @@ IndexView :: struct {
|
||||
index NodeId
|
||||
}
|
||||
|
||||
SelectionView :: struct {
|
||||
span Span
|
||||
base NodeId
|
||||
member NodeId
|
||||
}
|
||||
|
||||
ReturnView :: struct {
|
||||
span Span
|
||||
value ?NodeId
|
||||
@@ -450,6 +458,18 @@ add_index proc(builder @mut Builder, span Span, base, index NodeId) NodeId ! mem
|
||||
}, span)
|
||||
}
|
||||
|
||||
add_selection proc(
|
||||
builder @mut Builder,
|
||||
span Span,
|
||||
base, member NodeId,
|
||||
) NodeId ! mem.AllocError {
|
||||
return try add_node(builder, Node{
|
||||
kind = .expr_selection,
|
||||
data0 = NodeData{ node_id = base },
|
||||
data1 = NodeData{ node_id = member },
|
||||
}, span)
|
||||
}
|
||||
|
||||
add_expression_stmt proc(builder @mut Builder, expression NodeId) NodeId ! mem.AllocError {
|
||||
return try add_node(builder, Node{
|
||||
kind = .stmt_expr,
|
||||
@@ -903,6 +923,7 @@ expression_kind proc(kind NodeKind) ExprKind {
|
||||
.expr_array_type: .array_type
|
||||
.expr_array_literal: .array_literal
|
||||
.expr_index: .index
|
||||
.expr_selection: .selection
|
||||
.invalid_expr: .invalid
|
||||
else: unreachable
|
||||
}
|
||||
@@ -996,6 +1017,16 @@ index_expr proc(tree @Tree, id NodeId) IndexView {
|
||||
}
|
||||
}
|
||||
|
||||
selection_expr proc(tree @Tree, id NodeId) SelectionView {
|
||||
node :: get_node(&tree.storage, id)
|
||||
debug.assert(node.kind == .expr_selection)
|
||||
return SelectionView{
|
||||
span = get_span(&tree.storage, id),
|
||||
base = node.data0.node_id,
|
||||
member = node.data1.node_id,
|
||||
}
|
||||
}
|
||||
|
||||
node_span proc(tree @Tree, id NodeId) Span {
|
||||
return get_span(&tree.storage, id)
|
||||
}
|
||||
|
||||
@@ -348,8 +348,49 @@ compile proc(
|
||||
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?)
|
||||
resolution := try sema.resolve_packages(compilation.allocator, &tree, roots.items, &compilation.strings, diagnostic_store)
|
||||
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)
|
||||
checked := try typecheck.check(compilation.allocator, &tree, &resolution, diagnostic_store)
|
||||
defer typecheck.deinit(&checked)
|
||||
|
||||
@@ -66,6 +66,9 @@ Code :: enum {
|
||||
import_cycle
|
||||
invalid_import_path
|
||||
package_unavailable
|
||||
namespace_not_value
|
||||
not_namespace
|
||||
invalid_import_alias
|
||||
}
|
||||
|
||||
Diagnostic :: struct {
|
||||
@@ -153,5 +156,8 @@ messages :: $enummap.init(Code, []u8, {
|
||||
import_cycle = "package import cycle",
|
||||
invalid_import_path = "invalid import path",
|
||||
package_unavailable = "cannot load package directory",
|
||||
namespace_not_value = "package namespace cannot be used as a value",
|
||||
not_namespace = "member selection requires a package namespace",
|
||||
invalid_import_alias = "import requires an explicit valid identifier alias",
|
||||
})
|
||||
|
||||
|
||||
@@ -160,6 +160,14 @@ scan proc(
|
||||
cursor += width
|
||||
continue
|
||||
}
|
||||
if (char == '.') {
|
||||
try add_token(state, Token{
|
||||
span = make_span(source, cursor, cursor + 1),
|
||||
kind = .dot,
|
||||
})
|
||||
cursor += 1
|
||||
continue
|
||||
}
|
||||
|
||||
if char == ':' {
|
||||
if cursor + 1 < program.len and program[cursor + 1] == ':' {
|
||||
|
||||
@@ -112,7 +112,7 @@ scans_ranges_without_consuming_float_dots test {
|
||||
expected [19]TokenKind :: [
|
||||
.int, .range, .int, .int, .range_inclusive, .int,
|
||||
.float, .range, .float, .pipe, .at, .ident, .comma, .ident, .pipe,
|
||||
.range, .invalid, .invalid, .eof,
|
||||
.range, .dot, .invalid, .eof,
|
||||
]
|
||||
try testing.expect_equal(expected.len, state.tokens.items.len)
|
||||
for (expected) |kind, index| try testing.expect_equal(kind, state.tokens.items[index].kind)
|
||||
@@ -120,7 +120,7 @@ scans_ranges_without_consuming_float_dots test {
|
||||
try testing.expect_equal(u32(3), state.tokens.items[1].span.end)
|
||||
try testing.expect_equal(u32(7), state.tokens.items[4].span.start)
|
||||
try testing.expect_equal(u32(10), state.tokens.items[4].span.end)
|
||||
try testing.expect_equal(2, diagnostics.count(&diagnostic_store))
|
||||
try testing.expect_equal(1, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
scans_array_delimiters_and_mut_keyword test {
|
||||
|
||||
@@ -59,7 +59,7 @@ TokenKind :: enum {
|
||||
open_bracket, close_bracket
|
||||
open_curly, close_curly
|
||||
|
||||
comma
|
||||
comma, dot
|
||||
colon
|
||||
newline
|
||||
|
||||
|
||||
@@ -760,7 +760,7 @@ lower_expression proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocEr
|
||||
if (trap_type(state, id)) return ir.no_value
|
||||
return try lower_string(state, id)
|
||||
}
|
||||
.identifier: {
|
||||
.identifier, .selection: {
|
||||
if (trap_binding(state, id) or trap_type(state, id)) return ir.no_value
|
||||
binding :: sema.binding(state.resolution, id)
|
||||
location :: state.storage[usize(binding.data.declaration)]
|
||||
|
||||
@@ -658,6 +658,22 @@ parse_expr_bp proc(state @mut State, min_bp BindingPowerScalar) ast.NodeId ! Par
|
||||
left_grouped = false
|
||||
continue
|
||||
}
|
||||
if (token.kind == .dot) {
|
||||
state.next_token += 1
|
||||
member :: try consume(state, .ident)
|
||||
member_id :: try ast.add_identifier(state.builder, member.span, member.data.str_id)
|
||||
left = try ast.add_selection(
|
||||
state.builder,
|
||||
ast.Span{
|
||||
start = state.tokens[expression_start].span.start,
|
||||
end = member.span.end,
|
||||
source = member.span.source,
|
||||
},
|
||||
left, member_id,
|
||||
)
|
||||
left_grouped = false
|
||||
continue
|
||||
}
|
||||
if (token.kind == .range or token.kind == .range_inclusive) {
|
||||
if (min_bp > BindingPowerScalar(0)) break
|
||||
left = try validate_range_bound(state, left, left_grouped)
|
||||
|
||||
@@ -250,6 +250,32 @@ parses_call_statements_and_postfix_precedence test {
|
||||
)
|
||||
}
|
||||
|
||||
parses_namespace_selection_with_calls_indices_and_prefixes test {
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
source :: "main proc() int { return -library.factory()[0].member(1) + 2 }"
|
||||
scan_state := lexer.init(mem.c_allocator)
|
||||
defer lexer.deinit(&scan_state)
|
||||
try lexer.scan(&scan_state, source, &strings, &diagnostic_store)
|
||||
tree := try parse(mem.c_allocator, scan_state.tokens.items, source, &diagnostic_store)
|
||||
defer ast.deinit(&tree)
|
||||
function :: ast.function_decl(&tree, ast.package_decl(&tree, 0))
|
||||
returned :: ast.return_stmt(&tree, ast.block_statement(&tree, function.body, 0)).value?
|
||||
binary :: ast.binary_expr(&tree, returned)
|
||||
try testing.expect_equal(ast.BinaryOperator.add, binary.operator)
|
||||
call :: ast.unary_expr(&tree, binary.left).operand
|
||||
selected :: ast.selection_expr(&tree, ast.call_expr(&tree, call).callee)
|
||||
try testing.expect_equal("member", ast.source_text(&tree, ast.node_span(&tree, selected.member)))
|
||||
index :: ast.index_expr(&tree, selected.base)
|
||||
factory :: ast.selection_expr(&tree, ast.call_expr(&tree, index.base).callee)
|
||||
try testing.expect_equal("library", ast.source_text(&tree, ast.node_span(&tree, factory.base)))
|
||||
try testing.expect_equal("factory", ast.source_text(&tree, ast.node_span(&tree, factory.member)))
|
||||
try testing.expect_equal("library.factory()[0].member", ast.source_text(&tree, selected.span))
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
}
|
||||
|
||||
recovers_malformed_calls_and_preserves_siblings test {
|
||||
strings := strpool.init(mem.c_allocator)
|
||||
defer strpool.deinit(&strings)
|
||||
|
||||
+26
-10
@@ -171,7 +171,7 @@ check proc(
|
||||
for 0..count |i| {
|
||||
if (cells[i].role != .assignment_stmt) continue
|
||||
target :: ast.assignment_stmt(tree, ast.NodeId(i)).target
|
||||
if (ast.expr_kind(tree, target) != .identifier) continue
|
||||
if (!declaration_reference(tree, target)) continue
|
||||
binding :: sema.binding(resolution, target)
|
||||
if (binding.kind == .invalid) {
|
||||
cells[i].type = invalid_type(binding.data.diagnostic)
|
||||
@@ -185,7 +185,7 @@ check proc(
|
||||
# aliases share a type variable; explicit annotations and parameters are boundaries.
|
||||
for (0..count) |i| {
|
||||
id :: ast.NodeId(i)
|
||||
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue
|
||||
if (cells[i].role != .expression or !declaration_reference(tree, id)) continue
|
||||
binding :: sema.binding(resolution, id)
|
||||
if (binding.kind != .declaration) continue
|
||||
target :: binding.data.declaration
|
||||
@@ -197,7 +197,7 @@ check proc(
|
||||
if (cells[i].role == .declaration and !cells[i].fixed) {
|
||||
join(&state, id, ast.statement_decl(tree, id).initializer)
|
||||
}
|
||||
if (cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue
|
||||
if (cells[i].role != .expression or !declaration_reference(tree, id)) continue
|
||||
binding :: sema.binding(resolution, id)
|
||||
if (binding.kind == .declaration and !cells[i].assignment_value and !cells[usize(binding.data.declaration)].fixed) {
|
||||
join(&state, id, binding.data.declaration)
|
||||
@@ -212,7 +212,10 @@ check proc(
|
||||
if (assignment_values) {
|
||||
for (0..count) |i| {
|
||||
id :: ast.NodeId(i)
|
||||
if (!cells[i].assignment_value or cells[i].role != .expression or ast.expr_kind(tree, id) != .identifier) continue
|
||||
if (!cells[i].assignment_value
|
||||
or cells[i].role != .expression
|
||||
or !declaration_reference(tree, id)
|
||||
) continue
|
||||
binding :: sema.binding(resolution, id)
|
||||
if (binding.kind == .declaration) cells[i].type = types[usize(binding.data.declaration)]
|
||||
}
|
||||
@@ -677,7 +680,7 @@ collect_block proc(state @mut State, id, return_annotation ast.NodeId, in_loop b
|
||||
state.has_assignments = true
|
||||
state.cells[usize(statement)].type = Type{ kind = .void }
|
||||
binding :: sema.binding(state.resolution, assignment.target)
|
||||
if (binding.kind == .builtin) {
|
||||
if (binding.kind == .builtin or binding.kind == .namespace) {
|
||||
state.cells[usize(assignment.target)].role = .expression
|
||||
state.cells[usize(assignment.target)].type = invalid_type(try diagnostics.add(
|
||||
state.diagnostics, .invalid_assignment_target, ast.node_span(tree, assignment.target),
|
||||
@@ -760,11 +763,12 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
||||
.literal_float: state.cells[usize(id)].type = Type{ kind = .float }
|
||||
.literal_bool: state.cells[usize(id)].type = Type{ kind = .bool }
|
||||
.literal_string: state.cells[usize(id)].type = Type{ kind = .string }
|
||||
.identifier: {
|
||||
.identifier, .selection: {
|
||||
binding :: sema.binding(state.resolution, id)
|
||||
match binding.kind {
|
||||
.invalid: state.cells[usize(id)].type = invalid_type(binding.data.diagnostic)
|
||||
.builtin: state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .type_not_value, ast.node_span(tree, id)))
|
||||
.namespace: state.cells[usize(id)].type = invalid_type(try diagnostics.add(state.diagnostics, .namespace_not_value, ast.node_span(tree, id)))
|
||||
else: {}
|
||||
}
|
||||
}
|
||||
@@ -839,6 +843,12 @@ collect_expression proc(state @mut State, id ast.NodeId) void ! (mem.AllocError
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
declaration_reference proc(tree @ast.Tree, id ast.NodeId) bool {
|
||||
kind :: ast.expr_kind(tree, id)
|
||||
return kind == .identifier or kind == .selection
|
||||
}
|
||||
|
||||
@hide:file
|
||||
numeric_kind proc(kind TypeKind) bool {
|
||||
return kind == .int or kind == .float or kind == .usize
|
||||
@@ -1200,7 +1210,7 @@ constant_integer proc(state @mut State, id ast.NodeId) ?ConstantInteger {
|
||||
}
|
||||
return ConstantInteger{ magnitude = value }
|
||||
}
|
||||
.identifier: {
|
||||
.identifier, .selection: {
|
||||
binding :: sema.binding(state.resolution, id)
|
||||
if (binding.kind != .declaration) return null
|
||||
target :: binding.data.declaration
|
||||
@@ -1328,7 +1338,7 @@ infer_shape proc(state @mut State, id ast.NodeId, defaults bool) Type ! (mem.All
|
||||
if (state.cells[index].type.kind == .invalid) return state.cells[index].type
|
||||
state.cells[index].visiting = true
|
||||
defer state.cells[index].visiting = false
|
||||
if (state.cells[usize(id)].role == .expression and ast.expr_kind(state.tree, id) == .identifier) {
|
||||
if (state.cells[usize(id)].role == .expression and declaration_reference(state.tree, id)) {
|
||||
binding :: sema.binding(state.resolution, id)
|
||||
if (binding.kind == .declaration) {
|
||||
target :: binding.data.declaration
|
||||
@@ -1486,7 +1496,10 @@ prepare_ranges proc(state @mut State, assignment_values, constrain bool) void !
|
||||
}
|
||||
for 0..(state.cells.len) |i| {
|
||||
id :: ast.NodeId(i)
|
||||
if (state.cells[i].role != .expression or state.cells[i].assignment_value or ast.expr_kind(tree, id) != .identifier) continue
|
||||
if (state.cells[i].role != .expression
|
||||
or state.cells[i].assignment_value
|
||||
or !declaration_reference(tree, id)
|
||||
) continue
|
||||
binding :: sema.binding(state.resolution, id)
|
||||
if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].role == .loop_capture) {
|
||||
seed(state, id, group_type(state, binding.data.declaration))
|
||||
@@ -1544,7 +1557,10 @@ specialize_annotations proc(state @mut State) void ! (mem.AllocError | diagnosti
|
||||
}
|
||||
for 0..(state.cells.len) |i| {
|
||||
id :: ast.NodeId(i)
|
||||
if (state.cells[i].role != .expression or state.cells[i].assignment_value or ast.expr_kind(state.tree, id) != .identifier) continue
|
||||
if (state.cells[i].role != .expression
|
||||
or state.cells[i].assignment_value
|
||||
or !declaration_reference(state.tree, id)
|
||||
) continue
|
||||
binding :: sema.binding(state.resolution, id)
|
||||
if (binding.kind == .declaration and state.cells[usize(binding.data.declaration)].fixed) {
|
||||
seed(state, id, state.cells[usize(binding.data.declaration)].type)
|
||||
|
||||
+123
-20
@@ -42,10 +42,11 @@ intern_builtins proc(
|
||||
return ids
|
||||
}
|
||||
|
||||
BindingKind :: enum { none, declaration, builtin, invalid }
|
||||
BindingKind :: enum { none, declaration, namespace, builtin, invalid }
|
||||
|
||||
BindingData :: union {
|
||||
declaration ast.NodeId
|
||||
namespace ast.NodeId
|
||||
builtin BuiltinType
|
||||
diagnostic diagnostics.DiagnosticId
|
||||
}
|
||||
@@ -55,6 +56,14 @@ Binding :: struct {
|
||||
data BindingData = BindingData{ diagnostic = diagnostics.no_diagnostic }
|
||||
}
|
||||
|
||||
#! links a file-local import to a package root; failed loads retain their diagnostic.
|
||||
Import :: struct {
|
||||
node ast.NodeId
|
||||
name strpool.StringId
|
||||
target ?ast.NodeId
|
||||
diagnostic ?diagnostics.DiagnosticId = null
|
||||
}
|
||||
|
||||
@hide:file
|
||||
Storage :: struct {
|
||||
bindings []mut Binding
|
||||
@@ -78,6 +87,7 @@ State :: struct {
|
||||
bindings []mut Binding
|
||||
builtin_ids []strpool.StringId
|
||||
allocator mem.Allocator
|
||||
namespaces hashmap.AutoHashMap(ast.NodeId, Scope)
|
||||
}
|
||||
|
||||
#! borrows `tree`, its identifier `pool`, and `diagnostic_store` during resolution.
|
||||
@@ -88,9 +98,20 @@ resolve proc(
|
||||
tree @ast.Tree,
|
||||
pool @mut strpool.StringPool,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) Resolution ! (mem.AllocError | strpool.InternError | diagnostics.CapacityError) {
|
||||
) Resolution ! (
|
||||
mem.AllocError
|
||||
| strpool.InternError
|
||||
| diagnostics.CapacityError
|
||||
) {
|
||||
roots [1]ast.NodeId :: [tree.root]
|
||||
return try resolve_packages(allocator, tree, roots[..], pool, diagnostic_store)
|
||||
return try resolve_packages(
|
||||
allocator,
|
||||
tree,
|
||||
roots[..],
|
||||
&[],
|
||||
pool,
|
||||
diagnostic_store,
|
||||
)
|
||||
}
|
||||
|
||||
#! resolves each package root in its own namespace; AST IDs share the compilation arena.
|
||||
@@ -98,6 +119,7 @@ resolve_packages proc(
|
||||
allocator mem.Allocator,
|
||||
tree @ast.Tree,
|
||||
roots []ast.NodeId,
|
||||
imports []Import,
|
||||
pool @mut strpool.StringPool,
|
||||
diagnostic_store @mut diagnostics.Store,
|
||||
) Resolution ! (mem.AllocError | strpool.InternError | diagnostics.CapacityError) {
|
||||
@@ -112,9 +134,45 @@ resolve_packages proc(
|
||||
bindings = bindings,
|
||||
builtin_ids = builtin_ids[..],
|
||||
allocator = allocator,
|
||||
namespaces = hashmap.init(allocator),
|
||||
}
|
||||
defer {
|
||||
for (state.namespaces.entries) |entry| {
|
||||
if (entry.hash == 0) continue
|
||||
scope := entry.value
|
||||
hashmap.deinit(&scope.names)
|
||||
}
|
||||
hashmap.deinit(&state.namespaces)
|
||||
}
|
||||
|
||||
for (roots) |root| try resolve_package(&state, root)
|
||||
package_scopes :: try mem.alloc(Scope, allocator, roots.len)
|
||||
defer mem.free(allocator, package_scopes)
|
||||
file_scopes :: try mem.alloc(Scope, allocator, ast.source_files(tree).len)
|
||||
defer mem.free(allocator, file_scopes)
|
||||
for (file_scopes) |@scope| scope^ = Scope{ names = hashmap.init(allocator), parent = null }
|
||||
defer { for (file_scopes) |@scope| hashmap.deinit(&scope.names) }
|
||||
for (roots) |root, index| {
|
||||
package_scopes[index] = Scope{ names = hashmap.init(allocator), parent = null }
|
||||
scope := &package_scopes[index]
|
||||
errdefer hashmap.deinit(&scope.names)
|
||||
for (0..ast.package_decl_count_at(tree, root)) |i| {
|
||||
id :: ast.package_decl_at(tree, root, i)
|
||||
file_scopes[usize(ast.node_span(tree, id).source)].parent = scope
|
||||
match ast.decl_kind(tree, id) {
|
||||
.statement: try predeclare(&state, scope, id, ast.statement_decl(tree, id).name)
|
||||
.function: try predeclare(&state, scope, id, ast.function_decl(tree, id).name)
|
||||
.import, .invalid: {}
|
||||
}
|
||||
}
|
||||
hashmap.put(&state.namespaces, root, scope^) catch |err| {
|
||||
match err {
|
||||
.key_exists: unreachable
|
||||
else: return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for (imports) |imported| try resolve_import(&state, &file_scopes[usize(ast.node_span(tree, imported.node).source)], imported)
|
||||
for (roots) |root| try resolve_package(&state, root, file_scopes)
|
||||
|
||||
return Resolution{ storage = Storage{
|
||||
bindings = bindings,
|
||||
@@ -123,32 +181,53 @@ resolve_packages proc(
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_package proc(state @mut State, root ast.NodeId) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
resolve_package proc(
|
||||
state @mut State, root ast.NodeId, file_scopes []Scope,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
tree := state.tree^
|
||||
tree.root = root
|
||||
package_scope := Scope{
|
||||
names = hashmap.init(state.allocator),
|
||||
parent = null,
|
||||
}
|
||||
defer hashmap.deinit(&package_scope.names)
|
||||
for (0..ast.package_decl_count(&tree)) |i| {
|
||||
id :: ast.package_decl(&tree, i)
|
||||
scope :: &file_scopes[usize(ast.node_span(&tree, id).source)]
|
||||
match ast.decl_kind(&tree, id) {
|
||||
.statement: try predeclare(state, &package_scope, id, ast.statement_decl(&tree, id).name)
|
||||
.function: try predeclare(state, &package_scope, id, ast.function_decl(&tree, id).name)
|
||||
.import, .invalid: {}
|
||||
}
|
||||
}
|
||||
for (0..ast.package_decl_count(&tree)) |i| {
|
||||
id :: ast.package_decl(&tree, i)
|
||||
match ast.decl_kind(&tree, id) {
|
||||
.statement: try resolve_statement_decl(state, &package_scope, id)
|
||||
.function: try resolve_function(state, &package_scope, id)
|
||||
.statement: try resolve_statement_decl(state, scope, id)
|
||||
.function: try resolve_function(state, scope, id)
|
||||
.invalid: resolve_invalid(state, id)
|
||||
.import: {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@hide:file
|
||||
resolve_import proc(
|
||||
state @mut State, scope @mut Scope, imported Import,
|
||||
) void ! (mem.AllocError | diagnostics.CapacityError) {
|
||||
span :: ast.import_decl(state.tree, imported.node).span
|
||||
value := if (imported.diagnostic) |diagnostic|
|
||||
Binding{ kind = .invalid, data = BindingData{ diagnostic = diagnostic } }
|
||||
else if (imported.target) |target|
|
||||
Binding{ kind = .namespace, data = BindingData{ namespace = target } }
|
||||
else
|
||||
Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .package_unavailable, span),
|
||||
} }
|
||||
if (imported.name == strpool.no_str) {
|
||||
state.bindings[usize(imported.node)] = value
|
||||
return
|
||||
}
|
||||
if hashmap.get(&scope.names, imported.name) |_| {
|
||||
state.bindings[usize(imported.node)] = Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .duplicate_name, span),
|
||||
} }
|
||||
return
|
||||
}
|
||||
if lookup(state, scope.parent, imported.name) |_| {
|
||||
value = Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .duplicate_name, span),
|
||||
} }
|
||||
}
|
||||
try put_name(scope, imported.name, value)
|
||||
state.bindings[usize(imported.node)] = value
|
||||
}
|
||||
|
||||
#! releases binding storage with the allocator retained by resolve.
|
||||
@@ -349,6 +428,30 @@ resolve_expr proc(
|
||||
}
|
||||
}
|
||||
}
|
||||
.selection: {
|
||||
expression :: ast.selection_expr(tree, id)
|
||||
try resolve_expr(state, scope, expression.base)
|
||||
base :: state.bindings[usize(expression.base)]
|
||||
value := base
|
||||
if (base.kind != .invalid) {
|
||||
if (base.kind == .namespace) {
|
||||
namespace :: hashmap.get(&state.namespaces, base.data.namespace)?
|
||||
if hashmap.get(&namespace.names, ast.name_id(tree, expression.member)) |found| {
|
||||
value = found
|
||||
} else {
|
||||
value = Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .unknown_name, ast.node_span(tree, expression.member)),
|
||||
} }
|
||||
}
|
||||
} else {
|
||||
value = Binding{ kind = .invalid, data = BindingData{
|
||||
diagnostic = try diagnostics.add(state.diagnostics, .not_namespace, ast.node_span(tree, expression.base)),
|
||||
} }
|
||||
}
|
||||
}
|
||||
state.bindings[usize(id)] = value
|
||||
state.bindings[usize(expression.member)] = value
|
||||
}
|
||||
.unary: try resolve_expr(state, scope, ast.unary_expr(tree, id).operand)
|
||||
.binary: {
|
||||
expression :: ast.binary_expr(tree, id)
|
||||
|
||||
@@ -336,6 +336,99 @@ def check_packages(temporary, executable):
|
||||
return 15
|
||||
|
||||
|
||||
def check_namespace_packages(temporary, executable):
|
||||
project = temporary / "namespaces"
|
||||
|
||||
def put(path, text):
|
||||
target = project / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(text)
|
||||
return target
|
||||
|
||||
put("build.hon", "# project root\n")
|
||||
main = put("app/main.hon", """
|
||||
import "../left"
|
||||
right :: import "@right"
|
||||
import "../shared-link"
|
||||
main proc() int {
|
||||
functions :: [left.helper, right.helper]
|
||||
selected := left.helper
|
||||
selected = right.helper
|
||||
return sibling(18) + functions[0]() + selected() + shared.helper()
|
||||
}
|
||||
""")
|
||||
sibling = put("app/sibling.hon", """
|
||||
right :: import "../left"
|
||||
sibling proc(n int) int { return n * 2 + right.helper() - 1 }
|
||||
""")
|
||||
put("left/main.hon", """
|
||||
import "../shared"
|
||||
helper proc() int { return shared.helper() - 2 }
|
||||
main proc() int { return 99 }
|
||||
""")
|
||||
put("right/main.hon", """
|
||||
import "../shared-link"
|
||||
helper proc() int { return shared.helper() - 1 }
|
||||
""")
|
||||
dependency = put("shared/main.hon", """
|
||||
value :: 3
|
||||
helper proc() int { return value }
|
||||
identity proc(n f64) f64 { return n }
|
||||
fail proc() int { return 9223372036854775807 + 1 }
|
||||
""")
|
||||
(project / "shared-link").symlink_to(project / "shared", target_is_directory=True)
|
||||
app = main.parent
|
||||
check(app, executable, (b"42\n", None))
|
||||
emitted = run(str(HONEY), "emit-llvm", str(app))
|
||||
assert emitted.returncode == 0 and emitted.stderr == b"", outcome(emitted)
|
||||
sibling.write_text("sibling proc(n int) int { return n * 2 }\n")
|
||||
cases = [
|
||||
('import "../left"\nmain proc() int { return left.helper() }', b"1\n", None),
|
||||
('lib :: import "../shared"\nmain proc() f64 { f :: lib.identity\nreturn f(1.5) }', b"1.5\n", None),
|
||||
('lib :: import "../shared"\nmain proc() int { return lib.value }', b"3\n", None),
|
||||
('lib :: import "../shared"\nmain proc() int { return lib.absent() }', b"", b"unknown name"),
|
||||
('lib :: import "../shared"\nmain proc() int { return helper() }', b"", b"unknown name"),
|
||||
('import "../left"\nmain proc() int { return left.shared.helper() }', b"", b"unknown name"),
|
||||
('import "../shared"\nmain proc() int { return shared.helper(1) }', b"", b"argument count"),
|
||||
('import "../shared"\nmain proc() f64 { return shared.identity(true) }', b"", b"incompatible value type"),
|
||||
('import "../shared"\nmain proc() int { shared\nreturn 42 }', b"", b"namespace cannot be used as a value"),
|
||||
('import "../shared"\nmain proc() int { return shared.value.member }', b"", b"requires a package namespace"),
|
||||
('lib :: import "../absent"\nmain proc() int { return lib.helper() }', b"", b"cannot load package directory"),
|
||||
('lib :: import "../left"\nlib :: import "../right"\nmain proc() int { return lib.helper() }', b"1\n", b"duplicate name"),
|
||||
('lib :: import "../shared"\nlib :: 42\nmain proc() int { return lib.helper() }', b"", b"duplicate name"),
|
||||
('lib :: import "../shared"\nmain proc() int { lib :: 42\nreturn lib }', b"", b"duplicate name"),
|
||||
('int :: import "../shared"\nmain proc() int { return 42 }', b"", b"duplicate name"),
|
||||
('import "../shared"\nmain proc() int { shared.value = 42\nreturn 0 }', b"", b"mutable local binding"),
|
||||
('import "../shared"\nmain proc() int { shared = 42\nreturn 0 }', b"", b"mutable local binding"),
|
||||
('import "../shared"\nunused proc() int { return shared.absent() }\nmain proc() int { return 42 }', b"42\n", b"unknown name"),
|
||||
('import "../shared"\nmain proc() int { return shared.fail() }', b"", b"integer arithmetic overflow"),
|
||||
('import "../shared"\nmain proc() int { return shared. }', b"", b"unexpected token"),
|
||||
]
|
||||
for text, stdout, error in cases:
|
||||
main.write_text(text + "\n")
|
||||
check(app, executable, (stdout, error))
|
||||
# Imports in a sibling file are invisible even though its declarations are shared.
|
||||
main.write_text("main proc() int { return local.helper() }\n")
|
||||
sibling.write_text('local :: import "../shared"\nsibling proc(n int) int { return n }\n')
|
||||
check(app, executable, (b"", b"unknown name"))
|
||||
# A missing package selection reuses the loader diagnostic instead of cascading.
|
||||
main.write_text('lib :: import "../absent"\nmain proc() int { return lib.helper() }\n')
|
||||
diagnosed = run(str(HONEY), "run", str(app))
|
||||
assert diagnosed.stderr.count(b"cannot load package directory") == 1, outcome(diagnosed)
|
||||
assert b"unknown name" not in diagnosed.stderr, outcome(diagnosed)
|
||||
# Dependency diagnostics and reached traps preserve their original source file.
|
||||
main.write_text('import "../shared"\nmain proc() int { return shared.helper() }\n')
|
||||
dependency.write_text("helper proc() int { return missing }\n")
|
||||
check(app, executable, (b"", b"unknown name"))
|
||||
diagnosed = run(str(HONEY), "run", str(app))
|
||||
assert str(dependency.resolve()).encode() + b":1:" in diagnosed.stderr, outcome(diagnosed)
|
||||
dependency.write_text("\nhelper proc() int { return 9223372036854775807 + 1 }\n")
|
||||
check(app, executable, (b"", b"integer arithmetic overflow"))
|
||||
trapped = run(str(HONEY), "run", str(app))
|
||||
assert str(dependency.resolve()).encode() + b":2:" in trapped.stderr, outcome(trapped)
|
||||
return len(cases) + 4
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory(prefix="honey native ' ; $") as directory:
|
||||
temporary = Path(directory)
|
||||
@@ -350,6 +443,7 @@ def main():
|
||||
check(source, executable, (stdout, error))
|
||||
count += 1
|
||||
count += check_packages(temporary, executable)
|
||||
count += check_namespace_packages(temporary, executable)
|
||||
source = ROOT / "programs/factorial.hon"
|
||||
emitted = run(str(HONEY), "emit-llvm", str(source))
|
||||
assert emitted.returncode == 0 and emitted.stderr == b"", outcome(emitted)
|
||||
|
||||
Reference in New Issue
Block a user