diff --git a/changelog.md b/changelog.md index cc6f826eee7bd..fadeab945b1d3 100644 --- a/changelog.md +++ b/changelog.md @@ -334,6 +334,8 @@ - Added `dom.scrollIntoView` proc with options +- add `std/exceptions` containing `enforce` + ## Language changes - `nimscript` now handles `except Exception as e`. diff --git a/lib/std/exceptions.nim b/lib/std/exceptions.nim new file mode 100644 index 0000000000000..496536bf3d799 --- /dev/null +++ b/lib/std/exceptions.nim @@ -0,0 +1,28 @@ +import system/assertions +import std/private/miscdollars + +proc onEnforceFail[T](typ: typedesc, prefix: string, arg: T) {.noreturn, noinline.} = + ## Making this a proc reduces size of binaries + raise newException(typ, prefix & $arg) + +template enforce*[T](cond: untyped, arg: T, typ: typedesc = EnforceError) = + ## similar to `doAssert` but defaults to raising catchable exception + ## instead of `AssertionDefect`, and allows customizing the raised exception type. + # `-d:nimLeanMessages` further reduces size of binaries at expense of not + # showing location information; in future we can avoid generating un-necessary + # strings and forward `TLineInfo` directly (or some equivalent compact type), + # and then defer the string rendering until needed in `onEnforceFail`, + # reducing binary size while preserving location information. stacktraces + # can benefit from the same optimization. + runnableExamples: + let a = 1 + enforce a == 1, $(a,) + doAssertRaises(EnforceError): enforce a == 2, $(a,) + doAssertRaises(ValueError): enforce a == 2, $(a,), ValueError + const loc = instantiationInfo(fullPaths = compileOption("excessiveStackTrace")) + {.line: loc.}: + if not cond: + const prefix = + when defined(nimLeanMessages): "" + else: $loc & " `" & astToStr(cond) & "` " + onEnforceFail(typ, prefix, arg) diff --git a/lib/std/private/miscdollars.nim b/lib/std/private/miscdollars.nim index a41cf1bc1715a..bfdad19123b35 100644 --- a/lib/std/private/miscdollars.nim +++ b/lib/std/private/miscdollars.nim @@ -13,3 +13,13 @@ template toLocation*(result: var string, file: string | cstring, line: int, col: when declared(addInt): result.addInt col else: result.add $col result.add ")" + +type InstantiationInfo* = tuple[filename: string, line: int, column: int] + +proc `$`(x: int): string {.magic: "IntToStr", noSideEffect.} + +proc `$`*(info: InstantiationInfo): string = + # The +1 is needed here + # instead of overriding `$` (and changing its meaning), consider explicit name. + result = "" + result.toLocation(info.filename, info.line, info.column + 1) diff --git a/lib/system.nim b/lib/system.nim index cae0ec3e2c1ca..2244a6c85f897 100644 --- a/lib/system.nim +++ b/lib/system.nim @@ -582,7 +582,7 @@ type RootRef* = ref RootObj ## Reference to `RootObj`. -include "system/exceptions" +include "system/exceptions_impl" when defined(js) or defined(nimdoc): type diff --git a/lib/system/assertions.nim b/lib/system/assertions.nim index cd789845b57e5..b02b8f45d2482 100644 --- a/lib/system/assertions.nim +++ b/lib/system/assertions.nim @@ -2,24 +2,13 @@ ## ## **Note:** This module is reexported by `system` and thus does not need to be ## imported directly (with `system/assertions`). +## +## See also: `std/exceptions` when not declared(sysFatal): include "system/fatal" import std/private/miscdollars -# --------------------------------------------------------------------------- -# helpers - -type InstantiationInfo = tuple[filename: string, line: int, column: int] - -proc `$`(x: int): string {.magic: "IntToStr", noSideEffect.} -proc `$`(info: InstantiationInfo): string = - # The +1 is needed here - # instead of overriding `$` (and changing its meaning), consider explicit name. - result = "" - result.toLocation(info.filename, info.line, info.column + 1) - -# --------------------------------------------------------------------------- when not defined(nimHasSinkInference): {.pragma: nosinks.} @@ -94,6 +83,7 @@ template doAssertRaises*(exception: typedesc, code: untyped) = const begin = "expected raising '" & astToStr(exception) & "', instead" const msgEnd = " by: " & astToStr(code) template raisedForeign = raiseAssert(begin & " raised foreign exception" & msgEnd) + mixin `$` # for `tests/test_nimscript.nims` (import assertions appears before dollars) when Exception is exception: try: if true: diff --git a/lib/system/exceptions.nim b/lib/system/exceptions_impl.nim similarity index 98% rename from lib/system/exceptions.nim rename to lib/system/exceptions_impl.nim index 5dcd77bd0be88..c9212a0d458e5 100644 --- a/lib/system/exceptions.nim +++ b/lib/system/exceptions_impl.nim @@ -56,6 +56,8 @@ type CatchableError* = object of Exception ## \ ## Abstract class for all exceptions that are catchable. + EnforceError* = object of Exception ## \ + ## Default exception raised by `asserts.enforce`. IOError* = object of CatchableError ## \ ## Raised if an IO error occurred. EOFError* = object of IOError ## \ diff --git a/tests/stdlib/texceptions.nim b/tests/stdlib/texceptions.nim new file mode 100644 index 0000000000000..9aedcda6f963f --- /dev/null +++ b/tests/stdlib/texceptions.nim @@ -0,0 +1,21 @@ +#[ +see also tassert2 +]# + +import std/exceptions +import std/strutils + + + +# line 10 +block: ## checks AST isn't transformed as it used to + let a = 1 + enforce a == 1, $a + var raised = false + try: + enforce a > 1, $a + except EnforceError as e: + raised = true + doAssert e.msg.endsWith "texceptions.nim(16, 13) `a > 1` 1" + doAssert raised + doAssertRaises(EnforceError): enforce a > 1, $a