diff --git a/compiler/ast/report_enums.nim b/compiler/ast/report_enums.nim index d4da99e7f74b4..4b4afeeef41c9 100644 --- a/compiler/ast/report_enums.nim +++ b/compiler/ast/report_enums.nim @@ -423,6 +423,7 @@ type rsemCallingConventionMismatch rsemHasSideEffects rsemCantPassProcvar + rsemHookCannotRaise rsemUnlistedRaises rsemUnlistedEffects rsemOverrideSafetyMismatch diff --git a/compiler/front/cli_reporter.nim b/compiler/front/cli_reporter.nim index cee55b31a275b..de634c0cc3326 100644 --- a/compiler/front/cli_reporter.nim +++ b/compiler/front/cli_reporter.nim @@ -1303,6 +1303,9 @@ proc reportBody*(conf: ConfigRef, r: SemReport): string = of rsemXCannotRaiseY: result = "'$1' cannot raise '$2'" % [r.ast.render, r.raisesList.render] + of rsemHookCannotRaise: + result = "a hook routine is not allowed to raise. ($1)" % r.typ.render + of rsemUnlistedRaises, rsemWarnUnlistedRaises: result.add("$1 can raise an unlisted exception: " % r.ast.render, r.typ.render) diff --git a/compiler/mir/mirgen.nim b/compiler/mir/mirgen.nim index 572b334956ec4..b2e0aa7b93064 100644 --- a/compiler/mir/mirgen.nim +++ b/compiler/mir/mirgen.nim @@ -1459,12 +1459,13 @@ proc genIf(c: var TCtx, n: PNode, dest: Destination) = template genElifBranch(branch: PNode, extra: untyped) = ## Generates the code for a single ``nkElif(Branch|Expr)`` - let v = genUse(c, branch[0]) - c.subTree mnkIf: - c.use v - c.scope: - genBranch(c, branch.lastSon, dest) - extra + c.scope: + let v = genUse(c, branch[0]) + c.subTree mnkIf: + c.use v + c.scope: + genBranch(c, branch.lastSon, dest) + extra if n.len == 1: # an ``if`` statement/expression with a single branch. Don't emit the @@ -2058,6 +2059,10 @@ proc generateCode*(graph: ModuleGraph, env: var MirEnv, owner: PSym, c.scopeDepth = 1 c.add MirNode(kind: mnkScope) + if sfNeverRaises in owner.flags: + c.add MirNode(kind: mnkTry, len: 1) + c.add MirNode(kind: mnkStmtList) + if owner.kind in routineKinds: # add a 'def' for each ``sink`` parameter. This simplifies further # processing and analysis @@ -2070,6 +2075,21 @@ proc generateCode*(graph: ModuleGraph, env: var MirEnv, owner: PSym, c.add MirNode(kind: mnkNone) gen(c, body) + + if sfNeverRaises in owner.flags: + # if it's enforced that the procedure never raises, exceptions escaping + # the procedure terminate the program. This is achieved by wrapping the + # body in a catch-all exception handler + c.add endNode(mnkStmtList) + c.subTree MirNode(kind: mnkExcept, len: 1): + c.subTree mnkBranch: + c.subTree mnkVoid: + let p = c.graph.getCompilerProc("nimUnhandledException") + c.builder.buildCall c.env.procedures.add(p), p.typ, + typeOrVoid(c, p.typ[0]): + discard + c.add endNode(mnkTry) + c.add endNode(mnkScope) swap(c.env, env) # swap back diff --git a/compiler/sem/injectdestructors.nim b/compiler/sem/injectdestructors.nim index 8b5330280f1ae..8ad20cf2469d0 100644 --- a/compiler/sem/injectdestructors.nim +++ b/compiler/sem/injectdestructors.nim @@ -63,31 +63,6 @@ ## subsequently turning the assignment into a move and thus making the ## assertion fail with an ``IndexDefect``. -# 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 @@ -527,14 +502,7 @@ template buildVoidCall*(bu: var MirBuilder, env: var MirEnv, p: PSym, body: untyped) = let prc = p # prevent multi evaluation bu.subTree mnkVoid: - let kind = - if canRaise(optPanics in graph.config.globalOptions, prc.ast[namePos]): - mnkCheckedCall - else: - mnkCall - - # XXX: injected procedures should not introduce new control-flow paths - bu.subTree MirNode(kind: kind, typ: getVoidType(graph)): + bu.subTree MirNode(kind: mnkCall, typ: getVoidType(graph)): bu.use toValue(env.procedures.add(prc), prc.typ) body diff --git a/compiler/sem/liftdestructors.nim b/compiler/sem/liftdestructors.nim index 7493f01316d9c..8c66b42541622 100644 --- a/compiler/sem/liftdestructors.nim +++ b/compiler/sem/liftdestructors.nim @@ -57,7 +57,6 @@ type asgnForType: PType recurse: bool addMemReset: bool # add wasMoved() call after destructor call - canRaise: bool filterDiscriminator: PSym # we generating destructor for case branch c: PContext # c can be nil, then we are called from lambdalifting! idgen: IdGenerator @@ -152,8 +151,6 @@ proc genContainerOf(c: var TLiftCtx; objType: PType, field, x: PSym): PNode = proc destructorCall(c: var TLiftCtx; op: PSym; x: PNode): PNode = var destroy = newTreeIT(nkCall, x.info, op.typ[0]): [newSymNode(op), genAddr(c, x)] - if sfNeverRaises notin op.flags: - c.canRaise = true if c.addMemReset: result = newTree(nkStmtList): [destroy, genBuiltin(c, mWasMoved, "wasMoved", x)] @@ -303,8 +300,6 @@ proc newHookCall(c: var TLiftCtx; op: PSym; x, y: PNode): PNode = # localReport(c.config, x.info, "usage of '$1' is a user-defined error" % op.name.s) result = newNodeI(nkCall, x.info) result.add newSymNode(op) - if sfNeverRaises notin op.flags: - c.canRaise = true if op.typ.sons[1].kind == tyVar: result.add genAddr(c, x) else: @@ -322,8 +317,6 @@ proc newHookCall(c: var TLiftCtx; op: PSym; x, y: PNode): PNode = proc newOpCall(c: var TLiftCtx; op: PSym; x: PNode): PNode = result = newTreeIT(nkCall, x.info, op.typ[0]): [newSymNode(op), x] - if sfNeverRaises notin op.flags: - c.canRaise = true proc newDeepCopyCall(c: var TLiftCtx; op: PSym; x, y: PNode): PNode = result = newAsgnStmt(x, newOpCall(c, op, y)) @@ -948,7 +941,7 @@ proc produceSym(g: ModuleGraph; c: PContext; typ: PType; kind: TTypeAttachedOp; # bug #19205: Do not forget to also copy the hidden type field: genTypeFieldCopy(a, typ, result.ast[bodyPos], d, src) - if not a.canRaise: incl result.flags, sfNeverRaises + incl result.flags, sfNeverRaises completePartialOp(g, idgen.module, typ, kind, result) @@ -972,7 +965,7 @@ proc produceDestructorForDiscriminator*(g: ModuleGraph; typ: PType; field: PSym, result.ast[bodyPos].add v let placeHolder = newNodeIT(nkSym, info, getSysType(g, info, tyPointer)) fillBody(a, typ, result.ast[bodyPos], d, placeHolder) - if not a.canRaise: incl result.flags, sfNeverRaises + incl result.flags, sfNeverRaises template liftTypeBoundOps*(c: PContext; typ: PType; info: TLineInfo) = diff --git a/compiler/sem/sempass2.nim b/compiler/sem/sempass2.nim index 74b6e2e2c8984..d60c21463459b 100644 --- a/compiler/sem/sempass2.nim +++ b/compiler/sem/sempass2.nim @@ -1788,6 +1788,21 @@ proc trackProc*(c: PContext; s: PSym, body: PNode) = else: effects[tagEffects] = t.tags + # ensure that user-provided hooks have no effects and don't raise + if sfOverriden in s.flags: + # if raising was explicitly disabled (i.e., via ``.raises: []``), + # exceptions, if any, were already reported; don't report errors again in + # that case + if raisesSpec.isNil or raisesSpec.len > 0: + let newSpec = newNodeI(nkArgList, s.info) + checkRaisesSpec(g, rsemHookCannotRaise, newSpec, + t.exc, hints=off, nil) + # override the raises specification to prevent cascading errors: + effects[exceptionEffects] = newSpec + + # enforce that no defects escape the routine at run-time: + s.flags.incl sfNeverRaises + var mutationInfo = MutationInfo() var hasMutationSideEffect = false if {strictFuncs, views} * c.features != {}: diff --git a/compiler/sem/transf.nim b/compiler/sem/transf.nim index 829171cb34c26..0728f544adb86 100644 --- a/compiler/sem/transf.nim +++ b/compiler/sem/transf.nim @@ -373,9 +373,9 @@ proc transformWhile(c: PTransf; n: PNode): PNode = loop[0] = newIntTypeNode(1, c.graph.getSysType(info, tyBool)) loop[0].info = info - # XXX: we need to help ``closureiters`` (which doesn't support 'yield' in - # if conditions...) here and unpack complex condition expressions; - # 'yield' in 'while' conditions would not work otherwise + # unwrap the statement list expression. It helps with the following + # lowering, and it's also necessary for the closure iterator + # transformation var preamble = PNode(nil) if cond.kind in {nkStmtListExpr, nkStmtList}: preamble = newNodeI(nkStmtList, info, cond.len - 1) @@ -384,6 +384,16 @@ proc transformWhile(c: PTransf; n: PNode): PNode = cond = cond[^1] + # all definitions part of the condition expression are part of the while's + # scope, placing the expression into the if's condition slot would thus + # result in incorrect scoping + if not isAtom(cond): + let tmp = newTemp(c, cond.typ, cond.info) + if preamble.isNil: + preamble = newTree(nkStmtList) + preamble.add newTree(nkLetSection, newIdentDefs(tmp, cond)) + cond = tmp + let exit = newTreeI(nkIfStmt, info, newTreeI(nkElifBranch, info, diff --git a/compiler/vm/compilerbridge.nim b/compiler/vm/compilerbridge.nim index 6e14773492430..27ee3988b7a8f 100644 --- a/compiler/vm/compilerbridge.nim +++ b/compiler/vm/compilerbridge.nim @@ -225,7 +225,9 @@ proc buildError(c: TCtx, thread: VmThread, event: sink VmEvent): ExecErrorReport ## Creates an `ExecErrorReport` with the `event` and a stack-trace for ## `thread` let stackTrace = - if event.kind == vmEvtUnhandledException: + if event.kind == vmEvtUnhandledException and event.trace.len > 0: + # HACK: an unhandled exception can be reported without providing a trace. + # Ideally, that shouldn't happen createStackTrace(c, event.trace) else: createStackTrace(c, thread) diff --git a/compiler/vm/vm.nim b/compiler/vm/vm.nim index 67301e53a1ca3..cd22818da907c 100644 --- a/compiler/vm/vm.nim +++ b/compiler/vm/vm.nim @@ -286,22 +286,8 @@ template toException(x: DerefFailureCode): untyped = proc reportException(c: TCtx; trace: sink VmRawStackTrace, raised: LocHandle) = ## Reports the exception represented by `raised` by raising a `VmError` - - let name = $raised.getFieldHandle(1.fpos).deref().strVal - let msg = $raised.getFieldHandle(2.fpos).deref().strVal - - # The reporter expects the exception as a deserialized PNode-tree. Only the - # 2nd (name) and 3rd (msg) field are actually used, so instead of running - # full deserialization (which is also not possible due to no `PType` being - # available), we just create the necessary parts manually - - # TODO: the report should take the two strings directly instead - let empty = newNode(nkEmpty) - let ast = newTree(nkObjConstr, - empty, # constructor type; unused - empty, # unused - newStrNode(nkStrLit, name), - newStrNode(nkStrLit, msg)) + let ast = toExceptionAst($raised.getFieldHandle(1.fpos).deref().strVal, + $raised.getFieldHandle(2.fpos).deref().strVal) raiseVmError(VmEvent(kind: vmEvtUnhandledException, exc: ast, trace: trace)) func cleanUpReg(r: var TFullReg, mm: var VmMemoryManager) = diff --git a/compiler/vm/vmdeps.nim b/compiler/vm/vmdeps.nim index 59918cdb2af2f..95318902f0de3 100644 --- a/compiler/vm/vmdeps.nim +++ b/compiler/vm/vmdeps.nim @@ -428,3 +428,13 @@ proc errorReportToString*(c: ConfigRef, error: Report): string = # the report, so need to add `"Error: "` # manally to stay consistent with the old # output. + +proc toExceptionAst*(name, msg: sink string): PNode = + ## Creates the AST as for an exception object as expected by the report. + # TODO: the report should take the two strings directly instead + let empty = newNode(nkEmpty) + newTree(nkObjConstr, + empty, # constructor type; unused + empty, # unused + newStrNode(nkStrLit, name), + newStrNode(nkStrLit, msg)) diff --git a/compiler/vm/vmops.nim b/compiler/vm/vmops.nim index 70201fd21aec1..3d7db17358704 100644 --- a/compiler/vm/vmops.nim +++ b/compiler/vm/vmops.nim @@ -170,6 +170,17 @@ proc prepareExceptionWrapper(a: VmArgs) {.nimcall.} = deref(a.getHandle(1)).strVal, a.mem.allocator) +proc nimUnhandledExceptionWrapper(a: VmArgs) {.nimcall.} = + # setup the exception AST: + let + exc = a.heap[].tryDeref(a.currentException, noneType).value() + ast = toExceptionAst($exc.getFieldHandle(1.fpos).deref().strVal, + $exc.getFieldHandle(2.fpos).deref().strVal) + # report the unhandled exception: + # XXX: the current stack-trace should be passed along, but we don't + # have access to it here + raiseVmError(VmEvent(kind: vmEvtUnhandledException, exc: ast)) + proc prepareMutationWrapper(a: VmArgs) {.nimcall.} = discard "no-op" @@ -247,6 +258,7 @@ iterator basicOps*(): Override = systemop(getCurrentExceptionMsg) systemop(getCurrentException) systemop(prepareException) + systemop(nimUnhandledException) systemop(prepareMutation) override("stdlib.system.closureIterSetupExc", setCurrentExceptionWrapper) diff --git a/lib/std/tasks.nim b/lib/std/tasks.nim index e2ea5377f4388..ac18862228ee2 100644 --- a/lib/std/tasks.nim +++ b/lib/std/tasks.nim @@ -61,7 +61,7 @@ type Task* = object ## `Task` contains the callback and its arguments. callback: proc (args: pointer) {.nimcall, gcsafe.} args: pointer - destroy: proc (args: pointer) {.nimcall, gcsafe.} + destroy: proc (args: pointer) {.nimcall, gcsafe, raises: [].} proc `=copy`*(x: var Task, y: Task) {.error.} diff --git a/lib/system.nim b/lib/system.nim index 550b60ac13c17..72927a6b123c8 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -2348,6 +2348,9 @@ elif isNimVmTarget: proc prepareException(e: ref Exception, ename: cstring) {.compilerproc.} = discard + proc nimUnhandledException() {.compilerproc.} = + discard + proc closureIterSetupExc(e: ref Exception) {.compilerproc, inline.} = ## Used by the closure transformation pass for preparing for exception ## handling. Implemented as a callback. diff --git a/lib/system/excpt.nim b/lib/system/excpt.nim index 8603fce28b78d..961d02da475ce 100644 --- a/lib/system/excpt.nim +++ b/lib/system/excpt.nim @@ -427,6 +427,12 @@ when true: currException = nil quit(1) +proc nimUnhandledException() {.compilerproc, noreturn.} = + ## Called from generated code when propgation of an exception crosses a + ## routine boundary it shouldn't. + reportUnhandledError(currException) + quit(1) + proc pushActiveException(e: sink(ref Exception)) = e.up = activeException activeException = e diff --git a/lib/system/jssys.nim b/lib/system/jssys.nim index 86b749c47ada0..363d20a976af7 100644 --- a/lib/system/jssys.nim +++ b/lib/system/jssys.nim @@ -115,8 +115,7 @@ proc writeStackTrace() = proc getStackTrace*(): string = rawWriteStackTrace() proc getStackTrace*(e: ref Exception): string = e.trace -proc unhandledException(e: ref Exception) {. - compilerproc, asmNoStackFrame.} = +proc unhandledExceptionString(e: ref Exception): string = var buf = "" if e.msg.len != 0: add(buf, "Error: unhandled exception: ") @@ -128,7 +127,11 @@ proc unhandledException(e: ref Exception) {. add(buf, "]\n") when NimStackTrace: add(buf, rawWriteStackTrace()) - let cbuf = cstring(buf) + result = buf + +proc unhandledException(e: ref Exception) {. + compilerproc, asmNoStackFrame.} = + let cbuf = cstring(unhandledExceptionString(e)) framePtr = nil {.emit: """ if (typeof(Error) !== "undefined") { @@ -139,6 +142,29 @@ proc unhandledException(e: ref Exception) {. } """.} +proc nimUnhandledException() {.compilerproc, asmNoStackFrame.} = + # |NimSkull| exceptions are turned into JavaScript errors for the purpose + # of better error messages + when defined(nodejs): + {.emit: """ + if (lastJSError.m_type !== undefined) { + console.log(`toJSStr`(`unhandledExceptionString`(`lastJSError`))); + } else { + console.log('Error: unhandled exception: ', `lastJSError`) + } + process.exit(1); + """.} + else: + # it's currently not possible to truly panic (abort excution) for non- + # node.js JavaScript + {.emit: """ + if (lastJSError.m_type !== undefined) { + `unhandledException`(lastJSError); + } else { + throw lastJSError; + } + """.} + proc prepareException(e: ref Exception, ename: cstring) {. compilerproc, asmNoStackFrame.} = if e.name.isNil: diff --git a/lib/system/orc.nim b/lib/system/orc.nim index 38da71d9cdaba..ee44add02a0d2 100644 --- a/lib/system/orc.nim +++ b/lib/system/orc.nim @@ -27,7 +27,7 @@ const logOrc = defined(nimArcIds) type - TraceProc = proc (p, env: pointer) {.nimcall, benign.} + TraceProc = proc (p, env: pointer) {.nimcall, benign, raises: [], tags: [].} DisposeProc = proc (p: pointer) {.nimcall, benign.} template color(c): untyped = c.rc and colorMask @@ -472,7 +472,7 @@ proc GC_prepareOrc*(): int {.inline.} = roots.len proc GC_partialCollect*(limit: int) = partialCollect(limit) -proc GC_fullCollect* = +proc GC_fullCollect*() {.raises: [].} = ## Forces a full garbage collection pass. With `--gc:orc` triggers the cycle ## collector. This is an alias for `GC_runOrc`. collectCycles() diff --git a/tests/arc/tarcmisc.nim b/tests/arc/tarcmisc.nim index 0e1dda7fb33b9..3747f6240970d 100644 --- a/tests/arc/tarcmisc.nim +++ b/tests/arc/tarcmisc.nim @@ -112,7 +112,8 @@ type x: int proc `=destroy`(x: var AObj) = - close(x.io) + {.cast(raises: []).}: + close(x.io) echo "closed" var x = B(io: newStringStream("thestream")) diff --git a/tests/arc/tcontrolflow.nim b/tests/arc/tcontrolflow.nim index 80cc2b187eb8c..fd99f9141a9f7 100644 --- a/tests/arc/tcontrolflow.nim +++ b/tests/arc/tcontrolflow.nim @@ -1,12 +1,12 @@ discard """ output: '''begin A elif -end A destroyed +end A begin false if -end false destroyed +end false begin true if end true @@ -19,6 +19,9 @@ true # we use the -d:danger switch to detect uninitialized stack # slots more reliably (there shouldn't be any, of course). +# XXX: the test here need to be improved and turned into a proper +# specification + type Foo = object id: int diff --git a/tests/arc/topt_cursor.nim b/tests/arc/topt_cursor.nim index 2085b3c17d927..4f4cb9f59ff63 100644 --- a/tests/arc/topt_cursor.nim +++ b/tests/arc/topt_cursor.nim @@ -7,10 +7,11 @@ scope: try: def_cursor x: (string, int) = block L0: - if cond: - scope: - x = - break L0 + scope: + if cond: + scope: + x = + break L0 scope: x = def_cursor _0: (string, int) = x @@ -35,11 +36,13 @@ scope: while true: scope: def_cursor _1: File = f - def _2: bool = readLine(arg _1, name res) (raises) - def _3: bool = not(arg _2) - if _3: - scope: - break L0 + def :tmp: bool = readLine(arg _1, name res) (raises) + scope: + def_cursor _2: bool = :tmp + def _3: bool = not(arg _2) + if _3: + scope: + break L0 scope: scope: def_cursor x: string = res diff --git a/tests/arc/topt_no_cursor.nim b/tests/arc/topt_no_cursor.nim index 398a0fe593fb8..1dc6bcab5a425 100644 --- a/tests/arc/topt_no_cursor.nim +++ b/tests/arc/topt_no_cursor.nim @@ -33,17 +33,17 @@ scope: def_cursor _0: Node = target[] def_cursor _1: Node = _0[].parent def sibling: Node - =copy(name sibling, arg _1[].left) (raises) + =copy(name sibling, arg _1[].left) def_cursor _2: Node = sibling def saved: Node - =copy(name saved, arg _2[].right) (raises) + =copy(name saved, arg _2[].right) def_cursor _3: Node = sibling def_cursor _4: Node = saved def_cursor _6: Node = _4[].left - =copy(name _3[].right, arg _6) (raises) + =copy(name _3[].right, arg _6) def_cursor _5: Node = sibling - =sink(name _5[].parent, arg saved) (raises) - =destroy(name sibling) (raises) + =sink(name _5[].parent, arg saved) + =destroy(name sibling) -- end of expandArc ------------------------ --expandArc: p1 @@ -98,11 +98,13 @@ scope: while true: scope: def_cursor _1: int = i - def _2: bool = ltI(arg _1, arg L) - def _3: bool = not(arg _2) - if _3: - scope: - break L0 + def :tmp: bool = ltI(arg _1, arg L) + scope: + def_cursor _2: bool = :tmp + def _3: bool = not(arg _2) + if _3: + scope: + break L0 scope: scope: try: @@ -110,12 +112,13 @@ scope: def line: lent string = borrow a[_4] def_cursor _5: string = line[] def splitted: seq[string] = split(arg _5, arg " ", arg -1) (raises) - def_cursor _6: string = splitted[0] - def _7: bool = eqStr(arg _6, arg "opt") - if _7: - scope: - def_cursor _10: string = splitted[1] - =copy(name lan_ip, arg _10) + scope: + def_cursor _6: string = splitted[0] + def _7: bool = eqStr(arg _6, arg "opt") + if _7: + scope: + def_cursor _10: string = splitted[1] + =copy(name lan_ip, arg _10) def_cursor _8: string = lan_ip echo(arg type(array[0..0, string]), arg _8) (raises) def_cursor _9: string = splitted[1] @@ -130,7 +133,7 @@ scope: scope: try: def shadowScope: Scope - =copy(name shadowScope, arg c[].currentScope) (raises) + =copy(name shadowScope, arg c[].currentScope) rawCloseScope(arg c) (raises) scope: def_cursor _0: Scope = shadowScope @@ -143,11 +146,13 @@ scope: while true: scope: def_cursor _2: int = i - def _3: bool = ltI(arg _2, arg L) - def _4: bool = not(arg _3) - if _4: - scope: - break L0 + def :tmp: bool = ltI(arg _2, arg L) + scope: + def_cursor _3: bool = :tmp + def _4: bool = not(arg _3) + if _4: + scope: + break L0 scope: scope: def_cursor _5: int = i @@ -157,21 +162,22 @@ scope: addInterfaceDecl(arg c, consume _6) (raises) i = addI(arg i, arg 1) (raises) finally: - =destroy(name shadowScope) (raises) + =destroy(name shadowScope) -- end of expandArc ------------------------ --expandArc: treturn scope: try: def x: sink string - def_cursor _0: sink string = x - def _1: int = lengthStr(arg _0) - def _2: bool = eqI(arg _1, arg 2) - if _2: - scope: - result := move x - wasMoved(name x) - return + scope: + def_cursor _0: sink string = x + def _1: int = lengthStr(arg _0) + def _2: bool = eqI(arg _1, arg 2) + if _2: + scope: + result := move x + wasMoved(name x) + return def_cursor _3: sink string = x def _4: int = lengthStr(arg _3) def _5: string = $(arg _4) (raises) @@ -189,14 +195,15 @@ scope: this[].isValid = fileExists(arg _0) (raises) def _1: tuple[dir: string, front: string] block L0: - def_cursor _2: string = this[].value - def _3: bool = dirExists(arg _2) (raises) - if _3: - scope: - def _4: string - =copy(name _4, arg this[].value) - _1 := construct (consume _4, consume "") - break L0 + scope: + def_cursor _2: string = this[].value + def _3: bool = dirExists(arg _2) (raises) + if _3: + scope: + def _4: string + =copy(name _4, arg this[].value) + _1 := construct (consume _4, consume "") + break L0 scope: try: def_cursor _5: string = this[].value @@ -214,15 +221,16 @@ scope: =destroy(name _6) def par: tuple[dir: string, front: string] = move _1 block L1: - def_cursor _10: string = par.0 - def _11: bool = dirExists(arg _10) (raises) - if _11: - scope: - def_cursor _12: string = par.0 - def_cursor _13: string = par.1 - def _14: seq[string] = getSubDirs(arg _12, arg _13) (raises) - =sink(name this[].matchDirs, arg _14) - break L1 + scope: + def_cursor _10: string = par.0 + def _11: bool = dirExists(arg _10) (raises) + if _11: + scope: + def_cursor _12: string = par.0 + def_cursor _13: string = par.1 + def _14: seq[string] = getSubDirs(arg _12, arg _13) (raises) + =sink(name this[].matchDirs, arg _14) + break L1 scope: def _15: seq[string] = construct () =sink(name this[].matchDirs, arg _15) diff --git a/tests/arc/topt_refcursors.nim b/tests/arc/topt_refcursors.nim index 20b5823ee6680..e0d466b1f3ef5 100644 --- a/tests/arc/topt_refcursors.nim +++ b/tests/arc/topt_refcursors.nim @@ -11,11 +11,13 @@ scope: scope: def_cursor _0: Node = it def _1: bool = eqRef(arg _0, arg nil) - def _2: bool = not(arg _1) - def _3: bool = not(arg _2) - if _3: - scope: - break L0 + def :tmp: bool = not(arg _1) + scope: + def_cursor _2: bool = :tmp + def _3: bool = not(arg _2) + if _3: + scope: + break L0 scope: def_cursor _4: Node = it def_cursor _5: string = _4[].s @@ -29,11 +31,13 @@ scope: scope: def_cursor _7: Node = jt def _8: bool = eqRef(arg _7, arg nil) - def _9: bool = not(arg _8) - def _10: bool = not(arg _9) - if _10: - scope: - break L1 + def :tmp: bool = not(arg _8) + scope: + def_cursor _9: bool = :tmp + def _10: bool = not(arg _9) + if _10: + scope: + break L1 scope: def_cursor _11: Node = jt def_cursor ri: Node = _11[].ri diff --git a/tests/arc/topt_wasmoved_destroy_pairs.nim b/tests/arc/topt_wasmoved_destroy_pairs.nim index 3e23ce3ee2555..2d21d939f5bbd 100644 --- a/tests/arc/topt_wasmoved_destroy_pairs.nim +++ b/tests/arc/topt_wasmoved_destroy_pairs.nim @@ -8,11 +8,12 @@ scope: def b: seq[seq[int]] def x: seq[int] = f() (raises) block L0: - if cond: - scope: - def _0: seq[int] = move x - add(name a, consume _0) - break L0 + scope: + if cond: + scope: + def _0: seq[int] = move x + add(name a, consume _0) + break L0 scope: def _1: seq[int] = move x add(name b, consume _1) @@ -35,29 +36,33 @@ scope: while true: scope: def_cursor _0: int = i - def _1: bool = ltI(arg _0, arg b) - def _2: bool = not(arg _1) - if _2: - scope: - break L0 + def :tmp: bool = ltI(arg _0, arg b) + scope: + def_cursor _1: bool = :tmp + def _2: bool = not(arg _1) + if _2: + scope: + break L0 scope: scope: def_cursor i: int = i - def _3: bool = eqI(arg i, arg 2) - if _3: - scope: - return + scope: + def _3: bool = eqI(arg i, arg 2) + if _3: + scope: + return def _4: seq[int] =copy(name _4, arg x) add(name a, consume _4) i = addI(arg i, arg 1) (raises) block L1: - if cond: - scope: - def _5: seq[int] = move x - wasMoved(name x) - add(name a, consume _5) - break L1 + scope: + if cond: + scope: + def _5: seq[int] = move x + wasMoved(name x) + add(name a, consume _5) + break L1 scope: def _6: seq[int] = move x wasMoved(name x) @@ -72,17 +77,19 @@ scope: try: def str: string def x: string = boolToStr(arg cond) - if cond: - scope: - return + scope: + if cond: + scope: + return def _0: string = boolToStr(arg cond) str := move _0 - def _1: bool = not(arg cond) - if _1: - scope: - result := move str - wasMoved(name str) - return + scope: + def _1: bool = not(arg cond) + if _1: + scope: + result := move str + wasMoved(name str) + return finally: =destroy(name x) =destroy(name str) diff --git a/tests/errmsgs/tprefer_raise_spec_error.nim b/tests/errmsgs/tprefer_raise_spec_error.nim new file mode 100644 index 0000000000000..4d92f4de3f03e --- /dev/null +++ b/tests/errmsgs/tprefer_raise_spec_error.nim @@ -0,0 +1,16 @@ +discard """ + description: ''' + An error for violating the explicit `.raises` specification is preferred + over the error that hooks cannot raise + ''' + errormsg: "doRaise() can raise an unlisted exception: ref CatchableError" + line: 16 +""" + +type Obj = object + +proc doRaise() = + raise CatchableError.newException("") + +proc `=copy`(a: var Obj, b: Obj) {.raises: [].} = + doRaise() diff --git a/tests/gc/gctest.nim b/tests/gc/gctest.nim index f241bfaf2f052..36138ce1f1d35 100644 --- a/tests/gc/gctest.nim +++ b/tests/gc/gctest.nim @@ -60,8 +60,7 @@ proc caseTree(lvl: int = 0): PCaseNode = proc `=destroy`(n: var TNode) = assert(addr(n) != nil) - write(stdout, "finalizing: ") - writeLine(stdout, "not nil") + debugEcho "finalizing: not nil" var id: int = 1 diff --git a/tests/lang/s02_core/s99_hooks/README.md b/tests/lang/s02_core/s99_hooks/README.md new file mode 100644 index 0000000000000..786bff439dae6 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/README.md @@ -0,0 +1,21 @@ +## What belongs here + +This section contains tests related to hook procedures, that is, procedures: +- to which calls are statically inserted by the compiler +- that are invoked by the runtime at run-time + +This should cover: +- syntax +- restrictions on the routine definitions +- restrictions on the run-time behaviour (if any) +- where the hooks are injected + +## Assumptions + +- nothing beyond a single module/file +- assertions may still be built-ins +- user-defined types are supported and work +- procedures and calls thereof work +- `var T` is supported as a parameter's type +- raising and catching exceptions work +- tag effect tracking works \ No newline at end of file diff --git a/tests/lang/s02_core/s99_hooks/s99_escaping_defects/README.md b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/README.md new file mode 100644 index 0000000000000..1538b39ff42f5 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/README.md @@ -0,0 +1,3 @@ +If a Defect is raised from a hook routine at run-time, the program immediately +terminates (i.e., panics) and an unhandled exception is reported. This is the +case for both automatic and explicit calls of the hooks. \ No newline at end of file diff --git a/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t01_copy_hook.nim b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t01_copy_hook.nim new file mode 100644 index 0000000000000..af708b1088644 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t01_copy_hook.nim @@ -0,0 +1,17 @@ +discard """ + description: "`=copy` hooks panic when a defect escapes" + outputsub: "Error: unhandled exception: error [Defect]" + exitcode: 1 +""" + +type Type = object + +proc `=copy`(a: var Type, b: Type) = + raise (ref Defect)(msg: "error") + +try: + var x: Type + `=copy`(x, Type()) +finally: + # finally sections are not reached and no cleanup is performed + doAssert false diff --git a/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t02_sink_hook.nim b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t02_sink_hook.nim new file mode 100644 index 0000000000000..e6f7af55f62f4 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t02_sink_hook.nim @@ -0,0 +1,17 @@ +discard """ + description: "`=sink` hooks panic when a defect escapes" + outputsub: "Error: unhandled exception: error [Defect]" + exitcode: 1 +""" + +type Type = object + +proc `=sink`(a: var Type, b: Type) = + raise (ref Defect)(msg: "error") + +try: + var x: Type + `=sink`(x, Type()) +finally: + # finally sections are not reached and no cleanup is performed + doAssert false diff --git a/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t03_destroy_hook.nim b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t03_destroy_hook.nim new file mode 100644 index 0000000000000..5433c981d39a7 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t03_destroy_hook.nim @@ -0,0 +1,17 @@ +discard """ + description: "`=destroy` hooks panic when a defect escapes" + outputsub: "Error: unhandled exception: error [Defect]" + exitcode: 1 +""" + +type Type = object + +proc `=destroy`(a: var Type) = + raise (ref Defect)(msg: "error") + +try: + var x: Type + `=destroy`(x) +finally: + # finally sections are not reached and no cleanup is performed + doAssert false diff --git a/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t04_trace_hook.nim b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t04_trace_hook.nim new file mode 100644 index 0000000000000..65d62b289fc99 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t04_trace_hook.nim @@ -0,0 +1,17 @@ +discard """ + description: "`=trace` hooks panic when a defect escapes" + outputsub: "Error: unhandled exception: error [Defect]" + exitcode: 1 +""" + +type Type = object + +proc `=trace`(a: var Type, p: pointer) = + raise (ref Defect)(msg: "error") + +try: + var x: Type + `=trace`(x, nil) +finally: + # finally sections are not reached and no cleanup is performed + doAssert false diff --git a/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t05_deepcopy_hook.nim b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t05_deepcopy_hook.nim new file mode 100644 index 0000000000000..a4196256bc992 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/s99_escaping_defects/t05_deepcopy_hook.nim @@ -0,0 +1,16 @@ +discard """ + description: "`=deepcopy` hooks panic when a defect escapes" + outputsub: "Error: unhandled exception: error [Defect]" + exitcode: 1 +""" + +type Type = ref object + +proc `=deepcopy`(a: Type): Type = + raise (ref Defect)(msg: "error") + +try: + var x = `=deepcopy`(Type()) +finally: + # finally sections are not reached and no cleanup is performed + doAssert false diff --git a/tests/lang/s02_core/s99_hooks/t99_cannot_raise.nim b/tests/lang/s02_core/s99_hooks/t99_cannot_raise.nim new file mode 100644 index 0000000000000..0232aeed375b8 --- /dev/null +++ b/tests/lang/s02_core/s99_hooks/t99_cannot_raise.nim @@ -0,0 +1,30 @@ +discard """ + description: ''' + Hook routines are not allowed to raise exception. If they're inferred to + raise, a compile-time error is reported. + ''' + action: reject + matrix: "--errorMax:5" +""" + +type Type = object + +proc `=copy`(a: var Type, b: Type) = + raise (ref CatchableError)() #[tt.Error + ^ a hook routine is not allowed to raise. (ref CatchableError)]# + +proc `=sink`(a: var Type, b: Type) = + raise (ref CatchableError)() #[tt.Error + ^ a hook routine is not allowed to raise. (ref CatchableError)]# + +proc `=destroy`(a: var Type) = + raise (ref CatchableError)() #[tt.Error + ^ a hook routine is not allowed to raise. (ref CatchableError)]# + +proc `=trace`(a: var Type, env: pointer) = + raise (ref CatchableError)() #[tt.Error + ^ a hook routine is not allowed to raise. (ref CatchableError)]# + +proc `=deepCopy`(a: ref Type): ref Type = + raise (ref CatchableError)() #[tt.Error + ^ a hook routine is not allowed to raise. (ref CatchableError)]# diff --git a/tests/misc/tnew.nim b/tests/misc/tnew.nim index 318fa644be3f5..1c44bc7b457a8 100644 --- a/tests/misc/tnew.nim +++ b/tests/misc/tnew.nim @@ -20,8 +20,7 @@ type TStressTest = ref array[0..45, array[1..45, TNode]] proc `=destroy`(n: var TNode) = - write(stdout, n.data) - write(stdout, " is now freed\n") + debugEcho n.data, " is now freed" proc newNode(data: int, le, ri: PNode): PNode = new(result)