Skip to content

Commit 2c7477c

Browse files
committed
Experiment: scoping of locals in continuations (see #82)
Maybe not worth it, after all; much simpler, and more robust, to just document that introducing a continuation introduces a scope boundary.
1 parent 4d2e270 commit 2c7477c

File tree

1 file changed

+83
-13
lines changed

1 file changed

+83
-13
lines changed

Diff for: unpythonic/syntax/tailtools.py

+83-13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
BoolOp, And, Or,
1717
With, AsyncWith, If, IfExp, Try, Assign, Return, Expr,
1818
Await,
19+
Global, Nonlocal,
1920
copy_location)
2021
import sys
2122

@@ -34,6 +35,8 @@
3435
has_tco, sort_lambda_decorators,
3536
suggest_decorator_index,
3637
UnpythonicASTMarker, ExpandedContinuationsMarker)
38+
from .scopeanalyzer import (get_names_in_store_context, extract_args,
39+
collect_globals, collect_nonlocals)
3740

3841
from ..dynassign import dyn
3942
from ..fun import identity
@@ -883,32 +886,97 @@ def data_cb(tree): # transform an inert-data return value into a tail-call to c
883886
# specified inside the body of the macro invocation like PG's solution does.
884887
# Instead, we capture as the continuation all remaining statements (i.e.
885888
# those that lexically appear after the ``call_cc[]``) in the current block.
886-
def iscallcc(tree):
889+
def iscallccstatement(tree):
887890
if type(tree) not in (Assign, Expr):
888891
return False
889892
return isinstance(tree.value, CallCcMarker)
890-
def split_at_callcc(body):
893+
# owner: FunctionDef node, or `None` if the use site of the `call_cc` is not inside a function
894+
def split_at_callcc(owner, body):
891895
if not body:
892896
return [], None, []
893897
before, after = [], body
894898
while True:
895899
stmt, *after = after
896-
if iscallcc(stmt):
900+
if iscallccstatement(stmt):
897901
# after is always non-empty here (has at least the explicitified "return")
898902
# ...unless we're at the top level of the "with continuations" block
899903
if not after:
900904
raise SyntaxError("call_cc[] cannot appear as the last statement of a 'with continuations' block (no continuation to capture)") # pragma: no cover
901-
# TODO: To support Python's scoping properly in assignments after the `call_cc`,
902-
# TODO: we have to scan `before` for assignments to local variables (stopping at
903-
# TODO: scope boundaries; use `unpythonic.syntax.scoping.get_names_in_store_context`,
904-
# TODO: and declare those variables (plus any variables already declared as `nonlocal`
905-
# TODO: in `before`) as `nonlocal` in `after`. This way the binding will be shared
906-
# TODO: between the original context and the continuation. Also, propagate `global`.
907-
# See Politz et al 2013 (the "full monty" paper), section 4.2.
905+
after = patch_scoping(owner, before, stmt, after)
908906
return before, stmt, after
909907
before.append(stmt)
910908
if not after:
911909
return before, None, []
910+
# Try to maintain an illusion of Python's standard scoping rules across the split
911+
# into the parent context (`before`) and continuation closure (`after`).
912+
# See Politz et al 2013 (the "full monty" paper), section 4.2.
913+
#
914+
# TODO: We are still missing the case where a new local is introduced in the continuation.
915+
# TODO: Ideally, it should be made a nonlocal up to the top-level owner, where it should be defined;
916+
# TODO: this would allow a continuation to declare a variable that is then read by the `before` part.
917+
# TODO: (Right now that can be done, by simply declaring the variable and setting it to `None` (or
918+
# TODO: any value, really, in the top-level owner; it will then propagate.))
919+
# TODO: But we still can't easily replicate the behavior that accessing the name before a value
920+
# TODO: has been assigned to it should raise `UnboundLocalError`.
921+
#
922+
# TODO: Alternatively, we could declare `patch_scoping` a failed experiment, and just document
923+
# TODO: that a continuation is a scope boundary, with all the usual implications. (This is the
924+
# TODO: behavior up to 0.15.1, anyway, though it's not documented.)
925+
#
926+
# TODO: Then we can just forget about the whole thing and delete the `patch_scoping` function. :)
927+
#
928+
# owner: FunctionDef node, or `None` if the use site of the `call_cc` is not inside a function
929+
def patch_scoping(owner, before, callcc, after):
930+
# Determine the names of all variables that should be made local to the continuation function.
931+
# In the unexpanded code, the continuation doesn't look like a new scope, so by appearances,
932+
# these will effectively break the usual scoping rules. Thus this set should be kept minimal.
933+
# To allow the machinery to actually work, at least the parameters of the continuation function
934+
# *must* be allowed to shadow names from the parent scope.
935+
targets, starget, ignored_condition, ignored_thecall, ignored_altcall = analyze_callcc(callcc)
936+
if not targets and not starget:
937+
targets = ["_ignored_arg"] # this must match what `make_continuation` does, below
938+
# The assignment targets of the `call_cc` become parameters of the continuation function.
939+
# Furthermore, a continuation function generated by `make_continuation` always takes
940+
# the `cc` and `_pcc` parameters.
941+
afterargs = targets + ([starget] or []) + ["cc", "_pcc"]
942+
afterlocals = afterargs
943+
944+
if owner:
945+
# When `call_cc` is used inside a function, local variables of the
946+
# parent function (including parameters) become nonlocals in the
947+
# continuation.
948+
#
949+
# But only those that are not also locals of the continuation!
950+
# In that case, the local variable of the continuation overrides.
951+
# Locals of the continuation include its arguments, and any names in store context.
952+
beforelocals = set(extract_args(owner) + get_names_in_store_context(before))
953+
afternonlocals = list(beforelocals.difference(afterlocals))
954+
if afternonlocals: # TODO: Python 3.8: walrus assignment
955+
after.insert(0, Nonlocal(names=afternonlocals))
956+
else:
957+
# When `call_cc` is used at the top level of `with continuations` block,
958+
# the variables at that level become globals in the continuation.
959+
#
960+
# TODO: This **CANNOT** always work correctly, because we would need to know
961+
# TODO: whether the `with continuations` block itself is inside a function or not.
962+
# TODO: So we just assume it's outside any function.
963+
beforelocals = set(get_names_in_store_context(before))
964+
afternonlocals = list(beforelocals.difference(afterlocals))
965+
if afternonlocals: # TODO: Python 3.8: walrus assignment
966+
after.insert(0, Global(names=afternonlocals))
967+
968+
# Nonlocals of the parent function remain nonlocals in the continuation.
969+
# When `owner is None`, `beforenonlocals` will be empty.
970+
beforenonlocals = collect_nonlocals(before)
971+
if beforenonlocals: # TODO: Python 3.8: walrus assignment
972+
after.insert(0, Nonlocal(names=beforenonlocals))
973+
974+
# Globals of parent are also globals in the continuation.
975+
beforeglobals = collect_globals(before)
976+
if beforeglobals: # TODO: Python 3.8: walrus assignment
977+
after.insert(0, Global(names=beforeglobals))
978+
979+
return after # we mutate; return it just for convenience
912980
# TODO: To support named return values (`kwrets` in a `Values` object) from the `call_cc`'d function,
913981
# TODO: we need to change the syntax to something that allows us to specify which names are meant to
914982
# TODO: capture the positional return values, and which ones the named return values. Doing so will
@@ -947,7 +1015,7 @@ def maybe_starred(expr): # return [expr.id] or set starget
9471015
raise SyntaxError(f"call_cc[]: expected an assignment or a bare expr, got {stmt}") # pragma: no cover
9481016
# extract the function call(s)
9491017
if not isinstance(stmt.value, CallCcMarker): # both Assign and Expr have a .value
950-
assert False # we should get only valid call_cc[] invocations that pass the `iscallcc` test # pragma: no cover
1018+
assert False # we should get only valid call_cc[] invocations that pass the `iscallccstatement` test # pragma: no cover
9511019
theexpr = stmt.value.body # discard the AST marker
9521020
if not (type(theexpr) in (Call, IfExp) or (type(theexpr) in (Constant, NameConstant) and getconstant(theexpr) is None)):
9531021
raise SyntaxError("the bracketed expression in call_cc[...] must be a function call, an if-expression, or None") # pragma: no cover
@@ -966,6 +1034,7 @@ def extract_call(tree):
9661034
condition = altcall = None
9671035
thecall = extract_call(theexpr)
9681036
return targets, starget, condition, thecall, altcall
1037+
# owner: FunctionDef node, or `None` if the use site of the `call_cc` is not inside a function
9691038
def make_continuation(owner, callcc, contbody):
9701039
targets, starget, condition, thecall, altcall = analyze_callcc(callcc)
9711040

