Skip to content

Commit c452f3e

Browse files
committed
Improve undefined name detection
1 parent 705d486 commit c452f3e

File tree

6 files changed

+186
-182
lines changed

6 files changed

+186
-182
lines changed

DOCS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4913,11 +4913,11 @@ _Deprecated: `coconut.convenience` is a deprecated alias for `coconut.api`._
49134913

49144914
#### `get_state`
49154915

4916-
**coconut.api.get\_state**(_state_=`None`)
4916+
**coconut.api.get\_state**(_state_=`True`)
49174917

49184918
Gets a state object which stores the current compilation parameters. State objects can be configured with [**setup**](#setup) or [**cmd**](#cmd) and then used in [**parse**](#parse) or other endpoints.
49194919

4920-
If _state_ is `None`, gets a new state object, whereas if _state_ is `False`, the global state object is returned.
4920+
If _state_ is `True`, gets a new state object, whereas if _state_ is `False`, the global state object is returned.
49214921

49224922
#### `parse`
49234923

coconut/api.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@
5353
GLOBAL_STATE = None
5454

5555

56-
def get_state(state=None):
57-
"""Get a Coconut state object; None gets a new state, False gets the global state."""
56+
def get_state(state=True):
57+
"""Get a Coconut state object; True gets a new state, False gets the global state."""
5858
global GLOBAL_STATE
59-
if state is None:
59+
if state is None or state is True:
6060
return Command()
6161
elif state is False:
6262
if GLOBAL_STATE is None:

coconut/api.pyi

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,15 @@ class CoconutException(Exception):
3838
GLOBAL_STATE: Optional[Command] = None
3939

4040

41-
def get_state(state: Optional[Command] = None) -> Command:
41+
def get_state(state: Optional[Command] | bool = None) -> Command:
4242
"""Get a Coconut state object; None gets a new state, False gets the global state."""
4343
...
4444

4545

4646
def cmd(
4747
args: Text | bytes | Iterable,
4848
*,
49-
state: Command | None = ...,
49+
state: Optional[Command] | bool = ...,
5050
argv: Iterable[Text] | None = None,
5151
interact: bool = False,
5252
default_target: Text | None = None,
@@ -81,7 +81,7 @@ def setup(
8181
no_tco: bool = False,
8282
no_wrap: bool = False,
8383
pure: bool = False,
84-
state: Optional[Command] = ...,
84+
state: Optional[Command] | bool = ...,
8585
) -> None:
8686
"""Set up the given state object."""
8787
...
@@ -91,7 +91,7 @@ def warm_up(
9191
streamline: bool = False,
9292
enable_incremental_mode: bool = False,
9393
*,
94-
state: Optional[Command] = ...,
94+
state: Optional[Command] | bool = ...,
9595
) -> None:
9696
"""Warm up the given state object."""
9797
...
@@ -103,7 +103,7 @@ PARSERS: Dict[Text, Callable] = ...
103103
def parse(
104104
code: Text,
105105
mode: Text = ...,
106-
state: Optional[Command] = ...,
106+
state: Optional[Command] | bool = ...,
107107
keep_internal_state: Optional[bool] = None,
108108
) -> Text:
109109
"""Compile Coconut code."""
@@ -114,7 +114,7 @@ def coconut_exec(
114114
expression: Text,
115115
globals: Optional[Dict[Text, Any]] = None,
116116
locals: Optional[Dict[Text, Any]] = None,
117-
state: Optional[Command] = ...,
117+
state: Optional[Command] | bool = ...,
118118
keep_internal_state: Optional[bool] = None,
119119
) -> None:
120120
"""Compile and evaluate Coconut code."""
@@ -125,7 +125,7 @@ def coconut_eval(
125125
expression: Text,
126126
globals: Optional[Dict[Text, Any]] = None,
127127
locals: Optional[Dict[Text, Any]] = None,
128-
state: Optional[Command] = ...,
128+
state: Optional[Command] | bool = ...,
129129
keep_internal_state: Optional[bool] = None,
130130
) -> Any:
131131
"""Compile and evaluate Coconut code."""

coconut/compiler/compiler.py

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -635,20 +635,16 @@ def genhash(self, code, package_level=-1):
635635
def get_empty_scope(self, inner=False):
636636
"""Get an empty scope for the parsing_context."""
637637
parent = self.current_parsing_context("scope")
638+
if parent is not None and parent["all_vars"] is None:
639+
inner = True
638640
return {
639641
"final_vars": {},
640642
"pure_vars": {},
641643
"all_vars": None if inner else set(),
642644
"parent": parent,
645+
"callbacks": [],
643646
}
644647

645-
def init_parsing_context(self, inner=False):
646-
"""Initialize parsing context."""
647-
self.parsing_context = defaultdict(list)
648-
# initialize module-level scopes
649-
self.parsing_context["scope"].append(self.get_empty_scope(inner=inner))
650-
return self.parsing_context
651-
652648
def reset(self, keep_state=False, filename=None):
653649
"""Reset references.
654650
@@ -683,8 +679,7 @@ def reset(self, keep_state=False, filename=None):
683679
self.shown_warnings = set()
684680
if not keep_state:
685681
self.computation_graph_caches = defaultdict(staledict)
686-
self.final_checks = []
687-
self.init_parsing_context()
682+
self.parsing_context = defaultdict(list)
688683

689684
@contextmanager
690685
def inner_environment(self, ln=None):
@@ -702,11 +697,13 @@ def inner_environment(self, ln=None):
702697
num_lines, self.num_lines = self.num_lines, 0
703698
remaining_original, self.remaining_original = self.remaining_original, None
704699
shown_warnings, self.shown_warnings = self.shown_warnings, set()
705-
parsing_context = self.parsing_context
706-
self.init_parsing_context(inner=True)
700+
parsing_context, self.parsing_context = self.parsing_context, defaultdict(list)
707701
try:
708-
with ComputationNode.using_overrides():
709-
yield
702+
with self.add_to_parsing_context({
703+
"scope": self.get_empty_scope(inner=True),
704+
}):
705+
with ComputationNode.using_overrides():
706+
yield
710707
finally:
711708
self.outer_ln = outer_ln
712709
self.line_numbers = line_numbers
@@ -1525,8 +1522,8 @@ def parsing(self, keep_state=False, codepath=None):
15251522
"""Acquire the lock and reset the parser."""
15261523
filename = None if codepath is None else os.path.basename(codepath)
15271524
with self.lock:
1528-
self.reset(keep_state, filename)
15291525
Compiler.current_compiler = self
1526+
self.reset(keep_state, filename)
15301527
yield
15311528

15321529
def streamline(self, grammars, inputstring=None, force=False, inner=False):
@@ -1557,8 +1554,6 @@ def run_final_checks(self, original, keep_state=False):
15571554
if info["imported"] and not info["referenced"]:
15581555
for loc in info["imported"]:
15591556
self.strict_err_or_warn("found unused import " + repr(self.reformat(name, ignore_errors=True)), original, loc, noqa_able=True, endpoint=False)
1560-
for final_check in self.final_checks:
1561-
final_check()
15621557

15631558
def pickle_cache(self, original, cache_path, include_incremental=True):
15641559
"""Pickle the pyparsing cache for original to cache_path."""
@@ -1838,11 +1833,15 @@ def parse(
18381833
with logger.gather_parsing_stats():
18391834
try:
18401835
pre_procd = self.pre(inputstring, keep_state=keep_state, **preargs)
1841-
if isinstance(parser, tuple):
1842-
init_parser, line_parser = parser
1843-
parsed = self.parse_line_by_line(init_parser, line_parser, pre_procd)
1844-
else:
1845-
parsed = parse(parser, pre_procd, inner=False)
1836+
# handle module-level scope
1837+
with self.add_to_parsing_context({
1838+
"scope": self.get_empty_scope(inner=keep_state),
1839+
}):
1840+
if isinstance(parser, tuple):
1841+
init_parser, line_parser = parser
1842+
parsed = self.parse_line_by_line(init_parser, line_parser, pre_procd)
1843+
else:
1844+
parsed = parse(parser, pre_procd, inner=False)
18461845
out = self.post(parsed, keep_state=keep_state, **postargs)
18471846
except ParseBaseException as err:
18481847
raise self.make_parse_err(err)
@@ -4643,11 +4642,12 @@ def {func_name}({iter_var}):
46434642
def get_parent_expr_setnames(self):
46444643
"""Get all expr_setnames in parent contexts, but not the current context."""
46454644
expr_setname_context = self.current_parsing_context("expr_setnames")
4646-
parent_context = expr_setname_context["parent"]
46474645
parent_setnames = set()
4648-
while parent_context:
4649-
parent_setnames |= parent_context["new_names"]
4650-
parent_context = parent_context["parent"]
4646+
if expr_setname_context is not None:
4647+
parent_context = expr_setname_context["parent"]
4648+
while parent_context:
4649+
parent_setnames |= parent_context["new_names"]
4650+
parent_context = parent_context["parent"]
46514651
return parent_setnames
46524652

46534653
def handle_expr_scope_closure(self, name, loc):
@@ -5303,7 +5303,7 @@ def current_parsing_context(self, name, default=None):
53035303
return default
53045304

53055305
@contextmanager
5306-
def add_to_parsing_context(self, name_obj_dict, callbacks_keys_dict=None):
5306+
def add_to_parsing_context(self, name_obj_dict):
53075307
"""Put all the given objects on their respective parsing context stacks."""
53085308
for name, obj in name_obj_dict.items():
53095309
self.parsing_context[name].append(obj)
@@ -5312,11 +5312,9 @@ def add_to_parsing_context(self, name_obj_dict, callbacks_keys_dict=None):
53125312
finally:
53135313
for name in name_obj_dict:
53145314
popped_ctx = self.parsing_context[name].pop()
5315-
if callbacks_keys_dict is not None:
5316-
callbacks_key = callbacks_keys_dict.get(name)
5317-
if callbacks_key is not None:
5318-
for callback in popped_ctx[callbacks_key]:
5319-
callback()
5315+
if "callbacks" in popped_ctx:
5316+
for callback in popped_ctx["callbacks"]:
5317+
callback()
53205318

53215319
def funcname_typeparams_handle(self, tokens):
53225320
"""Handle function names with type parameters."""
@@ -5584,7 +5582,6 @@ def has_expr_setname_manage(self, original, loc, item):
55845582
"callbacks": [],
55855583
"loc": loc,
55865584
}},
5587-
callbacks_keys_dict={"expr_setnames": "callbacks"},
55885585
):
55895586
yield
55905587

@@ -5788,7 +5785,10 @@ def name_handle(self, original, loc, tokens, assign=False, outer_setname=False,
57885785
and name not in self.get_parent_expr_setnames()
57895786
))
57905787
):
5791-
self.final_checks.append(partial(self.check_undefined_name, original, loc, name, scope, self.outer_ln))
5788+
parent_scope = scope
5789+
while parent_scope["parent"] is not None:
5790+
parent_scope = parent_scope["parent"]
5791+
parent_scope["callbacks"].append(partial(self.check_undefined_name, original, loc, name, scope, self.outer_ln))
57925792

57935793
# only mark as final after all checks pass
57945794
if is_final:

coconut/root.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
VERSION = "3.2.0"
2727
VERSION_NAME = None
2828
# False for release, int >= 1 for develop
29-
DEVELOP = 15
29+
DEVELOP = 16
3030
ALPHA = False # for pre releases rather than post releases
3131

3232
assert DEVELOP is False or DEVELOP >= 1, "DEVELOP must be False or an int >= 1"

0 commit comments

Comments
 (0)