Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add std/asserts containing enforce; (catchable + customizable doAssert ) #15606

Closed
wants to merge 9 commits into from

Conversation

timotheecour
Copy link
Member

@timotheecour timotheecour commented Oct 17, 2020

fixes nim-lang/RFCs#266

links

see also nim-lang/RFCs#387 (NimLineInfo) which can be used in future work to further reduce binary size

@timotheecour timotheecour changed the title add std/assertions containing enforce; (catchable + customizable doAssert ) add std/asserts containing enforce; (catchable + customizable doAssert ) Oct 18, 2020
@timotheecour timotheecour requested a review from Araq November 7, 2020 00:44
@timotheecour timotheecour force-pushed the pr_enforces branch 2 times, most recently from b1ccee7 to c092265 Compare November 7, 2020 00:47
@timotheecour
Copy link
Member Author

@Araq PTAL; I've taken into account feedback from nim-lang/RFCs#266 by turning msg from optional to required; I've kept astToStr(cond) however as it's an important part of the exception message, just as it is with assert/doAssert.

@timotheecour timotheecour changed the title add std/asserts containing enforce; (catchable + customizable doAssert ) add std/asserts containing enforce; (catchable + customizable doAssert ) Jun 8, 2021
@timotheecour timotheecour marked this pull request as draft June 8, 2021 20:09
@timotheecour
Copy link
Member Author

timotheecour commented Jun 9, 2021

@Araq PTAL, see new updates below:

  • this PR provides syntax sugar for a very common operation, enforce(cond, msg), like doAssert(cond, msg) but for catchable exceptions (with custom exception)
  • it allows generating smaller binaries than with the standard pattern if not cond: raise newException(typ, msg), by using an intermediate onEnforceFail proc (eg 283K vs 619K in synthetic example below)
  • further binary size reduction can be obtained by passing -d:nimLeanMessages (eg 157K in example below)
  • it removes the need for procs like func invalidFormatString() {.noinline.} = raise ... (eg in strutils), since enforce can just be used directly (resulting also in simpler code); there are a lot of those, eg: parseutils.integerOutOfRangeError, jsonutils.raiseJsonException etc
  • see future work direction indicated in comment for std/asserts (regarding propagating TLineInfo-like compact object directly, allowing lazy rendering of location info)

benchmark

nim r -d:danger -o:/tmp/z02 -d:case2b main # classic approach, equivalent to: if not cond: raise ... 
nim r -d:danger -o:/tmp/z01 -d:case2a main # `enforce` (this PR)
nim r -d:danger -o:/tmp/z01lean -d:case2a -d:nimLeanMessages main # `enforce` + -d:nimLeanMessages

/tmp/z01lean: 157K
/tmp/z01: 283K
/tmp/z02: 619K

benchmark code

when true: # D20210608T230951
  import std/macros
  import std/asserts
  import std/private/miscdollars
  template enforce2*[T](cond: untyped, arg: T, typ: typedesc = CatchableError) =
    const loc = instantiationInfo(fullPaths = compileOption("excessiveStackTrace"))
    {.line: loc.}:
      if not cond:
        const prefix = $loc & " `" & astToStr(cond) & "` "
        raise newException(typ, prefix & $arg)

  macro genCode(fn, c, i) =
    result = newStmtList()
    let m = 1000-1
    var loc = instantiationInfo(fullPaths = compileOption("excessiveStackTrace"))
    for i in 0..<m:
      let m2 = newLit(m)
      let i2 = newLit(i)
      result.add quote do:
        c += 7 - `i2`
        # doesn't matter much which condition, the point is to generate
        # a distinct `prefix` for each enforce statement
        `fn` `c` + `i2` != -4924931111110 + 1000 - 1, (c, `i2`)

  import std/times
  template main2(algo, c)=
    let n = 10000000'u
    let t = cpuTime()
    for i in 0..<n:
      let s = $i
      c += s.len
      when algo == 1: genCode(enforce, c, i)
      elif algo == 2: genCode(enforce2, c, i)
      else: static: doAssert false
    let t2 = cpuTime()
    echo (t2 - t, astToStr(algo), c)

  proc main()=
    var c = 0
    for i in 0..<1:
      when defined case2a: main2(1, c)
      when defined case2b: main2(2, c)
  main()

