From 12a6cc20272714d9c06d49e9b6882cb029c86485 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Mon, 6 Apr 2020 21:52:02 +0300 Subject: [PATCH] Initial work on the error handling proposal The commit also adds a facility for writing the generated macro output to a file. It also introduces a new module named `results` that should eventually replace all usages of `import result`. --- stew/errorhandling.nim | 277 +++++++++++++++++++++ tests/test_errorhandling.nim | 37 +++ tests/test_errorhandling.nim.generated.nim | 17 ++ 3 files changed, 331 insertions(+) create mode 100644 stew/errorhandling.nim create mode 100644 tests/test_errorhandling.nim create mode 100644 tests/test_errorhandling.nim.generated.nim diff --git a/stew/errorhandling.nim b/stew/errorhandling.nim new file mode 100644 index 0000000..8014dbf --- /dev/null +++ b/stew/errorhandling.nim @@ -0,0 +1,277 @@ +import + typetraits, strutils, + shims/macros, results + +const + enforce_error_handling {.strdefine.}: string = "yes" + errorHandlingEnforced = parseBool(enforce_error_handling) + +type + VoidResult = object + Raising*[ErrorList: tuple, ResultType] = distinct ResultType + +let + raisesPragmaId {.compileTime.} = ident"raises" + +proc mergeTupleTypeSets(lhs, rhs: NimNode): NimNode = + result = newTree(nnkPar) + + for i in 1 ..< lhs.len: + result.add lhs[i] + + for i in 1 ..< rhs.len: + block findMatch: + for j in 1 ..< lhs.len: + if sameType(rhs[i], lhs[i]): + break findMatch + + result.add rhs[i] + +macro `++`*(lhs: type[tuple], rhs: type[tuple]): type = + result = mergeTupleTypeSets(getType(lhs)[1], getType(rhs)[1]) + +proc genForwardingCall(procDef: NimNode): NimNode = + result = newCall(procDef.name) + for param, _ in procDef.typedParams: + result.add param + +macro noerrors*(procDef: untyped) = + let raisesPragma = procDef.pragma.findPragma(raisesPragmaId) + if raisesPragma != nil: + error "You should not specify `noerrors` and `raises` at the same time", + raisesPragma + var raisesList = newTree(nnkBracket, bindSym"Defect") + procDef.addPragma newColonExpr(ident"raises", raisesList) + return procDef + +macro errors*(ErrorsTuple: typed, procDef: untyped) = + let raisesPragma = procDef.pragma.findPragma(raisesPragmaId) + if raisesPragma != nil: + error "You should not specify `errors` and `raises` at the same time", + raisesPragma + + var raisesList = newTree(nnkBracket, bindSym"Defect") + + for i in 1 ..< ErrorsTuple.len: + raisesList.add ErrorsTuple[i] + + procDef.addPragma newColonExpr(ident"raises", raisesList) + + when errorHandlingEnforced: + # We are going to create a wrapper proc or a template + # that calls the original one and wraps the returned + # value in a Raising type. To achieve this, we must + # generate a new name for the original proc: + + let + generateTemplate = true + OrigResultType = procDef.params[0] + + # Create the wrapper + var + wrapperDef: NimNode + RaisingType: NimNode + + if generateTemplate: + wrapperDef = newNimNode(nnkTemplateDef, procDef) + procDef.copyChildrenTo wrapperDef + # We must remove the raises list from the original proc + wrapperDef.pragma = newEmptyNode() + else: + wrapperDef = copy procDef + + # Change the original proc name + procDef.name = genSym(nskProc, $procDef.name) + + var wrapperBody = newNimNode(nnkStmtList, procDef.body) + if OrigResultType.kind == nnkEmpty or eqIdent(OrigResultType, "void"): + RaisingType = newTree(nnkBracketExpr, ident"Raising", + ErrorsTuple, bindSym"VoidResult") + wrapperBody.add( + genForwardingCall(procDef), + newCall(RaisingType, newTree(nnkObjConstr, bindSym"VoidResult"))) + else: + RaisingType = newTree(nnkBracketExpr, ident"Raising", + ErrorsTuple, OrigResultType) + wrapperBody.add newCall(RaisingType, genForwardingCall(procDef)) + + wrapperDef.params[0] = if generateTemplate: ident"untyped" + else: RaisingType + wrapperDef.body = wrapperBody + + result = newStmtList(procDef, wrapperDef) + else: + result = procDef + + storeMacroResult result + +macro checkForUnhandledErrors(origHandledErrors, raisedErrors: typed) = + # This macro is executed with two tuples: + # + # 1. The list of errors handled at the call-site which will + # have a line info matching the call-site. + # 2. The list of errors that the called function is raising. + # The lineinfo here points to the definition of the function. + + # For accidental reasons, the first tuple will be recognized as a + # typedesc, while the second won't be (beware because this can be + # considered a bug in Nim): + var handledErrors = getTypeInst(origHandledErrors) + if handledErrors.kind == nnkBracketExpr: + handledErrors = handledErrors[1] + + assert handledErrors.kind == nnkTupleConstr and + raisedErrors.kind == nnkTupleConstr + + # Here, we'll store the list of errors that the user missed: + var unhandledErrors = newTree(nnkPar) + + # We loop through the raised errors and check whether they have + # an appropriate handler: + for raised in raisedErrors: + block findHandler: + template tryFindingHandler(raisedType) = + for handled in handledErrors: + if sameType(raisedType, handled): + break findHandler + + tryFindingHandler raised + # A base type of the raised exception may be handled instead + for baseType in raised.baseTypes: + tryFindingHandler baseType + + unhandledErrors.add raised + + if unhandledErrors.len > 0: + let errMsg = "The following errors are not handled: $1" % [unhandledErrors.repr] + error errMsg, origHandledErrors + +template raising*[E, R](x: Raising[E, R]): R = + ## `raising` is used to mark locations in the code that might + ## raise exceptions. It disarms the type-safety checks imposed + ## by the `errors` pragma. + distinctBase(x) + +macro chk*[R, E](x: Raising[R, E], handlers: untyped): untyped = + ## The `chk` macro can be used in 2 different ways + ## + ## 1) Try to get the result of an expression. In case of any + ## errors, substitute the result with a default value: + ## + ## ``` + ## let x = chk(foo(), defaultValue) + ## ``` + ## + ## We'll handle this case with a simple rewrite to + ## + ## ``` + ## let x = try: distinctBase(foo()) + ## except CatchableError: defaultValue + ## ``` + ## + ## 2) Try to get the result of an expression while providing exception + ## handlers that must cover all possible recoverable errors. + ## + ## ``` + ## let x = chk foo(): + ## KeyError as err: defaultValue + ## ValueError: return + ## _: raise + ## ``` + ## + ## The above example will be rewritten to: + ## + ## ``` + ## let x = try: + ## foo() + ## except KeyError as err: + ## defaultValue + ## except ValueError: + ## return + ## except CatchableError: + ## raise + ## ``` + ## + ## Please note that the special case `_` is considered equivalent to + ## `CatchableError`. + ## + ## If the `chk` block lacks a default handler and there are unlisted + ## recoverable errors, the compiler will fail to compile the code with + ## a message indicating the missing ones. + let + RaisingType = getTypeInst(x) + ErrorsSetTuple = RaisingType[1] + ResultType = RaisingType[2] + + # The `try` branch is the same in all scenarios. We generate it here. + # The target AST looks roughly like this: + # + # TryStmt + # StmtList + # Call + # Ident "distinctBase" + # Call + # Ident "foo" + # ExceptBranch + # Ident "CatchableError" + # StmtList + # Ident "defaultValue" + result = newTree(nnkTryStmt, newStmtList( + newCall(bindSym"distinctBase", x))) + + # Check how the API was used: + if handlers.kind != nnkStmtList: + # This is usage type 1: chk(foo(), defaultValue) + result.add newTree(nnkExceptBranch, + bindSym("CatchableError"), + newStmtList(handlers)) + else: + var + # This will be a tuple of all the errors handled by the `chk` block. + # In the end, we'll compare it to the Raising list. + HandledErrorsTuple = newNimNode(nnkPar, x) + # Has the user provided a default `_: value` handler? + defaultCatchProvided = false + + for handler in handlers: + template err(msg: string) = error msg, handler + template unexpectedSyntax = err( + "The `chk` handlers block should consist of `ExceptionType: Value/Block` pairs") + + case handler.kind + of nnkCommentStmt: + continue + of nnkInfix: + if eqIdent(handler[0], "as"): + if handler.len != 4: + err "The expected syntax is `ExceptionType as exceptionVar: Value/Block`" + let + ExceptionType = handler[1] + exceptionVar = handler[2] + valueBlock = handler[3] + + HandledErrorsTuple.add ExceptionType + result.add newTree(nnkExceptBranch, infix(ExceptionType, "as", exceptionVar), + valueBlock) + else: + err "The '$1' operator is not expected in a `chk` block" % [$handler[0]] + of nnkCall: + if handler.len != 2: + unexpectedSyntax + let ExceptionType = handler[0] + if eqIdent(ExceptionType, "_"): + if defaultCatchProvided: + err "Only a single default handler is expected" + handler[0] = bindSym"CatchableError" + defaultCatchProvided = true + + result.add newTree(nnkExceptBranch, handler[0], handler[1]) + HandledErrorsTuple.add handler[0] + else: + unexpectedSyntax + + result = newTree(nnkStmtListExpr, + newCall(bindSym"checkForUnhandledErrors", HandledErrorsTuple, ErrorsSetTuple), + result) + + storeMacroResult result diff --git a/tests/test_errorhandling.nim b/tests/test_errorhandling.nim new file mode 100644 index 0000000..9ce12df --- /dev/null +++ b/tests/test_errorhandling.nim @@ -0,0 +1,37 @@ +import + ../stew/[shims/macros, errorhandling] + +proc bar(x: int): int {.noerrors.} = + 100 + +proc toString(x: int): string {.errors: (ValueError, KeyError, OSError).} = + $x + +proc main = + let + a = bar(10) + b = raising toString(20) + c = chk toString(30): + ValueError: "got ValueError" + KeyError as err: err.msg + OSError: raise + + echo a + echo b + echo c + +main() + +dumpMacroResults() + +when false: + type + ExtraErrors = KeyError|OSError + + #[ + proc map[A, E, R](a: A, f: proc (a: A): Raising[E, R])): string {. + errors: E|ValueError|ExtraErrors + .} = + $chk(f(a)) + ]# + diff --git a/tests/test_errorhandling.nim.generated.nim b/tests/test_errorhandling.nim.generated.nim new file mode 100644 index 0000000..7ea7734 --- /dev/null +++ b/tests/test_errorhandling.nim.generated.nim @@ -0,0 +1,17 @@ + +## /home/zahary/nimbus/vendor/nim-stew/tests/test_errorhandling.nim(7, 32) +proc toString_15835040(x: int): string {.raises: [Defect, KeyError, OSError].} = + result = $x + +template toString(x: int): untyped = + Raising[(ValueError, KeyError, OSError), string](toString_15835040(x)) + +## /home/zahary/nimbus/vendor/nim-stew/tests/test_errorhandling.nim(14, 12) +try: + [type node](Raising[(ValueError, KeyError, OSError), string](toString_15835040(30))) +except ValueError: + "got ValueError" +except KeyError as err: + err.msg +except OSError: + raise \ No newline at end of file