@@ -1069,19 +1138,20 @@ def transform(self, tree):
10691138
if type(tree) in (FunctionDef, AsyncFunctionDef):
10701139
tree.body = transform_callcc(tree, tree.body)
10711140
return self.generic_visit(tree)
1141+
# owner: FunctionDef node, or `None` if the use site of the `call_cc` is not inside a function
10721142
def transform_callcc(owner, body):
10731143
# owner: FunctionDef or AsyncFunctionDef node, or None (top level of block)
10741144
# body: list of stmts
10751145
# we need to consider only one call_cc in the body, because each one
10761146
# generates a new nested def for the walker to pick up.
1077-
before, callcc, after = split_at_callcc(body)
1147+
before, callcc, after = split_at_callcc(owner, body)
10781148
if callcc:
10791149
body = before + make_continuation(owner, callcc, contbody=after)
10801150
return body
10811151
# TODO: improve error reporting for stray call_cc[] invocations
10821152
class StrayCallccChecker(ASTVisitor):
10831153
def examine(self, tree):
1084-
if iscallcc(tree):
1154+
if iscallccstatement(tree):
10851155
raise SyntaxError("call_cc[...] only allowed at the top level of a def, or at the top level of the block; must appear as an expr or an assignment RHS") # pragma: no cover
10861156
if type(tree) in (Assign, Expr):
10871157
v = tree.value

0 commit comments

Comments
 (0)