@timotheecour timotheecour marked this pull request as ready for review June 9, 2021 06:35
@timotheecour timotheecour added the Ready For Review (please take another look): ready for next review round label Jun 9, 2021
@konsumlamm
Copy link
Contributor

What would happen with system/assertions then? Would it be moved or reexported in std/asserts?
And why can't we just extend doAssert in the first place?

@Araq
Copy link
Member

Araq commented Jun 10, 2021

I don't mind enforce but it has little to do with assert. Assert is for catching bugs, enforce is for input validation. At least that's what I get from your remark

it removes the need for procs like func invalidFormatString() {.noinline.} = raise ... (eg in strutils)

Seriously, error reporting is not bug detection and GCC error messages do not contain stack traces pointing into GCC's inner workings. This is widely appreciated to be a good thing.

@timotheecour
Copy link
Member Author

timotheecour commented Jun 10, 2021

I don't mind enforce but it has little to do with assert

how about std/exceptions (and moving system/exceptions (an include file) to system/exceptions_impl) ?

(precedent: https://dlang.org/library/std/exception/enforce.html, and IMO the name std/exceptions makes sense)

Seriously, error reporting is not bug detection and GCC error messages do not contain stack traces pointing into GCC's inner workings. This is widely appreciated to be a good thing.

I don't understand how that relates to either this PR or invalidFormatString: un-handled exceptions will contain stacktraces, as can be seen here which fails by calling invalidFormatString and shows a stacktrace:

import std/strutils; var a = "a1 $# $#" % ["foo"]

my point is two-fold:

A1:

after this PR, this pattern:

if s.len < 0: raise newException(EnforceError, "s.len >= 0 failed; " & $s.len)

simplifies to this:

enforce s.len < 0, s.len

and results in better codegen (smaller binary sizes), as illustrated in benchmark

A2:

procs like invalidFormatString (whose only purpose is to avoid bloating binary size, reduce compile times to some extent) can be removed in favor of enforce, which already handles this, eg:
before PR:

# strutils.nim:
func invalidFormatString() {.noinline.} =
  raise newException(ValueError, "invalid format string")
func addf*(s: var string, formatstr: string, a: varargs[string, `$`]) =
  ...
  if num > a.high: invalidFormatString()

after PR, using enforce:

# strutils.nim:
func addf*(s: var string, formatstr: string, a: varargs[string, `$`]) =
  ...
  enforce num <= a.high, "invalid format string", ValueError
  # or, showing more runtime context:
  enforce num <= a.high, $(num, a.high, formatstr)

  # or, lazily (if we make arg optional again):
  # note that this sitll will show the line + rendered conditions, which is more informative
  # than the generic (reused!) "invalid format string"
  enforce num <= a.high

@timotheecour
Copy link
Member Author

PTAL

I don't mind enforce but it has little to do with assert. Assert is for catching bugs, enforce is for input validation. At least that's what I get from your remark

done, I moved it to std/exceptions (see above comment)

@stale
Copy link

stale bot commented Jun 18, 2022

This pull request has been automatically marked as stale because it has not had recent activity. If you think it is still a valid PR, please rebase it on the latest devel; otherwise it will be closed. Thank you for your contributions.

@stale stale bot added the stale Staled PR/issues; remove the label after fixing them label Jun 18, 2022
@stale stale bot closed this Jul 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Ready For Review (please take another look): ready for next review round stale Staled PR/issues; remove the label after fixing them
Projects
None yet
Development

Successfully merging this pull request may close these issues.

enforce: like doAssert for for catchableExceptions + allows customizing exception type
3 participants