Skip to content

Commit 1234a44

Browse files
committed
Fail closed on file-backed imports before codegen
1 parent 3c8f17c commit 1234a44

14 files changed

Lines changed: 163 additions & 31 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Changed
11+
- File-backed local imports now fail closed before backend codegen, and docs
12+
now distinguish resolver validation from unsupported multi-file linking.
1113
- Tightened runnable examples so conditionals, callbacks, and state-machine
1214
demos exercise their documented branches in both native backends.
1315
- `examples/014_generics.a7` now runs real generic functions and generic struct

MISSING_FEATURES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
14. Installed CLI entrypoint (`a7`) is wired through `pyproject.toml`.
2929
15. Debug/release example artifact verification is available through `scripts/build_examples.py`.
3030
16. `run_all_tests.sh` includes C backend verification, both example E2E verifiers, selected Zig/C parity smoke checks, debug/release artifact builds, the error-stage matrix, docs style checks, and full pytest.
31-
17. Local file-based imports now fail closed during semantic analysis instead of swallowing module loading failures.
31+
17. Local file-based imports now fail closed during codegen modes instead of emitting unresolved backend code. Semantic mode still validates resolver loading.
3232
18. Zig unsupported expression fallbacks now fail as compiler-side codegen errors instead of generated `@compileError` expressions.
3333
19. `fall` now lowers in both Zig and C when used as the final direct
3434
statement of a non-final match case.
@@ -98,6 +98,7 @@
9898

9999
6. **Module-system parity**
100100
- Missing or broken local imports now fail closed.
101+
- Existing file-backed local imports resolve for semantic validation but are rejected before backend codegen until multi-file lowering/linking exists.
101102
- Built-in stdlib imports are virtual modules, but now participate in `ModuleResolver`/`ModuleTable` symbol registration like file-based modules.
102103
- `std/string`, `std/mem`, and `std/collections` are planned but not current public stdlib modules.
103104

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Use fixed-width integers such as `i32`, `i64`, `u32`, or `u64` when the data its
165165
- **Function Rules**: Direct and mutual recursion are semantic errors
166166
- **Expressions**: All operators with proper precedence, casts, if-expressions, struct/array literals, untagged union field literals/access
167167
- **Memory**: Property-based pointer syntax (`.adr`, `.val`), scalar/struct `new` and `del`, defer cleanup. Heap fixed arrays (`new [N]T`) are rejected until the language model is defined.
168-
- **Imports**: Module system with named imports, using imports, aliased imports
168+
- **Imports**: Virtual `std/io` and `std/math` modules with aliases; file-backed local imports resolve for validation but fail closed before backend codegen until module linking is implemented
169169
- **Generics**: Type parameters (`$T`), constraints, type sets, generic structs, generic struct literals, and simple top-level generic function calls in both backends
170170
- **Code Generation**: A7 → Zig and A7 → C backends
171171
- **Standard Library**: Registry with io and math modules, backend-specific mappings

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Features that are spec'd and partially implemented, or missing from one backend.
169169

170170
- [x] Stop treating import/module loading as best-effort.
171171
Files: `a7/compile.py`, `a7/module_resolver.py`
172-
Notes: fixed for local file-based imports; missing or broken dependencies now fail as semantic errors while virtual stdlib imports remain supported.
172+
Notes: missing or broken dependencies now fail as semantic errors while virtual stdlib imports remain supported. Existing file-backed imports are resolver-validated in semantic mode and fail closed before backend codegen until module linking exists.
173173

174174
- [x] Unify built-in stdlib imports with file-based module resolution.
175175
Files: `a7/module_resolver.py`, `a7/passes/name_resolution.py`, `a7/stdlib/__init__.py`

a7/compile.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919

2020
from rich.console import Console
2121

