Fix the DFA for "unstructured controlflow" (#14263)
* Fix the DFA for "unstructured controlflow" * Add testcase from #14233
This commit is contained in:
parent
1a1e9986a3
commit
42db75c970
3 changed files with 142 additions and 81 deletions
169
compiler/dfa.nim
169
compiler/dfa.nim
|
|
@ -45,16 +45,22 @@ type
|
||||||
ControlFlowGraph* = seq[Instr]
|
ControlFlowGraph* = seq[Instr]
|
||||||
|
|
||||||
TPosition = distinct int
|
TPosition = distinct int
|
||||||
|
|
||||||
TBlock = object
|
TBlock = object
|
||||||
|
joins: seq[Instr]
|
||||||
|
forks: seq[TPosition]
|
||||||
|
case isTryBlock: bool
|
||||||
|
of false:
|
||||||
label: PSym
|
label: PSym
|
||||||
fixups: seq[TPosition]
|
breakFixups: seq[(TPosition, seq[PNode])] #Contains the gotos for the breaks along with their pending finales
|
||||||
|
of true:
|
||||||
|
finale: PNode
|
||||||
|
raiseFixups: seq[TPosition] #Contains the gotos for the raises
|
||||||
|
|
||||||
Con = object
|
Con = object
|
||||||
code: ControlFlowGraph
|
code: ControlFlowGraph
|
||||||
inCall, inTryStmt: int
|
inCall, inTryStmt: int
|
||||||
blocks: seq[TBlock]
|
blocks: seq[TBlock]
|
||||||
tryStmtFixups: seq[TPosition]
|
|
||||||
forks: seq[TPosition]
|
|
||||||
owner: PSym
|
owner: PSym
|
||||||
|
|
||||||
proc debugInfo(info: TLineInfo): string =
|
proc debugInfo(info: TLineInfo): string =
|
||||||
|
|
@ -96,7 +102,7 @@ proc echoCfg*(c: ControlFlowGraph; start=0; last = -1) {.deprecated.} =
|
||||||
proc forkI(c: var Con; n: PNode): TPosition =
|
proc forkI(c: var Con; n: PNode): TPosition =
|
||||||
result = TPosition(c.code.len)
|
result = TPosition(c.code.len)
|
||||||
c.code.add Instr(n: n, kind: fork, dest: 0)
|
c.code.add Instr(n: n, kind: fork, dest: 0)
|
||||||
c.forks.add result
|
c.blocks[^1].forks.add result
|
||||||
|
|
||||||
proc gotoI(c: var Con; n: PNode): TPosition =
|
proc gotoI(c: var Con; n: PNode): TPosition =
|
||||||
result = TPosition(c.code.len)
|
result = TPosition(c.code.len)
|
||||||
|
|
@ -271,14 +277,27 @@ proc patch(c: var Con, p: TPosition) =
|
||||||
doAssert(low(int) div 2 + 1 < diff and diff < high(int) div 2)
|
doAssert(low(int) div 2 + 1 < diff and diff < high(int) div 2)
|
||||||
c.code[p].dest = diff
|
c.code[p].dest = diff
|
||||||
|
|
||||||
|
proc gen(c: var Con; n: PNode) # {.noSideEffect.}
|
||||||
|
|
||||||
proc popBlock(c: var Con; oldLen: int) =
|
proc popBlock(c: var Con; oldLen: int) =
|
||||||
for f in c.blocks[oldLen].fixups:
|
var exits: seq[TPosition]
|
||||||
c.patch(f)
|
exits.add c.gotoI(newNode(nkEmpty))
|
||||||
|
for f in c.blocks[oldLen].breakFixups:
|
||||||
|
c.patch(f[0])
|
||||||
|
for finale in f[1]:
|
||||||
|
c.gen(finale)
|
||||||
|
exits.add c.gotoI(newNode(nkEmpty))
|
||||||
|
for e in exits:
|
||||||
|
c.patch e
|
||||||
|
for j in c.blocks[oldLen].joins:
|
||||||
|
var patchedJ = j
|
||||||
|
patchedJ.dest -= c.code.len
|
||||||
|
c.code.add patchedJ
|
||||||
c.blocks.setLen(oldLen)
|
c.blocks.setLen(oldLen)
|
||||||
|
|
||||||
template withBlock(labl: PSym; body: untyped) {.dirty.} =
|
template withBlock(labl: PSym; body: untyped) {.dirty.} =
|
||||||
var oldLen {.gensym.} = c.blocks.len
|
var oldLen {.gensym.} = c.blocks.len
|
||||||
c.blocks.add TBlock(label: labl, fixups: @[])
|
c.blocks.add TBlock(isTryBlock: false, label: labl)
|
||||||
body
|
body
|
||||||
popBlock(c, oldLen)
|
popBlock(c, oldLen)
|
||||||
|
|
||||||
|
|
@ -286,8 +305,6 @@ proc isTrue(n: PNode): bool =
|
||||||
n.kind == nkSym and n.sym.kind == skEnumField and n.sym.position != 0 or
|
n.kind == nkSym and n.sym.kind == skEnumField and n.sym.position != 0 or
|
||||||
n.kind == nkIntLit and n.intVal != 0
|
n.kind == nkIntLit and n.intVal != 0
|
||||||
|
|
||||||
proc gen(c: var Con; n: PNode) # {.noSideEffect.}
|
|
||||||
|
|
||||||
when true:
|
when true:
|
||||||
proc genWhile(c: var Con; n: PNode) =
|
proc genWhile(c: var Con; n: PNode) =
|
||||||
# We unroll every loop 3 times. We emulate 0, 1, 2 iterations
|
# We unroll every loop 3 times. We emulate 0, 1, 2 iterations
|
||||||
|
|
@ -299,6 +316,7 @@ when true:
|
||||||
|
|
||||||
Becomes:
|
Becomes:
|
||||||
|
|
||||||
|
block:
|
||||||
if cond:
|
if cond:
|
||||||
body
|
body
|
||||||
if cond:
|
if cond:
|
||||||
|
|
@ -330,22 +348,22 @@ when true:
|
||||||
if isTrue(n[0]):
|
if isTrue(n[0]):
|
||||||
# 'while true' is an idiom in Nim and so we produce
|
# 'while true' is an idiom in Nim and so we produce
|
||||||
# better code for it:
|
# better code for it:
|
||||||
for i in 0..2:
|
|
||||||
withBlock(nil):
|
withBlock(nil):
|
||||||
|
for i in 0..2:
|
||||||
c.gen(n[1])
|
c.gen(n[1])
|
||||||
else:
|
else:
|
||||||
let oldForksLen = c.forks.len
|
withBlock(nil):
|
||||||
|
let oldForksLen = c.blocks[^1].forks.len
|
||||||
var endings: array[3, TPosition]
|
var endings: array[3, TPosition]
|
||||||
for i in 0..2:
|
for i in 0..2:
|
||||||
withBlock(nil):
|
|
||||||
c.gen(n[0])
|
c.gen(n[0])
|
||||||
endings[i] = c.forkI(n)
|
endings[i] = c.forkI(n)
|
||||||
c.gen(n[1])
|
c.gen(n[1])
|
||||||
for i in countdown(endings.high, 0):
|
for i in countdown(endings.high, 0):
|
||||||
let endPos = endings[i]
|
let endPos = endings[i]
|
||||||
c.patch(endPos)
|
c.patch(endPos)
|
||||||
c.joinI(c.forks.pop(), n)
|
c.joinI(c.blocks[^1].forks.pop(), n)
|
||||||
doAssert(c.forks.len == oldForksLen)
|
doAssert(c.blocks[^1].forks.len == oldForksLen)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
|
|
@ -356,7 +374,7 @@ else:
|
||||||
# body
|
# body
|
||||||
# jmp lab1
|
# jmp lab1
|
||||||
# lab2:
|
# lab2:
|
||||||
let oldForksLen = c.forks.len
|
let oldForksLen = c.blocks[^1].forks.len
|
||||||
let lab1 = c.genLabel
|
let lab1 = c.genLabel
|
||||||
withBlock(nil):
|
withBlock(nil):
|
||||||
if isTrue(n[0]):
|
if isTrue(n[0]):
|
||||||
|
|
@ -368,35 +386,15 @@ else:
|
||||||
c.gen(n[1])
|
c.gen(n[1])
|
||||||
c.jmpBack(n, lab1)
|
c.jmpBack(n, lab1)
|
||||||
c.patch(lab2)
|
c.patch(lab2)
|
||||||
setLen(c.forks, oldForksLen)
|
setLen(c.blocks[^1].forks, oldForksLen)
|
||||||
|
|
||||||
proc genBlock(c: var Con; n: PNode) =
|
|
||||||
withBlock(n[0].sym):
|
|
||||||
c.gen(n[1])
|
|
||||||
|
|
||||||
proc genJoins(c: var Con; n: PNode) =
|
|
||||||
for i in countdown(c.forks.high, 0): joinI(c, c.forks[i], n)
|
|
||||||
|
|
||||||
proc genBreak(c: var Con; n: PNode) =
|
|
||||||
genJoins(c, n)
|
|
||||||
let lab1 = c.gotoI(n)
|
|
||||||
if n[0].kind == nkSym:
|
|
||||||
#echo cast[int](n[0].sym)
|
|
||||||
for i in countdown(c.blocks.len-1, 0):
|
|
||||||
if c.blocks[i].label == n[0].sym:
|
|
||||||
c.blocks[i].fixups.add lab1
|
|
||||||
return
|
|
||||||
#globalError(n.info, "VM problem: cannot find 'break' target")
|
|
||||||
else:
|
|
||||||
c.blocks[c.blocks.high].fixups.add lab1
|
|
||||||
|
|
||||||
template forkT(n, body) =
|
template forkT(n, body) =
|
||||||
let oldLen = c.forks.len
|
let oldLen = c.blocks[^1].forks.len
|
||||||
let lab1 = c.forkI(n)
|
let lab1 = c.forkI(n)
|
||||||
body
|
body
|
||||||
c.patch(lab1)
|
c.patch(lab1)
|
||||||
c.joinI(lab1, n)
|
c.joinI(lab1, n)
|
||||||
setLen(c.forks, oldLen)
|
setLen(c.blocks[^1].forks, oldLen)
|
||||||
|
|
||||||
proc genIf(c: var Con, n: PNode) =
|
proc genIf(c: var Con, n: PNode) =
|
||||||
#[
|
#[
|
||||||
|
|
@ -435,7 +433,7 @@ proc genIf(c: var Con, n: PNode) =
|
||||||
join F1
|
join F1
|
||||||
|
|
||||||
]#
|
]#
|
||||||
let oldLen = c.forks.len
|
let oldLen = c.blocks[^1].forks.len
|
||||||
var endings: seq[TPosition] = @[]
|
var endings: seq[TPosition] = @[]
|
||||||
for i in 0..<n.len:
|
for i in 0..<n.len:
|
||||||
var it = n[i]
|
var it = n[i]
|
||||||
|
|
@ -448,8 +446,8 @@ proc genIf(c: var Con, n: PNode) =
|
||||||
for i in countdown(endings.high, 0):
|
for i in countdown(endings.high, 0):
|
||||||
let endPos = endings[i]
|
let endPos = endings[i]
|
||||||
c.patch(endPos)
|
c.patch(endPos)
|
||||||
c.joinI(c.forks.pop(), n)
|
c.joinI(c.blocks[^1].forks.pop(), n)
|
||||||
doAssert(c.forks.len == oldLen)
|
doAssert(c.blocks[^1].forks.len == oldLen)
|
||||||
|
|
||||||
proc genAndOr(c: var Con; n: PNode) =
|
proc genAndOr(c: var Con; n: PNode) =
|
||||||
# asgn dest, a
|
# asgn dest, a
|
||||||
|
|
@ -476,7 +474,7 @@ proc genCase(c: var Con; n: PNode) =
|
||||||
abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString}
|
abstractVarRange-{tyTypeDesc}).kind notin {tyFloat..tyFloat128, tyString}
|
||||||
|
|
||||||
var endings: seq[TPosition] = @[]
|
var endings: seq[TPosition] = @[]
|
||||||
let oldLen = c.forks.len
|
let oldLen = c.blocks[^1].forks.len
|
||||||
c.gen(n[0])
|
c.gen(n[0])
|
||||||
for i in 1..<n.len:
|
for i in 1..<n.len:
|
||||||
let it = n[i]
|
let it = n[i]
|
||||||
|
|
@ -493,27 +491,64 @@ proc genCase(c: var Con; n: PNode) =
|
||||||
for i in countdown(endings.high, 0):
|
for i in countdown(endings.high, 0):
|
||||||
let endPos = endings[i]
|
let endPos = endings[i]
|
||||||
c.patch(endPos)
|
c.patch(endPos)
|
||||||
c.joinI(c.forks.pop(), n)
|
c.joinI(c.blocks[^1].forks.pop(), n)
|
||||||
doAssert(c.forks.len == oldLen)
|
doAssert(c.blocks[^1].forks.len == oldLen)
|
||||||
|
|
||||||
|
proc genBlock(c: var Con; n: PNode) =
|
||||||
|
withBlock(n[0].sym):
|
||||||
|
c.gen(n[1])
|
||||||
|
|
||||||
|
proc genBreakOrRaiseAux(c: var Con, i: int, n: PNode) =
|
||||||
|
let lab1 = c.gotoI(n)
|
||||||
|
if c.blocks[i].isTryBlock:
|
||||||
|
c.blocks[i].raiseFixups.add lab1
|
||||||
|
else:
|
||||||
|
var trailingFinales: seq[PNode]
|
||||||
|
if c.inTryStmt > 0: #Ok, we are in a try, lets see which (if any) try's we break out from:
|
||||||
|
for b in countdown(c.blocks.high, i):
|
||||||
|
if c.blocks[b].isTryBlock:
|
||||||
|
trailingFinales.add c.blocks[b].finale
|
||||||
|
|
||||||
|
c.blocks[i].breakFixups.add (lab1, trailingFinales)
|
||||||
|
|
||||||
|
for b in countdown(c.blocks.high, i):
|
||||||
|
for f in countdown(c.blocks[b].forks.high, 0):
|
||||||
|
c.blocks[i].joins.add Instr(n: n, kind: join, dest: c.blocks[b].forks[f].int)
|
||||||
|
|
||||||
|
proc genBreak(c: var Con; n: PNode) =
|
||||||
|
if n[0].kind == nkSym:
|
||||||
|
#echo cast[int](n[0].sym)
|
||||||
|
for i in countdown(c.blocks.high, 0):
|
||||||
|
if not c.blocks[i].isTryBlock and c.blocks[i].label == n[0].sym:
|
||||||
|
genBreakOrRaiseAux(c, i, n)
|
||||||
|
return
|
||||||
|
#globalError(n.info, "VM problem: cannot find 'break' target")
|
||||||
|
else:
|
||||||
|
for i in countdown(c.blocks.high, 0):
|
||||||
|
if not c.blocks[i].isTryBlock:
|
||||||
|
genBreakOrRaiseAux(c, i, n)
|
||||||
|
return
|
||||||
|
|
||||||
proc genTry(c: var Con; n: PNode) =
|
proc genTry(c: var Con; n: PNode) =
|
||||||
let oldLen = c.forks.len
|
let oldForksLen = c.blocks[^1].forks.len
|
||||||
var endings: seq[TPosition] = @[]
|
var endings: seq[TPosition] = @[]
|
||||||
inc c.inTryStmt
|
|
||||||
let oldFixups = c.tryStmtFixups.len
|
|
||||||
|
|
||||||
|
let oldLen = c.blocks.len
|
||||||
|
c.blocks.add TBlock(isTryBlock: true, finale: if n[^1].kind == nkFinally: n[^1] else: newNode(nkEmpty))
|
||||||
|
|
||||||
|
inc c.inTryStmt
|
||||||
#let elsePos = c.forkI(n)
|
#let elsePos = c.forkI(n)
|
||||||
c.gen(n[0])
|
c.gen(n[0])
|
||||||
dec c.inTryStmt
|
dec c.inTryStmt
|
||||||
for i in oldFixups..c.tryStmtFixups.high:
|
|
||||||
let f = c.tryStmtFixups[i]
|
|
||||||
c.patch(f)
|
|
||||||
# we also need to produce join instructions
|
|
||||||
# for the 'fork' that might precede the goto instruction
|
|
||||||
if f.int-1 >= 0 and c.code[f.int-1].kind == fork:
|
|
||||||
c.joinI(TPosition(f.int-1), n)
|
|
||||||
|
|
||||||
setLen(c.tryStmtFixups, oldFixups)
|
for f in c.blocks[oldLen].raiseFixups:
|
||||||
|
c.patch(f)
|
||||||
|
for j in c.blocks[oldLen].joins:
|
||||||
|
var patchedJ = j
|
||||||
|
patchedJ.dest -= c.code.len
|
||||||
|
c.code.add patchedJ
|
||||||
|
|
||||||
|
c.blocks.setLen oldLen
|
||||||
|
|
||||||
#c.patch(elsePos)
|
#c.patch(elsePos)
|
||||||
for i in 1..<n.len:
|
for i in 1..<n.len:
|
||||||
|
|
@ -526,15 +561,15 @@ proc genTry(c: var Con; n: PNode) =
|
||||||
for i in countdown(endings.high, 0):
|
for i in countdown(endings.high, 0):
|
||||||
let endPos = endings[i]
|
let endPos = endings[i]
|
||||||
c.patch(endPos)
|
c.patch(endPos)
|
||||||
c.joinI(c.forks.pop(), n)
|
c.joinI(c.blocks[^1].forks.pop(), n)
|
||||||
|
|
||||||
# join the 'elsePos' forkI instruction:
|
# join the 'elsePos' forkI instruction:
|
||||||
#c.joinI(c.forks.pop(), n)
|
#c.joinI(c.blocks[^1].forks.pop(), n)
|
||||||
|
|
||||||
let fin = lastSon(n)
|
let fin = lastSon(n)
|
||||||
if fin.kind == nkFinally:
|
if fin.kind == nkFinally:
|
||||||
c.gen(fin[0])
|
c.gen(fin[0])
|
||||||
doAssert(c.forks.len == oldLen)
|
doAssert(c.blocks[^1].forks.len == oldForksLen)
|
||||||
|
|
||||||
template genNoReturn(c: var Con; n: PNode) =
|
template genNoReturn(c: var Con; n: PNode) =
|
||||||
# leave the graph
|
# leave the graph
|
||||||
|
|
@ -542,9 +577,12 @@ template genNoReturn(c: var Con; n: PNode) =
|
||||||
|
|
||||||
proc genRaise(c: var Con; n: PNode) =
|
proc genRaise(c: var Con; n: PNode) =
|
||||||
gen(c, n[0])
|
gen(c, n[0])
|
||||||
genJoins(c, n)
|
|
||||||
if c.inTryStmt > 0:
|
if c.inTryStmt > 0:
|
||||||
c.tryStmtFixups.add c.gotoI(n)
|
for i in countdown(c.blocks.high, 0):
|
||||||
|
if c.blocks[i].isTryBlock:
|
||||||
|
genBreakOrRaiseAux(c, i, n)
|
||||||
|
return
|
||||||
|
assert false #Unreachable
|
||||||
else:
|
else:
|
||||||
genNoReturn(c, n)
|
genNoReturn(c, n)
|
||||||
|
|
||||||
|
|
@ -557,8 +595,7 @@ proc genReturn(c: var Con; n: PNode) =
|
||||||
gen(c, n[0])
|
gen(c, n[0])
|
||||||
else:
|
else:
|
||||||
genImplicitReturn(c)
|
genImplicitReturn(c)
|
||||||
genJoins(c, n)
|
genBreakOrRaiseAux(c, 0, n)
|
||||||
genNoReturn(c, n)
|
|
||||||
|
|
||||||
const
|
const
|
||||||
InterestingSyms = {skVar, skResult, skLet, skParam, skForVar, skTemp}
|
InterestingSyms = {skVar, skResult, skLet, skParam, skForVar, skTemp}
|
||||||
|
|
@ -708,9 +745,12 @@ proc genCall(c: var Con; n: PNode) =
|
||||||
# lab1:
|
# lab1:
|
||||||
# join F1
|
# join F1
|
||||||
let endGoto = c.forkI(n)
|
let endGoto = c.forkI(n)
|
||||||
c.tryStmtFixups.add c.gotoI(n)
|
for i in countdown(c.blocks.high, 0):
|
||||||
|
if c.blocks[i].isTryBlock:
|
||||||
|
genBreakOrRaiseAux(c, i, n)
|
||||||
|
break
|
||||||
c.patch(endGoto)
|
c.patch(endGoto)
|
||||||
c.joinI(c.forks.pop(), n)
|
c.joinI(c.blocks[^1].forks.pop(), n)
|
||||||
dec c.inCall
|
dec c.inCall
|
||||||
|
|
||||||
proc genMagic(c: var Con; n: PNode; m: TMagic) =
|
proc genMagic(c: var Con; n: PNode; m: TMagic) =
|
||||||
|
|
@ -784,6 +824,7 @@ proc gen(c: var Con; n: PNode) =
|
||||||
proc constructCfg*(s: PSym; body: PNode): ControlFlowGraph =
|
proc constructCfg*(s: PSym; body: PNode): ControlFlowGraph =
|
||||||
## constructs a control flow graph for ``body``.
|
## constructs a control flow graph for ``body``.
|
||||||
var c = Con(code: @[], blocks: @[], owner: s)
|
var c = Con(code: @[], blocks: @[], owner: s)
|
||||||
|
withBlock(s):
|
||||||
gen(c, body)
|
gen(c, body)
|
||||||
genImplicitReturn(c)
|
genImplicitReturn(c)
|
||||||
shallowCopy(result, c.code)
|
shallowCopy(result, c.code)
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,7 @@ proc isLastRead(location: PNode; c: var Con; pc, comesFrom: int): int =
|
||||||
if variantA < 0: return -1
|
if variantA < 0: return -1
|
||||||
var variantB = isLastRead(location, c, pc + c.g[pc].dest, pc)
|
var variantB = isLastRead(location, c, pc + c.g[pc].dest, pc)
|
||||||
if variantB < 0: return -1
|
if variantB < 0: return -1
|
||||||
elif variantB == high(int):
|
pc = min(variantA, variantB)
|
||||||
variantB = variantA
|
|
||||||
pc = variantB
|
|
||||||
of InstrKind.join:
|
of InstrKind.join:
|
||||||
let dest = pc + c.g[pc].dest
|
let dest = pc + c.g[pc].dest
|
||||||
if dest == comesFrom: return pc + 1
|
if dest == comesFrom: return pc + 1
|
||||||
|
|
@ -140,9 +138,7 @@ proc isFirstWrite(location: PNode; c: var Con; pc, comesFrom: int; instr: int):
|
||||||
if variantA < 0: return -1
|
if variantA < 0: return -1
|
||||||
var variantB = isFirstWrite(location, c, pc + c.g[pc].dest, pc, instr + c.g[pc].dest)
|
var variantB = isFirstWrite(location, c, pc + c.g[pc].dest, pc, instr + c.g[pc].dest)
|
||||||
if variantB < 0: return -1
|
if variantB < 0: return -1
|
||||||
elif variantB == high(int):
|
pc = min(variantA, variantB)
|
||||||
variantB = variantA
|
|
||||||
pc = variantB
|
|
||||||
of InstrKind.join:
|
of InstrKind.join:
|
||||||
let dest = pc + c.g[pc].dest
|
let dest = pc + c.g[pc].dest
|
||||||
if dest == comesFrom: return pc + 1
|
if dest == comesFrom: return pc + 1
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ end false
|
||||||
begin true
|
begin true
|
||||||
if
|
if
|
||||||
end true
|
end true
|
||||||
|
7
|
||||||
'''
|
'''
|
||||||
cmd: "nim c --gc:arc -d:danger $file"
|
cmd: "nim c --gc:arc -d:danger $file"
|
||||||
disabled: "true"
|
disabled: "true"
|
||||||
|
|
@ -53,3 +54,26 @@ proc orIsHard(cond: bool) =
|
||||||
|
|
||||||
orIsHard(false)
|
orIsHard(false)
|
||||||
orIsHard(true)
|
orIsHard(true)
|
||||||
|
|
||||||
|
type
|
||||||
|
Control = ref object
|
||||||
|
x: int
|
||||||
|
|
||||||
|
MouseEvent = ref object
|
||||||
|
control: Control
|
||||||
|
button: int
|
||||||
|
|
||||||
|
proc run(data: Control) =
|
||||||
|
var evt = MouseEvent(button: 1)
|
||||||
|
evt.control = data
|
||||||
|
if evt.button == 1:
|
||||||
|
discard
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
echo data.x
|
||||||
|
|
||||||
|
var c = Control(x: 7)
|
||||||
|
|
||||||
|
run(c)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue