How raise-argument-error call gather more stacktrace?

I try to make may own version of raise-argument-error:

(define (raise-argument-error* a b c)
  (raise
    (exn:fail:contract
      (format "~a: contract violation\n  expected: ~a\n  given: ~a" a b c)
      (current-continuation-marks)))
  (void))

(define (test1 x)
  (when (> x 0)
    (raise-argument-error 'test1 "<= 0" x))
  (+ x 1))

(define (test2 x)
  (when (> x 0)
    (raise-argument-error* 'test2 "<= 0" x))
  (+ x 1))

Then, a call (test1 2) shows in stracktrace:

test-raise.rkt:12:4 
      (raise-argument-error 'test1 "<= 0" x))

test-raise.rkt:11:2 
    (when (> x 0)
    (raise-argument-error 'test1 "<= 0" x))

interactions from an unsaved editor:6:2 
  > (test1 2)

But (test2 2) gives :

test-raise.rkt:7:6 
        (current-continuation-marks)))

test-raise.rkt:5:4 
      (exn:fail:contract
      (format "~a: contract violation\n  expected: ~a\n  given: ~a" a b c)
      (current-continuation-marks)))

test-raise.rkt:4:2 
    (raise
    (exn:fail:contract
      (format "~a: contract violation\n  expected: ~a\n  given: ~a" a b c)
      (current-continuation-marks)))

test-raise.rkt:17:4 
      (raise-argument-error* 'test2 "<= 0" x))

interactions from an unsaved editor:10:2 
  > (test2 2)

Is it possible to add frame

    (when (> x 0)
    (raise-argument-error* 'test2 "<= 0" x))

like it is with raise-argument-error version?

Here’s a CP0’ed when expression in test1:

(#2%$call-setting-continuation-attachment
                           (#2%cons
                             errortrace-key3.2384
                             '((when (> x 0)
                                 (raise-argument-error 'test1 "<= 0" x))
                                #[path.rkt-io.sls-path-0 #vu8(47 85 115 101 114 115 47 115 111 114 97 119 101 101 47 112 108 97 121 103 114 111 117 110 100 47 97 97 46 114 107 116) unix]
                                6 2 55 57))
                           (lambda ()
                             (if (#2%$call-setting-continuation-attachment
                                   (#2%cons
                                     errortrace-key3.2384
                                     '((> x 0)
                                        #[path.rkt-io.sls-path-0 #vu8(47 85 115 101 114 115 47 115 111 114 97 119 101 101 47 112 108 97 121 103 114 111 117 110 100 47 97 97 46 114 107 116) unix]
                                        6 8 61 7))
                                   (lambda () (#2%> x_1.2387 0)))
                                 WHEN-BODY
                                 (#2%void))
                             (#2%void)))

And here’s a CP0’ed when expression in test2:

(#2%$call-setting-continuation-attachment
                           (#2%cons
                             errortrace-key3.2384
                             '((when (> x 0)
                                 (raise-argument-error* 'test1 "<= 0" x))
                                #[path.rkt-io.sls-path-0 #vu8(47 85 115 101 114 115 47 115 111 114 97 119 101 101 47 112 108 97 121 103 114 111 117 110 100 47 97 97 46 114 107 116) unix]
                                6 2 55 58))
                           (lambda ()
                             (if (#2%$call-setting-continuation-attachment
                                   (#2%cons
                                     errortrace-key3.2384
                                     '((> x 0)
                                        #[path.rkt-io.sls-path-0 #vu8(47 85 115 101 114 115 47 115 111 114 97 119 101 101 47 112 108 97 121 103 114 111 117 110 100 47 97 97 46 114 107 116) unix]
                                        6 8 61 7))
                                   (lambda () (#2%> x_1.2388 0)))
                                 WHEN-BODY
                                 (#2%void))))

where WHEN-BODY is a large expression that is mostly identical in both variants (except one major difference).

We can see that there’s an extra (#2%void) in the first variant, which makes the if expression no longer in the tail call position, leading to an extra frame. On the other hand, in the second variant, there is no extra (#2%void), so the if expression is in the tail call position, meaning the frame information inside will override the frame information about (when (> x 0) (raise-argument-error 'test1 "<= 0" x)).

I believe the difference is mostly in how the call of raise-argument-error is compiled to:

($app/no-return
                                           (#3%$top-level-value
                                             'raise-argument-error/user.rkt-rumble.sls-raise-argument-error/user-0)
                                           'test1
                                           "<= 0"
                                           x_1.2387)

In contrast, the call to raise-argument-error* is compiled to:

($app/value
                                           1/raise-argument-error*4.2385
                                           'test1
                                           "<= 0"
                                           x_1.2388)

and these are optimized differently by the compiler. One leads to an extra (#2%void).

My understanding is that $app/no-return and $app/value are things that are marked by the core compiler (i.e., the compiler knows what raise-argument-error does, so it specially treats the call differently). There’s not really a way you can influence it.

It is indeed surprising though that an optimization on $app/no-return doesn't preserve the tail call position.

@mflatt and @gus-massa would be able to tell if this is intentional.

I think, that it is intended. Otherwise in (define (f x) (if ... (do-something) (raise ...))) frame with (f x) would be lost (raise in tail position).

So, is it possible to make something by means of Racket? Or this behaviour is allowed only for builtins?

And, is it possible to send stack to the exception without last call to (current-continuation-marks)? I see, that abs shows only frame, where abs is called. But raise-blame-error also somehow omit it's inners in the continuation-marks...

Given what @sorawee discovered, if you grab the stackframe in the caller then it won't be overwritten:

(define (raise-argument-error* stk a b c)
  (raise
   (exn:fail:contract
    (format "~a: contract violation\n  expected: ~a\n  given: ~a" a b c)
    stk))
  (void))

(define (test3 x)
  (when (> x 0)
    (raise-argument-error* (current-continuation-marks) 'test3 "<= 0" x))
  (+ x 1))

This can be partially (though not perfectly) hidden from the caller:

(require syntax/parse/define)
(define-syntax-parse-rule (raise-argument-error*/stk . args)
  (raise-argument-error* (current-continuation-marks) . args))

(define (test4 x)
  (when (> x 0)
    (raise-argument-error*/stk 'test4 "<= 0" x))
  (+ x 1))

However, raise-argument-error*/stk doesn't work when used in conjunction with apply or other higher-order functions.

I see. But this way is not uniform with builtins.

Is the only way to make new correct exception to inject it into racket/error.ss?

No, I think the original raise-argument-error* is the way to go. The debug information injected by errortrace like what you see in when should not be relied upon.

Call stacks in the sense of continuation-mark-set->context (or the context... information in the error message) with possibly tail-call overwriting frames are to be expected.

DrRacket emits wrong messages for such exceptions.

> (require cldr/dates-modern)
> (ca-generic "ff")
. . AppData\Roaming\Racket\8.8\pkgs\cldr-core\cldr\core.rkt:40:10: locale "ff" is not in package: cldr-dates-modern

It should point to the place of the error: ..AppData\Roaming\Racket\8.8\pkgs\cldr-dates-modern\cldr\dates-modern.rkt:27:6

So any use of the original raise-argument-error* will print the line of raise-argument-error*'s definition instead of the line, where it is called. I'm sure, that it is wrong.

Maybe it is somehow possible to edit the continuation mark set context before savimg it to the exception struct?

For a normal package setup, the package code is precompiled and thus those package code doesn't have debug (i.e. errortrace) information. When running code from DrRacket, the exception will point to user code because only use code has debug information.

If you really want errortrace to operate on installed packages, you may consider using https://docs.racket-lang.org/errortrace-pkg/

For a normal package setup

How to do this? I installed cldr-dates-modern via DrRacket File / Install package.
How to make it precompiled?

Installing a package automatically compiles everything in it. If you have modified the files of the package, run raco setup or raco setup --pkgs PACKAGE again.

Thank you! DrRacket makes additional stackframes if I open source to look at.

To remove debug information created by DrRacket, you also need delete everything in **/compiled/drracket/errortrace