Appearance
| 1 | namespace Syntax.Process is | |
| 2 | use IO.Std | |
| 3 | ||
| 4 | use Logging | |
| 5 | ||
| 6 | use Semantic.Symbols.LOCAL_VARIABLE | |
| 7 | ||
| 8 | // Pre-`compile-expressions` analysis pass: walks the AST, | |
| 9 | // resolves every IDENTIFIER reference and ASSIGNMENT LHS, | |
| 10 | // marks each `LOCAL_VARIABLE` it sees as captured (if | |
| 11 | // referenced from inside an enclosing function — i.e. from | |
| 12 | // a closure body) and/or reassigned (if it appears on the | |
| 13 | // LHS of an assignment). At end of apply, sweeps the set of | |
| 14 | // touched locals and sets `is_boxed = is_captured /\ | |
| 15 | // is_reassigned`, so that subsequent `symbol_loader`-driven | |
| 16 | // IR emission switches to `Ghul.BOX[T]`-backed storage. | |
| 17 | // | |
| 18 | // The capture and reassignment marking runs on every build: | |
| 19 | // flow narrowing forms no facts on a local a closure body can | |
| 20 | // rewrite, and that question has to be answered the same way | |
| 21 | // in the editor as in a batch build. Deriving `is_boxed` from | |
| 22 | // those flags is IL-bound, because boxing is a code-generation | |
| 23 | // strategy and assignability of a captured local is keyed on | |
| 24 | // its mut marker alone. | |
| 25 | class MARK_BOXED_LOCALS: ScopedVisitor is | |
| 26 | _logger: Logger | |
| 27 | _touched: Collections.SET[LOCAL_VARIABLE] | |
| 28 | ||
| 29 | init( | |
| 30 | logger: Logger, | |
| 31 | symbol_table: Semantic.SYMBOL_TABLE, | |
| 32 | namespaces: Semantic.NAMESPACES | |
| 33 | ) | |
| 34 | is | |
| 35 | super.init(logger, symbol_table, namespaces) | |
| 36 | ||
| 37 | _logger = logger | |
| 38 | _touched = Collections.SET[LOCAL_VARIABLE]() | |
| 39 | si | |
| 40 | ||
| 41 | apply(root: Trees.Node, want_boxing: bool) is | |
| 42 | begin_marking() | |
| 43 | ||
| 44 | root.walk(self) | |
| 45 | ||
| 46 | end_marking(want_boxing) | |
| 47 | si | |
| 48 | ||
| 49 | // The marking split from `apply` so the incremental analysis | |
| 50 | // paths, which drive a pass over an edited subtree rather than | |
| 51 | // calling its `apply`, can bracket their own walk with it. | |
| 52 | begin_marking() is | |
| 53 | _touched.clear() | |
| 54 | si | |
| 55 | ||
| 56 | end_marking(want_boxing: bool) is | |
| 57 | // Sweep: a local is boxed iff (a) it was declared | |
| 58 | // with the `mut` modifier, (b) it's captured from | |
| 59 | // inside an enclosing closure body, and (c) it's | |
| 60 | // assigned anywhere. Plain captured immutable | |
| 61 | // locals stay value-captured (and any rogue | |
| 62 | // assignment hits the captured-assignment diagnostic | |
| 63 | // upstream). Plain mutable-but-not-captured locals | |
| 64 | // stay on the stack. | |
| 65 | if want_boxing then | |
| 66 | for local in _touched do | |
| 67 | if | |
| 68 | local.is_mutable_marked /\ | |
| 69 | local.is_captured /\ | |
| 70 | local.is_reassigned | |
| 71 | then | |
| 72 | local.is_boxed = true | |
| 73 | fi | |
| 74 | od | |
| 75 | fi | |
| 76 | ||
| 77 | _touched.clear() | |
| 78 | si | |
| 79 | ||
| 80 | // Identifier-as-expression reference. If it resolves to | |
| 81 | // a LOCAL_VARIABLE owned by a function outside our | |
| 82 | // current function context, this is a capture. | |
| 83 | visit(identifier: Trees.Expressions.IDENTIFIER) is | |
| 84 | ||
| 85 | let symbol = try_find(identifier.identifier) | |
| 86 | ||
| 87 | if !symbol? \/ !isa LOCAL_VARIABLE(symbol) then | |
| 88 | return | |
| 89 | fi | |
| 90 | ||
| 91 | let local = cast LOCAL_VARIABLE(symbol) | |
| 92 | let function = current_function | |
| 93 | ||
| 94 | if !function? then | |
| 95 | return | |
| 96 | fi | |
| 97 | ||
| 98 | _touched.add(local) | |
| 99 | ||
| 100 | if local.owner != function then | |
| 101 | local.is_captured = true | |
| 102 | fi | |
| 103 | si | |
| 104 | ||
| 105 | // Reassignment of a local: LHS resolves to a | |
| 106 | // LOCAL_VARIABLE. Field assignments (`obj.field = v`) | |
| 107 | // and the like get filtered by the LOCAL_VARIABLE cast. | |
| 108 | visit(assignment: Trees.Statements.ASSIGNMENT) is | |
| 109 | let targets = Collections.LIST[Trees.Expressions.Expression]() | |
| 110 | assignment.left.get_names_into(targets) | |
| 111 | ||
| 112 | for target in targets do | |
| 113 | if !isa Trees.Expressions.IDENTIFIER(target) then | |
| 114 | continue | |
| 115 | fi | |
| 116 | ||
| 117 | let symbol = try_find(target.identifier) | |
| 118 | ||
| 119 | if !symbol? \/ !isa LOCAL_VARIABLE(symbol) then | |
| 120 | continue | |
| 121 | fi | |
| 122 | ||
| 123 | _touched.add(symbol) | |
| 124 | symbol.is_reassigned = true | |
| 125 | ||
| 126 | // The assignment sits inside a function literal | |
| 127 | // that did not declare the local, so it writes the | |
| 128 | // enclosing scope's variable rather than one of its | |
| 129 | // own. Asking for the enclosing closure rather than | |
| 130 | // the enclosing function keeps a property accessor | |
| 131 | // body - whose scope shape differs from the one the | |
| 132 | // local was declared into - out of the question. | |
| 133 | let closure = current_closure | |
| 134 | ||
| 135 | if closure? /\ symbol.owner != closure then | |
| 136 | symbol.is_closure_assigned = true | |
| 137 | fi | |
| 138 | od | |
| 139 | si | |
| 140 | si | |
| 141 | si |