diff --git a/compiler/ast/ast_query.nim b/compiler/ast/ast_query.nim index 96d66336121..6789c98de95 100644 --- a/compiler/ast/ast_query.nim +++ b/compiler/ast/ast_query.nim @@ -67,8 +67,7 @@ const PersistentNodeFlags*: TNodeFlags = {nfBase2, nfBase8, nfBase16, nfDotSetter, nfDotField, nfIsRef, nfIsPtr, nfPreventCg, nfLL, - nfFromTemplate, nfDefaultRefsParam, - nfLastRead, nfFirstWrite} + nfFromTemplate, nfDefaultRefsParam} namePos* = 0 ## Name of the type/proc-like node patternPos* = 1 ## empty except for term rewriting macros diff --git a/compiler/ast/ast_types.nim b/compiler/ast/ast_types.nim index aa2b565d488..1c33fe996ee 100644 --- a/compiler/ast/ast_types.nim +++ b/compiler/ast/ast_types.nim @@ -306,7 +306,6 @@ type sfNeverRaises ## proc can never raise an exception, not even ## OverflowDefect or out-of-memory sfUsedInFinallyOrExcept ## symbol is used inside an 'except' or 'finally' - sfSingleUsedTemp ## For temporaries that we know will only be used once sfNoalias ## 'noalias' annotation, means C's 'restrict' sfEffectsDelayed ## an 'effectsDelayed' parameter @@ -500,8 +499,6 @@ type nfDefaultParam ## an automatically inserter default parameter nfDefaultRefsParam ## a default param value references another parameter ## the flag is applied to proc default values and to calls - nfLastRead ## this node is a last read - nfFirstWrite## this node is a first write nfHasComment ## node has a comment nfImplicitPragma ## node is a "singlePragma" this is a transition flag ## created as part of nkError refactoring for the pragmas diff --git a/compiler/ast/report_enums.nim b/compiler/ast/report_enums.nim index a55ed7eabca..36fb4417893 100644 --- a/compiler/ast/report_enums.nim +++ b/compiler/ast/report_enums.nim @@ -826,7 +826,6 @@ type rsemUnsafeSetLen = "UnsafeSetLen" rsemUnsafeDefault = "UnsafeDefault" rsemBindDeprecated - rsemUncollectableRefCycle = "CycleCreated" rsemObservableStores = "ObservableStores" rsemCaseTransition = "CaseTransition" rsemUseOfGc = "GcMem" # last ! diff --git a/compiler/ast/reports_sem.nim b/compiler/ast/reports_sem.nim index d75a8dd9443..3a1b5b45a18 100644 --- a/compiler/ast/reports_sem.nim +++ b/compiler/ast/reports_sem.nim @@ -67,9 +67,6 @@ type of rsemXCannotRaiseY: raisesList*: PNode - of rsemUncollectableRefCycle: - cycleField*: PNode - of rsemStrictNotNilExpr, rsemStrictNotNilResult: nilIssue*: Nilability nilHistory*: seq[SemNilHistory] diff --git a/compiler/front/cli_reporter.nim b/compiler/front/cli_reporter.nim index ee867fdf015..062abd59315 100644 --- a/compiler/front/cli_reporter.nim +++ b/compiler/front/cli_reporter.nim @@ -1909,13 +1909,6 @@ proc reportBody*(conf: ConfigRef, r: SemReport): string = of rsemObservableStores: result = "observable stores to '$1'" % r.ast.render - of rsemUncollectableRefCycle: - if r.cycleField == nil: - result = "'$#' creates an uncollectable ref cycle" % [r.ast.render] - else: - result = "'$#' creates an uncollectable ref cycle; annotate '$#' with .cursor" % [ - r.ast.render, r.cycleField.render] - of rsemResultUsed: result = "used 'result' variable" diff --git a/compiler/mir/analysis.nim b/compiler/mir/analysis.nim new file mode 100644 index 00000000000..3164957ec7e --- /dev/null +++ b/compiler/mir/analysis.nim @@ -0,0 +1,618 @@ +## This module implements various data-flow related analysis for MIR code. +## They're based on the ``mirexec`` traversal algorithms and require a +## ``Values`` dictionary and a ``ControlFlowGraph`` object, both +## corresponding to the code fragment (i.e. ``MirTree``) that is analysed. +## +## A ``Values`` dictionary stores information about the result of operations, +## namely, whether the value is owned and, for lvalues, the root. It also +## stores the lvalue effects of operations. An instance of the dictionary is +## created and initialized via the ``computeValuesAndEffects`` procedure. +## +## Each location that is not allocated via ``new`` or ``alloc`` is owned by a +## single handle (the name of a local, global, etc.), but can be aliased +## through both pointers and views. Once the owning handle goes out of scope, +## the lifetime of the corresponding locatins ends, irrespective of whether an +## unsafe alias (pointer) of it still exists. +## +## Instead of assigning a unique ID to each value/lvalue, they're identified +## via the operation sequence that produces them (stored as a ``NodePosition`` +## tuple). While the comparision is not as efficient as an equality test +## between two integers, it is still relatively cheap, and, in addition, also +## allows for part-of analysis without requiring complex algorithms or +## data-structures. +## +## Do note that it is assumed that there only exists one handle for each +## location -- pointers or views are not tracked. Reads or writes through +## aliases are not detected. +## +## ..note:: implementing this is possible. A second step after +## ``computeValuesAndEffects`` could perform an abstract execution of +## the MIR code to produce a conservative set of possible handles for +## each pointer-like dereferencing operation. The analysis routines +## would then compare the analysed handle with each set element, +## optionally taking types into account in order to reduce the number +## of comparisons (i.e. by not comparing handles of differing type) +## +## When a "before" or "after" relationship is mentioned in the context of +## operations, it doesn't refer to the relative memory location of the +## nodes representing the operations, but rather to the operations' +## control-flow relationship. If control-flow visits A first and then B, A is +## said to come before B and B to come after A. Not all operations are +## connected to each other through control-flow however, in which case the +## aforementioned relationship doesn't exist. + +import + std/[ + hashes, + tables + ], + compiler/ast/[ + ast_types, + ast_query + ], + compiler/mir/[ + mirtrees + ], + compiler/sem/[ + aliasanalysis, + mirexec, + typeallowed + ], + compiler/utils/[ + containers + ], + experimental/[ + dod_helpers + ] + +import std/packedsets + +type + Owned* {.pure.} = enum + no + yes + weak ## values derived from compound values (e.g. ``object``, ``tuple``, + ## etc.) that are weakly owned decay to no ownership. Rvalues are + ## weakly owned -- they can be consumed directly, but sub-values of + ## them can't + unknown + + ValueInfo = object + root: opt(NodeInstance) ## the root of the value (or 'none' if the + ## ``ValueInfo`` is invalid) + owns: Owned ## whether the handle owns its value + + Effect = object + kind: EffectKind + loc: OpValue ## the lvalue the effect applies to + + EffectMap = object + ## Accelerator structure that maps each operation to its lvalue effects. + list: seq[Effect] + map: Table[Operation, Slice[uint32]] + + Values* = object + ## Stores information about the produced values plus the lvalue effects of + ## operations + values: OrdinalSeq[OpValue, ValueInfo] + # XXX: `values` currently stores an entry for each *node*. Not every node + # represents an operation and we're also not interested in the value + # of every operation, only of those that appear in specific contexts. + # A ``Table`` could be used, but that would make lookup less + # efficient (although less used memory could also mean better memory + # locality) + effects: EffectMap + + AliveState = enum + unchanged + dead + alive + + ComputeAliveProc[T] = + proc(tree: MirTree, values: Values, loc: T, n: MirNode, + op: Operation): AliveState {.nimcall, noSideEffect.} + +const + ConsumeCtx* = {mnkConsume, mnkRaise, mnkDefUnpack} + ## if an lvalue is used as an operand to these operators, the value stored + ## in the named location is considered to be consumed (ownership over it + ## transfered to the operation) + UseContext* = {mnkArg, mnkDeref, mnkDerefView, mnkCast, mnkVoid, mnkIf, + mnkCase} + ConsumeCtx + ## using an lvalue as the operand to one of these operators means that + ## the content of the location is observed (when control-flow reaches the + ## operator). In other words, applying the operator result in a read + # FIXME: none-lvalue-conversions also count as reads, but because no + # distinction is being made between lvalue- and value-conversions + # at the MIR level, they're currently not considered. This is an + # issue and it needs to be fixed + + OpsWithEffects = {mnkCall, mnkMagic, mnkAsgn, mnkFastAsgn, mnkSwitch, + mnkInit, mnkRegion} + ## the set of operations that can have lvalue-parameterized or general + ## effects + +func hash(x: Operation): Hash {.borrow.} + +func skipConversions(tree: MirTree, val: OpValue): OpValue = + ## Returns the value without conversions applied + var p = NodePosition(val) + while tree[p].kind in {mnkStdConv, mnkConv}: + p = previous(tree, p) + + result = OpValue(p) + +template getRoot*(v: Values, val: OpValue): OpValue = + OpValue v.values[val].root[] + +template owned*(v: Values, val: OpValue): Owned = + v.values[val].owns + +func setOwned*(v: var Values, val: OpValue, owns: Owned) {.inline.} = + v.values[val].owns = owns + +func toLvalue*(v: Values, val: OpValue): LvalueExpr {.inline.} = + (NodePosition v.values[val].root[], + NodePosition val) + +iterator effects(v: Values, op: Operation): lent Effect = + ## Yields all location-related effects of the given operation `op` in the + ## order they were registered + let s = v.effects.map.getOrDefault(op, 1.uint32..0.uint32) + for i in s: + yield v.effects.list[i] + +func decayed(x: ValueInfo): ValueInfo {.inline.} = + ## Turns 'weak' ownership into 'no' ownership + result = x + if result.owns == Owned.weak: + result.owns = Owned.no + +func add(m: var EffectMap, op: Operation, effects: openArray[Effect]) = + ## Registers `effects` with `op` in the map `m` + let start = m.list.len + m.list.add effects + m.map[op] = start.uint32 .. m.list.high.uint32 + +func computeValuesAndEffects*(body: MirTree): Values = + ## Creates a ``Values`` dictionary with all operation effects collected and + ## (static) value roots computed. Value ownership is already computed where it + ## is possible to do so by just taking the static operation sequences into + ## account (i.e. no control- or data-flow analysis is performed) + var + stack: seq[Effect] + # more than 65K nested effects seems unlikely + num: seq[uint16] + + result.values.newSeq(body.len) + + template inherit(i, source: NodePosition) = + result.values[OpValue i] = result.values[OpValue source] + + template inheritDecay(i, source: NodePosition) = + result.values[OpValue i] = decayed result.values[OpValue source] + + template popEffects(op: Operation) = + let v = num.pop().int + if v < stack.len: + result.effects.add op, toOpenArray(stack, v, stack.high) + stack.setLen(v) + + # we're doing three things here: + # 1. propagate the value root + # 2. propagate ownership status + # 3. collect the lvalue effects for operations + # + # This is done in a single forward iteration over all nodes in the code + # fragment -- nodes that don't represent operations are ignored. + # Effects are collected by looking for 'tag' operations. Each occurrence of + # an 'arg-block' node starts a new "frame". When a 'tag' operation is + # encountered, the corresponding ``Effect`` information is added to the + # frame. At the end of the 'arg-block', the frame is popped and the effects + # collected as part of it are registered to the arg-block's corresponding + # operation + + for i, n in body.pairs: + template start(owned: Owned) = + result.values[OpValue i] = + ValueInfo(root: someOpt(NodeInstance i), owns: owned) + + case n.kind + of mnkOpParam: + # XXX: the body of regions are not yet analysed (they're skipped over). + # Once they are, the ownership status of an `opParam` depends on + # the corresponding argument. Values coming from 'name' and 'arg' + # arguments are not owned, but for those coming from 'consume' + # arguments, it depends (i.e. ``unknown``) + start: Owned.no + of mnkDeref, mnkDerefView, mnkConst, mnkType, mnkNone, mnkCast: + start: Owned.no + of mnkLiteral, mnkProc: + # literals are always owned (each instance can be mutated without + # impacting the others). Because of their copy-on-write mechanism, + # this also includes string literals + start: Owned.yes + of mnkTemp, mnkLocal, mnkGlobal, mnkParam: + # more context is required to know whether the value is owned + start: Owned.unknown + of mnkConstr: + # the result of a ``seq`` construction via ``constr`` is essentially a + # non-owning view into constant data + start: + if n.typ.skipTypes(abstractInst).kind == tySequence: Owned.no + else: Owned.weak + of mnkObjConstr: + start: + if n.typ.skipTypes(abstractInst).kind == tyRef: Owned.yes + else: Owned.weak + # ``mnkObjConstr`` is a sub-tree, so in order to keep the inheriting + # logic simple, the 'end' node for the sub-tree uses the same + # ``ValueInfo`` as the start node + inherit(findEnd(body, i), i) + of mnkCall, mnkMagic: + # we currently can't reason about which location(s) views alias, so + # we always treat values accessed through them as not owned + start: + if directViewType(n.typ) != noView: Owned.no + else: Owned.weak + of mnkStdConv, mnkConv: + inherit(i, i - 1) + of mnkAddr, mnkView, mnkPathPos, mnkPathVariant: + inheritDecay(i, i - 1) + of mnkPathArray: + # inherit from the first operand (i.e. the array-like value) + inheritDecay(i, NodePosition operand(body, Operation(i), 0)) + of mnkPathNamed: + inheritDecay(i, i - 1) + if sfCursor in n.field.flags: + # any lvalue derived from a cursor location is non-owning + result.values[OpValue i].owns = Owned.no + + of mnkArgBlock: + num.add stack.len.uint16 # remember the current top-of-stack + of mnkTag: + stack.add Effect(kind: n.effect, loc: OpValue(i - 1)) + of mnkEnd: + if n.start == mnkArgBlock: + popEffects(Operation(i+1)) + + of AllNodeKinds - InOutNodes - InputNodes - {mnkEnd}: + discard "leave uninitialized" + +func isAlive*(tree: MirTree, cfg: ControlFlowGraph, v: Values, + span: Slice[NodePosition], loc: LvalueExpr, + pos: NodePosition): bool = + ## Computes if the location named by `loc` does contain a value at `pos` + ## (i.e. is alive). The performed data-flow analysis only considers code + ## inside `span` + template toLvalue(val: OpValue): LvalueExpr = + toLvalue(v, val) + + template overlaps(val: OpValue): bool = + overlaps(tree, loc, toLvalue val) != no + + # this is a reverse data-flow problem. We follow all control-flow paths from + # `pos` backwards until either there's no path left to follow or one of them + # reaches a potential mutation of `loc`, in which case the underlying location + # is considered to be alive. A path is not followed further if it reaches an + # operation that "kills" the `loc` (removes its value, e.g. by moving it + # somewhere else) + + var exit = false + for i, n in traverseReverse(tree, cfg, span, pos, exit): + case n.kind + of OpsWithEffects: + # iterate over the effects and look for the ones involving the analysed + # location + for effect in effects(v, Operation i): + case effect.kind + of ekMutate, ekReassign: + if overlaps(effect.loc): + # consider ``a.b = x`` (A) and ``a.b.c.d.e = y`` (B). If the + # analysed l-value expression is ``a.b.c`` then both A and B mutate + # it (either fully or partially). If traversal reaches what's + # possibly a mutation of the analysed location, it means that the + # location needs to be treated as being alive at `pos`, so we can + # return already + return true + + of ekKill: + if isPartOf(tree, loc, toLvalue effect.loc) == yes: + exit = true + break + + of ekInvalidate: + discard + + if tree[loc.root].kind == mnkGlobal and + n.kind == mnkCall and geMutateGlobal in n.effects: + # an unspecified global is mutated and we're analysing a location + # derived from a global -> assume the analysed global is mutated + return true + + of ConsumeCtx: + let opr = unaryOperand(tree, Operation i) + if v.owned(opr) == Owned.yes: + if isPartOf(tree, loc, toLvalue opr) == yes: + # the location's value is consumed and it becomes empty. No operation + # coming before the current one can change that, so we can stop + # traversing the current path + exit = true + + # partially consuming the location does *not* change the alive state + + else: + discard "not relevant" + + # no mutation is directly connected to `pos`. The location is not alive + result = false + +func isLastRead*(tree: MirTree, cfg: ControlFlowGraph, values: Values, + span: Slice[NodePosition], loc: LvalueExpr, pos: NodePosition + ): bool = + ## Performs data-flow analysis to compute whether the value that `loc` + ## evaluates to at `pos` is *not* observed by operations that have a + ## control-flow dependency on the operation/statement at `pos` and + ## are located inside `span`. + ## It's important to note that this analysis does not test whether the + ## underlying *location* is accessed, but rather the *value* it stores. If a + ## new value is assigned to the underlying location which is then accessed + ## after, it won't cause the analysis to return false + template toLvalue(val: OpValue): LvalueExpr = + toLvalue(values, val) + + var state: TraverseState + for i, n in traverse(tree, cfg, span, pos, state): + case n.kind + of OpsWithEffects: + for effect in effects(values, Operation i): + let cmp = compareLvalues(tree, loc, toLvalue effect.loc) + case effect.kind + of ekReassign: + if isAPartOfB(cmp) == yes: + # the location is reassigned -> all operations coming after will + # observe a different value + state.exit = true + break + elif isBPartOfA(cmp) != no: + # the location is partially written to -> the relevant values is + # observed + return false + + of ekMutate: + if cmp.overlaps != no: + # the location is partially written to + return false + + of ekKill: + if isAPartOfB(cmp) == yes: + # the location is definitely killed, it no longer stores the value + # we're interested in + state.exit = true + break + + of ekInvalidate: + discard + + if tree[loc.root].kind == mnkGlobal and + n.kind == mnkCall and geMutateGlobal in n.effects: + # an unspecified global is mutated and we're analysing a location + # derived from a global -> assume that it's a read/use + return false + + of UseContext - {mnkDefUnpack}: + if overlaps(tree, loc, toLvalue unaryOperand(tree, Operation i)) != no: + return false + + of DefNodes: + # passing a value to a 'def' is also a use + if hasInput(tree, Operation i) and + overlaps(tree, loc, toLvalue unaryOperand(tree, Operation i)) != no: + return false + + else: + discard + + # no further read of the value is connected to `pos` + result = true + +func isLastWrite*(tree: MirTree, cfg: ControlFlowGraph, values: Values, + span: Slice[NodePosition], loc: LvalueExpr, pos: NodePosition + ): tuple[result, exits, escapes: bool] = + ## Computes if the location `loc` is not reassigned to or modified while it + ## still contains the value it contains at `pos`. In other words, computes + ## whether a reassignment or mutation that has a control-flow dependency on + ## `pos` and is located inside `span` observes the current value. + ## + ## In addition, whether the `pos` is connected to a structured or + ## unstructured exit of `span` is also returned + template toLvalue(val: OpValue): LvalueExpr = + toLvalue(values, val) + + var state: TraverseState + for i, n in traverse(tree, cfg, span, pos, state): + case n.kind + of OpsWithEffects: + for effect in effects(values, Operation i): + let cmp = compareLvalues(tree, loc, toLvalue effect.loc) + case effect.kind + of ekReassign, ekMutate, ekInvalidate: + # note: since we don't know what happens to the location when it is + # invalidated, the effect is also included here + if cmp.overlaps != no: + return (false, false, false) + + of ekKill: + if isAPartOfB(cmp) == yes: + state.exit = true + break + + # partially killing the analysed location is not considered to be a + # write + + if tree[loc.root].kind == mnkGlobal and + n.kind == mnkCall and geMutateGlobal in n.effects: + # an unspecified global is mutated and we're analysing a location + # derived from a global + return (false, false, false) + + else: + discard + + result = (true, state.exit, state.escapes) + +func computeAliveOp*[T: PSym | TempId]( + tree: MirTree, values: Values, loc: T, n: MirNode, op: Operation): AliveState = + ## Computes the state of `loc` at the *end* of the given operation. The + ## operands are expected to *not* alias with each other. The analysis + ## result will be wrong if they do + + func isAnalysedLoc[T](n: MirNode, loc: T): bool = + when T is TempId: + n.kind == mnkTemp and n.temp == loc + elif T is PSym: + n.kind in {mnkLocal, mnkParam, mnkGlobal} and n.sym.id == loc.id + else: + {.error.} + + template isRootOf(val: OpValue): bool = + isAnalysedLoc(tree[values.getRoot(val)], loc) + + template sameLocation(val: OpValue): bool = + isAnalysedLoc(tree[skipConversions(tree, val)], loc) + + case n.kind + of OpsWithEffects: + # iterate over the lvalue effects of the processed operation and check + # whether one of them affects the state of `loc`. If one does, further + # iteration is not required, as the underlying locations of the operands + # must not alias with each other. + for effect in effects(values, op): + case effect.kind + of ekMutate, ekReassign: + if isRootOf(effect.loc): + # the analysed location or one derived from it is mutated + return alive + + of ekKill: + if sameLocation(effect.loc): + # the location is killed + return dead + + of ekInvalidate: + discard "cannot be reasoned about here" + + when T is PSym: + # XXX: testing the symbol's flags is okay for now, but a different + # approach has to be used once moving away from storing ``PSym``s + # in ``MirNodes`` + if sfGlobal in loc.flags and + n.kind == mnkCall and geMutateGlobal in n.effects: + # the operation mutates global state and we're analysing a global + result = alive + + of ConsumeCtx: + let opr = unaryOperand(tree, op) + if values.owned(opr) == Owned.yes and sameLocation(opr): + # the location's value is consumed + result = dead + + else: + discard + +func computeAlive*[T](tree: MirTree, cfg: ControlFlowGraph, values: Values, + span: Slice[NodePosition], loc: T, hasInitialValue: bool, + op: static ComputeAliveProc[T] + ): tuple[alive, escapes: bool] = + ## Computes whether the location is alive when `span` is exited via either + ## structured or unstructured control-flow. A location is considered alive + ## if it contains a value + + # assigning to or mutating the analysed location makes it become alive, + # because it then stores a value. Consuming its value or using ``wasMoved`` + # on it "kills" it (it no longer contains a value) + + var exit = false + for i, n in traverseFromExits(tree, cfg, span, exit): + case op(tree, values, loc, n, Operation i) + of dead: + exit = true + of alive: + # the location is definitely alive when leaving the span via + # unstructured control-flow + return (true, true) + of unchanged: + discard + + if exit and hasInitialValue: + # an unstructured exit is connected to the start of the span and the + # location starts initialized + return (true, true) + + # check if the location is alive at the structured exit of the span + for i, n in traverseReverse(tree, cfg, span, span.b + 1, exit): + case op(tree, values, loc, n, Operation i) + of dead: + exit = true + of alive: + # the location is definitely alive when leaving the span via + # structured control-flow + return (true, false) + of unchanged: + discard + + result = (exit and hasInitialValue, false) + +proc doesGlobalEscape*(tree: MirTree, scope: Slice[NodePosition], + start: NodePosition, s: PSym): bool = + ## Computes if the global `s` potentially "escapes". A global escapes if it + ## is not declared at module scope and is used inside a procedure that is + ## then called outside the analysed global's scope. Example: + ## + ## .. code-block:: nim + ## + ## # a.nim + ## var p: proc() + ## block: + ## var x = Resource(...) + ## proc prc() = + ## echo x + ## + ## p = prc # `x` "escapes" here + ## # uncommenting the below would make `x` not escape + ## # p = nil + ## + ## p() + ## + # XXX: to implement this, one has to first collect side-effectful procedures + # defined inside either the same or nested scopes and their + # address taken (``sfSideEffect`` and ``sfAddrTaken``). The + # ``sfSideEffect`` flag only indicates whether a procedure accesses + # global state, not if the global in question (`s`) is modified / + # observed -- recursively applying the analysis to the procedures' + # bodies would be necessary for that. + # Then look for all assignments with one of the collect procedures as + # the source operand and perform an analysis similar to the one + # performed by ``isLastRead`` to check if the destination still + # contains the procedural value at the of the scope. If it does, the + # global escapes + # XXX: as an escaping global is a semantic error, it would make more sense + # to detect and report it during semantic analysis instead -- the + # required DFA is not as simple there as it is with the MIR however + result = false + +func isConsumed*(tree: MirTree, val: OpValue): bool = + ## Computes if `val` is definitely consumed. This is the case if it's + ## directly used in a consume context, ignoring lvalue conversions + var dest = NodePosition(val) + while true: + dest = sibling(tree, dest) + + case tree[dest].kind + of mnkConv, mnkStdConv: + # XXX: only lvalue conversions should be skipped + discard "skip conversions" + of ConsumeCtx: + return true + else: + return false \ No newline at end of file diff --git a/compiler/mir/mirbridge.nim b/compiler/mir/mirbridge.nim index 5fe6c7db9e9..c2f522f79ff 100644 --- a/compiler/mir/mirbridge.nim +++ b/compiler/mir/mirbridge.nim @@ -55,6 +55,31 @@ let reprConfig = block: rc.flags.incl trfShowSymKind rc +# NOTE: the ``echoX`` are used as a temporary solution for inspecting inputs +# and outputs in the context of compiler debugging until a more +# structured/integrated solution is implemented + +proc echoInput(config: ConfigRef, owner: PSym, body: PNode) = + ## If requested via the define, renders the input AST `body` and writes the + ## result out through ``config.writeLine``. + if config.getStrDefine("nimShowMirInput") == owner.name.s: + writeBody(config, "-- input AST: " & owner.name.s): + config.writeln(treeRepr(config, body, reprConfig)) + +proc echoMir(config: ConfigRef, owner: PSym, tree: MirTree) = + ## If requested via the define, renders the `tree` and writes the result out + ## through ``config.writeln``. + if config.getStrDefine("nimShowMir") == owner.name.s: + writeBody(config, "-- MIR: " & owner.name.s): + config.writeln(print(tree)) + +proc echoOutput(config: ConfigRef, owner: PSym, body: PNode) = + ## If requested via the define, renders the output AST `body` and writes the + ## result out through ``config.writeLine``. + if config.getStrDefine("nimShowMirOutput") == owner.name.s: + writeBody(config, "-- output AST: " & owner.name.s): + config.writeln(treeRepr(config, body, reprConfig)) + proc canonicalize*(graph: ModuleGraph, idgen: IdGenerator, owner: PSym, body: PNode, options: set[GenOption]): PNode = ## No MIR passes exist yet, so the to-and-from translation is treated as a @@ -85,25 +110,35 @@ proc canonicalize*(graph: ModuleGraph, idgen: IdGenerator, owner: PSym, proc canonicalizeWithInject*(graph: ModuleGraph, idgen: IdGenerator, owner: PSym, body: PNode, options: set[GenOption]): PNode = - ## Performs either the canonicalization *or* cursor inference plus - ## destructor injection - - # the output of ``canonicalize`` confuses either ``computeCursors``, - # ``injectDestructorCalls``, or both enough for them to produce code - # where expected destructor calls are missing. As a temporary solution, the - # canonicalization step is skipped for bodies that require destructor - # injection - # FIXME: this is a severe problem, as it means that ``canonicalize`` is, in - # fact, **not** run for all code that is reaching the code-generators. - # The issue needs to be fixed before transformations and lowerings can - # be turned into MIR passes - if shouldInjectDestructorCalls(owner): - if optCursorInference in graph.config.options: - computeCursors(owner, body, graph) - - injectDestructorCalls(graph, idgen, owner, body) - else: - canonicalize(graph, idgen, owner, body, {}) + ## Transforms `body` through the following steps: + ## 1. cursor inference + ## 2. translation to MIR code + ## 3. application of the ``injectdestructors`` pass + ## 4. translation back to AST + ## + ## Cursor inference and destructor injection are only performed if `owner` + ## is eligible according to ``injectdestructors.shouldInjectDestructorCalls`` + let config = graph.config + + # cursor inference is not a MIR pass yet, so it has to be applied before + # the MIR translation/processing + let inject = shouldInjectDestructorCalls(owner) + if inject and optCursorInference in config.options: + computeCursors(owner, body, graph) + + echoInput(config, owner, body) + + # step 1: generate a ``MirTree`` from the input AST + var (tree, sourceMap) = generateCode(graph, owner, options, body) + echoMir(config, owner, tree) + + # step 2: run the ``injectdestructors`` pass + if inject: + injectDestructorCalls(graph, idgen, owner, tree, sourceMap) + + # step 3: translate the MIR code back to an AST + result = generateAST(graph, idgen, owner, tree, sourceMap) + echoOutput(config, owner, result) proc canonicalizeSingle*(graph: ModuleGraph, idgen: IdGenerator, owner: PSym, n: PNode, options: set[GenOption]): PNode = diff --git a/compiler/mir/mirchangesets.nim b/compiler/mir/mirchangesets.nim index 18e5db9cdc1..1b7f95c9ca5 100644 --- a/compiler/mir/mirchangesets.nim +++ b/compiler/mir/mirchangesets.nim @@ -97,14 +97,14 @@ func span[T](a, b: T): HOslice[T] {.inline.} = func empty[T](x: typedesc[HOslice[T]]): HOslice[T] {.inline.} = HOslice[T](a: default(T), b: default(T)) -func row(start, fin: NodePosition, src: HOSlice[NodeIndex]; +func row(start, fin: NodePosition, src: HOslice[NodeIndex]; source = NodePosition(0)): Row {.inline.} = ## Convenience constructor for ``Row`` Row(orig: span(NodeIndex(start), NodeIndex(fin)), src: src, source: source.uint32) -func addSingle(s: var MirNodeSeq, n: sink MirNode): HOSlice[NodeIndex] = +func addSingle(s: var MirNodeSeq, n: sink MirNode): HOslice[NodeIndex] = s.add n result = single(s.high.NodeIndex) @@ -179,7 +179,7 @@ template replaceMulti*(c: var Changeset, name, body: untyped) = body swap(c.nodes, name) - let next = sibling(tree(c), c.pos) + let next = sibling(c.tree, c.pos) c.rows.add row(c.pos, next, span(start, c.nodes.len.NodeIndex), c.pos) c.pos = next @@ -187,7 +187,7 @@ func remove*(c: var Changeset) = ## Records the removal of the currently pointed to sub-tree let next = sibling(c.tree, c.pos) # use an empty source slice - c.rows.add row(c.pos, next, empty(HOSlice[NodeIndex])) + c.rows.add row(c.pos, next, empty(HOslice[NodeIndex])) c.pos = next func skip*(c: var Changeset, num: Natural) = diff --git a/compiler/mir/mirtrees.nim b/compiler/mir/mirtrees.nim index 3c95d31c92a..0b040f766cf 100644 --- a/compiler/mir/mirtrees.nim +++ b/compiler/mir/mirtrees.nim @@ -326,8 +326,11 @@ const ## Convenience set containing all existing node kinds DefNodes* = {mnkDef, mnkDefCursor, mnkDefUnpack} + ## Node kinds that represent definition statements (i.e. something that + ## introduces a named entity) - SubTreeNodes* = {mnkArgBlock..mnkBranch, mnkObjConstr} + DefNodes + SubTreeNodes* = {mnkObjConstr, mnkArgBlock, mnkRegion, mnkStmtList, mnkScope, + mnkIf..mnkBlock, mnkBranch } + DefNodes ## Nodes that mark the start of a sub-tree. They're always matched with a ## corrsponding ``mnkEnd`` node @@ -335,10 +338,10 @@ const ## Nodes that aren't sub-trees InputNodes* = {mnkProc..mnkNone, mnkArgBlock} - ## Expression roots + ## Nodes that can appear in the position of inputs/operands but that + ## themselves don't have any operands InOutNodes* = {mnkMagic, mnkCall, mnkPathNamed..mnkPathVariant, mnkConstr, - mnkObjConstr, mnkView, - mnkTag, mnkCast, mnkDeref, mnkAddr, + mnkObjConstr, mnkView, mnkTag, mnkCast, mnkDeref, mnkAddr, mnkDerefView, mnkStdConv, mnkConv} ## Operations that act as both input and output SourceNodes* = InputNodes + InOutNodes @@ -358,14 +361,12 @@ const ArgumentNodes ## Operators and statements that must not have argument-blocks as input - Operators* = {mnkFastAsgn..mnkMagic, mnkTag, mnkRaise..mnkDerefView, - mnkStdConv..mnkCast} - StmtNodes* = {mnkScope, mnkRepeat, mnkTry, mnkBlock, mnkBreak, mnkReturn, mnkPNode} + DefNodes ## Nodes that act as statements syntax-wise SymbolLike* = {mnkProc, mnkConst, mnkGlobal, mnkParam, mnkLocal} + ## Nodes for which the `sym` field is available NoLabel* = LabelId(0) @@ -383,7 +384,7 @@ template `[]`*(x: LabelId): uint32 = uint32(x) - 1 # make ``NodeInstance`` available to be used with ``OptIndex``: -template indexLike(_: typedesc[NodeInstance]) = discard +template indexLike*(_: typedesc[NodeInstance]) = discard # XXX: ideally, the arithmetic operations on ``NodePosition`` should not be # exported. How the nodes are stored should be an implementation detail @@ -432,7 +433,9 @@ func parentEnd*(tree: MirTree, n: NodePosition): NodePosition = # enclosing `n` result = n - var depth = 1 + # start at depth '2' if `n` starts a sub-tree itself. The terminator of said + # sub-tree would be treated as the parent's end otherwise + var depth = 1 + ord(tree[n].kind in SubTreeNodes) while depth > 0: inc result diff --git a/compiler/modules/magicsys.nim b/compiler/modules/magicsys.nim index 296259893ff..4bee26a258a 100644 --- a/compiler/modules/magicsys.nim +++ b/compiler/modules/magicsys.nim @@ -11,6 +11,7 @@ import compiler/utils/[ + idioms, platform, ], compiler/ast/[ @@ -175,24 +176,22 @@ proc getNimScriptSymbol*(g: ModuleGraph; name: string): PSym = proc resetNimScriptSymbols*(g: ModuleGraph) = initStrTable(g.exposed) -proc getMagicEqSymForType*(g: ModuleGraph; t: PType; info: TLineInfo): PSym = +func getMagicEqForType*(t: PType): TMagic = + ## Returns the ``mEqX`` magic for the given type `t`. case t.kind of tyInt, tyInt8, tyInt16, tyInt32, tyInt64, tyUInt, tyUInt8, tyUInt16, tyUInt32, tyUInt64: - result = getSysMagic(g, info, "==", mEqI) - of tyEnum: - result = getSysMagic(g, info, "==", mEqEnum) - of tyBool: - result = getSysMagic(g, info, "==", mEqB) - of tyRef, tyPtr, tyPointer: - result = getSysMagic(g, info, "==", mEqRef) - of tyString: - result = getSysMagic(g, info, "==", mEqStr) - of tyChar: - result = getSysMagic(g, info, "==", mEqCh) - of tySet: - result = getSysMagic(g, info, "==", mEqSet) - of tyProc: - result = getSysMagic(g, info, "==", mEqProc) + mEqI + of tyEnum: mEqEnum + of tyBool: mEqB + of tyRef, tyPtr, tyPointer: mEqRef + of tyString: mEqStr + of tyChar: mEqCh + of tySet: mEqSet + of tyProc: mEqProc else: - g.config.globalReport(info, reportTyp(rsemNoMagicEqualsForType, t)) + unreachable(t.kind) + +proc getMagicEqSymForType*(g: ModuleGraph; t: PType; info: TLineInfo): PSym = + let magic = getMagicEqForType(t) + result = getSysMagic(g, info, "==", magic) diff --git a/compiler/sem/aliasanalysis.nim b/compiler/sem/aliasanalysis.nim new file mode 100644 index 00000000000..1a988df96f1 --- /dev/null +++ b/compiler/sem/aliasanalysis.nim @@ -0,0 +1,168 @@ +## This module implements a simple alias analysis based on MIR operation +## sequences, used to compute the relationship between two lvalues, i.e. if +## the underlying locations potentially overlap or are part sub- or +## super-locations of each other. +## +## Only lvalues with statically know roots (derived from named locals, global, +## etc.) are supported. +## +## The main procedure is ``compareLvalues``: it performs the comparison and +## returns the result as an opaque object ``CmpLocsResult``, which can then be +## queried for the relationship between the two inputs. +## +## Whether two lvalues refer to overlapping locations is not always statically +## known (e.g. when accessing an array with a value only known at run-time) -- +## if it is not, the uncertainity is communicated via the ``maybe`` result. + +import + compiler/ast/[ + ast + ], + compiler/mir/[ + mirtrees + ], + compiler/utils/[ + idioms + ] + +type + Ternary* = enum + no, maybe, yes + + LvalueExpr* = tuple[root, last: NodePosition] + # XXX: ``LvalueExpr`` fit well at one point, but it doesn't anymore. The + # name ``Handle`` might be a better fit now + + CmpLocsResult* = object + endA, endB: bool ## whether the `last` operation of the provided sequences + ## was reached + overlaps: Ternary + +const + Roots = SymbolLike + {mnkTemp, mnkCall, mnkMagic, mnkDeref, mnkDerefView} + +func isSameRoot(an, bn: MirNode): bool = + if an.kind != bn.kind: + return false + + case an.kind + of SymbolLike: + result = an.sym.id == bn.sym.id + of mnkTemp: + result = an.temp == bn.temp + of Roots - SymbolLike - {mnkTemp}: + result = false + else: + unreachable(an.kind) + +func sameIndex(a, b: MirNode): Ternary = + if a.kind != b.kind or a.kind != mnkLiteral: + maybe + else: + if a.lit.intVal == b.lit.intVal: + yes + else: + no + +proc compareLvalues(body: MirTree, a, b: NodePosition, + lastA, lastB: NodePosition): CmpLocsResult = + ## Performs the comparision and computes the information to later derive the + ## facts from (e.g. if A is a part B) + if not isSameRoot(body[a], body[b]): + return CmpLocsResult(overlaps: no) + + var + a = a + 1 + b = b + 1 + + const IgnoreSet = {mnkStdConv, mnkConv, mnkAddr, mnkView} + ## we can skip all conversions, since we know that they must be lvalue + ## conversions; ``compareLvalues`` is only used for lvalues, which + ## can't result from non-lvalue conversions. + ## Both the 'addr' and 'view' operatio create a first-class handle to + ## the reference location, so we also skip them. This allows for ``a.x`` + ## to be treated as refering to same location as ``a.x.addr`` (which is + ## correct) + + var overlaps = yes # until proven otherwise + while overlaps != no and a <= lastA and b <= lastB: + while body[a].kind in IgnoreSet: + inc a + + while body[b].kind in IgnoreSet: + inc b + + if a > lastA or b > lastB: + break + + let + an {.cursor.} = body[a] + bn {.cursor.} = body[b] + + if an.kind != bn.kind: + overlaps = no + break + + case an.kind + of mnkPathNamed, mnkPathVariant: + if an.field.id != bn.field.id: + overlaps = no + break + + of mnkPathPos: + if an.position != bn.position: + overlaps = no + break + + of mnkPathArray: + # -1 = the arg-block end + # -2 = the arg node + # -3 = the operand + overlaps = sameIndex(body[a - 3], body[b - 3]) + if overlaps == no: + break + + of ArgumentNodes: + # this happens when invoking a path operator with more than one + # operand. Skip to the end of the arg-block + a = parentEnd(body, a) + b = parentEnd(body, b) + else: + unreachable(an.kind) + + inc a + inc b + + result = CmpLocsResult(endA: a > lastA, endB: b > lastB, overlaps: overlaps) + +func isAPartOfB*(r: CmpLocsResult): Ternary {.inline.} = + result = r.overlaps + if result != no and not r.endB and r.endA: + # B either is or maybe is a part of A, but A is not a part of B + result = no + +func isBPartOfA*(r: CmpLocsResult): Ternary {.inline.} = + result = r.overlaps + if result != no and not r.endA and r.endB: + result = no + +func isSame*(r: CmpLocsResult): bool {.inline.} = + ## Returns whether A and B refer to the exact same location + ## (ignoring the type) + result = r.overlaps == yes and r.endA and r.endB + +template overlaps*(r: CmpLocsResult): Ternary = + r.overlaps + +func compareLvalues*(tree: MirTree, ea, eb: LvalueExpr): CmpLocsResult {.inline.} = + result = compareLvalues(tree, ea.root, eb.root, ea.last, eb.last) + +func overlaps*(tree: MirTree, ea, eb: LvalueExpr): Ternary {.inline.} = + ## Convenience wrapper + compareLvalues(tree, ea.root, eb.root, ea.last, eb.last).overlaps + +func isPartOf*(tree: MirTree, ea, eb: LvalueExpr): Ternary {.inline.} = + ## Computes if the location named by `ea` is part of `eb`. Also evaluates to + ## 'yes' if both name the exact same location + let cmp = compareLvalues(tree, ea.root, eb.root, ea.last, eb.last) + result = isAPartOfB(cmp) \ No newline at end of file diff --git a/compiler/sem/injectdestructors.nim b/compiler/sem/injectdestructors.nim index b84c910b6e8..9fe439e0e0e 100644 --- a/compiler/sem/injectdestructors.nim +++ b/compiler/sem/injectdestructors.nim @@ -7,1088 +7,1393 @@ # distribution, for details about the copyright. # -## Injects destructor calls into Nim code as well as -## an optimizer that optimizes copies to moves. This is implemented as an -## AST to AST transformation so that every backend benefits from it. - -## See doc/destructors.rst for a spec of the implemented rewrite rules +## This module implements the following MIR passes: +## - the pass for injecting temporaries for unconsumed rvalues that have +## destructors (``injectTemporaries``) +## - the 'switch' operation lowering (``lowerBranchSwitch``) +## - the pass for rewriting assignments into call to the respective +## lifetime-tracking hooks +## - the pass for introducing copies for unowned values passed to ``sink`` +## parameters +## - the destructor (i.e. ``=destroy`` hook) injection +## +## Overview +## ======== +## +## The injection of temporaries is required to prevent leaks. Only locations +## can be destroyed, so if the result of a procedure call is a resource that +## requires cleanup and is not directly consumed (by assigning it to a +## location or passing it to a ``sink`` argument), it is materialized into a +## temporary. The analysis of what requires destruction only takes entities +## (globals, locals, temporaries, etc.) into account that are explicitly +## defined in the code fragment (``MirTree``), so the changes performed by the +## temporary injection have to be visible to it. +## +## An analysis pass is performed that collects all entities that require +## destruction into an ``EntityDict``. These are: locals, temporaries, ``sink`` +## parameters, and globals (with some exceptions). If a location has no +## type-bound ``=destroy`` hook (both user-provided and lifted), it is not +## included. +## +## .. note: for globals, only those that are not defined at module or +## procedure scope and are not thread-local variables are collected. +## Except for thread-local variables, the others are destroyed at the +## end of the program. +## +## As an optimization, only entities for which it can't be statically proven +## that they don't contain a value at the end of their scope are collected. +## +## Next, an instance of a ``Values`` dictionary corresponding to the input +## code-fragment is created and initialized. For all arguments that appear in +## a consume context (e.g. passed to ``sink`` argument, assignment source) +## and for which the ownership status could not be resolved to either 'yes' or +## 'no' by ``analysis.computeValuesAndEffects``, a data-flow analysis is +## performed to figure out the status (see ``solveOwnership``). +## +## Using the now resolved ownership status of all expressions, the next +## analysis step computes which locations need to be destroyed via a destructor +## call (see ``computeDestructors``). +## +## As the last step, the assignment rewriting and destructor injection is +## performed, using the previously gathered data. +## +## For the assignment rewriting, if the source operand of an assignment is +## owned, a move is used instead of a copy. +## +## Ownership analysis +## ================== +## +## Reassigning or reading from a location through a handle that is not the +## owning one is **not** detected by the analysis. In the following case +## (assuming no cursor inference): +## +## .. code-block::nim +## +## var a = @[1, 2] +## let p = addr a +## +## var b = a +## b.add 3 +## doAssert p[][0] == 1 +## +## a = ... # force the earlier move to be destructive +## +## the analysis will detect `var b = a` to be the last usage of `a`, +## subsequently turning the assignment into a move and thus making the +## assertion fail with an ``IndexDefect``. +## +## Escaping temporaries +## ==================== +## +## There exists the general problem of both temporaries and rvalues escaping +## in the context of consumed arguments. Consider: +## +## .. code-block::nim +## +## proc f_sink(x: sink Obj, y: int) = discard +## +## f_sink(create(), callThatRaises()) # 1 +## +## var x = Obj() +## f_sink(notLastUseOf x, callThatRaises()) # 2 +## +## var y = Obj() +## f_sink(lastUseOf y, callThatRaises()) # 3 +## +## var z = Obj() +## f_sink(lastUseOf z, callThatRaises()) # 4 +## z = Obj() +## +## For #1, the temporary injection pass recognizes that the result of +## ``create()`` is used in a consume context and thus doesn't inject a +## temporary. This then causes the value to leak when ``callThatRaises()`` +## raises an exception. Note that the raising of an exception is only used +## as an example -- the same issue is present with all other unstructured +## control-flow (``return``, ``break``, etc.). +## +## Fixing this would require for the temporary injection pass to check if +## the consume is connected to the call on all control-flow paths and only +## then omit the temporary. A clean solution that introduces no duplication of +## logic would be to use the ``ControlFlowGraph`` for this, but it is not yet +## available at that point. +## +## #2, #3, and #4 are variations of the same problem. Consume-argument handling +## happens concurrently to destructor injection and a communication channel +## between the two would be required in order to notify the destructor +## injection pass about the introduced temporaries. + +# XXX: there exists an effect-related problem with the lifetime-tracking hooks +# (i.e. ``=copy``, ``=sink``, ``=destroy``). The assignment rewriting and, +# to some degree, the destructor injection can be seen as a +# refinement/expansion/lowering and should thus not introduce (observable) +# side-effects (mutation of global state, exceptional control-flow, etc.) -- +# it also violates the MIR specification. All three hooks are currently +# allowed to have side-effects, which violates the aforementioned rules. +# It also causes the concrete issue of cyclic dependencies: for example, +# the move analyser uses data-flow analysis (which requires a control-flow +# graph) in order to decide where to move and where to copy. If whether a +# copy or move is used affects the control-flow graph, the move analyser +# depends on its own output, which while possible to make work, would +# likely introduce a large amount of complexity. +# There are two possible solutions: +# 1. disallow lifetime-tracking hooks from having any side-effects +# 2. at least for the ``=copy`` and ``=sink`` hooks, each assignment +# could be said to have the union of the effects from both hooks. +# Those can be computed when generating the MIR code, as types and +# their type-bound operations are already figured out at that point. +# It's more complicated for ``=destroy`` hooks, since they are +# injected rather than being the result of an expansion. The current +# plan is to introduce the MIR concept of dedicated "scope finalizers", +# which could be used to attach the effects gathered from all possible +# destructor calls to + +# XXX: not being able to rewrite an assignment into a call to the copy hook +# because it is disabled is a semantic error, meaning that it should +# be detected and reported during semantic analysis, not as part of +# mid-end processing. Implementing this is not easily possible, however, +# as it would either require duplicating the logic from ``analysis.nim`` +# for ``PNode`` AST (this is non-trivial) or somehow running the analysis +# part of this module at the end of the second semantic pass +# (``sempass2``). import std/[ - intsets, + algorithm, + hashes, + packedsets, strtabs, - strutils, tables ], compiler/ast/[ ast, - astalgo, - renderer, - types, - typesrenderer, - idents, lineinfos, + types + ], + compiler/mir/[ + analysis, + astgen, + mirchangesets, + mirconstr, + mirtrees, + sourcemaps ], compiler/modules/[ magicsys, modulegraphs ], compiler/front/[ - msgs, - options + options, + msgs ], compiler/sem/[ - dfa, - lowerings, - parampatterns, - sighashes, + aliasanalysis, liftdestructors, - optimizer + mirexec, + sighashes + ], + compiler/utils/[ + cursors, + idioms ] -from std/options as std_options import some, none - # xxx: reports are a code smell meaning data types are misplaced -from compiler/ast/reports_sem import SemReport, - reportAst, - reportSem +from compiler/ast/reports_sem import SemReport from compiler/ast/report_enums import ReportKind -from compiler/ast/trees import exprStructuralEquivalent, getRoot +from compiler/sem/semdata import makeVarType type - Con = object - owner: PSym - g: ControlFlowGraph + AnalyseCtx = object + cfg: ControlFlowGraph graph: ModuleGraph - inLoop, inLoopCond: int - uninit: IntSet # set of uninit'ed vars - uninitComputed: bool - idgen: IdGenerator - - Scope = object # we do scope-based memory management. - # a scope is comparable to an nkStmtListExpr like - # (try: statements; dest = y(); finally: destructors(); dest) - vars: seq[PSym] - wasMoved: seq[PNode] - final: seq[PNode] # finally section - needsTry: bool - parent: ptr Scope - - ProcessMode = enum - normal - consumed - sinkArg - -const toDebug {.strdefine.} = "" -when toDebug.len > 0: - var shouldDebug = false - -template dbg(body) = - when toDebug.len > 0: - if shouldDebug: - body - -proc hasDestructor(c: Con; t: PType): bool {.inline.} = - result = ast.hasDestructor(t) - when toDebug.len > 0: - # for more effective debugging - if not result and c.graph.config.selectedGC in {gcArc, gcOrc}: - assert(not containsGarbageCollectedRef(t)) - -proc getTemp(c: var Con; s: var Scope; typ: PType; info: TLineInfo): PNode = - let sym = newSym(skTemp, getIdent(c.graph.cache, ":tmpD"), nextSymId c.idgen, c.owner, info) - sym.typ = typ - s.vars.add(sym) - result = newSymNode(sym) - -proc nestedScope(parent: var Scope): Scope = - Scope(vars: @[], wasMoved: @[], final: @[], needsTry: false, parent: addr(parent)) - -proc p(n: PNode; c: var Con; s: var Scope; mode: ProcessMode): PNode -proc moveOrCopy(dest, ri: PNode; c: var Con; s: var Scope; isDecl = false): PNode - -import sets, hashes - -proc hash(n: PNode): Hash = hash(cast[pointer](n)) - -proc aliasesCached(cache: var Table[(PNode, PNode), AliasKind], obj, field: PNode): AliasKind = - let key = (obj, field) - if not cache.hasKey(key): - cache[key] = aliases(obj, field) - cache[key] -type - State = ref object - lastReads: IntSet - potentialLastReads: IntSet - notLastReads: IntSet - alreadySeen: HashSet[PNode] - -proc preprocessCfg(cfg: var ControlFlowGraph) = - for i in 0.. cfg.len: - cfg[i].dest = cfg.len - i - -proc mergeStates(a: var State, b: sink State) = - # Inplace for performance: - # lastReads = a.lastReads + b.lastReads - # potentialLastReads = (a.potentialLastReads + b.potentialLastReads) - (a.notLastReads + b.notLastReads) - # notLastReads = a.notLastReads + b.notLastReads - # alreadySeen = a.alreadySeen + b.alreadySeen - # b is never nil - if a == nil: - a = b - else: - a.lastReads.incl b.lastReads - a.potentialLastReads.incl b.potentialLastReads - a.potentialLastReads.excl a.notLastReads - a.potentialLastReads.excl b.notLastReads - a.notLastReads.incl b.notLastReads - a.alreadySeen.incl b.alreadySeen - -proc computeLastReadsAndFirstWrites(cfg: ControlFlowGraph) = - var cache = initTable[(PNode, PNode), AliasKind]() - template aliasesCached(obj, field: PNode): AliasKind = - aliasesCached(cache, obj, field) - - var cfg = cfg - preprocessCfg(cfg) - - var states = newSeq[State](cfg.len + 1) - states[0] = State() - - for pc in 0.. sink 's'. - state.lastReads.incl r - state.potentialLastReads.excl r - elif cfg[r].n.aliasesCached(cfg[pc].n) != no: - # only partially writes to 's' --> can't sink 's', so this def reads 's' - # or maybe writes to 's' --> can't sink 's' - cfg[r].n.comment = '\n' & $pc - state.potentialLastReads.excl r - state.notLastReads.incl r - - var alreadySeenThisNode = false - for s in state.alreadySeen: - if cfg[pc].n.aliasesCached(s) != no or s.aliasesCached(cfg[pc].n) != no: - alreadySeenThisNode = true; break - if alreadySeenThisNode: cfg[pc].n.flags.excl nfFirstWrite - else: cfg[pc].n.flags.incl nfFirstWrite - - state.alreadySeen.incl cfg[pc].n - - mergeStates(states[pc + 1], move(states[pc])) - of use: - var potentialLastReadsCopy = state.potentialLastReads - for r in potentialLastReadsCopy: - if cfg[pc].n.aliasesCached(cfg[r].n) != no or cfg[r].n.aliasesCached(cfg[pc].n) != no: - cfg[r].n.comment = '\n' & $pc - state.potentialLastReads.excl r - state.notLastReads.incl r - - state.potentialLastReads.incl pc - - state.alreadySeen.incl cfg[pc].n - - mergeStates(states[pc + 1], move(states[pc])) - of goto: - mergeStates(states[pc + cfg[pc].dest], move(states[pc])) - of fork: - var copy = State() - copy[] = states[pc][] - mergeStates(states[pc + cfg[pc].dest], copy) - mergeStates(states[pc + 1], move(states[pc])) - - let lastReads = (states[^1].lastReads + states[^1].potentialLastReads) - states[^1].notLastReads - var lastReadTable: Table[PNode, seq[int]] - for position, node in cfg: - if node.kind == use: - lastReadTable.mgetOrPut(node.n, @[]).add position - for node, positions in lastReadTable: - block checkIfAllPosLastRead: - for p in positions: - if p notin lastReads: break checkIfAllPosLastRead - node.flags.incl nfLastRead - -proc isLastRead(n: PNode; c: var Con): bool = - let m = dfa.skipConvDfa(n) - (m.kind == nkSym and sfSingleUsedTemp in m.sym.flags) or nfLastRead in m.flags - -proc isFirstWrite(n: PNode; c: var Con): bool = - let m = dfa.skipConvDfa(n) - nfFirstWrite in m.flags - -proc initialized(code: ControlFlowGraph; pc: int, - init, uninit: var IntSet; until: int): int = - ## Computes the set of definitely initialized variables across all code paths - ## as an IntSet of IDs. - var pc = pc - while pc < code.len: - case code[pc].kind - of goto: - pc += code[pc].dest - of fork: - var initA = initIntSet() - var initB = initIntSet() - var variantA = pc + 1 - var variantB = pc + code[pc].dest - while variantA != variantB: - if max(variantA, variantB) > until: - break - if variantA < variantB: - variantA = initialized(code, variantA, initA, uninit, min(variantB, until)) + EntityName = object + ## A unique identifier for an entity in the context of a ``MirTree``, + ## which is meant to be used as a ``Table`` or ``HashSet`` key. + ## + ## Internally, two integers are used: the first integer is the integer + ## value of a name's node kind, while what the second integer represents + ## depends on the entity kind. + a: array[2, int] + + EntityInfo = object + def: NodePosition ## the position of the 'def' for the entity + scope: Slice[NodePosition] ## the scope the entity is defined in + + EntityDict = Table[EntityName, EntityInfo] + ## Entity dictionary. Stores all entities relevant to destructor + ## injection and the move analyser + + AnalysisResults = object + ## Bundled-up immutable state needed for assignment rewriting. Since + ## they're immutable, ``Cursor``s are used in order to not copy + # XXX: ideally, views types (i.e. ``lent``) would be used here + v: Cursor[Values] + entities: Cursor[EntityDict] + destroy: Cursor[seq[(NodePosition, bool)]] + + LocalDiagKind = enum + ldkPassCopyToSink ## a copy is introduced in a consume context + ldkUnavailableTypeBound ## a type-bound operator is requested but not + ## available + + LocalDiag = object + ## A temporary diagnostic representation that is later turned into a + ## ``SemReport`` + pos: NodePosition ## the location of the report + case kind: LocalDiagKind + of ldkUnavailableTypeBound: + op: TTypeAttachedOp + of ldkPassCopyToSink: + discard + + Lvalue = distinct OpValue + ## An ``OpValue`` that names a location + +const + skipAliases = {tyGenericInst, tyAlias, tySink} + ## the set of types to not consider when looking up a type-bound operator + +iterator ritems[T](x: openArray[T]): lent T = + ## Iterates and yields the items from the container `x` in reverse + var i = x.high + while i >= 0: + yield x[i] + dec i + +func conv[A, B](x: Slice[A], _: typedesc[B]): Slice[B] {.inline.} = + B(x.a) .. B(x.b) + +func hash(x: EntityName): int = + result = 0 !& x.a[0] !& x.a[1] + result = !$result + +func toName(n: MirNode): EntityName = + ## Creates a unique representation for the entity the name node `n` + ## references + result.a[0] = n.kind.int + result.a[1] = + case n.kind + of SymbolLike: n.sym.id + of mnkTemp: n.temp.int + else: unreachable(n.kind) + +func getAliveRange(entities: EntityDict, name: EntityName, exists: var bool + ): Slice[NodePosition] = + ## Returns the maximum lifespan of the entity with the given `name`. + ## `exists` is used to output whether there exists an entity with the given + ## `name` in `entities` + let info = + entities.getOrDefault(name, EntityInfo(scope: conv(1..0, NodePosition))) + + exists = info.scope.a <= info.scope.b + if exists: + # the entity is not alive before its definition, hence the usage of + # ``info.def`` for the start and not ``info.scope.b`` + result = info.def .. info.scope.b + +func paramType(p: PSym, i: Natural): PType = + assert p.kind in routineKinds + p.typ[1 + i] + +proc getVoidType(g: ModuleGraph): PType {.inline.} = + g.getSysType(unknownLineInfo, tyVoid) + +proc getOp(g: ModuleGraph, t: PType, kind: TTypeAttachedOp): PSym = + result = getAttachedOp(g, t, kind) + if result == nil or result.ast.isGenericRoutine: + # give up and find the canonical type instead: + let h = sighashes.hashType(t, {CoType, CoDistinct}) + let canon = g.canonTypes.getOrDefault(h) + if canon != nil: + result = getAttachedOp(g, canon, kind) + +proc needsMarkCyclic(graph: ModuleGraph, typ: PType): bool = + # skip distinct types too so that a ``distinct ref`` also gets marked as + # cyclic at runtime + graph.config.selectedGC == gcOrc and cyclicType(typ.skipTypes(skipAliases + {tyDistinct})) + +func isNamed(tree: MirTree, v: Values, val: OpValue): bool = + ## Returns whether `val` is an lvalue that names a location derived from + ## a named entity. For example, ``local.a.b`` is such a location. + tree[v.getRoot(val)].kind in {mnkLocal, mnkGlobal, mnkParam, mnkTemp} + +func getDefEntity(tree: MirTree, n: NodePosition): NodePosition = + assert tree[n].kind in DefNodes + n + 1 + +func skipTag(tree: MirTree, n: Operation): OpValue = + ## Returns the input to the tag operation `n` + assert tree[n].kind == mnkTag + unaryOperand(tree, n) + +# --------- compute routines --------------- + +iterator nodesWithScope(tree: MirTree): (NodePosition, lent MirNode, Slice[NodePosition]) = + ## Iterates over all nodes in `tree` and yields them together with the span + ## of their enclosing scope + var scopeStack: seq[Slice[NodePosition]] + # the logic relies on the assumption that there exists a scope around + # every 'def' + + # XXX: profiling showed that a significant amount of time is spent in + # ``computeSpan`` and adding elements to the `scopeStack`. An approach + # where a scope's span is only computed when needed might be better + for i, n in tree.pairs: + case n.kind + of mnkScope: + # start a new scope. The start and end node/token are not included in + # the span + let span = computeSpan(tree, i) + scopeStack.add (span.a + 1)..(span.b - 1) + of mnkEnd: + if n.start == mnkScope: + # leave the current scope: + scopeStack.setLen(scopeStack.len - 1) + + else: + yield (i, n, scopeStack[^1]) + + #result.pos = p + +func isTopLevel(tree: MirTree, scope: Slice[NodePosition]): bool = + # XXX: this relies on an implementation detail of how scopes are + # emitted. The better solution is to not emit 'def's for globals + # at module-scope in the first place. Those should be stored as + # module attachments, also simplifying the destructor injection for + # them + scope.a == NodePosition(1) + +func initEntityDict(tree: MirTree, owner: PSym): EntityDict = + ## Collects the names of all analysable locations relevant to destructor + ## injection and the move analyser. This includes: locals, temporaries, sink + ## parameters and, with some restrictions, globals. + ## + ## Only owning locations that store values representing *resources* are + ## relevant, so locations with no destructor for their type (not a resource) + ## and cursor locations (non-owning) are not include in the dictionary. + for i, n, scope in nodesWithScope(tree): + case n.kind + of mnkDef, mnkDefUnpack: + let entity = tree[getDefEntity(tree, i)] + + let t = + case entity.kind + of mnkParam: + assert isSinkTypeForParam(entity.sym.typ) + entity.sym.typ + of mnkLocal: + assert sfCursor notin entity.sym.flags + entity.sym.typ + of mnkTemp: + entity.typ + of mnkGlobal: + if sfThread in entity.sym.flags or isTopLevel(tree, scope) or + owner.kind != skModule: + # we can't reason about: + # - threadvars: because they're not destroyed at the module level + # - top-level globals: because we don't have access to the full + # top-level code of a module, and they might also be exported + # - procedure-level globals: they're destroyed at the end of their + # owning module to which we have no access to here + # XXX: none of those should reach here in the first place + # (i.e. no 'def' should be emitted for them) + nil + else: + entity.sym.typ + else: - variantB = initialized(code, variantB, initB, uninit, min(variantA, until)) - pc = min(variantA, variantB) - # we add vars if they are in both branches: - for v in initA: - if v in initB: - init.incl v - of use: - let v = code[pc].n.sym - if v.kind != skParam and v.id notin init: - # attempt to read an uninit'ed variable - uninit.incl v.id - inc pc - of def: - let v = code[pc].n.sym - init.incl v.id - inc pc - return pc - -proc isCursor(n: PNode): bool = - case n.kind - of nkSym: - sfCursor in n.sym.flags - of nkDotExpr: - isCursor(n[1]) - of nkCheckedFieldExpr: - isCursor(n[0]) - else: - false - -template isUnpackedTuple(n: PNode): bool = - ## we move out all elements of unpacked tuples, - ## hence unpacked tuples themselves don't need to be destroyed - (n.kind == nkSym and n.sym.kind == skTemp and n.sym.typ.kind == tyTuple) - -proc checkForErrorPragma(c: Con; t: PType; ri: PNode; opname: string) = - var rep = SemReport( - kind: rsemUnavailableTypeBound, - typ: t, - str: opname, - ast: ri, - sym: c.owner - ) + nil # not a location (e.g. a procedure) + + if t != nil and hasDestructor(t): + let re = toName(entity) + # XXX: because of an issue with semantic analysis (see + # ``tests/arc/tstrformat.nim``) it can happen that the same entity + # is defined multiple times. In order to not silently generate + # incorrect code, ``doAssert`` is misused to report an internal + # compiler error here. Using the proper facility would require + # access to a ``ConfigRef`` (which we neither do nor should have) + doAssert re notin result, "entity appears in a 'def' multiple times" + result[re] = EntityInfo(def: i, scope: scope) - if (opname == "=" or opname == "=copy") and ri != nil: - if ri.comment.startsWith('\n'): - rep.missingTypeBoundElaboration.anotherRead = some( - c.g[parseInt(ri.comment[1..^1])].n.info ) + else: + discard + +func computeOwnership(tree: MirTree, cfg: ControlFlowGraph, values: Values, + entities: EntityDict, lval: LvalueExpr, pos: NodePosition + ): Owned = + case tree[lval.root].kind + of mnkObjConstr, mnkConstr, mnkMagic, mnkCall: + # all values derived from a constructor-operation that reach here are + # guaranteed to own (see ``analyser.computeValuesAndEffects``). + Owned.yes + of mnkLocal, mnkParam, mnkGlobal, mnkTemp: + # only entities that are relevant for destructor injection have an entry in + # `entities`. Those that don't also can't be consumed (because we either + # can't reason about them or they're non-owning locations), so values + # derived from them are treated as non-owning + # TODO: this currently also includes the ``result`` variable. It's possible + # to analyse it too -- we just need to make sure to treat an + # otherwise last-read as not a last-read if it is connected to a + # procedure exit. A slightly different approach would be to add a + # pseudo-use at the end of the body and make all procedure exits + # visit it first + var exists = false + let aliveRange = entities.getAliveRange(toName(tree[lval.root]), exists) + + if exists and isLastRead(tree, cfg, values, aliveRange, lval, pos): + Owned.yes + else: + Owned.no + else: + unreachable() + +func solveOwnership(tree: MirTree, cfg: ControlFlowGraph, values: var Values, + entities: EntityDict) = + ## Ensures that the ownership status of values used in a consume context is + ## certain (i.e. either owned or not owned) + # we're only interested about the ownership status of values used in a consume + # context + for i, n in tree.pairs: + case n.kind + of ConsumeCtx: + let opr = unaryOperand(tree, Operation i) - elif ri.kind == nkSym and - ri.sym.kind == skParam and - not isSinkType(ri.sym.typ): - rep.missingTypeBoundElaboration.tryMakeSinkParam = true + if values.owned(opr) in {unknown, weak} and hasDestructor(tree[opr].typ): + # unresolved onwership status and has a destructors + values.setOwned(opr): + computeOwnership(tree, cfg, values, entities, + values.toLvalue(opr), i) - localReport(c.graph.config, ri.info, rep) + else: + discard "nothing to do" + +type DestructionMode = enum + demNone ## location doesn't need to be destroyed because it contains no + ## value when control-flow exits the enclosing scope + demNormal ## the location contains a value when the scope is exited via + ## structured control-flow + demFinally ## the location contains a value when the scope is exited via + ## unstructured control-flow + +func requiresDestruction(tree: MirTree, cfg: ControlFlowGraph, values: Values, + span: Slice[NodePosition], def: Operation, + entity: MirNode): DestructionMode = + template computeAlive(loc: untyped, hasInit: bool, op: untyped): untyped = + computeAlive(tree, cfg, values, span, loc, hasInit, op) + + # XXX: a 'def' is not an operation. It defines an entity, optionally with a + # starting value, but doesn't produce a value itself + + let r = + case entity.kind + of mnkParam, mnkLocal, mnkGlobal: + # ``sink`` parameter locations always start with an initial value + computeAlive(entity.sym, (entity.kind == mnkParam or hasInput(tree, def)), + computeAliveOp[PSym]) + + of mnkTemp: + # unpacked tuples don't need to be destroyed because all elements are + # moved out of them + if tree[def].kind != mnkDefUnpack: + computeAlive(entity.temp, hasInput(tree, def), + computeAliveOp[TempId]) + else: + (alive: false, escapes: false) -proc makePtrType(c: var Con, baseType: PType): PType = - result = newType(tyPtr, nextTypeId c.idgen, c.owner) - addSonSkipIntLit(result, baseType, c.idgen) + else: + unreachable(entity.kind) -proc genOp(c: var Con; op: PSym; dest: PNode): PNode = - let addrExp = newTreeIT(nkHiddenAddr, dest.info, makePtrType(c, dest.typ)): dest - result = newTree(nkCall, newSymNode(op), addrExp) + result = + if r.escapes: demFinally + elif r.alive: demNormal + else: demNone + +func computeDestructors(tree: MirTree, cfg: ControlFlowGraph, values: Values, + entities: EntityDict): seq[(NodePosition, bool)] = + ## Computes and collects which locations present in `entities` need to be + ## destroyed at the exit of their enclosing scope in order to prevent the + ## values they still store from staying alive. + ## + ## Special handling is required if the scope is exited via unstructured + ## control-flow while the location is still alive (its value is then said + ## to "escape") + for _, info in entities.pairs: + let + def = info.def ## the position of the entity's definition + start = sibling(tree, def) + entity = tree[getDefEntity(tree, def)] + scope = start .. info.scope.b + + if entity.kind == mnkGlobal and + doesGlobalEscape(tree, scope, start, entity.sym): + # TODO: handle escaping globals. Either report a warning, an error, or + # defer destruction of the global to the end of the program + discard + + case requiresDestruction(tree, cfg, values, scope, Operation def, entity) + of demNormal: + result.add (def, false) + of demFinally: + result.add (def, true) + of demNone: + discard + +# --------- analysis routines -------------- + +func isAlive(tree: MirTree, cfg: ControlFlowGraph, v: Values, + entities: EntityDict, val: Lvalue): bool = + ## Computes if `val` refers to a location that contains a value at the point + ## in time where `val` is computed + let + pos = NodePosition(val) + root = v.getRoot(OpValue val) + + case tree[root].kind + of mnkLocal, mnkParam, mnkGlobal, mnkTemp: + let scope = + # XXX: the way the ``result`` variable is detected here is a hack. It + # should be treated as any other local in the context of the MIR + if tree[root].kind in SymbolLike and tree[root].sym.kind == skResult: + NodePosition(0) .. NodePosition(tree.high) + else: + var exists: bool + let s = entities.getAliveRange(toName(tree[root]), exists) + if exists: s + else: return true # not something we can analyse -> assume alive -proc genOp(c: var Con; t: PType; kind: TTypeAttachedOp; dest, ri: PNode): PNode = - var op = getAttachedOp(c.graph, t, kind) - if op == nil or op.ast.isGenericRoutine: - # give up and find the canonical type instead: - let h = sighashes.hashType(t, {CoType, CoDistinct}) - let canon = c.graph.canonTypes.getOrDefault(h) - if canon != nil: - op = getAttachedOp(c.graph, canon, kind) - - c.graph.config.internalAssert(op != nil, dest.info): - "internal error: '" & AttachedOpToStr[kind] & - "' operator not found for type " & typeToString(t) - - c.graph.config.internalAssert(not op.ast.isGenericRoutine, dest.info): - "internal error: '" & AttachedOpToStr[kind] & "' operator is generic" - - dbg: - if kind == attachedDestructor: - echo "destructor is ", op.id, " ", op.ast - if sfError in op.flags: checkForErrorPragma(c, t, ri, AttachedOpToStr[kind]) - c.genOp(op, dest) - -proc genDestroy(c: var Con; dest: PNode): PNode = - let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) - result = c.genOp(t, attachedDestructor, dest, nil) - -proc canBeMoved(c: Con; t: PType): bool {.inline.} = - let t = t.skipTypes({tyGenericInst, tyAlias, tySink}) - result = getAttachedOp(c.graph, t, attachedSink) != nil - -proc isNoInit(dest: PNode): bool {.inline.} = - result = dest.kind == nkSym and sfNoInit in dest.sym.flags - -proc genSink(c: var Con; dest, ri: PNode, isDecl = false): PNode = - if (c.inLoopCond == 0 and (isUnpackedTuple(dest) or isDecl or - (isAnalysableFieldAccess(dest, c.owner) and isFirstWrite(dest, c)))) or - isNoInit(dest): - # optimize sink call into a bitwise memcopy - result = newTree(nkFastAsgn, dest, ri) + isAlive(tree, cfg, v, scope, (NodePosition root, pos), pos) else: - let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) - if getAttachedOp(c.graph, t, attachedSink) != nil: - result = c.genOp(t, attachedSink, dest, ri) - result.add ri - else: - # the default is to use combination of `=destroy(dest)` and - # and copyMem(dest, source). This is efficient. - result = newTree(nkStmtList, c.genDestroy(dest), newTree(nkFastAsgn, dest, ri)) + # something that we can't analyse (e.g. a dereferenced pointer). We have + # to be conservative and assume that the location the lvalue names already + # stores a value + true + +func needsReset(tree: MirTree, cfg: ControlFlowGraph, ar: AnalysisResults, + src: Lvalue): bool = + ## Computes whether a reset needs to be injected for `src` in order to + ## prevent the current value the underlying location contains from being + ## observed. + ## + ## This is relevant for when ownership of a value is transferred, as the + ## transferral doesn't imply a change to neither the previous owner + ## (location) nor the value itself. As long as the location is not observed + ## to still contain the value it now no longer owns, this is not a problem. + ## If it can't be proven that the unowned value is observed (which could + ## cause problems like, for example, double-frees), the location is + ## explicitly reset (i.e. the value removed from it). + let root = ar.v[].getRoot(OpValue src) + # XXX: the way the ``result`` variable is detected here is a hack. It + # should be treated as any other local in the context of MIR. The + # fact that the result variable is potentially used outside the + # procedure's body should be encoded by inserting a special 'use' + # operation that has a control-flow dependency on *all* other + # operations + if tree[root].kind in SymbolLike and tree[root].sym.kind == skResult: + return true -proc isCriticalLink(dest: PNode): bool {.inline.} = - #[ - Lins's idea that only "critical" links can introduce a cycle. This is - critical for the performance gurantees that we strive for: If you - traverse a data structure, no tracing will be performed at all. - ORC is about this promise: The GC only touches the memory that the - mutator touches too. + var exists = false + let aliveRange = ar.entities[].getAliveRange(toName(tree[root]), exists) - These constructs cannot possibly create cycles:: + if not exists: + # the entity needs can't be reasoned about in the current context -> assume + # that it needs to be reset + return true - local = ... + let res = isLastWrite(tree, cfg, ar.v[], aliveRange, + toLvalue(ar.v[], OpValue src), NodePosition(src) + 1) - new(x) - dest = ObjectConstructor(field: noalias(dest)) + if res.result: + if res.escapes or res.exits: + let def = aliveRange.a + assert tree[def].kind in DefNodes - But since 'ObjectConstructor' is already moved into 'dest' all we really have - to look for is assignments to local variables. - ]# - result = dest.kind != nkSym + # check if there exists a destructor call that would observe the + # location's value: + for it in ar.destroy[].items: + if def == it[0]: + if (it[1] and res.escapes) or res.exits: + # there exists a destructor call for the location -> the current + # value is observed + return true -proc finishCopy(c: var Con; result, dest: PNode; isFromSink: bool) = - if c.graph.config.selectedGC == gcOrc: - let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct}) - if cyclicType(t): - result.add boolLit(c.graph, result.info, isFromSink or isCriticalLink(dest)) + # no need to continue searching + break -proc genMarkCyclic(c: Con; result, dest: PNode) = - if c.graph.config.selectedGC == gcOrc: - let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink, tyDistinct}) - if cyclicType(t): - if t.kind == tyRef: - result.add callCodegenProc(c.graph, "nimMarkCyclic", dest.info, dest) - else: - let xenv = genBuiltin(c.graph, c.idgen, mAccessEnv, "accessEnv", dest) - xenv.typ = getSysType(c.graph, dest.info, tyPointer) - result.add callCodegenProc(c.graph, "nimMarkCyclic", dest.info, xenv) - -proc genCopyNoCheck(c: var Con; dest, ri: PNode): PNode = - let t = dest.typ.skipTypes({tyGenericInst, tyAlias, tySink}) - result = c.genOp(t, attachedAsgn, dest, ri) - assert ri.typ != nil - -proc genCopy(c: var Con; dest, ri: PNode): PNode = - let t = dest.typ - result = c.genCopyNoCheck(dest, ri) - assert ri.typ != nil - -proc genDiscriminantAsgn(c: var Con; s: var Scope; n: PNode): PNode = - # discriminator is ordinal value that doesn't need sink destroy - # but fields within active case branch might need destruction - - # tmp to support self assignments - let tmp = c.getTemp(s, n[1].typ, n.info) - - result = newTree(nkStmtList) - result.add newTree(nkFastAsgn, tmp, p(n[1], c, s, consumed)) - result.add p(n[0], c, s, normal) - - let le = p(n[0], c, s, normal) - let leDotExpr = if le.kind == nkCheckedFieldExpr: le[0] else: le - let objType = leDotExpr[0].typ - - if hasDestructor(c, objType): - # generate: if le != tmp: `=destroy`(le) - let branchDestructor = produceDestructorForDiscriminator(c.graph, objType, leDotExpr[1].sym, n.info, c.idgen) - let cond = newTreeIT(nkInfix, n.info, getSysType(c.graph, unknownLineInfo, tyBool)): - [newSymNode(getMagicEqSymForType(c.graph, le.typ, n.info)), le, tmp] - let notExpr = newTreeIT(nkPrefix, n.info, getSysType(c.graph, unknownLineInfo, tyBool)): - [newSymNode(createMagic(c.graph, c.idgen, "not", mNot)), cond] - result.add newTree(nkIfStmt, newTree(nkElifBranch, notExpr, c.genOp(branchDestructor, le))) - result.add newTree(nkFastAsgn, le, tmp) - -proc genWasMoved(c: var Con, n: PNode): PNode = - result = newTreeI(nkCall, n.info): - [newSymNode(createMagic(c.graph, c.idgen, "wasMoved", mWasMoved)), - copyTree(n)] #mWasMoved does not take the address - #if n.kind != nkSym: - # message(c.graph.config, n.info, warnUser, "wasMoved(" & $n & ")") - -proc genDefaultCall(t: PType; c: Con; info: TLineInfo): PNode = - result = newTreeIT(nkCall, info, t): - newSymNode(createMagic(c.graph, c.idgen, "default", mDefault)) - -proc destructiveMoveVar(n: PNode; c: var Con; s: var Scope): PNode = - # generate: (let tmp = v; reset(v); tmp) - if not hasDestructor(c, n.typ): - assert n.kind != nkSym or not hasDestructor(c, n.sym.typ) - result = copyTree(n) + # no mutation nor destructor call observes the current value -> no reset + # is needed + result = false else: - var temp = newSym(skLet, getIdent(c.graph.cache, "blitTmp"), nextSymId c.idgen, c.owner, n.info) - temp.typ = n.typ - let tempAsNode = newSymNode(temp) - - var vpart = newTreeI(nkIdentDefs, tempAsNode.info): - [tempAsNode, newNodeI(nkEmpty, tempAsNode.info), n] - - let v = newTreeI(nkLetSection, n.info): vpart - - let nn = skipConv(n) - result = newNodeIT(nkStmtListExpr, n.info, n.typ) - result.add v - c.genMarkCyclic(result, nn) - result.add c.genWasMoved(nn) - result.add tempAsNode - -proc isCapturedVar(n: PNode): bool = - let root = getRoot(n) - if root != nil: result = root.name.s[0] == ':' - -proc passCopyToSink(n: PNode; c: var Con; s: var Scope): PNode = - result = newNodeIT(nkStmtListExpr, n.info, n.typ) - let tmp = c.getTemp(s, n.typ, n.info) - if hasDestructor(c, n.typ): - result.add c.genWasMoved(tmp) - var m = c.genCopy(tmp, n) - m.add p(n, c, s, normal) - c.finishCopy(m, n, isFromSink = true) - result.add m - if isLValue(n) and - not isCapturedVar(n) and - n.typ.skipTypes(abstractInst).kind != tyRef: - - localReport(c.graph.config, n, reportSem rsemCopiesToSink) - + # the presence of the value is observed -> a reset is required + result = true + +# ------- code generation routines -------- + +# XXX: there are two problems here: +# 1. the code is unnecessarily complex, manual, and error-prone +# 2. the generated code is invalid, due to the missing type +# information on some nodes +# The second one only causes no issues because there are no further +# passes happening past ``rewriteAssignments``, ``astgen`` is not as +# strict as it should be, and the nodes in question don't translate +# to ``PNode``s. +# +# The way ``mirgen`` emits nodes is much simpler, easier to follow, and +# also less error prone. It also enforces that nodes are correctly typed +# (in the sense that types are present, not that they're valid). The +# attempt at generalizing the builder routines (i.e. ``mirconstr``) is +# not developed far enough yet to be of much use here. +# +# There is also the problem that the order in which it makes sense to +# generate node sequences is in many cases not the order in which they +# need to appear in the final stream. This causes friction with the +# ``Changesets`` API and is the cause of the rather nested and awkward +# code that is currently present. Some way of passing abstract node +# sequences around / forwarding them would help here. One approach could +# be to represent them as closures (lazy generation, dynamic dispatch, +# the environment creation overhead might be significant), another one to +# emit them into a staging buffer first and then pass a handle to said +# span around. I'd favor the latter. + +proc compilerProc(graph: ModuleGraph, name: string): MirNode = + MirNode(kind: mnkProc, sym: getCompilerProc(graph, name)) + +func undoConversions(buf: var MirNodeSeq, tree: MirTree, src: OpValue) = + ## When performing a destructive move for ``ref`` values, it's possible for + ## the source to be an lvalue conversion -- in that case, we want pass the + ## uncoverted root location to the ``wasMoved`` operation. To do so, we apply + ## the conversions in *reverse*. ``astgen`` detects this pattern and removes + ## the conversions that cancel each other out. + var p = NodePosition(src) + while tree[p].kind in {mnkStdConv, mnkConv}: + p = previous(tree, p) + buf.add MirNode(kind: mnkConv, typ: tree[p].typ) + +func genDefTemp(buf: var MirNodeSeq, id: TempId, typ: PType) = + buf.subTree MirNode(kind: mnkDef): + buf.add MirNode(kind: mnkTemp, typ: typ, temp: id) + +template genWasMoved(buf: var MirNodeSeq, graph: ModuleGraph, body: untyped) = + argBlock(buf): + body + buf.add MirNode(kind: mnkTag, effect: ekKill) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkMagic, + typ: graph.getSysType(unknownLineInfo, tyVoid), + magic: mWasMoved) + buf.add MirNode(kind: mnkVoid) + +proc genDestroy(buf: var MirNodeSeq, graph: ModuleGraph, t: PType, + target: sink MirNode) = + let destr = getOp(graph, t, attachedDestructor) + + argBlock(buf): + buf.add MirNode(kind: mnkProc, sym: destr) + buf.add MirNode(kind: mnkArg) + buf.add target + buf.add MirNode(kind: mnkTag, typ: destr.paramType(0), effect: ekMutate) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkCall, typ: getVoidType(graph)) + buf.add MirNode(kind: mnkVoid) + +proc genInjectedSink(buf: var MirNodeSeq, graph: ModuleGraph, t: PType) = + ## Generates either a call to the ``=sink`` hook, or (if none exists), a + ## sink emulated via a destructor-call + bitwise-copy. The output is meant + ## to be placed inside a region. + let op = getAttachedOp(graph, t, attachedSink) + if op != nil: + argBlock(buf): + buf.add MirNode(kind: mnkProc, sym: op) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkOpParam, param: 0) + buf.add MirNode(kind: mnkTag, typ: op.paramType(0), effect: ekMutate) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkOpParam, param: 1) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkCall, typ: getVoidType(graph)) + buf.add MirNode(kind: mnkVoid) + else: + # without a sink hook, a ``=destroy`` + blit-copy is used + genDestroy(buf, graph, t, MirNode(kind: mnkOpParam, param: 0)) + + argBlock(buf): + buf.add MirNode(kind: mnkOpParam, param: 0) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkOpParam, param: 1) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkFastAsgn) + +proc genSinkFromTemporary(buf: var MirNodeSeq, graph: ModuleGraph, t: PType, + tmp: TempId) = + ## Similar to ``genInjectedSink`` but generates code for destructively + ## moving a the source operand into a temporary first + let op = getAttachedOp(graph, t, attachedSink) + + buf.add MirNode(kind: mnkOpParam, param: 1) + buf.genDefTemp(tmp, t) + + genWasMoved(buf, graph): + buf.add MirNode(kind: mnkOpParam, param: 1) + + if op != nil: + argBlock(buf): + buf.add MirNode(kind: mnkProc, sym: op) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkOpParam, param: 0) + buf.add MirNode(kind: mnkTag, typ: op.paramType(0), effect: ekMutate) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkTemp, temp: tmp) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkCall, typ: getVoidType(graph)) + buf.add MirNode(kind: mnkVoid) else: - if c.graph.config.selectedGC in {gcArc, gcOrc}: - assert(not containsManagedMemory(n.typ)) + # without a sink hook, a ``=destroy`` + blit-copy is used + genDestroy(buf, graph, t, MirNode(kind: mnkOpParam, param: 0)) + + argBlock(buf): + buf.add MirNode(kind: mnkOpParam, param: 0) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkTemp, temp: tmp) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkFastAsgn) + +proc genCopy(buf: var MirTree, graph: ModuleGraph, t: PType, + dst, src: sink MirNode, maybeCyclic: bool) = + ## Emits a ``=copy`` hook call from with `dst`, `src`, and (if necessary) + ## `maybeCyclic` as the arguments + let op = getOp(graph, t, attachedAsgn) + assert op != nil + + argBlock(buf): + buf.add MirNode(kind: mnkProc, sym: op) + buf.add MirNode(kind: mnkArg) + buf.add dst + buf.add MirNode(kind: mnkTag, typ: op.paramType(0), effect: ekMutate) + buf.add MirNode(kind: mnkArg) + buf.add src + buf.add MirNode(kind: mnkArg) + + if graph.config.selectedGC == gcOrc and + cyclicType(t.skipTypes(skipAliases + {tyDistinct})): + # pass whether the copy can potentially introduce cycles as the third + # parameter: + buf.add MirNode(kind: mnkLiteral, + lit: boolLit(graph, unknownLineInfo, maybeCyclic)) + buf.add MirNode(kind: mnkArg) + + buf.add MirNode(kind: mnkCall, typ: getVoidType(graph)) + buf.add MirNode(kind: mnkVoid) + +proc genMarkCyclic(buf: var MirTree, graph: ModuleGraph, typ: PType, + dest: sink MirNode) = + ## Emits a call to ``nimMarkCyclic`` for `dest` if required by its `typ` + if graph.config.selectedGC == gcOrc: + # also skip distinct types so that a ``distinct ref`` gets marked as + # cyclic too + let t = typ.skipTypes(skipAliases + {tyDistinct}) + if cyclicType(t): + argBlock(buf): + buf.add compilerProc(graph, "nimMarkCyclic") + buf.add MirNode(kind: mnkArg) + + buf.add dest + if t.kind == tyProc: + # a closure. Only the environment needs to be marked as potentially + # cyclic + buf.add MirNode(kind: mnkMagic, typ: getSysType(graph, unknownLineInfo, tyPointer), + magic: mAccessEnv) + + buf.add MirNode(kind: mnkArg) + + buf.add MirNode(kind: mnkCall, typ: getVoidType(graph)) + buf.add MirNode(kind: mnkVoid) + +proc expandAsgn(tree: MirTree, ctx: AnalyseCtx, ar: AnalysisResults, + typ: PType, source: OpValue, asgn: Operation, + c: var Changeset) = + ## Expands an assignment into either a copy or move + let + dest = skipTag(tree, operand(tree, asgn, 0)) + relation = compareLvalues(tree, toLvalue(ar.v[], source), + toLvalue(ar.v[], dest)) + + c.seek(NodePosition asgn) + + if relation.isSame: + # a self-assignment. We can't remove the arg-block (it might have + # side-effects), so the assignment is replaced with a + # no-op instead + c.replaceMulti(buf): + buf.subTree MirNode(kind: mnkRegion): discard + + elif owned(ar.v[], source) == Owned.yes: + # we own the source value -> sink + c.replaceMulti(buf): + let fromLvalue = isNamed(tree, ar.v[], source) + + if tree[asgn].kind != mnkInit and + isAlive(tree, ctx.cfg, ar.v[], ar.entities[], Lvalue dest): + # there already exists a value in the destination location -> use the + # sink operation + buf.subTree MirNode(kind: mnkRegion): + if fromLvalue: + if isAPartOfB(relation) != no: + # this is a potential part-to-whole assignment, e.g.: ``x = x.y``. + # We need to move the source value into a temporary first, as + # ``=sink`` would otherwise destroy ``x`` first, also destroying + # ``x.y`` in the process + genSinkFromTemporary(buf, ctx.graph, typ, c.getTemp()) + else: + genInjectedSink(buf, ctx.graph, typ) - if n.typ.skipTypes(abstractInst).kind in {tyOpenArray, tyVarargs}: - localReport(c.graph.config, n.info, reportAst( - rsemCannotCreateImplicitOpenarray, n)) + if needsReset(tree, ctx.cfg, ar, Lvalue source): + genWasMoved(buf, ctx.graph): + buf.add MirNode(kind: mnkOpParam, param: 1) + undoConversions(buf, tree, source) - result.add newTree(nkAsgn, tmp, p(n, c, s, normal)) - # Since we know somebody will take over the produced copy, there is - # no need to destroy it. - result.add tmp + else: + # the value is only accessible through the source expression, a + # destructive move is not required + genInjectedSink(buf, ctx.graph, typ) -proc isDangerousSeq(t: PType): bool {.inline.} = - let t = t.skipTypes(abstractInst) - result = t.kind == tySequence + else: + # the destination location doesn't contain a value yet (which would + # need to be destroyed first otherwise) -> a bitwise copy can be used + if fromLvalue: + # we don't need to check for part-to-whole assignments here, because + # if the destination location has no value, so don't locations derived + # from it, in which case it doesn't matter when the reset happens + buf.subTree MirNode(kind: mnkRegion): + argBlock(buf): + buf.add MirNode(kind: mnkOpParam, param: 0) + buf.add MirNode(kind: mnkArg) + buf.add MirNode(kind: mnkOpParam, param: 1) + buf.add MirNode(kind: mnkArg) + + buf.add MirNode(kind: mnkFastAsgn) + + # XXX: the reset could be omitted for part-to-whole assignments + if needsReset(tree, ctx.cfg, ar, Lvalue source): + genWasMoved(buf, ctx.graph): + buf.add MirNode(kind: mnkOpParam, param: 1) + undoConversions(buf, tree, source) -proc containsConstSeq(n: PNode): bool = - if n.kind == nkBracket and n.len > 0 and n.typ != nil and isDangerousSeq(n.typ): - return true - result = false - case n.kind - of nkExprEqExpr, nkExprColonExpr, nkHiddenStdConv, nkHiddenSubConv: - result = containsConstSeq(n[1]) - of nkObjConstr, nkClosure: - for i in 1.. 0: - # unpacked tuple needs reset at every loop iteration - res.add newTree(nkFastAsgn, v, genDefaultCall(v.typ, c, v.info)) - elif sfThread notin v.sym.flags and sfCursor notin v.sym.flags: - # do not destroy thread vars for now at all for consistency. - if sfGlobal in v.sym.flags and s.parent == nil: #XXX: Rethink this logic (see tarcmisc.test2) - c.graph.globalDestructors.add c.genDestroy(v) - else: - s.final.add c.genDestroy(v) - -proc processScope(c: var Con; s: var Scope; ret: PNode): PNode = - result = newNodeI(nkStmtList, ret.info) - if s.vars.len > 0: - let varSection = newNodeI(nkVarSection, ret.info) - for tmp in s.vars: - varSection.add newTree(nkIdentDefs, - [newSymNode(tmp), newNodeI(nkEmpty, ret.info), newNodeI(nkEmpty, ret.info)]) - result.add varSection - if s.wasMoved.len > 0 or s.final.len > 0: - let finSection = newNodeI(nkStmtList, ret.info) - for m in s.wasMoved: finSection.add m - for i in countdown(s.final.high, 0): finSection.add s.final[i] - if s.needsTry: - result.add newTryFinally(ret, finSection) - else: - result.add ret - result.add finSection - else: - result.add ret - - if s.parent != nil: s.parent[].needsTry = s.parent[].needsTry or s.needsTry - -template processScopeExpr(c: var Con; s: var Scope; ret: PNode, processCall: untyped): PNode = - assert not ret.typ.isEmptyType - var result = newNodeI(nkStmtListExpr, ret.info) - # There is a possibility to do this check: s.wasMoved.len > 0 or s.final.len > 0 - # later and use it to eliminate the temporary when theres no need for it, but its - # tricky because you would have to intercept moveOrCopy at a certain point - let tmp = c.getTemp(s.parent[], ret.typ, ret.info) - tmp.sym.flags.incl sfSingleUsedTemp - let cpy = if hasDestructor(c, ret.typ): - s.parent[].final.add c.genDestroy(tmp) - moveOrCopy(tmp, ret, c, s, isDecl = true) - else: - newTree(nkFastAsgn, tmp, p(ret, c, s, normal)) - - if s.vars.len > 0: - let varSection = newNodeI(nkVarSection, ret.info) - for tmp in s.vars: - varSection.add newTree(nkIdentDefs, # TODO XXX lowerings.nim???????? - [newSymNode(tmp), newNodeI(nkEmpty, ret.info), newNodeI(nkEmpty, ret.info)]) - result.add varSection - let finSection = newNodeI(nkStmtList, ret.info) - for m in s.wasMoved: finSection.add m - for i in countdown(s.final.high, 0): finSection.add s.final[i] - if s.needsTry: - result.add newTryFinally(newTree(nkStmtListExpr, cpy, processCall(tmp, s.parent[])), finSection) - else: - result.add cpy - result.add finSection - result.add processCall(tmp, s.parent[]) - - if s.parent != nil: s.parent[].needsTry = s.parent[].needsTry or s.needsTry - - result - -template handleNestedTempl(n, processCall: untyped, willProduceStmt = false) = - template maybeVoid(child, s): untyped = - if isEmptyType(child.typ): p(child, c, s, normal) - else: processCall(child, s) - - case n.kind - of nkStmtList, nkStmtListExpr: - # a statement list does not open a new scope - if n.len == 0: return n - result = copyNode(n) - for i in 0.. copy + c.replaceMulti(buf): + # copies to locals or globals can't introduce cyclic structures, as + # those are standlone and not part of any other structure + let maybeCyclic = + tree[dest].kind notin {mnkLocal, mnkTemp, mnkParam, mnkGlobal} + + buf.subTree MirNode(kind: mnkRegion): + genCopy(buf, ctx.graph, typ, + MirNode(kind: mnkOpParam, param: 0), + MirNode(kind: mnkOpParam, param: 1), + maybeCyclic) + +proc consumeArg(tree: MirTree, ctx: AnalyseCtx, ar: AnalysisResults, + typ: PType, src: OpValue, c: var Changeset) = + ## Injects the ownership-transfer related logic needed for when a value is + ## consumed. Since the value is not passed by reference to the ``sink`` + ## parameter, the source location has to be reset, as it'd otherwise contain + ## a value that it no longer owns, while the rest of the code still operates + ## under the assumption that it owns the value. + if isNamed(tree, ar.v[], src): + let + markCyclic = needsMarkCyclic(ctx.graph, typ) + reset = needsReset(tree, ctx.cfg, ar, Lvalue src) + + if not markCyclic and not reset: + # if the value is not something that needs to be marked as cyclic + # nor is the source a location that needs to be reset, we skip + # injecting a temporary and pass the argument directly + return + + let tmp = c.getTemp() + + c.insert(NodeInstance src, buf): + buf.subTree MirNode(kind: mnkRegion): + buf.add MirNode(kind: mnkOpParam, param: 0) + buf.genDefTemp(tmp, typ) + + genMarkCyclic(buf, ctx.graph, typ, MirNode(kind: mnkOpParam, param: 0)) + if reset: + genWasMoved(buf, ctx.graph): + buf.add MirNode(kind: mnkOpParam, param: 0) + undoConversions(buf, tree, src) + + buf.add MirNode(kind: mnkTemp, typ: typ, temp: tmp) + +proc insertCopy(tree: MirTree, graph: ModuleGraph, typ: PType, + maybeCyclic: bool, c: var Changeset) = + ## Generates a call to the `typ`'s ``=copy`` hook that uses the contextual + ## input as the source value + let tmp = c.getTemp() + c.insert(NodeInstance c.position, buf): + buf.subTree MirNode(kind: mnkRegion): + argBlock(buf): discard + buf.add MirNode(kind: mnkMagic, typ: typ, magic: mDefault) + buf.genDefTemp(tmp, typ) + + genCopy(buf, graph, typ, + MirNode(kind: mnkTemp, typ: typ, temp: tmp), + MirNode(kind: mnkOpParam, param: 0), + maybeCyclic) + + buf.add MirNode(kind: mnkTemp, typ: typ, temp: tmp) + +proc rewriteAssignments(tree: MirTree, ctx: AnalyseCtx, ar: AnalysisResults, + diags: var seq[LocalDiag], c: var Changeset) = + ## Rewrites assignments to relevant locations into calls to either the + ## ``=copy`` or ``=sink`` hook (see ``expandAsgn`` for more details), + ## using the previously computed ownership information to decide. + ## + ## Also injects the necessary callsite logic for arguments passed to + ## 'consume' argument sinks. The argument can only be consumed if it is + ## *owned* -- if it isn't, a temporary copy is introduced and passed to the + ## parameter instead. If a copy is required, a diagnostic is added to + ## `msgs`. + ## + ## If the ``=copy`` hook is requested but not available because it's + ## disabled, a diagnostic is added to `msgs`. + # XXX: the procedure does too much and is thus too complex. Splitting the + # 'consume' handling into a separate procedure would makes sense, but + # would likely also be less efficient due to the required extra + # (linear) tree traversal. + # Another possible improvement is moving the actual hook injection + # to a follow-up pass. The pass here would only inject ``mCopyAsgn`` and + # ``mSinkAsgn`` magics, which the aforementioned follow-up pass then + # expands into the hook calls. This would simplify the logic here a bit + for i, n in tree.pairs: case n.kind - of nkBracket, nkTupleConstr, nkClosure, nkCurly: - # Let C(x) be the construction, 'x' the vector of arguments. - # C(x) either owns 'x' or it doesn't. - # If C(x) owns its data, we must consume C(x). - # If it doesn't own the data, it's harmful to destroy it (double frees etc). - # We have the freedom to choose whether it owns it or not so we are smart about it - # and we say, "if passed to a sink we demand C(x) to own its data" - # otherwise we say "C(x) is just some temporary storage, it doesn't own anything, - # don't destroy it" - # but if C(x) is a ref it MUST own its data since we must destroy it - # so then we have no choice but to use 'sinkArg'. - let m = if mode == normal: normal - else: sinkArg - - result = copyTree(n) - for i in ord(n.kind == nkClosure).. 0: - result.add moveOrCopy(v, genDefaultCall(v.typ, c, v.info), c, s, isDecl = v.kind == nkSym) - else: # keep the var but transform 'ri': - var v = copyNode(n) - var itCopy = copyNode(it) - for j in 0.. 0 and isDangerousSeq(ri.typ): - result = c.genCopy(dest, ri) - result.add p(ri, c, s, consumed) - c.finishCopy(result, dest, isFromSink = false) - else: - result = c.genSink(dest, p(ri, c, s, consumed), isDecl) - of nkObjConstr, nkTupleConstr, nkClosure, nkCharLit..nkNilLit: - result = c.genSink(dest, p(ri, c, s, consumed), isDecl) - of nkSym: - if isSinkParam(ri.sym) and isLastRead(ri, c): - # Rule 3: `=sink`(x, z); wasMoved(z) - let snk = c.genSink(dest, ri, isDecl) - result = newTree(nkStmtList, snk, c.genWasMoved(ri)) - elif ri.sym.kind != skParam and ri.sym.owner == c.owner and - isLastRead(ri, c) and canBeMoved(c, dest.typ) and not isCursor(ri): - # Rule 3: `=sink`(x, z); wasMoved(z) - let snk = c.genSink(dest, ri, isDecl) - result = newTree(nkStmtList, snk, c.genWasMoved(ri)) - else: - result = c.genCopy(dest, ri) - result.add p(ri, c, s, consumed) - c.finishCopy(result, dest, isFromSink = false) - of nkHiddenSubConv, nkHiddenStdConv, nkConv, nkObjDownConv, nkObjUpConv, nkCast: - result = c.genSink(dest, p(ri, c, s, sinkArg), isDecl) - of nkStmtListExpr, nkBlockExpr, nkIfExpr, nkCaseStmt, nkTryStmt: - template process(child, s): untyped = moveOrCopy(dest, child, c, s, isDecl) - # We know the result will be a stmt so we use that fact to optimize - handleNestedTempl(ri, process, willProduceStmt = true) - of nkRaiseStmt: - result = pRaiseStmt(ri, c, s) - else: - if isAnalysableFieldAccess(ri, c.owner) and isLastRead(ri, c) and - canBeMoved(c, dest.typ): - # Rule 3: `=sink`(x, z); wasMoved(z) - let snk = c.genSink(dest, ri, isDecl) - result = newTree(nkStmtList, snk, c.genWasMoved(ri)) - else: - result = c.genCopy(dest, ri) - result.add p(ri, c, s, consumed) - c.finishCopy(result, dest, isFromSink = false) - -proc computeUninit(c: var Con) = - if not c.uninitComputed: - c.uninitComputed = true - c.uninit = initIntSet() - var init = initIntSet() - discard initialized(c.g, pc = 0, init, c.uninit, int.high) - -proc injectDefaultCalls(n: PNode, c: var Con) = - case n.kind - of nkVarSection, nkLetSection: - for it in n: - if it.kind == nkIdentDefs and it[^1].kind == nkEmpty: - computeUninit(c) - for j in 0.. 0: - shouldDebug = toDebug == owner.name.s or toDebug == "always" - if sfGeneratedOp in owner.flags or (owner.kind == skIterator and isInlineIterator(owner.typ)): - return n - var c = Con(owner: owner, graph: g, g: constructCfg(owner, n), idgen: idgen) - dbg: - echo "\n### ", owner.name.s, ":\nCFG:" - echoCfg(c.g) - echo n - - computeLastReadsAndFirstWrites(c.g) - - var scope: Scope - let body = p(n, c, scope, normal) - - if owner.kind in {skProc, skFunc, skMethod, skIterator, skConverter}: - let params = owner.typ.n - for i in 1..---------transformed-to--------->" - echo renderTree(result, {renderIds}) + g.globalDestructors.add genDestroy(g, idgen, owner, global) + +proc injectDestructorCalls*(g: ModuleGraph; idgen: IdGenerator; owner: PSym; + tree: var MirTree, sourceMap: var SourceMap) = + ## The ``injectdestructors`` pass entry point. The pass is made up of + ## multiple sub-passes, hence the mutable `tree` and `sourceMap` (as opposed + ## to returning a ``Changeset``). + ## + ## For now, semantic errors and other diagnostics related to lifetime-hook + ## usage are also reported here. + + # the 'def' for non-analysable globals stay the same (i.e. none are added + # or removed), so semantics wise, it doesn't matter at which point we scan + # for them. There is less MIR code before applying all sub-passes than + # there is after, so we perform the scanning first in order to reduce the + # amount of nodes we have to scan + deferGlobalDestructors(tree, g, idgen, owner) + + template apply(c: Changeset) = + ## Applies the changeset to both the + let prepared = prepare(c, sourceMap) + updateSourceMap(sourceMap, prepared) + apply(tree, prepared) + + # apply the first batch of passes: + block: + var changes = initChangeset(tree) + + injectTemporaries(tree, changes) + + # the VM implements branch switching itself - performing the lowering for + # code meant to run in it would be harmful + # FIXME: discriminant assignment lowering also needs to be disabled for + # when generating code running at compile-time (e.g. inside a + # macro) + # XXX: the lowering *is* always necessary, because the destructors for + # fields inside switched-away-from branches won't be called + # otherwise + # TODO: make the branch-switch lowering a separate and standalone pass -- + # it's not directly related to the rest of the processing here + if g.config.backend != backendNimVm: + for i, n in tree.pairs: + if n.kind == mnkSwitch: + changes.seek(i) + changes.replaceMulti(buf): + lowerBranchSwitch(buf, tree, g, idgen, Operation i) + + apply(changes) + + # apply the second batch of passes: + block: + var + changes = initChangeset(tree) + diags: seq[LocalDiag] + + let + actx = AnalyseCtx(graph: g, cfg: computeCfg(tree)) + entities = initEntityDict(tree, owner) + + var values = computeValuesAndEffects(tree) + solveOwnership(tree, actx.cfg, values, entities) + + let destructors = computeDestructors(tree, actx.cfg, values, entities) + + # only inject destructors for calls to ``new`` if destructor-based + # ref-counting is used + if g.config.selectedGC in {gcHooks, gcArc, gcOrc}: + lowerNew(tree, g, changes) + + rewriteAssignments( + tree, actx, + AnalysisResults(v: cursor(values), + entities: cursor(entities), + destroy: cursor(destructors)), + diags, changes) + + # turn the collected diagnostics into reports and report them: + reportDiagnostics(g, tree, sourceMap, owner, diags) + + injectDestructors(tree, g, destructors, changes) + + apply(changes) if g.config.arcToExpand.hasKey(owner.name.s): + # the diagnostic expects a ``PNode`` AST, so we first have to tranlsate + # the MIR code into one. While this is inefficient, ``expandArc`` is only + # meant as a utility, so it's okay for now. + let n = generateAST(g, idgen, owner, tree, sourceMap) g.config.localReport(SemReport( kind: rsemExpandArc, - ast: n, sym: owner, - expandedAst: result - )) + expandedAst: n + )) \ No newline at end of file diff --git a/compiler/sem/mirexec.nim b/compiler/sem/mirexec.nim new file mode 100644 index 00000000000..159dbb7ac9a --- /dev/null +++ b/compiler/sem/mirexec.nim @@ -0,0 +1,748 @@ +## This module implements algorithms for traversing a control-flow graph (=CFG) +## in forward or backward direction. In the abstract, it means visiting the +## nodes in topological-order (forward) or post-order (backward), with special +## handling for loops. +## +## In the compiler, this is mainly used for control- and/or data-flow +## analysis, meant to propagate properties through the graph or to to answer +## questions such as: "is point A connected to point B?", "is X initialized on +## all paths?", etc. +## +## Before traversing a control-flow graph, one has to be created via +## ``computeCfg`` first. Instead of a pointer-based graph structure, the graph +## is represented via a linear list of instruction. +## +## TODO: expand this section with how the algorithms used by for- and backward +## traversal work + +# XXX: with a small to medium amount of work, the algorithms and +# ``ControlFlowGraph`` can be generalized to work independent from the +# MIR. The current ``computeCfg`` would be moved to somewhere else + +import + std/[ + algorithm, + packedsets, + sets, + tables + ], + compiler/ast/[ + ast_types + ], + compiler/mir/[ + mirtrees + ], + compiler/utils/[ + idioms + ] + +type + Opcode = enum + opFork ## branching control-flow that cannot introduce a cycle + opGoto ## unconditional jump that cannot introduce a cycle + opLoop ## unconditional jump to the start of a loop. The start of a cycle + opJoin ## defines a join point + + # TODO: make both types distinct + InstrPos = int32 + JoinId = uint32 + + Instr = object + node: NodePosition + case op: Opcode + of opFork, opGoto, opLoop: + dest: JoinId + of opJoin: + id: JoinId + + ControlFlowGraph* = object + ## The control-flow graph is represented as a linear sequence of + ## instructions + instructions: seq[Instr] + map: seq[InstrPos] ## joind ID -> instruction + + TraverseState* = object + exit*: bool ## used to communicate to ``traverse`` that the active path + ## should be "killed" (not followed further). Reset to `false` + ## whenever control is passed back to the iterator. + ## When traversal is finished, `exit` is set to 'true' + ## if traversal reached the end of the given span, 'false' + ## otherwise + escapes*: bool ## whether control-flow escapes the given span + + LoopEntry = tuple + start: JoinId ## the join point that marks the start of the loop + fin: InstrPos ## the 'loop' instruction + isRun: bool ## whether the loop is currently processed + + Time = uint16 + ## Represents the abstract time that a basic block is visited at. + ## For simplicity, the zero-representation (`0`) means invalid / + ## uninitialized. Because of this, a higher time value means "visited + ## earlier" + ## + ## Using ``uint16`` the upper limit for the length of the longest chain of + ## basic blocks is 2^16-1. In practice, a few optimizations are applied in + ## order to increase the effective upper limit for some situtations. + + ExecState = object + ## Execution environment state for backward traversal + visited: seq[Time] + ## - for loop joins: the `top` time when entering the loop + ## - for normal joins: the earliest time at which the join point was + ## reached + ## - for both: '0' if the join point was never reached + loops: seq[LoopEntry] + ## the loop stack. Remembers the loops the current basic block is + ## located inside + + time: Time + ## the current time, or `0`, if the next basic block is not connected to + ## the start + top: Time + ## if `time` is lower or equal to `top`, the next basic block wasn't + ## visited yet + bottom: Time + ## the lowest value `time` had. Provides the time to resume at after + ## exiting a loop + + pc: InstrPos + ## the program counter. Points to the CFG instruction that is executed + ## next + i: NodePosition + ## points to the next item to yield + +# TODO: copied from ``ast_query.nim``. Either export the original or use a +# different approach - duplicating the constant is not acceptable +const magicsThatCanRaise = { + mNone, mSlurp, mStaticExec, mParseExprToAst, mParseStmtToAst, mEcho} + +func incl[T](s: var seq[T], v: sink T) = + ## If not present already, adds `v` to the sorted ``seq`` `s` + var i = 0 + while i < s.len and s[i] < v: + inc i + + if i >= s.len or s[i] != v: + s.insert(v, i) + +func compare(a: Instr, b: NodePosition): int = + ord(a.node) - ord(b) + +template lowerBound(c: ControlFlowGraph, node: NodePosition): InstrPos = + lowerBound(c.instructions, node, compare).InstrPos + +template upperBound(c: ControlFlowGraph, node: NodePosition): InstrPos = + upperBound(c.instructions, node, compare).InstrPos + +func `[]`(c: ControlFlowGraph, pc: SomeInteger): lent Instr {.inline.} = + c.instructions[pc] + +func computeCfg*(tree: MirTree): ControlFlowGraph = + ## Computes the control-flow graph for the given `tree`. This is a very + ## expensive operation! The high cost is due to two essential reasons: + ## + ## 1. a control-flow graph needs to materialize all edges for a given `tree` + ## 2. the amount of allocations/bookkeeping necessary to do so + ## + ## The most amount of bookkeeping is required for finalizers and exception. + + type + FinalizerState = object + span: Slice[NodePosition] + next: seq[NodePosition] ## the exits registered to the finalizer + + ClosureEnv = object + ## Using a real closure (i.e. ``.closure``) would be a bit too costly, + ## so a manual implementation is used instead + instrs: seq[Instr] + finalizers: seq[FinalizerState] + joins: Table[NodePosition, JoinId] + + JoinPoint = object + id: JoinId + node: NodePosition + isNew: bool + + func considerFinalizer(target: NodePosition, + f: var FinalizerState): NodePosition = + ## Redirects a jump to `target` through a finalizer if one is active and + ## the jump crosses its boundary + if target in f.span: + target + else: + if target notin f.next: + f.next.incl target + + f.span.b + + func addJoin(env: var ClosureEnv, dest: NodePosition): JoinPoint = + let + nextId = env.joins.len.JoinId + id = env.joins.mgetOrPut(dest, nextId) + + JoinPoint(id: id, node: dest, isNew: id == nextId) + + func addEdge(env: var ClosureEnv, a, b: NodePosition, + isLoop: bool): JoinPoint = + let target = + if isLoop: + assert a > b, "a loop CFG edge must describe backwards control-flow" + # a loop edge can't cross the boundary of a finalizer + b + else: + assert a < b, "a non-loop CFG edge must describe forward control-flow" + if env.finalizers.len > 0: considerFinalizer(b, env.finalizers[^1]) + else: b + + addJoin(env, target) + + func commit(env: var ClosureEnv, p: JoinPoint) = + ## Adds a 'join' instruction using the info from `p`, but only if it's a + ## new join point (i.e. one that has no other sources yet) + if p.isNew: + env.instrs.add Instr(node: p.node, op: opJoin, id: p.id) + + var + env: ClosureEnv + handlers: seq[NodePosition] + + template goto(a, b: NodePosition) = + let + x = a + p = env.addEdge(x, b, isLoop=false) + + env.instrs.add Instr(op: opGoto, node: x, dest: p.id) + commit(env, p) + + template fork(a, b: NodePosition) = + let + x = a + p = env.addEdge(x, b, isLoop=false) + + env.instrs.add Instr(op: opFork, node: x, dest: p.id) + commit(env, p) + + template loop(a, b: NodePosition) = + let + x = a + p = env.addEdge(x, b, isLoop=true) + + commit(env, p) # the 'join' comes first + env.instrs.add Instr(op: opLoop, node: x, dest: p.id) + + let exit = NodePosition(tree.len) + + for i, n in tree.pairs: + case n.kind + of mnkCall: + if geRaises in n.effects: + # the control-flow graph only encodes procedure-local control-flow, so + # a procedure call that might raise an exception is treated as forking + # to the closest handler + fork(i): + if handlers.len > 0: handlers[^1] + else: exit + + of mnkMagic: + if n.magic in magicsThatCanRaise: + fork(i): + if handlers.len > 0: handlers[^1] + else: exit + + of mnkIf: + fork i, findEnd(tree, i) + of mnkBranch: + if n.len > 0: + # only fork if the branch has a condition (i.e. it's not a "catch-all" + # branch) + fork i, sibling(tree, i) + + of mnkRegion: + # a region is specified to have no obsersvable control-flow effects, so + # we effectively skip it. The control-flow instructions for the + # intra-region control-flow are still generated -- by default, they're + # just skipped over + goto i, findEnd(tree, i) + of mnkRepeat: + # add a loop-edge between the end of the 'repeat' block and its start + loop findEnd(tree, i), i + of mnkTry: + if n.len > 0: + let first = childIdx(tree, i, 1) # position of the first attachement + # register the exception handler: + if tree[first].kind == mnkExcept: + handlers.add first + + # register the finalizer: + if n.len == 2 or tree[first].kind == mnkFinally: + env.finalizers.add FinalizerState(span: i .. childIdx(tree, i, n.len)) + + let body = childIdx(tree, i, 0) + assert tree[body].kind in SubTreeNodes + # the body of the 'try' goes to the end of the 'try-except-finally' + # block on exit: + goto findEnd(tree, body), findEnd(tree, i) + of mnkExcept: + assert handlers[^1] == i + # pop the handler so that it doesn't apply to itself + handlers.setLen(handlers.len - 1) + of mnkFinally: + # pop the finalizer so that it doesn't apply to itself + let finalizer = env.finalizers.pop() + assert finalizer.span.b == i + + let e = findEnd(tree, i) + # where control-flow continues after exiting the finalizer depends on + # the jumps it intercepted. We represent this in the CFG by emitting a + # 'fork' instruction targeting the destination of each intercepted jump + for x in 0.. 0: + goto(e, finalizer.next[^1]) + + of mnkBreak: + let label = n.label + var target: NodePosition + if label.isNone: + # unnamed break - exit the enclosing loop + target = findParent(tree, i, mnkRepeat) + else: + # goto the exit of the block with the matching label + target = findParent(tree, i, mnkBlock) + while tree[target].label != label: + target = findParent(tree, target-1, mnkBlock) + + goto i, findEnd(tree, target) + of mnkReturn: + goto i, exit + of mnkRaise: + # when raising an exception, control-flow is transferred to the enclosing + # handler. If none exists in the current tree (i.e. procedure), it's + # treated the same as a return + goto(i): + if handlers.len > 0: handlers[^1] + else: exit + + of mnkEnd: + case n.start + of mnkBranch: + # XXX: the goto is redundant/unnecessary if the body doesn't have a + # structured exit + # only create an edge for the exit branches that are not the last one + if tree[i+1].kind != mnkEnd: + goto i, parentEnd(tree, i) + of mnkExcept: + # after a structured exit of the handler, control-flow continues after + # the code section it is attached to. Node-wise, this is the ``end`` + # node terminating the ``try`` the handler is part of + # TODO: with this approach, either the last branch needs to fork + # control-flow to the next exception handler or ``mirgen`` has to + # introduce a catch-all handler + if tree[i+1].kind == mnkFinally: + # only add an edge if the there's a finalizer -- no edge is needed + # if there's none + goto i, parentEnd(tree, i) + else: + discard + + else: + discard "not relevant" + + swap(env.instrs, result.instructions) + + assert result.map.len <= 1 + # to make the edge creation above simpler, the instructions are not added in + # the correct order, meaning that we have to sort them here: + sort(result.instructions, proc(a, b: auto): int = ord(a.node) - ord(b.node)) + + # looking up the position of the ``opcJoin`` instruction that defines a given + # join point is a very common operation, so we cache this information for + # efficiency + result.map.newSeq(env.joins.len) + for i, instr in result.instructions.lpairs: + if instr.op == opJoin: + result.map[instr.id] = InstrPos(i) + +iterator traverse*(tree: MirTree, c: ControlFlowGraph, + span: Slice[NodePosition], start: NodePosition, + state: var TraverseState): (NodePosition, lent MirNode) = + ## Starts at `start + 1` and traverses/yields all basic blocks inside `span` + ## in control-flow order. That is, except for in the context of loops, each + ## basic block is yielded before those having a control-flow dependency on + ## it. Traversal begins at `start`, which is allowed to point inside a basic + ## block. + ## + ## The same basic block may be yielded multiple times. This is not a general + ## limitation, but rather because of a shortcut taken by the implementation. + ## + ## `state` is used for bi-directional communication -- see the documentation + ## of ``TraverseState`` for more information. + assert start in span + var + i = start + 1 + pc: InstrPos + queue: seq[InstrPos] + visited: PackedSet[JoinId] + + + state = TraverseState() + + template resume() = + if queue.len > 0: + pc = queue[0] + queue.delete(0) + + assert c[pc].op == opJoin + i = c[pc].node - 1 + else: + # no more threads left -> exit + break + + template push(target: JoinId) = + ## If the destination position is inside the active span, adds + ## it to the execution queue, effectively starting a new thread. Records + ## an escape otherwise + let dst = c.map[target] + if c[dst].node in span: + queue.incl dst + else: + state.escapes = true + + template blockEnd(): NodePosition = + if pc < c.instructions.len: + c[pc].node + else: + NodePosition(tree.len) + + template abort() = + ## Exit the current thread and continue with the next one in the queue + resume() + next = blockEnd() + + pc = lowerBound(c, i) + var next = blockEnd() + + # XXX: this loop can be optimized further. Instead of yielding each item + # from separately, the basic block could be yielded as a slice instead + while i <= span.b: + yield (i, tree[i]) + + if state.exit or i == start: + state.exit = false + abort() + + # if at the end of a basic block, execute all CFG instructions associated + # with it: + while i == next: + let instr = c[pc] + + case instr.op + of opGoto: + push(instr.dest) + resume() + of opFork: + push(instr.dest) + of opLoop: + if not visited.containsOrIncl(instr.dest): + push(instr.dest) + + resume() + of opJoin: + if queue.len > 0 and queue[0] == pc: + # this case happens for the following CFG sequence: + # fork 0 + # 0: join + # which is generated for e.g. an ``else`` branch + queue.delete(0) + + inc pc + next = blockEnd() + + inc i + + assert queue.len <= 1 + + state.exit = i == span.b + 1 + +template active(s: ExecState): bool = + # if a thread is selected and it's either the or derived from the main + # thread, execution is active + s.time != 0 and s.time.uint16 <= s.top + +template step(s: var ExecState) = + dec s.time + # remember the lowest time value we've reached so far: + s.bottom = min(s.bottom, s.time) + +func processJoin(id: JoinId, s: var ExecState, c: ControlFlowGraph) {.inline.} = + ## Processes a 'join' instruction in the context of reverse traversal + + if s.loops.len > 0 and s.loops[^1].start == id: + # the join point is the start of a loop + let (_, p, isRetry) = s.loops[^1] + # note that at this point, `s.active` can only be true if this is the first + # we're reaching the loop start while being active. For each following + # visit, active will always be 'false' + if s.visited[id] == 0 and s.active: + # this is the first time we're reaching this loop start -- jump back + # to the end of the loop and set `isRetry` for this loop-start to + # true so that we know whether it'a a loop exit the next time we reach it + s.loops[^1].isRun = true + + # jump to the end of the loop: + s.pc = p + s.i = c[p].node + # remember the current `top` and prevent blocks already visited during + # the first pass from being visited again by setting `top` to the current + # time: + s.visited[id] = s.top + # XXX: it might be possible to set `time` to `bottom` here (doing so + # didn't cause any test failures), which would increase the amount + # basic blocks that can be traversed before exhausting the + # ``uint16`` range. Howerver, before applying the this optimization, it must + # first be formally proven to be correct + s.time = s.bottom + s.top = s.time + else: + if isRetry: + # we finished the loop. Restore the `top` value to what is was when + # entering the loop and use `bottom` as the time for the next basic + # block. The latter is important for nested loops + s.time = s.bottom + s.top = s.visited[id] + # XXX: as an optimization, `s.bottom` and `s.top` can both be reset + # to ``high(Time)``, increasing the amount of basic blocks that + # can be traversed in the situation where there are multiple + # top-level loops (i.e. loops not nested in other ones). Do note + # that this only if it's guaranteed that forward control-flow + # cannot jump *into* a loop (which is the case all NimSkull code + # *except* state machines realized via the ``.goto`` pragma) + + # the loop was processed, pop it from the stack: + s.loops.setLen(s.loops.len - 1) + + else: + # only remember the earliest time the join point was reached: + s.visited[id] = max(s.time, s.visited[id]) + s.time = s.visited[id] + +iterator traverseReverse*(tree: MirTree, c: ControlFlowGraph, + span: Slice[NodePosition], start: NodePosition, + exit: var bool): (NodePosition, lent MirNode) = + ## Starts at `start - 1` and visits and returns all basic blocks inside + ## `span` in post-order. + ## + ## `span` being empty is supported: nothing is returned in that case. + ## + ## Similar to ``traverse``, `exit` is used, with the same meaning, for + ## bi-directional communication. + var s: ExecState + + # simplify further processing by making sure that `span` is something sane + let span = + if span.a <= span.b: + assert start-1 in span, "`start` not inside `span`" + span + else: + start..start-1 # `span` is empty + + s.visited.newSeq(c.map.len) + s.pc = upperBound(c, span.b) - 1 + # start activity *after* the start position is reached so that + # the start node itself is not yielded + s.i = start - 1 + s.top = high(Time) + s.time = s.top + s.bottom = s.time + + exit = false + + var trace = "" + # move the program counter to the CFG instruction that marks the start of the + # basic block `start` is located inside. While doing so, collect the loops + # the start position is located inside: + while s.pc >= 0 and c[s.pc].node > s.i: + let instr = c[s.pc] + case instr.op + of opLoop: + s.loops.add (instr.dest, s.pc, false) + of opJoin: + # the start of a loop; pop the previous loop entry: + if s.loops.len > 0 and s.loops[^1].start == instr.id: + s.loops.setLen(s.loops.len - 1) + of opGoto, opFork: + discard + + dec s.pc + + # perform the traversal: + while s.i >= span.a: + # execute all instructions located at the end of the basic block (if we're + # at the end of one): + while s.pc >= 0 and c[s.pc].node == s.i: + let instr = c[s.pc] + + case instr.op + of opGoto: + # resume with the time from the target + s.time = s.visited[instr.dest] + of opFork: + # time is always shortest distance to the next join point, hence the + # use of ``max`` (and not ``min``) + s.time = max(s.time, s.visited[instr.dest]) + of opLoop: + # remember the loop instruction so that we can jump back to it when + # reaching the start of the loop + s.loops.add (instr.dest, s.pc, false) + s.time = 0 # disable execution + of opJoin: + processJoin(instr.id, s, c) + + dec s.pc + + # `next` is the position of the first item in the current basic block, + # taking the provided `span` into account + let next = + if s.pc >= 0: max(c[s.pc].node + 1, span.a) + else: span.a + + assert next <= s.i + + let cross = next <= start and s.i >= start + ## whether `start` is part of the next basic block + + # if they weren't visited yet, return all items in the current basic + # block: + if s.active: + # prevent the first half of the basic block we started inside to be + # returned: + let adjusted = + if cross: start + else: next + + # TODO: yield ``next..s.i`` instead and let the callsite do the + # iteration. It's much more flexible and we no longer need the + # `tree` parameter + + while s.i >= adjusted and not exit: + yield (s.i, tree[s.i]) + dec s.i + + step(s) + + if exit: + exit = false + s.time = 0 # disable execution + + else: + s.i = next - 1 + + if cross: + # we've reached the start position, so set the time to what it was at + # the start + s.time = high(Time) + + exit = s.active + +iterator traverseFromExits*(tree: MirTree, c: ControlFlowGraph, + span: Slice[NodePosition], exit: var bool + ): (NodePosition, lent MirNode) = + ## Similar to ``traverseReverse``, but starts traversal at each unstructured + ## exit of `span`. Here, unstructured exit means that the control-flow leaves + ## `span` via a 'goto' or 'fork'. + ## + ## For the algorithm to work correctly, it is important that span does not + ## cross a loop. That is, both start and the end of loop need to be present + ## in `span`. + ## + ## `exit` works the same way as it does for ``traverseReverse`` + const EntryTime = high(Time) + var s: ExecState + + s.i = span.b + s.pc = upperBound(c, s.i) - 1 + s.visited.newSeq(c.map.len) + + s.time = 0 # start as disabled + s.top = EntryTime + s.bottom = EntryTime + + exit = false + + template exits(target: JoinId): bool = + c[c.map[target]].node notin span + + # for the most part similar to the loop in ``traverse``, but with special + # handling for jumps outside of `span` + while s.i >= span.a: + # execute all instructions located at the end of the basic block (if we're + # at the end of one): + while s.pc >= 0 and c[s.pc].node == s.i: + let instr = c[s.pc] + + case instr.op + of opGoto: + s.time = + if exits(instr.dest): EntryTime + else: s.visited[instr.dest] + of opFork: + s.time = + if exits(instr.dest): EntryTime + else: max(s.time, s.visited[instr.dest]) + of opLoop: + # remember the loop instruction so that we can jump back to it when + # reaching the start of the loop + s.loops.add (instr.dest, s.pc, false) + s.time = 0 # disable execution + of opJoin: + processJoin(instr.id, s, c) + + dec s.pc + + # `next` is the position of the first item in the current basic block, + # taking the provided `span` into account + let next = + if s.pc >= 0: max(c[s.pc].node + 1, span.a) + else: span.a + + assert next <= s.i + + # if they weren't visited yet, return all items in the current basic + # block: + if s.active: + # we want to yield all nodes up to and including `next` + while s.i >= next and not exit: + yield (s.i, tree[s.i]) + dec s.i + + # perform the time step. This has to happen *before* potentially setting + # `time` to 0 + step(s) + + if exit: + exit = false + s.time = 0 # disable execution + + else: + # -1 so that `i` points to the last itme of the next basic block + s.i = next - 1 + + exit = s.active + + +func `$`*(c: ControlFlowGraph): string = + ## Renders the instructions of `c` as a human-readable text representation + for i, n in c.instructions.pairs: + case n.op + of opJoin: + result.add $n.id & ": join" + of opGoto, opFork, opLoop: + result.add $n.op & " " & $n.dest + + result.add " -> " & $ord(n.node) & "\n" \ No newline at end of file diff --git a/compiler/sem/optimizer.nim b/compiler/sem/optimizer.nim deleted file mode 100644 index 9a16794767f..00000000000 --- a/compiler/sem/optimizer.nim +++ /dev/null @@ -1,287 +0,0 @@ -# -# -# The Nim Compiler -# (c) Copyright 2020 Andreas Rumpf -# -# See the file "copying.txt", included in this -# distribution, for details about the copyright. -# - -## Optimizer: -## - elide 'wasMoved(x); destroy(x)' pairs -## - recognize "all paths lead to 'wasMoved(x)'" - -import - compiler/ast/[ast, renderer], std/intsets - -from compiler/ast/trees import exprStructuralEquivalent - -const - nfMarkForDeletion = nfNone # faster than a lookup table - -type - BasicBlock = object - wasMovedLocs: seq[PNode] - kind: TNodeKind - hasReturn, hasBreak: bool - label: PSym # can be nil - parent: ptr BasicBlock - - Con = object - somethingTodo: bool - inFinally: int - -proc nestedBlock(parent: var BasicBlock; kind: TNodeKind): BasicBlock = - BasicBlock(wasMovedLocs: @[], kind: kind, hasReturn: false, hasBreak: false, - label: nil, parent: addr(parent)) - -proc breakStmt(b: var BasicBlock; n: PNode) = - var it = addr(b) - while it != nil: - it.wasMovedLocs.setLen 0 - it.hasBreak = true - - if n.kind == nkSym: - if it.label == n.sym: break - else: - # unnamed break leaves the block is nkWhileStmt or the like: - if it.kind in {nkWhileStmt, nkBlockStmt, nkBlockExpr}: break - - it = it.parent - -proc returnStmt(b: var BasicBlock) = - b.hasReturn = true - var it = addr(b) - while it != nil: - it.wasMovedLocs.setLen 0 - it = it.parent - -proc mergeBasicBlockInfo(parent: var BasicBlock; this: BasicBlock) {.inline.} = - if this.hasReturn: - parent.wasMovedLocs.setLen 0 - parent.hasReturn = true - -proc wasMovedTarget(matches: var IntSet; branch: seq[PNode]; moveTarget: PNode): bool = - result = false - for i in 0.. 0 and (b.hasReturn or b.hasBreak): - discard "cannot optimize away the destructor" - else: - c.wasMovedDestroyPair b, n - special = true - elif s.name.s == "=sink": - reverse = true - - if not special: - if not reverse: - for i in 0 ..< n.len: - analyse(c, b, n[i]) - else: - #[ Test destructor/tmatrix.test3: - Prevent this from being elided. We should probably - find a better solution... - - `=sink`(b, - - let blitTmp = b; - wasMoved(b); - blitTmp + a) - `=destroy`(b) - - ]# - for i in countdown(n.len-1, 0): - analyse(c, b, n[i]) - if canRaise(n[0]): returnStmt(b) - - of nkSym: - # any usage of the location before destruction implies we - # cannot elide the 'wasMoved(x)': - b.invalidateWasMoved n - - of nkNone..pred(nkSym), succ(nkSym)..nkNilLit, nkTypeSection, nkProcDef, nkConverterDef, - nkMethodDef, nkIteratorDef, nkMacroDef, nkTemplateDef, nkLambda, nkDo, - nkFuncDef, nkConstSection, nkConstDef, nkIncludeStmt, nkImportStmt, - nkExportStmt, nkPragma, nkCommentStmt, nkTypeOfExpr, nkMixinStmt, - nkBindStmt: - discard "do not follow the construct" - - of nkAsgn, nkFastAsgn: - # reverse order, see remark for `=sink`: - analyse(c, b, n[1]) - analyse(c, b, n[0]) - - of nkIfStmt, nkIfExpr: - let isExhaustive = n[^1].kind in {nkElse, nkElseExpr} - var wasMovedSet: seq[PNode] = @[] - - for i in 0 ..< n.len: - var branch = nestedBlock(b, n[i].kind) - - analyse(c, branch, n[i]) - mergeBasicBlockInfo(b, branch) - if isExhaustive: - if i == 0: - wasMovedSet = move(branch.wasMovedLocs) - else: - wasMovedSet.intersect(branch.wasMovedLocs) - for i in 0..$s$i", val): + raise ValueError.newException("syntax error at end: " & line) + + list[^1].node = NodePosition(val) + + result = ControlFlowGraph(instructions: list, map: map) + +proc parse2(str: string): Program = + ## Parses a ``Program`` object from its text representation + var + nameToId: Table[int, uint32] + + func parseOp2(input: string, r: var POpcode, start: int): int = + for e, s in [def: "def", use: "use"]: + if input.substr(start).startsWith(s): + r = e + result = s.len + break + + template getId(name: int): JoinId = + nameToId.mgetOrPut(name, nameToId.len.JoinId) + + template cfgCode: untyped = result.cfg.instructions + template map: untyped = result.cfg.map + template code: untyped = result.code + + for line in splitLines(str): + if scanf(line, "$s$."): + # skip empty lines + continue + + var + name: int + op: Opcode + op2: POpcode + + if scanf(line, "$s$i$s:$sjoin", name): + code.add PInstr(op: cflow) + + let id = getId(name) + cfgCode.add Instr(op: opJoin, id: id) + + if int(id + 1) > map.len: + map.setLen(id+1) + map[id] = InstrPos(cfgCode.high) + elif scanf(line, "$s${parseOp} $i", op, name): + code.add PInstr(op: cflow) + cfgCode.add Instr(op: op) + cfgCode[^1].dest = getId(name) + elif scanf(line, "$s${parseOp2} :$i", op2, name): + code.add PInstr(op: op2, id: name) + result.map[name] = NodePosition(code.high) + continue + else: + raise ValueError.newException("syntax error in line: " & line) + + cfgCode[^1].node = NodePosition(code.high) + +func `==`(a, b: Instr): bool = + if a.op != b.op or a.node != b.node: + return false + + result = + case a.op + of opFork, opGoto, opLoop: a.dest == b.dest + of opJoin: a.id == b.id + +func `==`(a, b: ControlFlowGraph): bool = + ## Compares two CFGs for structural equality. Differing join IDs are ignored + ## as long as they point to the same instruction + if a.instructions.len != b.instructions.len: + return false + + for i in 0.. 1 + goto 1 -> 2 + loop 0 -> 3 + 1: join -> 3 + goto 2 -> 4 + 2: join -> 6 + """) + +# -------------- test for the traversal routines + +# TODO: also test forward traversal and backward traversal from all exits + +func isConnected(p: Program, defId, useId: int): bool = + ## Computes and returns whether the 'use' with id `useId` is connected to + ## the 'def' with id `defId`. Also validates that each instruction is really + ## only visited once. + var + tree = newSeq[MirNode](p.code.len) + visited = newSeq[bool](p.code.len) + exit = false + + result = false + for i, _ in traverseReverse(tree, p.cfg, NodePosition(0)..NodePosition(tree.high), p.map[useId], exit): + doAssert not visited[int i] + visited[int i] = true + + case p.code[int i].op + of def: + if p.code[int i].id == defId: + result = true + of use, cflow: + discard "ignore" + +proc useChain(p: Program, defId, start: int): seq[int] = + ## Computes and returns the 'use's connected to the 'use' with ID `start`. + ## Reaching `defId` breaks the chain. The list is sorted in post-order. + ## Also validates that each instruction is really only visited once. + # TODO: remove `tree` once traversal is separated from the MIR + var + tree = newSeq[MirNode](p.code.len) + visited = newSeq[bool](p.code.len) + exit = false + + for i, _ in traverseReverse(tree, p.cfg, NodePosition(0)..NodePosition(tree.high), p.map[start], exit): + doAssert not visited[int i], + "instruction already visited; either the algorithm or CFG is broken" + visited[int i] = true + + let instr = p.code[int i] + case instr.op + of def: + if instr.id == defId: + # found the def; quit the path + exit = true + of use: + result.add instr.id + of cflow: + discard "ignore" + +block infinite_loop: + # while true: + # discard + let p = parse2(""" + def :1 + 0: join + use :2 + use :3 + loop 0 + use :4 + """) + + doAssert(isConnected(p, 1, 2)) + doAssert(not isConnected(p, 1, 4)) + doAssert useChain(p, 1, 3) == [2, 3] + doAssert useChain(p, 1, 4) == [] + +block nested_loop: + # while cond1: + # while cond2: + # discard + let p = parse2(""" + 0: join + def :1 + 1: join + use :2 + fork 2 + use :3 + goto 3 + 2: join + use :4 + loop 1 + 3: join + use :5 + loop 0 + """) + + doAssert useChain(p, 1, start = 5) == [3, 2, 4] + +block two_nested_loops: + # two nested loops inside a single loop: + # + # while true: + # while cond2: + # discard + # while cond3: + # discard + + let p = parse2(""" + def :1 + 0: join + 1: join + use :2 + fork 2 + use :3 + goto 3 + 2: join + use :4 + loop 1 + 3: join + use :5 + 4: join + use :6 + fork 5 + use :7 + goto 6 + 5: join + use :8 + loop 4 + 6: join + use :9 + loop 0 + """) + + doAssert useChain(p, 1, start = 3) == [2, 4, 9, 7, 6, 8, 5, 3] + doAssert useChain(p, 1, start = 4) == [2, 4, 9, 7, 6, 8, 5, 3] + doAssert useChain(p, 1, start = 5) == [3, 2, 4, 9, 7, 6, 8, 5] + +block nested_loop_exit: + # block exit: + # while cond1: + # while true: + # break exit + let p = parse2(""" + def :1 + 0: join + use :4 + fork 1 + use :2 + goto 3 + 1: join + 2: join + use :3 + goto 3 + loop 2 + loop 0 + 3: join + use :5 + """) + + doAssert useChain(p, 1, 2) == [4] + doAssert useChain(p, 1, 3) == [4] + doAssert useChain(p, 1, 4) == [] + doAssert useChain(p, 1, 5) == [3, 2, 4] + +block nested_infinite_loop: + # while cond: + # while true: + # discard + + let p = parse2(""" + def :1 + 0: join + use :2 + fork 1 + use :3 + goto 2 + 1: join + use :4 + loop 1 + loop 0 + 2: join + use :5 + """) + + doAssert useChain(p, 1, 2) == [] + doAssert useChain(p, 1, 3) == [2] + doAssert useChain(p, 1, 4) == [4, 2] + doAssert useChain(p, 1, 5) == [3, 2] + +block nested_if_in_loop: + # while true: + # if cond: + # if cond2: + # break + let p = parse2(""" + def :1 + 0: join + use :2 + fork 1 + use :3 + fork 2 + use :4 + goto 3 + 2: join + 1: join + use :5 + loop 0 + 3: join + use :6 + """) + + doAssert useChain(p, 1, 2) == [5, 3, 2] + doAssert useChain(p, 1, 3) == [2, 5, 3] + doAssert useChain(p, 1, 4) == [3, 2, 5] + doAssert useChain(p, 1, 5) == [3, 2, 5] + doAssert useChain(p, 1, 6) == [4, 3, 2, 5] + +block try_in_loop: + # while true: + # try: + # if a: + # continue + # if b: + # break + # finally: + # discard + let p = parse2(""" + def :1 + 0: join + use :2 + fork 1 + use :3 + goto 3 # continue + 1: join + + fork 2 + use :4 + goto 3 + 2: join + + goto 5 + 3: join + use :5 + fork 5 + goto 6 + + 5: join + use :6 + loop 0 + 6: join + use :7 + """) + + doAssert useChain(p, 1, 2) == [6, 5, 4, 3, 2] + doAssert useChain(p, 1, 3) == [2, 6, 5, 4, 3] + doAssert useChain(p, 1, 4) == [2, 6, 5, 4, 3] + doAssert useChain(p, 1, 5) == [4, 3, 2, 6, 5] + doAssert useChain(p, 1, 6) == [5, 4, 3, 2, 6] + doAssert useChain(p, 1, 7) == [5, 4, 3, 2, 6] diff --git a/tests/lang_objects/destructor/mhelper.nim b/tests/lang_objects/destructor/mhelper.nim new file mode 100644 index 00000000000..eef63c90f69 --- /dev/null +++ b/tests/lang_objects/destructor/mhelper.nim @@ -0,0 +1,80 @@ +## Helper module containing utilities for the lifetime-hook related tests + + +type + Resource* = object + ## Something that represents a resource + content*: bool + + Value*[T] = object + has: bool + content*: T + +var numCopies*, numSinks*, numDestroy*: int + +## ------ `Resource` hooks + +proc `=destroy`(x: var Resource) = + if x.content: + inc numDestroy + +proc `=copy`(x: var Resource, y: Resource) = + `=destroy`(x) + x.content = y.content + if x.content: + inc numCopies + +proc `=sink`(x: var Resource, y: Resource) = + `=destroy`(x) + x.content = y.content + if x.content: + inc numSinks + +## ------ `Value` hooks + +proc `=destroy`[T](x: var Value[T]) = + if x.has: + inc numDestroy + +proc `=copy`[T](x: var Value[T], y: Value[T]) = + `=destroy`(x) + wasMoved(x) + if y.has: + inc numCopies + x.has = true + x.content = y.content + +proc `=sink`[T](x: var Value[T], y: Value[T]) = + `=destroy`(x) + wasMoved(x) + if y.has: + inc numSinks + x.has = true + x.content = y.content + + +template test*(name, code: untyped) = + ## Helper template for a test case that checks the `numCopies`, `numSinks`, + ## or `numDestroy` counters + block name: + # reset the counters first: + numCopies = 0 + numSinks = 0 + numDestroy = 0 + + code + +# use templates in order to interfere as little as possible with the tests + +template initResource*(): untyped = + Resource(content: true) + +template initValue*[T](x: T): untyped = + Value[T](has: true, content: x) + + +func use*[T](x: T) = + discard + +proc mutate*[T](x: var T) = + discard \ No newline at end of file diff --git a/tests/lang_objects/destructor/teval_order.nim b/tests/lang_objects/destructor/teval_order.nim new file mode 100644 index 00000000000..89cc01b71bd --- /dev/null +++ b/tests/lang_objects/destructor/teval_order.nim @@ -0,0 +1,82 @@ +discard """ + description: ''' + Tests to make sure that the left-to-right evaluation order is respected for + assignments involving types with destructors + ''' + target: "c js !vm" + matrix: "--cursorInference:off" +""" + +## knownIssue: no destructor injection is performed for the VM target + +import mhelper + +proc testBlockExpr() = + var test = 0 + var x: Value[int] + (test = 1; x) = block: + doAssert test == 1 + initValue(1) + + doAssert x.content == 1 + +proc testIfExpr() = + var test = 0 + var x: Value[int] + (test = 1; x) = + if test == 1: + initValue(1) + else: + initValue(2) + + doAssert x.content == 1 + +proc testCaseExpr() = + var test: range[0..2] = 0 + var x: Value[int] + (test = 1; x) = + case test + of 0, 2: doAssert false, "evaluation order violation"; initValue(2) + of 1: initValue(1) + + doAssert x.content == 1 + +proc testTryExpr() = + var test = 0 + var x: Value[int] ## something that has a destructor + + # check that the effects of the lhs are evaluated before the ``except`` + # clause: + (test = 1; x) = + try: + if true: + raise CatchableError.newException("") + + # we can't use the raise directly, as the ``try``-clause needs a type + initValue(2) + except: + doAssert test == 1 + initValue(1) + + doAssert x.content == 1 + + # check that the effects of the lhs are evaluated before the ``try`` clause: + test = 0 + + (test = 1; x) = + try: + if test != 1: + raise CatchableError.newException("") + + initValue(1) + except: + # if we reach here, the assertion above failed + doAssert false, "evaluation order violation" + initValue(2) + + doAssert x.content == 1 + +testBlockExpr() +testIfExpr() +testCaseExpr() +testTryExpr() \ No newline at end of file diff --git a/tests/lang_objects/destructor/tglobals.nim b/tests/lang_objects/destructor/tglobals.nim new file mode 100644 index 00000000000..be8cc2f8909 --- /dev/null +++ b/tests/lang_objects/destructor/tglobals.nim @@ -0,0 +1,32 @@ +discard """ + description: "Tests for globals that have lifetime hooks attached" + targets: "c js !vm" + matrix: "--cursorInference:off" +""" + +import mhelper except test + +numCopies = 0 +numDestroy = 0 +block move_non_module_scope_global: + var x = initResource() + var y = x # move -- this is the last use of `x` + +doAssert numCopies == 0 +doAssert numDestroy == 1 + +numCopies = 0 +numDestroy = 0 +block consider_call_with_side_effects: + # for this test, it's important for both variables to not be located at + # module-scope but also not inside a procedure + var x = initResource() + var y = x # must copy... + + proc p() = + mutate(x) + + p() # <-- because `x` is mutated here + +doAssert numCopies == 1 +doAssert numDestroy == 2 diff --git a/tests/lang_objects/destructor/tlate_use.nim b/tests/lang_objects/destructor/tlate_use.nim new file mode 100644 index 00000000000..99d00c0421f --- /dev/null +++ b/tests/lang_objects/destructor/tlate_use.nim @@ -0,0 +1,24 @@ +discard """ + description: ''' + The lvalue passed to a ``var`` parameter is considered to be read/used when + control-flow reaches the call, not when it reaches the argument expression + ''' + action: reject + cmd: "nim $target --filenames=canonical --msgFormat=sexp $options $file" + targets: "c js !vm" + nimoutFormat: sexp +""" + +type Obj = object + +proc `=copy`(x: var Obj, y: Obj) {.error.} + +proc f_sink(x: var Obj, y: sink Obj) = + discard + +proc main() = + var s = Obj() + f_sink(s, s) #[tt.Error + ^ (SemUnavailableTypeBound) (:str "=copy")]# + +main() \ No newline at end of file diff --git a/tests/lang_objects/destructor/tlvalue_conversions.nim b/tests/lang_objects/destructor/tlvalue_conversions.nim new file mode 100644 index 00000000000..5c30a65c555 --- /dev/null +++ b/tests/lang_objects/destructor/tlvalue_conversions.nim @@ -0,0 +1,76 @@ +discard """ + description: ''' + Tests for assignments of types with lifetime hooks where l-value + conversions are involved + ''' + targets: "c !js !vm" + matrix: "--gc:arc --cursorInference:off" +""" + +# knownIssues: both the JS and VM target don't yet support ``--gc:arc``:option: + +import mhelper + +block sink_from_ref_with_conversion: + # test that moving from a ``ref`` location works when there's a conversion in + # between + type + A = ref object of RootObj + B = ref object of A + + proc prc(cond: bool) = + var + ref1 = B() + ref2 = B() + + # force a destroy call for both `ref1` and `ref2` + if cond: + return + + var y: A = ref1 # uses a blit copy + y = ref2 # use the ``=sink`` hook + + doAssert y != nil + + prc(false) + # test that all allocated object were destroyed: + doAssert getOccupiedMem() == 0 + +block move_from_lvalue_conversion: + type + ObjA = object + ObjB = distinct ObjA + + proc `=destroy`(x: var ObjA) = + doAssert false + + proc `=destroy`(x: var ObjB) = + discard "okay; do nothing" + + proc test() = + # destructor elision must take the lvalue conversion into account. `a` must + # not be destroyed here + var a = ObjA() + var b = ObjB(a) # no copy must be used here + + test() + +test move_from_temporary_with_conversion: + type + DValue = distinct Value[int] + Wrapper = object + a: Value[int] + + proc `=destroy`(x: var DValue) = + `=destroy`(Value[int] x) + + # use a procedure so that a temporary is created + proc make(): Wrapper = + Wrapper(a: initValue(1)) + + proc prc() = + # move out of the temporary's field + var b = DValue(make().a) + + prc() + doAssert numDestroy == 1 \ No newline at end of file diff --git a/tests/lang_objects/destructor/tobjfield_analysis.nim b/tests/lang_objects/destructor/tobjfield_analysis.nim index 83f394c3bdd..9c50a5e3543 100644 --- a/tests/lang_objects/destructor/tobjfield_analysis.nim +++ b/tests/lang_objects/destructor/tobjfield_analysis.nim @@ -49,3 +49,23 @@ proc main = main() +import mhelper + +test constructed_object_field_access: + type Wrapper = object + a: Value[int] + b: Value[int] + + proc prc() = + # a literal constructor expression (which ``initValue`` also expands to) + # not used *directly* in a consume context only produces a non-owning + # container + # TODO: this doesn't match the behaviour the specification describes (i.e. + # constructor expression having the same semantics as procedure calls + # in the context of lifetime hook injection). The behaviour here + # should either be explicitly added to the spec or changed + var x = Wrapper(a: initValue(1), b: initValue(2)).a # use a copy + + prc() + + doAssert numCopies == 1 \ No newline at end of file diff --git a/tests/lang_objects/destructor/tself_assign.nim b/tests/lang_objects/destructor/tself_assign.nim new file mode 100644 index 00000000000..d7f82e7820b --- /dev/null +++ b/tests/lang_objects/destructor/tself_assign.nim @@ -0,0 +1,26 @@ +discard """ + description: "Tests for self assignments of types with lifetime hooks" + targets: "c js !vm" + matrix: "--cursorInference:off" +""" + +import mhelper + +test self_assignment_with_side_effect: + # test that for a statically detectable self assignment where both the source + # and destination expressions have side-effects, the assignment is + # eliminated, but the expressions still evalauted for their side-effects + + proc prc() = + var x = initResource() + var i = 0 + # the compiler must detect that the below is a self assignment + (inc i; x) = (inc i; x) + + doAssert i == 2 + + prc() + + doAssert numCopies == 0 + doAssert numSinks == 0 + doAssert numDestroy == 1 diff --git a/tests/lang_objects/destructor/ttuple_unpacking.nim b/tests/lang_objects/destructor/ttuple_unpacking.nim new file mode 100644 index 00000000000..360c1fd8e64 --- /dev/null +++ b/tests/lang_objects/destructor/ttuple_unpacking.nim @@ -0,0 +1,80 @@ +discard """ + description: ''' + Tests for tuple unpacking in the presence of types with lifetime hooks + ''' + targets: "c js !vm" + matrix: "--cursorInference:off" +""" + +import mhelper + +type Obj = object + ## Used to track what lifetime hook calls are injected + +proc `=copy`(x: var Obj, y: Obj) = + inc numCopies + +proc `=destroy`(x: var Obj) = + inc numDestroy + +test unpack_complex_expression: + # test unpacking a tuple resulting from a complex expression + proc prc(cond: bool) = + # don't use a static condition; the optimizer might remove the not-taken + # branch prior to assignment rewriting otherwise + let (a, b) = + if cond: (initResource(), initResource()) + else: (initResource(), initResource()) + + prc(true) + doAssert numCopies == 0 + doAssert numDestroy == 2 + +test no_extra_call_for_repack: + proc make(): tuple[a, b, c: Obj] = + # pack a tuple. No copy nor destroy hook calls must be injected here + (Obj(), Obj(), Obj()) + + proc repack(): tuple[a, b, c: Obj] = + # unpack and repack the tuple. No copy nor destroy hook calls must be + # injected here + let (a, b, c) = make() + result = (a: a, b: b, c: c) + + proc prc() = + discard repack() + # the tuple is not consumed; each element must be destroyed at the end of + # the procedure + + prc() + + doAssert numCopies == 0 + doAssert numDestroy == 3 + +test raise_in_constructor_expression: + # XXX: this test is only tangentially related to tuple unpacking. It should + # be moved somewhere else + + proc init(): Resource = + # the ``init`` procedure is used so that a temporary is definitely used + result = initResource() + + proc doRaise(cond: bool): Resource = + # a procedure that only in theory returns a resource. At run-time it always + # raises an exception + if cond: + raise (ref CatchableError)() + + proc make(): (Resource, Resource) = + # the temporary resulting from the ``init`` call must be destroyed and + # no valid value must be observable at the callsite of ``make`` + result = (init(), + doRaise(true)) + + try: + discard make() + except: + discard + + doAssert numCopies == 0 + doAssert numDestroy == 1 \ No newline at end of file diff --git a/tests/lang_objects/destructor/tv2_cast.nim b/tests/lang_objects/destructor/tv2_cast.nim index 917cf0eb3d5..3f5175507a9 100644 --- a/tests/lang_objects/destructor/tv2_cast.nim +++ b/tests/lang_objects/destructor/tv2_cast.nim @@ -6,77 +6,71 @@ destroying O1''' cmd: '''nim c --gc:arc --expandArc:main --expandArc:main1 --expandArc:main2 --expandArc:main3 --hints:off --assertions:off $file''' nimout: '''--expandArc: main -var - data - :tmpD - :tmpD_1 - :tmpD_2 -data = - wasMoved(:tmpD) - `=copy`(:tmpD, cast[string]( - :tmpD_2 = encode(cast[seq[byte]]( - :tmpD_1 = newString(100) - :tmpD_1)) - :tmpD_2)) - :tmpD -`=destroy`(:tmpD_2) -`=destroy_1`(:tmpD_1) -`=destroy_1`(data) +var :tmp +var data +var :tmp_1 +try: + var :tmp_2 = encode do: + var :tmp_3 = newString(100) + :tmp = :tmp_3 + cast[[type node]](:tmp) + :tmp_1 = :tmp_2 + var :tmp_4 = cast[string](:tmp_1) + `=copy`(data, :tmp_4) +finally: + `=destroy`(:tmp_1) + `=destroy_1`(:tmp) + `=destroy_1`(data) -- end of expandArc ------------------------ --expandArc: main1 -var - s - data - :tmpD - :tmpD_1 -s = newString(100) -data = - wasMoved(:tmpD) - `=copy`(:tmpD, cast[string]( - :tmpD_1 = encode(toOpenArrayByte(s, 0, len(s) - 1)) - :tmpD_1)) - :tmpD -`=destroy`(:tmpD_1) -`=destroy_1`(data) -`=destroy_1`(s) +var s +var :tmp +var data +try: + s = newString(100) + var :tmp_1 = encode(toOpenArrayByte(s, 0, `-`(len(s), 1))) + :tmp = :tmp_1 + var :tmp_2 = cast[string](:tmp) + `=copy`(data, :tmp_2) +finally: + `=destroy`(:tmp) + `=destroy_1`(data) + `=destroy_1`(s) -- end of expandArc ------------------------ --expandArc: main2 -var - s - data - :tmpD - :tmpD_1 -s = newSeq(100) -data = - wasMoved(:tmpD) - `=copy`(:tmpD, cast[string]( - :tmpD_1 = encode(s) - :tmpD_1)) - :tmpD -`=destroy`(:tmpD_1) -`=destroy_1`(data) -`=destroy`(s) +var s +var :tmp +var data +try: + s = newSeq(100) + var :tmp_1 = encode(s) + :tmp = :tmp_1 + var :tmp_2 = cast[string](:tmp) + `=copy`(data, :tmp_2) +finally: + `=destroy`(:tmp) + `=destroy_1`(data) + `=destroy`(s) -- end of expandArc ------------------------ --expandArc: main3 -var - data - :tmpD - :tmpD_1 - :tmpD_2 -data = - wasMoved(:tmpD) - `=copy`(:tmpD, cast[string]( - :tmpD_2 = encode do: - :tmpD_1 = newSeq(100) - :tmpD_1 - :tmpD_2)) - :tmpD -`=destroy`(:tmpD_2) -`=destroy`(:tmpD_1) -`=destroy_1`(data) +var :tmp +var data +var :tmp_1 +try: + var :tmp_2 = encode do: + var :tmp_3 = newSeq(100) + :tmp = :tmp_3 + :tmp + :tmp_1 = :tmp_2 + var :tmp_4 = cast[string](:tmp_1) + `=copy`(data, :tmp_4) +finally: + `=destroy`(:tmp_1) + `=destroy`(:tmp) + `=destroy_1`(data) -- end of expandArc ------------------------''' """ diff --git a/tests/lang_objects/destructor/tview_ownership.nim b/tests/lang_objects/destructor/tview_ownership.nim new file mode 100644 index 00000000000..2042d9fa2bf --- /dev/null +++ b/tests/lang_objects/destructor/tview_ownership.nim @@ -0,0 +1,35 @@ +discard """ + description: "Tests that views are correctly treated as not owning" + matrix: "--gc:arc --cursorInference:off" + targets: "c !js !vm" +""" + +import mhelper + +template testCase(name: untyped, pairs: array, code: untyped) {.dirty.} = + block: + numCopies = 0 + numDestroy = 0 + + # wrap the code in a procedure so that variables defined in it are not + # globals (which would interfere with the test) + proc inner() = + code + + inner() + + doAssert (numCopies, numDestroy) in pairs + +testCase "openarray", [(0, 2), (1, 3)]: + let + x = [initValue("a"), initValue("b")] + y = toOpenArray(x, 0, 1)[0] # may copy + +testCase "openarray", [(1, 3)]: + var + x = [initValue("a"), initValue("b")] + y = toOpenArray(x, 0, 1)[0] # must copy + + # modify the source location so that a copy is required: + x[0].content = "c" + doAssert y.content == "a" \ No newline at end of file