22+
from .ast_nodes import ASTNode, NodeKind
2223
from .ast_preprocessor import ASTPreprocessor
2324
from .backends import get_backend
24-
from .errors import CompilerError, ParseError, SemanticError, display_error, display_errors
25+
from .errors import CompilerError, ParseError, SemanticError, SemanticErrorType, display_error, display_errors
2526
from .formatters import ConsoleFormatter, JSONFormatter, MarkdownFormatter
2627
from .parser import Parser
2728
from .passes import GenericLoweringPass, NameResolutionPass, SemanticValidationPass, TypeCheckingPass
@@ -238,12 +239,22 @@ def compile_file_detailed(
238239
module_resolver.load_program_dependencies(ast, str(input_path))
239240
except SemanticError as e:
240241
import_errors.append(e)
242+
backend_import_errors: list[Any] = []
243+
codegen_modes = {CompileMode.COMPILE, CompileMode.PIPELINE, CompileMode.DOC}
244+
if not import_errors and self.mode in codegen_modes:
245+
backend_import_errors = self._backend_unsupported_import_errors(
246+
ast=ast,
247+
module_resolver=module_resolver,
248+
filename=str(input_path),
249+
source_lines=source_lines,
250+
)
241251

242252
name_resolver = NameResolutionPass()
243253
name_resolver.source_lines = source_lines
244254
symbol_table = name_resolver.analyze(ast, str(input_path))
245255
nr_ok = len(name_resolver.errors) == 0
246256
import_ok = len(import_errors) == 0
257+
backend_import_ok = len(backend_import_errors) == 0
247258
semantic_passes.append(
248259
{
249260
"name": "Import Resolution",
@@ -253,6 +264,15 @@ def compile_file_detailed(
253264
)
254265
if import_errors:
255266
all_errors.extend(import_errors)
267+
semantic_passes.append(
268+
{
269+
"name": "Backend Import Support",
270+
"ok": backend_import_ok,
271+
"errors": len(backend_import_errors),
272+
}
273+
)
274+
if backend_import_errors:
275+
all_errors.extend(backend_import_errors)
256276
semantic_passes.append(
257277
{
258278
"name": "Name Resolution",
@@ -263,7 +283,7 @@ def compile_file_detailed(
263283
if name_resolver.errors:
264284
all_errors.extend(name_resolver.errors)
265285

266-
if import_ok and nr_ok:
286+
if import_ok and backend_import_ok and nr_ok:
267287
type_checker = TypeCheckingPass(symbol_table)
268288
type_checker.source_lines = source_lines
269289
type_checker.analyze(ast, str(input_path))
@@ -479,6 +499,39 @@ def _emit_success(self, result: CompilationResult) -> None:
479499
if result.doc_path:
480500
console.print(f"[blue]📄[/blue] Documentation written to {result.doc_path}")
481501

502+
def _backend_unsupported_import_errors(
503+
self,
504+
*,
505+
ast: ASTNode,
506+
module_resolver: Any,
507+
filename: str,
508+
source_lines: list[str],
509+
) -> list[SemanticError]:
510+
"""Reject file-backed modules until codegen can lower/link their symbols."""
511+
if ast.kind != NodeKind.PROGRAM:
512+
return []
513+
514+
errors: list[SemanticError] = []
515+
for decl in ast.declarations or []:
516+
if decl.kind != NodeKind.IMPORT:
517+
continue
518+
module_path = decl.module_path or ""
519+
if module_resolver.is_virtual_module(module_path):
520+
continue
521+
errors.append(
522+
SemanticError.from_type(
523+
SemanticErrorType.UNSUPPORTED_IMPORT,
524+
span=decl.span,
525+
filename=filename,
526+
source_lines=source_lines,
527+
custom_message=(
528+
f"File-backed import '{module_path}' resolves, but {self.backend} "
529+
"backend lowering/linking for local modules is not implemented yet"
530+
),
531+
)
532+
)
533+
return errors
534+
482535
def _finish_with_failure(
483536
self,
484537
result: CompilationResult,

a7/errors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class SemanticErrorType(Enum):
8787
CIRCULAR_IMPORT = "circular_import"
8888
MODULE_NOT_FOUND = "module_not_found"
8989
IMPORT_NAME_CONFLICT = "import_name_conflict"
90+
UNSUPPORTED_IMPORT = "unsupported_import"
9091

9192
# Generic errors
9293
GENERIC_PARAM_MISMATCH = "generic_param_mismatch"
@@ -244,6 +245,7 @@ def get_semantic_error_message(error_type: SemanticErrorType) -> str:
244245
SemanticErrorType.CIRCULAR_IMPORT: "Circular import detected",
245246
SemanticErrorType.MODULE_NOT_FOUND: "Module not found",
246247
SemanticErrorType.IMPORT_NAME_CONFLICT: "Import name conflicts with existing definition",
248+
SemanticErrorType.UNSUPPORTED_IMPORT: "Unsupported import",
247249

248250
# Generic errors
249251
SemanticErrorType.GENERIC_PARAM_MISMATCH: "Generic parameter count mismatch",
@@ -293,6 +295,7 @@ def get_semantic_error_advice(error_type: SemanticErrorType) -> str:
293295
SemanticErrorType.CIRCULAR_IMPORT: "Reorganize modules to remove circular dependencies",
294296
SemanticErrorType.MODULE_NOT_FOUND: "Check the module path and ensure the file exists",
295297
SemanticErrorType.IMPORT_NAME_CONFLICT: "Use an alias for the import or rename the conflicting definition",
298+
SemanticErrorType.UNSUPPORTED_IMPORT: "Use a current virtual stdlib import or keep file-backed modules in the same source file until backend linking is implemented",
296299

297300
# Generic errors
298301
SemanticErrorType.GENERIC_PARAM_MISMATCH: "Provide the correct number of generic type arguments",

docs/SPEC.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1420,9 +1420,12 @@ integrate_2d :: fn(f: fn(f64, f64) f64, bounds: [4]f64, steps: [2]usize) f64 {
14201420

14211421
## 10. Modules and Visibility
14221422

1423-
### 10.1 File-Based Module System
1423+
### 10.1 File-Based Module Model
14241424

14251425
Every A7 source file is a module. There is no explicit `module` keyword.
1426+
Current backend codegen does not link multiple `.a7` files yet; file-backed
1427+
imports resolve during semantic validation but are rejected before Zig/C
1428+
emission.
14261429

14271430
```a7
14281431
// File: vector.a7
@@ -1452,26 +1455,21 @@ normalize :: fn(v: ref Vec3) {
14521455
### 10.2 Import Statements
14531456

14541457
```a7
1455-
// Import by filename (without .a7 extension)
1456-
math :: import "math"
1457-
io :: import "io"
1458+
// Current backend-lowered virtual stdlib imports
1459+
math :: import "std/math"
1460+
io :: import "std/io"
14581461
1459-
// Import user library
1460-
mylib :: import "mylib"
1462+
// Aliases are supported for virtual stdlib imports
1463+
console :: import "std/io"
14611464
1462-
// Import with alias
1463-
vec :: import "vector"
1464-
1465-
// Import specific items
1465+
// Parsed/resolver forms for file-backed modules
14661466
import "vector" { Vec3, dot }
1467-
1468-
// Import all public items
14691467
using import "vector"
1470-
1471-
// Relative imports
14721468
sibling :: import "./sibling"
1473-
parent :: import "../utils"
14741469
subfolder :: import "subfolder/helper"
1470+
1471+
// Parent traversal imports are rejected by the resolver
1472+
parent :: import "../utils" // error
14751473
```
14761474

14771475
### 10.3 Standard Library Status
@@ -1483,7 +1481,10 @@ Current implementation:
14831481
- Virtual stdlib modules are registered through the module resolver and may be
14841482
imported with arbitrary local aliases, for example
14851483
`console :: import "std/io"`.
1486-
- Local file imports such as `./vector` resolve from on-disk `.a7` files.
1484+
- Local file imports such as `./vector` can resolve from on-disk `.a7` files
1485+
during semantic validation, but backend lowering/linking for file-backed
1486+
modules is not implemented yet. Compile/pipeline/doc modes reject them before
1487+
codegen instead of emitting invalid Zig/C.
14871488

14881489
Planned, not implemented as public stdlib modules yet:
14891490

site/public/docs/features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Language
44

55
- Static typing with inference in supported contexts.
6-
- Functions, structs, enums, untagged union field literals/access, aliases, modules, and generics.
6+
- Functions, structs, enums, untagged union field literals/access, aliases, virtual stdlib modules, and generics.
77
- Simple top-level generic function calls, type-set constraints, and used generic struct instances lower in both Zig and C backends.
88
- `if`, `while`, `for`, `for-in`, `match`, labeled loops, `break`, and `continue`.
99
- Arrays, slices, string slices, pointers, references, and manual `new` / `del`.
@@ -28,4 +28,4 @@
2828

2929
## Current Limits
3030

31-
The status page is canonical for remaining gaps. Key limits include complete memory/lifetime guarantees, broader generic propagation beyond current function and struct instance coverage, parsed-only variadic declarations, reserved-but-unimplemented intrinsics beyond `@type_set(...)`, tagged union workflows, and arbitrary symbolic inequality reasoning.
31+
The status page is canonical for remaining gaps. Key limits include complete memory/lifetime guarantees, backend lowering/linking for file-backed local modules, broader generic propagation beyond current function and struct instance coverage, parsed-only variadic declarations, reserved-but-unimplemented intrinsics beyond `@type_set(...)`, tagged union workflows, and arbitrary symbolic inequality reasoning.

site/public/docs/guide/features.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
## Language
44

55
- Static typing with inference in supported contexts.
6-
- Functions, structs, enums, untagged union field literals/access, aliases, modules, and generics.
6+
- Functions, structs, enums, untagged union field literals/access, aliases, virtual stdlib modules, and generics.
77
- Simple top-level generic function calls and used generic struct instances lower in both Zig and C backends.
88
- `if`, `while`, `for`, `for-in`, `match`, labeled loops, `break`, and `continue`.
99
- Arrays, slices, string slices, pointers, references, and manual `new` / `del`.
@@ -28,4 +28,4 @@
2828

2929
## Current Limits
3030

31-
The status page is canonical for remaining gaps. Key limits include complete memory/lifetime guarantees, broader generic propagation beyond current function and struct instance coverage, tagged union workflows, and arbitrary symbolic inequality reasoning.
31+
The status page is canonical for remaining gaps. Key limits include complete memory/lifetime guarantees, backend lowering/linking for file-backed local modules, broader generic propagation beyond current function and struct instance coverage, tagged union workflows, and arbitrary symbolic inequality reasoning.

site/public/docs/language.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ These modules are virtual built-ins registered through the module resolver, so
5151
local aliases such as `console :: import "std/io"` and
5252
`mathlib :: import "std/math"` lower the same way as `io` and `math`.
5353

54+
File-backed local imports can be resolved during semantic validation, but
55+
Zig/C backend lowering and linking for multiple `.a7` files is not implemented
56+
yet. Compile, pipeline, and doc modes reject file-backed imports before codegen
57+
instead of emitting unresolved target code.
58+
5459
Source stubs such as `mem` and `string` exist in the repository but are not registered public stdlib modules yet.
5560

5661
## Current Syntax Limits

0 commit comments

Comments
 (0)