Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/import_as.rw
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import import_basic_lib as m

def main() -> int:
print(m.add(2, 3))
print(m.mul(4, 5))
return 0
2 changes: 2 additions & 0 deletions examples/import_as.rw.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
5
20
14 changes: 5 additions & 9 deletions rwc/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,18 +158,14 @@ def parse_module(self) -> A.Module:
def parse_import(self) -> A.Import:
if self.cur.kind == TokenKind.KW_FROM:
return self._parse_from_import()
# Plain `import IDENT NEWLINE`. `import x as m` (PR3) not yet supported.
# Plain `import IDENT [as IDENT] NEWLINE`.
kw = self.eat(TokenKind.KW_IMPORT, "'import'")
name_tok = self.eat(TokenKind.IDENT, "module name after 'import'")
if self.cur.kind == TokenKind.KW_AS:
raise ParserError(
"`import x as y` is not yet supported",
self.cur.line,
self.cur.col,
max(1, len(self.cur.value)),
)
alias = None
if self.match(TokenKind.KW_AS):
alias = self.eat(TokenKind.IDENT, "alias after 'as'").value
self.eat(TokenKind.NEWLINE)
return A.Import(name_tok.value, None, None, kw.line, kw.col)
return A.Import(name_tok.value, alias, None, kw.line, kw.col)

def _parse_from_import(self) -> A.Import:
# `from IDENT import IDENT [as IDENT] { ',' IDENT [as IDENT] }`
Expand Down
35 changes: 27 additions & 8 deletions rwc/sema.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ def __init__(self, program: LoadedProgram, filename: str) -> None:
self.type_alias_map: Dict[str, T.Type] = {}
# The module currently being collected/checked. None = entry (root).
self.current_module: Optional[str] = None
# Module names visible to the current module via `import`.
# PR1: a module can call `m.f()` only for an `m` it imported.
self._visible_imports: set[str] = set()
# Qualifiers visible to the current module via `import`/`import as`:
# visible name -> real module name. `import x` -> {x: x}; `import x as m`
# -> {m: x}. A qualified call `q.f()` is valid only for a `q` in here.
self._import_env: Dict[str, str] = {}
# Names brought into the current module via `from x import y [as w]`:
# local name -> resolved (module, name). PR2.
self._from_env: Dict[str, FuncKey] = {}
Expand Down Expand Up @@ -182,13 +183,30 @@ def analyze(self) -> SemaResult:
# `current_module` / visible imports / from-bindings are scoped correctly.
for mod_name, mod in self._all_modules:
self.current_module = mod_name
self._visible_imports = {imp.module for imp in mod.imports}
self._import_env = self._build_import_env(mod)
self._from_env = self._build_from_env(mod_name, mod)
for fn in mod.functions:
self._check_func(fn)

return self.result

def _build_import_env(self, mod: A.Module) -> Dict[str, str]:
"""Resolve module qualifiers for one module: visible name -> real
module. Only plain/aliased imports (names is None) contribute; from
imports bind individual names instead. Detects duplicate qualifiers."""
env: Dict[str, str] = {}
for imp in mod.imports:
if imp.names is not None:
continue # `from x import ...` does not introduce a qualifier
visible = imp.alias or imp.module
if visible in env:
raise CompileError(Diagnostic(
self.filename, imp.line, imp.col, max(1, len(visible)),
f"duplicate import qualifier `{visible}`",
))
env[visible] = imp.module
return env

def _build_from_env(self, mod_name: Optional[str], mod: A.Module) -> Dict[str, FuncKey]:
"""Resolve `from x import y [as w]` bindings for one module: local name
-> (source_module, source_name). Detects collisions with local
Expand Down Expand Up @@ -605,14 +623,15 @@ def _resolve_user_func(self, call: A.Call) -> FuncKey:
raise. Does not type-check arguments. Records the result in
call_resolution for irgen."""
if call.module is not None:
# Qualified call `mod.func`: `mod` must be imported by the current
# module; builtins can never be qualified.
if call.module not in self._visible_imports:
# Qualified call `q.func`: `q` must be a visible import qualifier
# (a module name or an `as` alias); builtins can never be qualified.
if call.module not in self._import_env:
raise CompileError(Diagnostic(
self.filename, call.line, call.col, len(call.module),
f"module '{call.module}' is not imported here",
))
key: FuncKey = (call.module, call.callee)
real_module = self._import_env[call.module]
key: FuncKey = (real_module, call.callee)
if key not in self.result.functions:
raise CompileError(Diagnostic(
self.filename, call.line, call.col, len(call.callee),
Expand Down
2 changes: 1 addition & 1 deletion tests/test_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def _build_and_run(rw_path: Path) -> tuple[int, str]:

@pytest.mark.parametrize(
"name",
["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic", "list_basic", "option_basic", "result_basic", "for_count", "ternary", "file_io", "file_par", "num_literals", "bitops", "break_continue", "assert_ok", "type_alias", "math_basic", "import_basic", "import_from"],
["hello", "arith", "fib", "while_count", "spawn_basic", "spawn_many", "spawn_string", "string_ops", "bytes_basic", "list_basic", "option_basic", "result_basic", "for_count", "ternary", "file_io", "file_par", "num_literals", "bitops", "break_continue", "assert_ok", "type_alias", "math_basic", "import_basic", "import_from", "import_as"],
)
def test_synchronous_example(name: str):
rw_path = EXAMPLES / f"{name}.rw"
Expand Down
14 changes: 10 additions & 4 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,16 @@ def test_import_after_def_is_error():
parse_src(src)


def test_import_as_not_yet_supported():
src = "import math_lib as m\n\ndef main() -> int:\n return 0\n"
with pytest.raises(ParserError):
parse_src(src)
def test_parse_import_as_alias():
src = "import math_lib as m\n\ndef main() -> int:\n return m.add(1, 2)\n"
mod = parse_src(src)
assert len(mod.imports) == 1
imp = mod.imports[0]
assert imp.module == "math_lib"
assert imp.alias == "m"
assert imp.names is None
call = mod.functions[0].body[0].value
assert isinstance(call, A.Call) and call.module == "m" and call.callee == "add"


def test_parse_from_import_single():
Expand Down
33 changes: 33 additions & 0 deletions tests/test_sema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1485,3 +1485,36 @@ def test_from_import_collides_with_builtin(tmp_path):
lib="def print(a: int) -> int:\n return a\n",
)
assert "collides with a builtin" in ei.value.diagnostic.message


# ----- import-as aliases (spec 17, PR3) -----

def test_import_as_resolves_via_alias(tmp_path):
res = _check_program(
tmp_path,
"import lib as m\n\ndef main() -> int:\n return m.add(1, 2)\n",
lib="def add(a: int, b: int) -> int:\n return a + b\n",
)
assert ("lib", "add") in res.functions


def test_import_as_original_name_not_visible(tmp_path):
# After `import lib as m`, the bare `lib.` qualifier is no longer valid.
with pytest.raises(CompileError) as ei:
_check_program(
tmp_path,
"import lib as m\n\ndef main() -> int:\n return lib.add(1, 2)\n",
lib="def add(a: int, b: int) -> int:\n return a + b\n",
)
assert "not imported" in ei.value.diagnostic.message


def test_duplicate_import_qualifier_errors(tmp_path):
with pytest.raises(CompileError) as ei:
_check_program(
tmp_path,
"import lib\nimport other as lib\n\ndef main() -> int:\n return lib.add(1, 2)\n",
lib="def add(a: int, b: int) -> int:\n return a + b\n",
other="def add(a: int, b: int) -> int:\n return a - b\n",
)
assert "duplicate import qualifier" in ei.value.diagnostic.message
Loading
Loading