using call/cc with let-assert

This commit is contained in:
2026-05-11 17:00:46 +02:00
parent 9d4b4fff33
commit 20b54b8447
2 changed files with 187 additions and 127 deletions
+2 -2
View File
@@ -65,8 +65,8 @@
) )
(define-syntax let/assert (define-syntax let/assert
(syntax-rules () (syntax-rules (fail)
((_ fail ((v rest ...) ...) b1 ...) ((_ ((v rest ...) ...) b1 ...)
(call/cc (call/cc
(λ (fail) (λ (fail)
(let* ((v (assert-expr fail (rest ...))) (let* ((v (assert-expr fail (rest ...)))
+182 -122
View File
@@ -3,196 +3,256 @@
@(require (for-label racket/base @(require (for-label racket/base
"../main.rkt")) "../main.rkt"))
@title{let-assert} @title{let/assert}
@author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]]
@defmodule[let-assert] @defmodule[let-assert]
This module provides @racket[let/assert], a small sequential binding form This module provides @racket[let/assert], a small sequential binding
with local assertions. It is useful for defensive programming around FFI form for defensive programming. It is especially useful around FFI calls,
bindings: checks for null pointers, exit codes, and similar failure values can where failure is often reported by ordinary values: @racket[#f] for a
be kept close to the binding that produced them, while the body stays on the null pointer, a negative integer for an error code, or a non-zero integer
happy path. When an assertion fails, the whole @racket[let/assert] expression for a failed C-style status result.
returns the associated fallback value.
@section{Binding with assertions} The form is implemented with @racket[call/cc]. A failed assertion does
not raise an exception. Instead, it invokes an internal escape
continuation and returns the supplied failure value as the result of the
whole @racket[let/assert] expression.
@defform[ @defform[
(let/assert (binding ...) body ...+) (let/assert
#:grammar ((id expr)
([binding [id expr] (id expr assert-expr failure-expr)
[id expr predicate-expr fallback-expr]])]{ ...)
body ...)
]{
The form expands to an internal @racket[let*] structure. Bindings are Evaluates the bindings from left to right, using @racket[let*]
therefore evaluated from left to right, and later bindings may refer to semantics. Later bindings may refer to earlier ones.
earlier ones.
A binding of the form @racket[[id expr]] simply binds @racket[id] to
@racket[expr].
A binding of the form A binding of the form
@racket[[id expr predicate-expr fallback-expr]] evaluates @racket[expr] once,
applies @racket[predicate-expr] to the result, and binds @racket[id] to that
result when the predicate accepts it. If the predicate returns @racket[#f],
the body is not evaluated and the whole @racket[let/assert] form returns
@racket[fallback-expr].
The fallback expression is evaluated only when the assertion fails. Internally, @racketblock[
the failing assertion raises a private exception carrying that value; the (id expr)
exception is caught by @racket[let/assert] itself. ]
is a plain sequential binding.
A binding of the form
@racketblock[
(id expr assert-expr failure-expr)
]
evaluates @racket[expr] once and applies @racket[assert-expr] to the
result. If the assertion succeeds, @racket[id] is bound to the original
value. If the assertion fails, @racket[failure-expr] is returned as the
result of the whole @racket[let/assert] form.
The escape continuation is internal. It is not part of the surface syntax
and is not visible in the body.
@racketblock[
(define (half-positive n)
(let/assert
((x n a->0? 'not-positive))
(/ x 2)))
]
The result is @racket[5] for @racket[(half-positive 10)] and
@racket['not-positive] for @racket[(half-positive 0)].
Because @racket[let/assert] uses @racket[call/cc], failure is local
control flow. There is no exception object, no exception handler, and no
dynamic catching of unrelated failures.
} }
@racketblock[ @section{Assertion constructors}
(let/assert ([x 10 (a->? 0) 'too-small]
[y (+ x 2) (a-=? 12) 'wrong-value])
y)
]
The example returns @racket[12]. The second binding can use @racket[x],
because @racket[let/assert] has @racket[let*] scoping.
@section{FFI-style examples}
FFI libraries often report errors by returning a null pointer or a negative
integer status code. With @racket[let/assert], those checks can be written
next to the operation that may fail.
@racketblock[
(define (open-ffmpeg-instance)
(let/assert ([fh (fmpg-init) a-!nullptr? #f])
fh))
]
Here @racket[fmpg-init] is expected to return either a valid native handle or
@racket[#f]. If the handle is @racket[#f], the complete
@racket[let/assert] form returns @racket[#f]. Otherwise the valid handle is
returned.
A slightly larger example can combine a pointer check with an FFmpeg-style
return-code check:
@racketblock[
(define (decode-next! ds)
(let/assert ([pkt (av-packet-alloc) a-!nullptr? 'packet-allocation-failed]
[ret (read-selected-audio-packet! ds pkt)
(a->=? 0)
'read-packet-failed]
[pcm (receive-available-frames! ds)
bytes?
'decode-failed])
pcm))
]
The body contains only the successful path. If packet allocation fails, the
result is @racket['packet-allocation-failed]. If reading the packet returns a
negative status code, the result is @racket['read-packet-failed]. If decoding
does not produce bytes, the result is @racket['decode-failed].
@section{Creating assertion factories}
@defform[(make-assert name not-name pred)]{ @defform[(make-assert name not-name pred)]{
Defines two assertion factories in a definition context. Defines two assertion-constructor forms.
The form @racket[(name constant)] expands to a unary predicate that applies The generated @racket[name] form creates a predicate that compares its
@racket[pred] to the value being checked and @racket[constant]. The form argument with a constant using @racket[pred]:
@racket[(not-name constant)] expands to the negated variant.
For example: @racketblock[
((name const) value)
]
is equivalent to:
@racketblock[
(λ (x) (pred x const))
]
The generated @racket[not-name] form creates the negated predicate:
@racketblock[
(λ (x) (not (pred x const)))
]
For example, the module defines:
@racketblock[ @racketblock[
(make-assert a-eq? a-!eq? eq?) (make-assert a-eq? a-!eq? eq?)
(make-assert a->? a-<=? >)
(make-assert a->=? a-<? >=)
(make-assert a-=? a-!=? =)
] ]
defines @racket[a-eq?] and @racket[a-!eq?]. Consequently, The negated forms are logical negations of the positive predicate.
@racket[(a-eq? #f)] produces a predicate that accepts values that are
@racket[eq?] to @racket[#f], while @racket[(a-!eq? #f)] accepts values that
are not @racket[eq?] to @racket[#f].
} }
@section{Assertion factories} @defform[(a-eq? const)]{
Creates a predicate that accepts values @racket[eq?] to @racket[const].
@defform[(a-eq? constant)]{
Produces a unary predicate that accepts values for which
@racket[(eq? value constant)] is true.
} }
@defform[(a-!eq? constant)]{ @defform[(a-!eq? const)]{
Produces a unary predicate that accepts values for which Creates a predicate that accepts values not @racket[eq?] to
@racket[(eq? value constant)] is false. @racket[const].
} }
@defform[(a->? constant)]{ @defform[(a->? const)]{
Produces a unary predicate that accepts values greater than Creates a predicate that accepts values greater than @racket[const].
@racket[constant].
} }
@defform[(a-<=? constant)]{ @defform[(a-<=? const)]{
Produces a unary predicate that rejects values greater than Creates the negation of @racket[(a->? const)].
@racket[constant].
} }
@defform[(a->=? constant)]{ @defform[(a->=? const)]{
Produces a unary predicate that accepts values greater than or equal to Creates a predicate that accepts values greater than or equal to
@racket[constant]. @racket[const].
} }
@defform[(a-<? constant)]{ @defform[(a-<? const)]{
Produces a unary predicate that rejects values greater than or equal to Creates the negation of @racket[(a->=? const)].
@racket[constant].
} }
@defform[(a-=? constant)]{ @defform[(a-=? const)]{
Produces a unary predicate that accepts values numerically equal to Creates a predicate that accepts values numerically equal to
@racket[constant]. @racket[const].
} }
@defform[(a-!=? constant)]{ @defform[(a-!=? const)]{
Produces a unary predicate that accepts values not numerically equal to Creates the negation of @racket[(a-=? const)].
@racket[constant].
} }
@section{Ready-made predicates} @section{Built-in predicates}
@defproc[(a-=0? [x number?]) boolean?]{ @defproc[(a-=0? [x number?]) boolean?]{
Returns @racket[#t] when @racket[x] is numerically equal to zero. Accepts values numerically equal to zero.
} }
@defproc[(a-!=0? [x number?]) boolean?]{ @defproc[(a-!=0? [x number?]) boolean?]{
Returns @racket[#t] when @racket[x] is not numerically equal to zero. Accepts values not numerically equal to zero.
} }
@defproc[(a->0? [x real?]) boolean?]{ @defproc[(a->0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is greater than zero. Accepts values greater than zero.
} }
@defproc[(a->=0? [x real?]) boolean?]{ @defproc[(a->=0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is greater than or equal to zero. Accepts values greater than or equal to zero.
} }
@defproc[(a-<0? [x real?]) boolean?]{ @defproc[(a-<0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is less than zero. Accepts values less than zero.
} }
@defproc[(a-<=0? [x real?]) boolean?]{ @defproc[(a-<=0? [x real?]) boolean?]{
Returns @racket[#t] when @racket[x] is less than or equal to zero. Accepts values less than or equal to zero.
} }
@defthing[a-true? procedure?]{ @defthing[a-true? procedure?]{
A predicate equivalent to @racket[(a-eq? #t)]. A predicate that accepts only @racket[#t].
} }
@defthing[a-false? procedure?]{ @defthing[a-false? procedure?]{
A predicate equivalent to @racket[(a-eq? #f)]. A predicate that accepts only @racket[#f].
} }
@defthing[a-nullptr? procedure?]{ @defthing[a-nullptr? procedure?]{
A predicate equivalent to @racket[(a-eq? #f)]. The name is intended for FFI A predicate that accepts @racket[#f]. This is intended for FFI bindings
code where @racket[#f] is used to represent a null pointer. where a native null pointer is represented as @racket[#f].
} }
@defthing[a-!nullptr? procedure?]{ @defthing[a-!nullptr? procedure?]{
A predicate equivalent to @racket[(a-!eq? #f)]. It accepts values that are A predicate that accepts values that are not @racket[#f]. This is useful
not @racket[#f], which is often the success case for pointer-returning FFI for checking successful pointer allocation in FFI code.
calls.
} }
@section{Examples}
A simple status-code check:
@racketblock[
(define (open-status r)
(let/assert
((status r a-=0? 'open-failed))
'opened))
]
Here @racket[(open-status 0)] returns @racket['opened], while
@racket[(open-status -1)] returns @racket['open-failed].
A pointer-style check:
@racketblock[
(define (use-context maybe-ctx)
(let/assert
((ctx maybe-ctx a-!nullptr? 'no-context))
ctx))
]
Here @racket[#f] is treated as a failed native pointer result.
Plain bindings and asserted bindings may be mixed. Since the form uses
@racket[let*] semantics, later expressions can refer to earlier values:
@racketblock[
(define (make-scaled-size width height scale)
(let/assert
((w width a->0? 'bad-width)
(h height a->0? 'bad-height)
(s scale a->0? 'bad-scale)
(area (* w h)))
(* area s)))
]
The expression @racket[area] is only evaluated after @racket[w] and
@racket[h] have passed their assertions.
@subsection{FFmpeg-style null pointers}
Many FFmpeg functions return a native pointer, where @racket[#f]
represents a null pointer. Since @racket[let/assert] uses sequential
@racket[let*] semantics, later bindings may depend on earlier checked
pointer results.
@racketblock[
(define (make-stream-codec-context stream)
(let/assert
((codecpar (AVStream-codecpar stream) a-!nullptr?
'missing-codec-parameters)
(codec (avcodec-find-decoder
(AVCodecParameters-codec-id codecpar))
a-!nullptr?
'decoder-not-found)
(ctx (avcodec-alloc-context3 codec) a-!nullptr?
'alloc-codec-context-failed))
ctx))
]
If @racket[avcodec-find-decoder] returns @racket[#f], the whole
@racket[let/assert] expression returns @racket['decoder-not-found].
The second binding is then not evaluated.
If @racket[codec] is a valid pointer, it is used by the next binding.
If @racket[avcodec-alloc-context3] returns @racket[#f], the expression
returns @racket['alloc-codec-context-failed]. Otherwise @racket[ctx] is
bound to the allocated context and returned by the body.