diff --git a/compiler/ast/reports.nim b/compiler/ast/reports.nim index 2b07fc42e01..46167aa45d3 100644 --- a/compiler/ast/reports.nim +++ b/compiler/ast/reports.nim @@ -923,7 +923,7 @@ type func incl*(s: var ReportSet, id: ReportId) = s.ids.incl uint32(id) func contains*(s: var ReportSet, id: ReportId): bool = s.ids.contains uint32(id) -func addReport*(list: var ReportList, report: Report): ReportId = +func addReport*(list: var ReportList, report: sink Report): ReportId = ## Add report to the report list list.list.add report result = ReportId(uint32(list.list.high) + 1) diff --git a/compiler/sem/sem.nim b/compiler/sem/sem.nim index 30290fb7ea8..73a878a67fe 100644 --- a/compiler/sem/sem.nim +++ b/compiler/sem/sem.nim @@ -449,15 +449,11 @@ proc tryConstExpr(c: PContext, n: PNode): PNode = c.config.m.errorOutputs = {} c.config.errorMax = high(int) # `setErrorMaxHighMaybe` not appropriate here - try: - result = evalConstExpr(c.module, c.idgen, c.graph, e) - if result == nil or result.kind == nkEmpty: - result = nil - else: - result = fixupTypeAfterEval(c, result, e) - - except ERecoverableError: + result = evalConstExpr(c.module, c.idgen, c.graph, e) + if result == nil or result.kind in {nkEmpty, nkError}: result = nil + else: + result = fixupTypeAfterEval(c, result, e) c.config.errorCounter = oldErrorCount c.config.errorMax = oldErrorMax @@ -475,13 +471,20 @@ proc semConstExpr(c: PContext, n: PNode): PNode = if result == nil: #if e.kind == nkEmpty: globalReport(n.info, errConstExprExpected) result = evalConstExpr(c.module, c.idgen, c.graph, e) - if result == nil or result.kind == nkEmpty: - if e.info != n.info: + assert result != nil + case result.kind + of {nkEmpty, nkError}: + let withContext = e.info != n.info + if withContext: pushInfoContext(c.config, n.info) + + if result.kind == nkEmpty: localReport(c.config, e.info, SemReport(kind: rsemConstExprExpected)) - popInfoContext(c.config) else: - localReport(c.config, e.info, SemReport(kind: rsemConstExprExpected)) + localReport(c.config, result) + + if withContext: + popInfoContext(c.config) # error correction: result = e else: diff --git a/compiler/vm/vm.nim b/compiler/vm/vm.nim index 1db390277c7..290e34fc004 100644 --- a/compiler/vm/vm.nim +++ b/compiler/vm/vm.nim @@ -17,6 +17,8 @@ import parseutils ], ast/[ + errorhandling, + errorreporting, lineinfos, renderer, # toStrLit implementation trees, @@ -54,12 +56,14 @@ import vmprofiler, gorgeimpl, vmdeps, + vmerrors, vmgen, vmdef ] import ast/ast except getstr +import std/options as stdoptions const @@ -71,22 +75,21 @@ when hasFFI: const errIllegalConvFromXtoY = "illegal conversion from '$1' to '$2'" -proc stackTraceImpl( +proc createStackTrace( c: TCtx, sframe: StackFrameIndex, pc: int, - lineInfo: TLineInfo, - infoOrigin: InstantiationInfo, recursionLimit: int = 100 - ) = - ## Generate and report the stack trace starting at frame `sframe` (inclusive). - ## The generated trace length is capped at `recursionLimit`, but always includes - ## the entry function + ): SemReport = + ## Generates a stack-trace report starting at frame `sframe` (inclusive). + ## The amount of entries in the trace is limited to `recursionLimit`, further + ## entries are skipped, though the entry function is always included in the + ## trace assert c.sframes.len > 0 - var res = SemReport(kind: rsemVmStackTrace) - res.currentExceptionA = c.currentExceptionA - res.currentExceptionB = c.currentExceptionB + result = SemReport(kind: rsemVmStackTrace) + result.currentExceptionA = c.currentExceptionA + result.currentExceptionB = c.currentExceptionB block: var i = sframe @@ -105,54 +108,22 @@ proc stackTraceImpl( # the elements are added to the trace in reverse order # (the most recent function is first in the list, not last). # This needs to be accounted for by the actual reporting logic - res.stacktrace.add((sym: f.prc, location: c.debug[pc])) + result.stacktrace.add((sym: f.prc, location: c.debug[pc])) pc = f.comesFrom inc count if count > recursionLimit: - res.skipped = count - recursionLimit + result.skipped = count - recursionLimit - assert res.stacktrace.len() <= recursionLimit # post condition check + assert result.stacktrace.len() <= recursionLimit # post condition check - let action = if c.mode == emRepl: doRaise else: doNothing - let report = wrap(res, infoOrigin, lineInfo) - - c.config.handleReport(report, infoOrigin, action) - - -template stackTrace( - c: TCtx, - sframe: StackFrameIndex, - pc: int, - sem: ReportTypes, - info: TLineInfo, - ) = - stackTraceImpl(c, sframe, pc, info, instLoc()) - localReport(c.config, info, sem) - return - -template stackTrace( - c: TCtx, - sframe: StackFrameIndex, - pc: int, - sem: ReportTypes, - ) = - stackTraceImpl(c, sframe, pc, c.debug[pc], instLoc()) - localReport(c.config, c.debug[pc], sem) - return proc reportException(c: TCtx; sframe: StackFrameIndex, raised: PNode) = - # REFACTOR VM implementation relies on the `stackTrace` calling return, - # but in this proc we are retuning only from it's body, so calling - # `reportException()` does not stop vm loops. This needs to be cleaned up - # - invisible injection of the `return` to control flow of execution is - # an absolute monkey-tier hack. - stackTrace( - c, sframe, c.exceptionInstr, - reportAst(rsemVmUnhandledException, raised)) - + ## Raises an unhandled guest exception `VmError` with `raised` + ## representing the exception + raiseVmError(reportAst(rsemVmUnhandledException, raised)) when not defined(nimComputedGoto): {.pragma: computedGoto.} @@ -534,7 +505,12 @@ proc opConv(c: TCtx; dest: var TFullReg, src: TFullReg, desttyp, srctyp: PType): asgnComplex(dest, src) proc compile(c: var TCtx, s: PSym): int = - result = vmgen.genProc(c, s) + let r = vmgen.genProc(c, s) + if unlikely(not r.success): + raiseVmError(r.report) + + result = r.start + when debugEchoCode: c.codeListing(s, nil, start = result) @@ -543,9 +519,7 @@ template handleJmpBack() {.dirty.} = if allowInfiniteLoops in c.features: c.loopIterations = c.config.maxLoopIterationsVM else: - stackTraceImpl(c, tos, pc, c.debug[pc], instLoc()) - globalReport( - c.config, c.debug[pc], reportSem(rsemVmTooManyIterations)) + raiseVmError(reportSem(rsemVmTooManyIterations)) dec(c.loopIterations) @@ -566,7 +540,7 @@ proc setLenSeq(c: TCtx; node: PNode; newLen: int; info: TLineInfo) = template maybeHandlePtr(node2: PNode, reg: TFullReg, isAssign2: bool): bool = let node = node2 # prevent double evaluation if node.kind == nkNilLit: - stackTrace(c, tos, pc, reportSem(rsemVmNilAccess)) + raiseVmError(reportSem(rsemVmNilAccess)) let typ = node.typ if nfIsPtr in node.flags or (typ != nil and typ.kind == tyPtr): assert node.kind == nkIntLit, $(node.kind) @@ -574,7 +548,7 @@ template maybeHandlePtr(node2: PNode, reg: TFullReg, isAssign2: bool): bool = let typ2 = if typ.kind == tyPtr: typ[0] else: typ if not derefPtrToReg(node.intVal, typ2, reg, isAssign = isAssign2): # tyObject not supported in this context - stackTrace(c, tos, pc, reportTyp(rsemVmDerefUnsupportedPtr, typ)) + raiseVmError(reportTyp(rsemVmDerefUnsupportedPtr, typ)) true else: false @@ -587,11 +561,18 @@ template takeAddress(reg, source) = when defined(gcDestructors): GC_ref source -proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = - assert sframe == c.sframes.high +proc rawExecute(c: var TCtx, pc: var int, tos: var StackFrameIndex): TFullReg = + ## Runs the execution loop, starting in frame `tos` at program counter `pc`. + ## In the case of an error, raises an exception of type `VmError`. + ## + ## If the loop was exited due to an error, `pc` and `tos` will point to the + ## faulting instruction and the active stack-frame respectively. + ## + ## If the loop exits without errors, `pc` points to the last executed + ## instruction and `tos` refers to the stack-frame it executed on + ## + ## tos means top-of-stack - var pc = start - var tos = sframe # Top-of-stack # Used to keep track of where the execution is resumed. var savedPC = -1 var savedFrame: StackFrameIndex @@ -632,6 +613,14 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = tos = nf updateRegsAlias + template guestValidate(cond: bool, msg: string) = + ## Ensures that a guest-input related condition holds true and raises + ## a `VmError` if it doesn't + if unlikely(not cond): # Treat input violations as unlikely + # TODO: don't use a string message here; use proper reports + raiseVmError(reportStr(rsemVmErrInternal, msg)) + + proc reportVmIdx(usedIdx, maxIdx: SomeInteger): SemReport = SemReport( kind: rsemVmIndexError, @@ -727,10 +716,9 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = of rkNodeAddr: regs[ra].intVal = cast[int](regs[rb].nodeAddr) else: - stackTrace( - c, tos, pc, - reportStr( - rsemVmErrInternal, "opcCastPtrToInt: got " & $regs[rb].kind)) + raiseVmError(reportStr( + rsemVmErrInternal, + "opcCastPtrToInt: got " & $regs[rb].kind)) of 2: # tyRef regs[ra].intVal = cast[int](regs[rb].node) @@ -743,14 +731,14 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = of rkInt: node2.intVal = regs[rb].intVal of rkNode: if regs[rb].node.typ.kind notin PtrLikeKinds: - stackTrace(c, tos, pc, reportStr( + raiseVmError(reportStr( rsemVmErrInternal, "opcCastIntToPtr: regs[rb].node.typ: " & $regs[rb].node.typ.kind)) node2.intVal = regs[rb].node.intVal else: - stackTrace(c, tos, pc, reportStr( + raiseVmError(reportStr( rsemVmErrInternal, "opcCastIntToPtr: regs[rb].kind: " & $regs[rb].kind)) @@ -785,7 +773,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = # a = b[c] decodeBC(rkNode) if regs[rc].intVal > high(int): - stackTrace(c, tos, pc, reportVmIdx(regs[rc].intVal, high(int))) + raiseVmError(reportVmIdx(regs[rc].intVal, high(int))) let idx = regs[rc].intVal.int let src = regs[rb].node @@ -794,22 +782,22 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = regs[ra].node = newNodeI(nkCharLit, c.debug[pc]) regs[ra].node.intVal = src.strVal[idx].ord else: - stackTrace(c, tos, pc, reportVmIdx(idx, src.strVal.len - 1)) + raiseVmError(reportVmIdx(idx, src.strVal.len - 1)) elif src.kind notin {nkEmpty..nkFloat128Lit} and idx <% src.len: regs[ra].node = src[idx] else: - stackTrace(c, tos, pc, reportVmIdx(idx, src.safeLen-1)) + raiseVmError(reportVmIdx(idx, src.safeLen-1)) of opcLdArrAddr: # a = addr(b[c]) decodeBC(rkNodeAddr) if regs[rc].intVal > high(int): - stackTrace(c, tos, pc, reportVmIdx(regs[rc].intVal, high(int))) + raiseVmError(reportVmIdx(regs[rc].intVal, high(int))) let idx = regs[rc].intVal.int let src = if regs[rb].kind == rkNode: regs[rb].node else: regs[rb].nodeAddr[] if src.kind notin {nkEmpty..nkTripleStrLit} and idx <% src.len: takeAddress regs[ra], src.sons[idx] else: - stackTrace(c, tos, pc, reportVmIdx(idx, src.safeLen-1)) + raiseVmError(reportVmIdx(idx, src.safeLen-1)) of opcLdStrIdx: decodeBC(rkInt) let idx = regs[rc].intVal.int @@ -817,12 +805,12 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if idx <% s.len: regs[ra].intVal = s[idx].ord else: - stackTrace(c, tos, pc, reportVmIdx(idx, s.len-1)) + raiseVmError(reportVmIdx(idx, s.len-1)) of opcLdStrIdxAddr: # a = addr(b[c]); similar to opcLdArrAddr decodeBC(rkNode) if regs[rc].intVal > high(int): - stackTrace(c, tos, pc, reportVmIdx(regs[rc].intVal, high(int))) + raiseVmError(reportVmIdx(regs[rc].intVal, high(int))) let idx = regs[rc].intVal.int let s = regs[rb].node.strVal.addr # or `byaddr` @@ -835,7 +823,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = node.flags.incl nfIsPtr regs[ra].node = node else: - stackTrace(c, tos, pc, reportVmIdx(idx, s[].len-1)) + raiseVmError(reportVmIdx(idx, s[].len-1)) of opcWrArr: # a[b] = c @@ -846,12 +834,12 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if idx <% arr.strVal.len: arr.strVal[idx] = chr(regs[rc].intVal) else: - stackTrace(c, tos, pc, reportVmIdx(idx, arr.strVal.len-1)) + raiseVmError(reportVmIdx(idx, arr.strVal.len-1)) elif idx <% arr.len: writeField(arr[idx], regs[rc]) else: - stackTrace(c, tos, pc, reportVmIdx(idx, arr.safeLen - 1)) + raiseVmError(reportVmIdx(idx, arr.safeLen - 1)) of opcLdObj: # a = b.c @@ -862,7 +850,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = # for nkPtrLit, this could be supported in the future, use something like: # derefPtrToReg(src.intVal + offsetof(src.typ, rc), typ_field, regs[ra], isAssign = false) # where we compute the offset in bytes for field rc - stackTrace(c, tos, pc, reportAst(rsemVmNilAccess, src, str = $rc)) + raiseVmError(reportAst(rsemVmNilAccess, src, str = $rc)) of nkObjConstr: let n = src[rc + 1].skipColon regs[ra].node = n @@ -875,7 +863,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = let src = if regs[rb].kind == rkNode: regs[rb].node else: regs[rb].nodeAddr[] case src.kind of nkEmpty..nkNilLit: - stackTrace(c, tos, pc, reportSem(rsemVmNilAccess)) + raiseVmError(reportSem(rsemVmNilAccess)) of nkObjConstr: let n = src.sons[rc + 1] if n.kind == nkExprColonExpr: @@ -891,7 +879,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = let shiftedRb = rb + ord(regs[ra].node.kind == nkObjConstr) let dest = regs[ra].node if dest.kind == nkNilLit: - stackTrace(c, tos, pc, reportSem(rsemVmNilAccess)) + raiseVmError(reportSem(rsemVmNilAccess)) elif dest[shiftedRb].kind == nkExprColonExpr: writeField(dest[shiftedRb][1], regs[rc]) else: @@ -902,7 +890,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if idx <% regs[ra].node.strVal.len: regs[ra].node.strVal[idx] = chr(regs[rc].intVal) else: - stackTrace(c, tos, pc, reportVmIdx(idx, regs[ra].node.strVal.len-1)) + raiseVmError(reportVmIdx(idx, regs[ra].node.strVal.len-1)) of opcAddrReg: decodeB(rkRegisterAddr) @@ -915,7 +903,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = of rkNodeAddr: # bug #14339 regs[ra].nodeAddr = regs[rb].nodeAddr else: - stackTrace(c, tos, pc, reportStr( + raiseVmError(reportStr( rsemVmErrInternal, "limited VM support for 'addr', got kind: " & $regs[rb].kind)) of opcLdDeref: @@ -937,8 +925,9 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = ensureKind(rkNode) regs[ra].node = regs[rb].node else: - stackTrace(c, tos, pc, reportStr( - rsemVmNilAccess, " kind: " & $regs[rb].kind)) + raiseVmError(reportStr( + rsemVmNilAccess, + " kind: " & $regs[rb].kind)) of opcWrDeref: # a[] = c; b unused @@ -953,7 +942,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = # TODO: This should likely be handled differently in vmgen. let nAddr = regs[ra].nodeAddr if nAddr[] == nil: - stackTrace(c, tos, pc, reportStr( + raiseVmError(reportStr( rsemVmErrInternal, "opcWrDeref internal error")) # refs bug #16613 if (nfIsRef notin nAddr[].flags and nfIsRef notin n.flags): nAddr[][] = n[] else: nAddr[] = n @@ -963,7 +952,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if not maybeHandlePtr(regs[ra].node, regs[rc], true): regs[ra].node[] = regs[rc].regToNode[] regs[ra].node.flags.incl nfIsRef - else: stackTrace(c, tos, pc, reportSem(rsemVmNilAccess)) + else: raiseVmError(reportSem(rsemVmNilAccess)) of opcAddInt: decodeBC(rkInt) let @@ -973,7 +962,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if (sum xor bVal) >= 0 or (sum xor cVal) >= 0: regs[ra].intVal = sum else: - stackTrace(c, tos, pc, reportSem(rsemVmOverOrUnderflow)) + raiseVmError(reportSem(rsemVmOverOrUnderflow)) of opcAddImmInt: decodeBImm(rkInt) #message(c.config, c.debug[pc], warnUser, "came here") @@ -985,7 +974,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if (sum xor bVal) >= 0 or (sum xor cVal) >= 0: regs[ra].intVal = sum else: - stackTrace(c, tos, pc, reportSem(rsemVmOverOrUnderflow)) + raiseVmError(reportSem(rsemVmOverOrUnderflow)) of opcSubInt: decodeBC(rkInt) let @@ -995,7 +984,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if (diff xor bVal) >= 0 or (diff xor not cVal) >= 0: regs[ra].intVal = diff else: - stackTrace(c, tos, pc, reportSem(rsemVmOverOrUnderflow)) + raiseVmError(reportSem(rsemVmOverOrUnderflow)) of opcSubImmInt: decodeBImm(rkInt) let @@ -1005,7 +994,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if (diff xor bVal) >= 0 or (diff xor not cVal) >= 0: regs[ra].intVal = diff else: - stackTrace(c, tos, pc, reportSem(rsemVmOverOrUnderflow)) + raiseVmError(reportSem(rsemVmOverOrUnderflow)) of opcLenSeq: decodeBImm(rkInt) #assert regs[rb].kind == nkBracket @@ -1059,16 +1048,16 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = elif 32.0 * abs(resAsFloat - floatProd) <= abs(floatProd): regs[ra].intVal = product else: - stackTrace(c, tos, pc, reportSem(rsemVmOverOrUnderflow)) + raiseVmError(reportSem(rsemVmOverOrUnderflow)) of opcDivInt: decodeBC(rkInt) if regs[rc].intVal == 0: - stackTrace(c, tos, pc, reportSem(rsemVmDivisionByConstZero)) + raiseVmError(reportSem(rsemVmDivisionByConstZero)) else: regs[ra].intVal = regs[rb].intVal div regs[rc].intVal of opcModInt: decodeBC(rkInt) if regs[rc].intVal == 0: - stackTrace(c, tos, pc, reportSem(rsemVmDivisionByConstZero)) + raiseVmError(reportSem(rsemVmDivisionByConstZero)) else: regs[ra].intVal = regs[rb].intVal mod regs[rc].intVal of opcAddFloat: @@ -1215,7 +1204,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if val != int64.low: regs[ra].intVal = -val else: - stackTrace(c, tos, pc, reportSem(rsemVmOverOrUnderflow)) + raiseVmError(reportSem(rsemVmOverOrUnderflow)) of opcUnaryMinusFloat: decodeB(rkFloat) assert regs[rb].kind == rkFloat @@ -1276,7 +1265,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if regs[ra].node.kind == nkBracket: regs[ra].node.add(copyValue(regs[rb].regToNode)) else: - stackTrace(c, tos, pc, reportSem(rsemVmNilAccess)) + raiseVmError(reportSem(rsemVmNilAccess)) of opcGetImpl: decodeB(rkNode) var a = regs[rb].node @@ -1286,7 +1275,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = else: copyTree(a.sym.ast) regs[ra].node.flags.incl nfIsRef else: - stackTrace(c, tos, pc, reportSem(rsemVmNodeNotASymbol)) + raiseVmError(reportSem(rsemVmNodeNotASymbol)) of opcGetImplTransf: decodeB(rkNode) @@ -1309,7 +1298,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = else: newSymNode(a.sym.skipGenericOwner) regs[ra].node.flags.incl nfIsRef else: - stackTrace(c, tos, pc, reportSem(rsemVmNodeNotASymbol)) + raiseVmError(reportSem(rsemVmNodeNotASymbol)) of opcSymIsInstantiationOf: decodeBC(rkInt) let a = regs[rb].node @@ -1320,7 +1309,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = if sfFromGeneric in a.sym.flags and a.sym.owner == b.sym: 1 else: 0 else: - stackTrace(c, tos, pc, reportSem(rsemVmNodeNotAProcSymbol)) + raiseVmError(reportSem(rsemVmNodeNotAProcSymbol)) of opcEcho: let rb = instr.regB template fn(s: string) = @@ -1363,7 +1352,7 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = let rc = instr.regC if not (leValueConv(regs[rb].regToNode, regs[ra].regToNode) and leValueConv(regs[ra].regToNode, regs[rc].regToNode)): - stackTrace(c, tos, pc, reportStr( + raiseVmError(reportStr( rsemVmIllegalConv, errIllegalConvFromXtoY % [ $regs[ra].regToNode, @@ -1385,19 +1374,18 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = currentLineInfo: c.debug[pc])) elif importcCond(c, prc): if compiletimeFFI notin c.config.features: - globalReport(c.config, c.debug[pc], SemReport(kind: rsemVmEnableFFIToImportc)) + raiseVmError(SemReport(kind: rsemVmEnableFFIToImportc)) # we pass 'tos.slots' instead of 'regs' so that the compiler can keep # 'regs' in a register: when hasFFI: if prc.position - 1 < 0: - globalError( - c.config, - c.debug[pc], + raiseVmError( reportStr(rsemVmGlobalError, "VM call invalid: prc.position: " & $prc.position)) let prcValue = c.globals[prc.position-1] if prcValue.kind == nkEmpty: - globalReport(c.config, c.debug[pc], "cannot run " & prc.name.s) + raiseVmError( + reportStr(rsemVmErrInternal, "cannot run " & prc.name.s)) var slots2: TNodeSeq slots2.setLen(tos.slots.len) for i in 0.. max: - stackTrace(c, tos, pc, reportSem(rsemVmOutOfRange)) + raiseVmError(reportSem(rsemVmOutOfRange)) of opcNarrowU: decodeB(rkInt) regs[ra].intVal = regs[ra].intVal and ((1'i64 shl rb)-1) @@ -1724,9 +1712,9 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = let idx = regs[rc].intVal.int let src = regs[rb].node if src.kind in {nkEmpty..nkNilLit}: - stackTrace(c, tos, pc, reportAst(rsemVmCannotGetChild, src)) + raiseVmError(reportAst(rsemVmCannotGetChild, src)) elif idx >=% src.len: - stackTrace(c, tos, pc, reportVmIdx(idx, src.len - 1)) + raiseVmError(reportVmIdx(idx, src.len - 1)) else: regs[ra].node = src[idx] of opcNSetChild: @@ -1734,21 +1722,21 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = let idx = regs[rb].intVal.int var dest = regs[ra].node if nfSem in dest.flags and allowSemcheckedAstModification notin c.config.legacyFeatures: - stackTrace(c, tos, pc, reportSem(rsemVmCannotModifyTypechecked)) + raiseVmError(reportSem(rsemVmCannotModifyTypechecked)) elif dest.kind in {nkEmpty..nkNilLit}: - stackTrace(c, tos, pc, reportAst(rsemVmCannotSetChild, dest)) + raiseVmError(reportAst(rsemVmCannotSetChild, dest)) elif idx >=% dest.len: - stackTrace(c, tos, pc, reportVmIdx(idx, dest.len - 1)) + raiseVmError(reportVmIdx(idx, dest.len - 1)) else: dest[idx] = regs[rc].node of opcNAdd: decodeBC(rkNode) var u = regs[rb].node if nfSem in u.flags and allowSemcheckedAstModification notin c.config.legacyFeatures: - stackTrace(c, tos, pc, reportSem(rsemVmCannotModifyTypechecked)) + raiseVmError(reportSem(rsemVmCannotModifyTypechecked)) elif u.kind in {nkEmpty..nkNilLit}: echo c.config $ c.debug[pc] - stackTrace(c, tos, pc, reportAst(rsemVmCannotAddChild, u)) + raiseVmError(reportAst(rsemVmCannotAddChild, u)) else: u.add(regs[rc].node) regs[ra].node = u @@ -1757,9 +1745,9 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = let x = regs[rc].node var u = regs[rb].node if nfSem in u.flags and allowSemcheckedAstModification notin c.config.legacyFeatures: - stackTrace(c, tos, pc, reportSem(rsemVmCannotModifyTypechecked)) + raiseVmError(reportSem(rsemVmCannotModifyTypechecked)) elif u.kind in {nkEmpty..nkNilLit}: - stackTrace(c, tos, pc, reportAst(rsemVmCannotAddChild, u)) + raiseVmError(reportAst(rsemVmCannotAddChild, u)) else: for i in 0.. 0: typ = typ[0] createStr regs[ra] regs[ra].node.strVal = typ.typeToString(preferExported) @@ -2362,22 +2353,97 @@ proc rawExecute(c: var TCtx, start: int, sframe: StackFrameIndex): TFullReg = inc pc -proc execute(c: var TCtx, start: int, frame: sink TStackFrame): PNode {.inline.} = +type ExecutionResult = object + case success: bool + of false: + stackTrace: SemReport ## The report storing the stack-trace + report: SemReport ## The report detailing the error + of true: + result: PNode ## The evaluated value/tree (nkEmpty if nothing was returned) + +proc execute(c: var TCtx, start: int, frame: sink TStackFrame): ExecutionResult {.inline.} = assert c.sframes.len == 0 c.sframes.add frame - result = rawExecute(c, start, 0).regToNode - assert c.sframes.len == 1 - c.sframes.setLen(0) + var pc = start + var sframe = c.sframes.high + try: + let r = rawExecute(c, pc, sframe).regToNode + assert c.sframes.len == 1 + result = ExecutionResult(success: true, result: r) + except VmError as e: + # Execution failed, generate a stack-trace and wrap the error payload + # into an `ExecutionResult` + let trace = createStackTrace(c, sframe, pc) + result = ExecutionResult( + success: false, + stackTrace: trace, + report: move e.report) + + if result.report.location.isNone(): + # Use the location info of the failing instruction if none is provided + result.report.location = some(c.debug[pc]) + + # Set stack-trace report information + result.stackTrace.location = some(c.debug[pc]) + result.stackTrace.reportInst = toReportLineInfo(instLoc()) + + finally: + # Clean up frames whenever leaving execution + c.sframes.setLen(0) + +proc unpackResult(res: sink ExecutionResult; config: ConfigRef, node: PNode; wrap = true): PNode = + ## Unpacks the execution result. If the result represents a failure, returns + ## a new `nkError` wrapping `node`. Otherwise, returns the value/tree result, + ## optionally filling in the node's `info` with that of `node`, if not + ## present already. + ## + ## `wrap` (defaulting to true) indicates whether the above described + ## behaviour should take place or if the error should be handled + ## directly (via `handleReport`) + if res.success: + result = res.result + if node != nil and result.info.line < 0: + result.info = node.info + else: + let errKind = res.report.kind -proc execute(c: var TCtx, start: int): PNode = + let stId = config.addReport(wrap(res.stackTrace)) + let rId = config.addReport(wrap(res.report)) + + if wrap: + result = newError(config, node, errKind, rId, instLoc()) + result.sons.add(newIntNode(nkIntLit, ord(stId))) + else: + # XXX: `report` is a temporary solution in order to report errors + # from locations where no node is returned (e.g. `evalStaticStmt`) + config.handleReport(stId, instLoc(), doNothing) + config.handleReport(rId, instLoc(), doNothing) + +proc execute(c: var TCtx, start: int): ExecutionResult = # XXX: instead of building the object first and then adding it to the list, it could # also be done in reverse. Which style is prefered? var tos = TStackFrame(prc: nil, comesFrom: 0, next: -1) newSeq(tos.slots, c.prc.regInfo.len) execute(c, start, tos) +proc unpackResult(r: sink VmGenResult, conf: ConfigRef, node: PNode, start: var int): PNode = + ## Unpacks the vmgen result. If the result represents a failure, returns a + ## new `nkError` wrapping `node`. Otherwise, sets `start` to the instruction + ## offset given by the result and returns `nil` + if r.success: + result = nil + start = r.start + else: + let kind = r.report.kind + let rid = conf.addReport(wrap(r.report)) + + result = newError(conf, node, kind, rid, instLoc()) + proc execProc*(c: var TCtx; sym: PSym; args: openArray[PNode]): PNode = + # XXX: `localReport` is still used here since execProc is only used by the + # VM's compilerapi (`nimeval`) whose users don't know about nkError yet + c.loopIterations = c.config.maxLoopIterationsVM if sym.kind in routineKinds: if sym.typ.len-1 != args.len: @@ -2389,7 +2455,11 @@ proc execProc*(c: var TCtx; sym: PSym; args: openArray[PNode]): PNode = got: toInt128(args.len)))) else: - let start = genProc(c, sym) + var start: int + result = genProc(c, sym).unpackResult(c.config, nil, start) + if unlikely(result != nil): + localReport(c.config, result) + return nil var tos = TStackFrame(prc: sym, comesFrom: 0, next: -1) let maxSlots = sym.offset @@ -2402,26 +2472,37 @@ proc execProc*(c: var TCtx; sym: PSym; args: openArray[PNode]): PNode = for i in 1..= high(TRegister): - globalReport(cc.config, cc.bestEffort, SemReport( - kind: rsemTooManyRegistersRequired)) + fail(cc.bestEffort, rsemTooManyRegistersRequired) result = TRegister(max(c.regInfo.len, start)) c.regInfo.setLen int(result)+1 @@ -273,7 +320,7 @@ proc getTempRange(cc: var TCtx; n: int; kind: TSlotKind): TRegister = for k in result..result+n-1: c.regInfo[k] = (inUse: true, kind: kind) return if c.regInfo.len+n >= high(TRegister): - globalReport(cc.config, cc.bestEffort, reportSem(rsemTooManyRegistersRequired)) + fail(cc.bestEffort, rsemTooManyRegistersRequired) result = TRegister(c.regInfo.len) setLen c.regInfo, c.regInfo.len+n for k in result..result+n-1: c.regInfo[k] = (inUse: true, kind: kind) @@ -386,7 +433,7 @@ proc genBreak(c: var TCtx; n: PNode) = if c.prc.blocks[i].label == n[0].sym: c.prc.blocks[i].fixups.add lab1 return - globalReport(c.config, n.info, reportSem(rsemVmCannotFindBreakTarget)) + fail(n.info, rsemVmCannotFindBreakTarget) else: c.prc.blocks[c.prc.blocks.high].fixups.add lab1 @@ -488,7 +535,7 @@ proc genLiteral(c: var TCtx; n: PNode): int = proc unused(c: TCtx; n: PNode; x: TDest) {.inline.} = if x >= 0: - globalReport(c.config, n.info, reportAst(rsemVmNotUnused, n)) + fail(n.info, rsemVmNotUnused, n) proc genCase(c: var TCtx; n: PNode; dest: var TDest) = # if (!expr1) goto lab1; @@ -627,11 +674,11 @@ proc needsAsgnPatch(n: PNode): bool = proc genField(c: TCtx; n: PNode): TRegister = if n.kind != nkSym or n.sym.kind != skField: - globalReport(c.config, n.info, reportAst(rsemNotAFieldSymbol, n)) + fail(n.info, rsemNotAFieldSymbol, ast = n) let s = n.sym if s.position > high(typeof(result)): - globalReport(c.config, n.info, reportSym(rsemVmTooLargetOffset, s)) + fail(n.info, rsemVmTooLargetOffset, sym = s) result = s.position @@ -678,7 +725,7 @@ proc genAsgnPatch(c: var TCtx; le: PNode, value: TRegister) = c.freeTemp(dest) of nkError: # XXX: do a better job with error generation - globalReport(c.config, le.info, reportAst(rsemVmCannotGenerateCode, le)) + fail(le.info, rsemVmCannotGenerateCode, le) else: discard @@ -942,10 +989,13 @@ proc genCastIntFloat(c: var TCtx; n: PNode; dest: var TDest) = genLit(c, n[1], dest) else: # todo: support cast from tyInt to tyRef - globalReport(c.config, n.info, SemReport( - kind: rsemVmCannotCast, - typeMismatch: @[c.config.typeMismatch( - actual = dst, formal = src)])) + raiseVmGenError( + SemReport( + kind: rsemVmCannotCast, + typeMismatch: @[c.config.typeMismatch( + actual = dst, formal = src)]), + n.info, + instLoc()) proc genVoidABC(c: var TCtx, n: PNode, dest: TDest, opcode: TOpcode) = unused(c, n, dest) @@ -1358,8 +1408,8 @@ proc genMagic(c: var TCtx; n: PNode; dest: var TDest; m: TMagic) = c.genCall(n, dest) of mExpandToAst: if n.len != 2: - globalReport(c.config, n.info, reportStr( - rsemVmBadExpandToAst, "expandToAst requires 1 argument")) + fail(n.info, rsemVmBadExpandToAst, + str = "expandToAst requires 1 argument") let arg = n[1] if arg.kind in nkCallKinds: @@ -1370,20 +1420,17 @@ proc genMagic(c: var TCtx; n: PNode; dest: var TDest; m: TMagic) = # do not call clearDest(n, dest) here as getAst has a meta-type as such # produces a value else: - globalReport(c.config, n.info, reportStr( - rsemVmBadExpandToAst, "expandToAst requires a call expression")) + fail(n.info, rsemVmBadExpandToAst, + str = "expandToAst requires a call expression") of mSizeOf: - globalReport(c.config, n.info, reportStr( - rsemMissingImportcCompleteStruct, "sizeof")) + fail(n.info, rsemMissingImportcCompleteStruct, str = "sizeof") of mAlignOf: - globalReport(c.config, n.info, reportStr( - rsemMissingImportcCompleteStruct, "alignof")) + fail(n.info, rsemMissingImportcCompleteStruct, str = "alignof") of mOffsetOf: - globalReport(c.config, n.info, reportStr( - rsemMissingImportcCompleteStruct, "offsetof")) + fail(n.info, rsemMissingImportcCompleteStruct, str = "offsetof") of mRunnableExamples: discard "just ignore any call to runnableExamples" @@ -1403,8 +1450,7 @@ proc genMagic(c: var TCtx; n: PNode; dest: var TDest; m: TMagic) = c.genUnaryABC(n, dest, opcNodeId) else: # mGCref, mGCunref, - globalReport(c.config, n.info, reportStr( - rsemVmCannotGenerateCode, $m)) + fail(n.info, rsemVmCannotGenerateCode, str = $m) proc unneededIndirection(n: PNode): bool = @@ -1482,18 +1528,11 @@ proc setSlot(c: var TCtx; v: PSym) = if v.position == 0: v.position = getFreeRegister(c, if v.kind == skLet: slotFixedLet else: slotFixedVar, start = 1) -proc cannotEval(c: TCtx; n: PNode) {.noinline.} = - globalReport(c.config, n.info, reportAst(rsemVmCannotEvaluateAtComptime, n)) - # HACK REFACTOR FIXME With current compiler 'arhitecture' this call - # MUST raise an exception that is captured by `sem.tryConstExpr` in sem. In - # the future this needs to be removed, `checkCanEval` must return a - # `true/false` bool. - # - # For more elaborate explanation of the related code see the comment - # https://github.com/nim-works/nimskull/pull/94#issuecomment-1006927599 - # - # This code must not be reached - raiseRecoverableError("vmgen.cannotEval failed") +func cannotEval(c: TCtx; n: PNode) {.noinline, noreturn.} = + raiseVmGenError( + reportAst(rsemVmCannotEvaluateAtComptime, n), + n.info, + instLoc()) func isOwnedBy(a, b: PSym): bool = var a = a.owner @@ -1551,7 +1590,7 @@ proc genAsgn(c: var TCtx; le, ri: PNode; requiresCopy: bool) = case le.kind of nkError: # XXX: do a better job with error generation - globalReport(c.config, le.info, reportAst(rsemVmCannotGenerateCode, le)) + fail(le.info, rsemVmCannotGenerateCode, le) of nkBracketExpr: let dest = c.genx(le[0], {gfNode}) @@ -1840,8 +1879,10 @@ proc getNullValueAux(t: PType; obj: PNode, result: PNode; conf: ConfigRef; currP doAssert obj.sym.position == currPosition inc currPosition else: - globalReport(conf, result.info, reportAst( - rsemVmCannotCreateNullElement, obj)) + # XXX: `getNullValueAux` is called from both inside `vmgen` and + # `rawExecute` so we use `raiseVmError` here + raiseVmError(reportAst( + rsemVmCannotCreateNullElement, obj), result.info) proc getNullValue(typ: PType, info: TLineInfo; conf: ConfigRef): PNode = var t = skipTypes(typ, abstractRange+{tyStatic, tyOwned}-{tyTypeDesc}) @@ -1884,9 +1925,7 @@ proc getNullValue(typ: PType, info: TLineInfo; conf: ConfigRef): PNode = of tySequence, tyOpenArray: result = newNodeIT(nkBracket, info, t) else: - globalReport(conf, info, reportTyp(rsemVmCannotCreateNullElement, t)) - - result = newNodeI(nkEmpty, info) + raiseVmError(reportTyp(rsemVmCannotCreateNullElement, t), info) proc genVarSection(c: var TCtx; n: PNode) = for a in n: @@ -1997,8 +2036,7 @@ proc genObjConstr(c: var TCtx, n: PNode, dest: var TDest) = dest, idx, tmp) c.freeTemp(tmp) else: - globalReport(c.config, n.info, reportAst( - rsemVmInvalidObjectConstructor, it)) + fail(n.info, rsemVmInvalidObjectConstructor, it) proc genTupleConstr(c: var TCtx, n: PNode, dest: var TDest) = if dest < 0: dest = c.getTemp(n.typ) @@ -2018,7 +2056,7 @@ proc genTupleConstr(c: var TCtx, n: PNode, dest: var TDest) = c.preventFalseAlias(it, opcWrObj, dest, i.TRegister, tmp) c.freeTemp(tmp) -proc genProc*(c: var TCtx; s: PSym): int +proc genProc*(c: var TCtx; s: PSym): VmGenResult func matches(s: PSym; x: string): bool = let y = x.split('.') @@ -2046,7 +2084,7 @@ proc gen(c: var TCtx; n: PNode; dest: var TDest; flags: TGenFlags = {}) = case n.kind of nkError: # XXX: do a better job with error generation - globalReport(c.config, n.info, reportAst(rsemVmCannotGenerateCode, n)) + fail(n.info, rsemVmCannotGenerateCode, n) of nkSym: let s = n.sym @@ -2058,7 +2096,7 @@ proc gen(c: var TCtx; n: PNode; dest: var TDest; flags: TGenFlags = {}) = of skProc, skFunc, skConverter, skMacro, skTemplate, skMethod, skIterator: # 'skTemplate' is only allowed for 'getAst' support: if s.kind == skIterator and s.typ.callConv == TCallingConvention.ccClosure: - globalReport(c.config, n.info, reportSym(rsemVmNoClosureIterators, s)) + fail(n.info, rsemVmNoClosureIterators, sym = s) if procIsCallback(c, s): discard elif importcCond(c, s): c.importcSym(n.info, s) genLit(c, n, dest) @@ -2081,16 +2119,18 @@ proc gen(c: var TCtx; n: PNode; dest: var TDest; flags: TGenFlags = {}) = if c.prc.sym != nil and c.prc.sym.kind == skMacro: genRdVar(c, n, dest, flags) else: - globalReport(c.config, n.info, reportSym( - rsemVmCannotGenerateCode, s, + fail(n.info, + rsemVmCannotGenerateCode, + sym = s, str = "Attempt to generate VM code for generic parameter in non-macro proc" - )) + ) else: - globalReport(c.config, n.info, reportSym( - rsemVmCannotGenerateCode, s, + fail(n.info, + rsemVmCannotGenerateCode, + sym = s, str = "Unexpected symbol for VM code - " & $s.kind - )) + ) of nkCallKinds: if n[0].kind == nkSym: let s = n[0].sym @@ -2201,7 +2241,7 @@ proc gen(c: var TCtx; n: PNode; dest: var TDest; flags: TGenFlags = {}) = if n.typ != nil and n.typ.isCompileTimeOnly: genTypeLit(c, n.typ, dest) else: - globalReport(c.config, n.info, reportAst(rsemVmCannotGenerateCode, n)) + fail(n.info, rsemVmCannotGenerateCode, n) func removeLastEof(c: var TCtx) = let last = c.code.len-1 @@ -2211,25 +2251,33 @@ func removeLastEof(c: var TCtx) = c.code.setLen(last) c.debug.setLen(last) -proc genStmt*(c: var TCtx; n: PNode): int = +proc genStmt*(c: var TCtx; n: PNode): VmGenResult = c.removeLastEof - result = c.code.len - var d: TDest = -1 - c.gen(n, d) - c.gABC(n, opcEof) - c.config.internalAssert(d < 0, n.info, "VM problem: dest register is set") + let start = c.code.len + result = wrap(start): + var d: TDest = -1 + c.gen(n, d) + c.gABC(n, opcEof) + c.config.internalAssert(d < 0, n.info, "VM problem: dest register is set") + -proc genExpr*(c: var TCtx; n: PNode, requiresValue = true): int = +proc genExpr*(c: var TCtx; n: PNode, requiresValue = true): VmGenResult = c.removeLastEof - result = c.code.len + let start = c.code.len var d: TDest = -1 - c.gen(n, d) + result = wrap(start): + c.gen(n, d) + + if unlikely(not result.success): return + if d < 0: c.config.internalAssert(not requiresValue, n.info, "VM problem: dest register is not set") d = 0 c.gABC(n, opcEof, d) + result = VmGenResult(success: true, start: start) + #echo renderTree(n) #c.echoCode(result) @@ -2303,7 +2351,7 @@ proc optimizeJumps(c: var TCtx; start: int) = c.finalJumpTarget(i, d - i) else: discard -proc genProc(c: var TCtx; s: PSym): int = +proc genProc*(c: var TCtx; s: PSym): VmGenResult = let pos = c.procToCodePos.getOrDefault(s.id) wasNotGenProcBefore = pos == 0 @@ -2321,8 +2369,8 @@ proc genProc(c: var TCtx; s: PSym): int = c.code.setLen(last) c.debug.setLen(last) #c.removeLastEof - result = c.code.len+1 # skip the jump instruction - c.procToCodePos[s.id] = result + let start = c.code.len+1 # skip the jump instruction + c.procToCodePos[s.id] = start # thanks to the jmp we can add top level statements easily and also nest # procs easily: let body = transformBody(c.graph, c.idgen, s, cache = not isCompileTimeProc(s)) @@ -2341,12 +2389,16 @@ proc genProc(c: var TCtx; s: PSym): int = #let env = s.ast[paramsPos].lastSon.sym #assert env.position == 2 c.prc.regInfo.add (inUse: true, kind: slotFixedLet) - gen(c, body) + + result = wrap(start): + gen(c, body) + if unlikely(not result.success): return + # generate final 'return' statement: c.gABC(body, opcRet) c.patch(procStart) c.gABC(body, opcEof, eofInstr.regA) - c.optimizeJumps(result) + c.optimizeJumps(start) s.offset = c.prc.regInfo.len #if s.name.s == "main" or s.name.s == "[]": # echo renderTree(body) @@ -2354,4 +2406,4 @@ proc genProc(c: var TCtx; s: PSym): int = c.prc = oldPrc else: c.prc.regInfo.setLen s.offset - result = pos + result = VmGenResult(success: true, start: pos) \ No newline at end of file diff --git a/compiler/vm/vmops.nim b/compiler/vm/vmops.nim index 02f84167628..935156b45e9 100644 --- a/compiler/vm/vmops.nim +++ b/compiler/vm/vmops.nim @@ -152,25 +152,6 @@ when defined(nimHasInvariant): of MultipleValueSetting.cincludes: copySeq(conf.cIncludes) of MultipleValueSetting.clibs: copySeq(conf.cLibs) -proc stackTrace2(c: PCtx, report: SemReport, n: PNode) = - # TODO: we need a proper way for a VmCallback to raise errors. Right now, - # `stackTrace2` doesn't abort execution (both callback and vm), which will - # probably lead to the compiler crashing sooner or later - - # Add a temporary stack frame so that the callback call shows up in the trace - # XXX: unfortunately, we don't know who called us and from where. The stack - # trace will only contain the callback as a single entry - c.sframes.add TStackFrame(prc: c.prc.sym, comesFrom: 0, next: -1) - - stackTrace( - c[], - c.sframes.high, - # TODO: `c.exceptionInstr` is completely wrong here. It refers to the instruction - # that most recently raised (or 0 if raise was never called yet). - c.exceptionInstr, report, n.info) - # Remove temporary frame again - discard c.sframes.pop() - proc registerAdditionalOps*(c: PCtx) = template wrapIterator(fqname: string, iter: untyped) = @@ -266,16 +247,16 @@ proc registerAdditionalOps*(c: PCtx) = registerCallback c, "stdlib.macros.symBodyHash", proc (a: VmArgs) = let n = getNode(a, 0) if n.kind != nkSym: - stackTrace2(c, reportAst( - rsemVmNodeNotASymbol, n, str = "symBodyHash()"), n) + raiseVmError(reportAst( + rsemVmNodeNotASymbol, n, str = "symBodyHash()"), n.info) setResult(a, $symBodyDigest(c.graph, n.sym)) registerCallback c, "stdlib.macros.isExported", proc(a: VmArgs) = let n = getNode(a, 0) if n.kind != nkSym: - stackTrace2(c, reportAst( - rsemVmNodeNotASymbol, n, str = "isExported()"), n) + raiseVmError(reportAst( + rsemVmNodeNotASymbol, n, str = "isExported()"), n.info) setResult(a, sfExported in n.sym.flags)