diff --git a/main.rkt b/main.rkt index f06f306..471b30f 100644 --- a/main.rkt +++ b/main.rkt @@ -65,8 +65,8 @@ ) (define-syntax let/assert - (syntax-rules () - ((_ fail ((v rest ...) ...) b1 ...) + (syntax-rules (fail) + ((_ ((v rest ...) ...) b1 ...) (call/cc (λ (fail) (let* ((v (assert-expr fail (rest ...))) diff --git a/scrbl/let-assert.scrbl b/scrbl/let-assert.scrbl index 57a9e6e..3971c29 100644 --- a/scrbl/let-assert.scrbl +++ b/scrbl/let-assert.scrbl @@ -3,196 +3,256 @@ @(require (for-label racket/base "../main.rkt")) -@title{let-assert} +@title{let/assert} + @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @defmodule[let-assert] -This module provides @racket[let/assert], a small sequential binding form -with local assertions. It is useful for defensive programming around FFI -bindings: checks for null pointers, exit codes, and similar failure values can -be kept close to the binding that produced them, while the body stays on the -happy path. When an assertion fails, the whole @racket[let/assert] expression -returns the associated fallback value. +This module provides @racket[let/assert], a small sequential binding +form for defensive programming. It is especially useful around FFI calls, +where failure is often reported by ordinary values: @racket[#f] for a +null pointer, a negative integer for an error code, or a non-zero integer +for a failed C-style status result. -@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[ -(let/assert (binding ...) body ...+) -#:grammar -([binding [id expr] - [id expr predicate-expr fallback-expr]])]{ +(let/assert + ((id expr) + (id expr assert-expr failure-expr) + ...) + body ...) +]{ -The form expands to an internal @racket[let*] structure. Bindings are -therefore evaluated from left to right, and later bindings may refer to -earlier ones. - -A binding of the form @racket[[id expr]] simply binds @racket[id] to -@racket[expr]. +Evaluates the bindings from left to right, using @racket[let*] +semantics. Later bindings may refer to earlier ones. 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, -the failing assertion raises a private exception carrying that value; the -exception is caught by @racket[let/assert] itself. +@racketblock[ +(id expr) +] + +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[ -(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} +@section{Assertion constructors} @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 -@racket[pred] to the value being checked and @racket[constant]. The form -@racket[(not-name constant)] expands to the negated variant. - -For example: +The generated @racket[name] form creates a predicate that compares its +argument with a constant using @racket[pred]: @racketblock[ -(make-assert a-eq? a-!eq? eq?) +((name const) value) ] -defines @racket[a-eq?] and @racket[a-!eq?]. Consequently, -@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]. +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[ +(make-assert a-eq? a-!eq? eq?) +(make-assert a->? a-<=? >) +(make-assert a->=? a-=) +(make-assert a-=? a-!=? =) +] + +The negated forms are logical negations of the positive predicate. } -@section{Assertion factories} - -@defform[(a-eq? constant)]{ -Produces a unary predicate that accepts values for which -@racket[(eq? value constant)] is true. +@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 false. +@defform[(a-!eq? const)]{ +Creates a predicate that accepts values not @racket[eq?] to +@racket[const]. } -@defform[(a->? constant)]{ -Produces a unary predicate that accepts values greater than -@racket[constant]. +@defform[(a->? const)]{ +Creates a predicate that accepts values greater than @racket[const]. } -@defform[(a-<=? constant)]{ -Produces a unary predicate that rejects values greater than -@racket[constant]. +@defform[(a-<=? const)]{ +Creates the negation of @racket[(a->? const)]. } -@defform[(a->=? constant)]{ -Produces a unary predicate that accepts values greater than or equal to -@racket[constant]. +@defform[(a->=? const)]{ +Creates a predicate that accepts values greater than or equal to +@racket[const]. } -@defform[(a-=? const)]. } -@defform[(a-=? constant)]{ -Produces a unary predicate that accepts values numerically equal to -@racket[constant]. +@defform[(a-=? const)]{ +Creates a predicate that accepts values numerically equal to +@racket[const]. } -@defform[(a-!=? constant)]{ -Produces a unary predicate that accepts values not numerically equal to -@racket[constant]. +@defform[(a-!=? const)]{ +Creates the negation of @racket[(a-=? const)]. } -@section{Ready-made predicates} +@section{Built-in predicates} @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?]{ -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?]{ -Returns @racket[#t] when @racket[x] is greater than zero. +Accepts values greater than zero. } @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?]{ -Returns @racket[#t] when @racket[x] is less than zero. +Accepts values less than zero. } @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?]{ -A predicate equivalent to @racket[(a-eq? #t)]. +A predicate that accepts only @racket[#t]. } @defthing[a-false? procedure?]{ -A predicate equivalent to @racket[(a-eq? #f)]. +A predicate that accepts only @racket[#f]. } @defthing[a-nullptr? procedure?]{ -A predicate equivalent to @racket[(a-eq? #f)]. The name is intended for FFI -code where @racket[#f] is used to represent a null pointer. +A predicate that accepts @racket[#f]. This is intended for FFI bindings +where a native null pointer is represented as @racket[#f]. } @defthing[a-!nullptr? procedure?]{ -A predicate equivalent to @racket[(a-!eq? #f)]. It accepts values that are -not @racket[#f], which is often the success case for pointer-returning FFI -calls. -} \ No newline at end of file +A predicate that accepts values that are not @racket[#f]. This is useful +for checking successful pointer allocation in FFI code. +} + +@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. \ No newline at end of file