16
16
BoolOp , And , Or ,
17
17
With , AsyncWith , If , IfExp , Try , Assign , Return , Expr ,
18
18
Await ,
19
+ Global , Nonlocal ,
19
20
copy_location )
20
21
import sys
21
22
34
35
has_tco , sort_lambda_decorators ,
35
36
suggest_decorator_index ,
36
37
UnpythonicASTMarker , ExpandedContinuationsMarker )
38
+ from .scopeanalyzer import (get_names_in_store_context , extract_args ,
39
+ collect_globals , collect_nonlocals )
37
40
38
41
from ..dynassign import dyn
39
42
from ..fun import identity
@@ -883,32 +886,97 @@ def data_cb(tree): # transform an inert-data return value into a tail-call to c
883
886
# specified inside the body of the macro invocation like PG's solution does.
884
887
# Instead, we capture as the continuation all remaining statements (i.e.
885
888
# those that lexically appear after the ``call_cc[]``) in the current block.
886
- def iscallcc (tree ):
889
+ def iscallccstatement (tree ):
887
890
if type (tree ) not in (Assign , Expr ):
888
891
return False
889
892
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 ):
891
895
if not body :
892
896
return [], None , []
893
897
before , after = [], body
894
898
while True :
895
899
stmt , * after = after
896
- if iscallcc (stmt ):
900
+ if iscallccstatement (stmt ):
897
901
# after is always non-empty here (has at least the explicitified "return")
898
902
# ...unless we're at the top level of the "with continuations" block
899
903
if not after :
900
904
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 )
908
906
return before , stmt , after
909
907
before .append (stmt )
910
908
if not after :
911
909
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
912
980
# TODO: To support named return values (`kwrets` in a `Values` object) from the `call_cc`'d function,
913
981
# TODO: we need to change the syntax to something that allows us to specify which names are meant to
914
982
# 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
947
1015
raise SyntaxError (f"call_cc[]: expected an assignment or a bare expr, got { stmt } " ) # pragma: no cover
948
1016
# extract the function call(s)
949
1017
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
951
1019
theexpr = stmt .value .body # discard the AST marker
952
1020
if not (type (theexpr ) in (Call , IfExp ) or (type (theexpr ) in (Constant , NameConstant ) and getconstant (theexpr ) is None )):
953
1021
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):
966
1034
condition = altcall = None
967
1035
thecall = extract_call (theexpr )
968
1036
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
969
1038
def make_continuation (owner , callcc , contbody ):
970
1039
targets , starget , condition , thecall , altcall = analyze_callcc (callcc )
971
1040
@@ -1069,19 +1138,20 @@ def transform(self, tree):
1069
1138
if type (tree ) in (FunctionDef , AsyncFunctionDef ):
1070
1139
tree .body = transform_callcc (tree , tree .body )
1071
1140
return self .generic_visit (tree )
1141
+ # owner: FunctionDef node, or `None` if the use site of the `call_cc` is not inside a function
1072
1142
def transform_callcc (owner , body ):
1073
1143
# owner: FunctionDef or AsyncFunctionDef node, or None (top level of block)
1074
1144
# body: list of stmts
1075
1145
# we need to consider only one call_cc in the body, because each one
1076
1146
# 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 )
1078
1148
if callcc :
1079
1149
body = before + make_continuation (owner , callcc , contbody = after )
1080
1150
return body
1081
1151
# TODO: improve error reporting for stray call_cc[] invocations
1082
1152
class StrayCallccChecker (ASTVisitor ):
1083
1153
def examine (self , tree ):
1084
- if iscallcc (tree ):
1154
+ if iscallccstatement (tree ):
1085
1155
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
1086
1156
if type (tree ) in (Assign , Expr ):
1087
1157
v = tree .value
0 commit comments