From 0bbda2e6d6a77029fface2150ce69d635d05ab55 Mon Sep 17 00:00:00 2001 From: Hans Dijkema Date: Mon, 11 May 2026 17:13:09 +0200 Subject: [PATCH] call/cc variant of define/return --- scrbl/define-return-contract.scrbl | 138 +++++++++++++++++++++-------- scrbl/define-return.scrbl | 95 ++++++++++---------- 2 files changed, 147 insertions(+), 86 deletions(-) diff --git a/scrbl/define-return-contract.scrbl b/scrbl/define-return-contract.scrbl index 6764ab2..e566f28 100644 --- a/scrbl/define-return-contract.scrbl +++ b/scrbl/define-return-contract.scrbl @@ -2,35 +2,40 @@ @(require (for-label racket/base racket/contract + (only-in ffi/unsafe cpointer?) "../contract.rkt")) -@title{define/return/contract} +@title{define/contract/return} @author[@author+email["Hans Dijkema" "hans@dijkewijk.nl"]] @defmodule[define-return/contract] -The @racketmodname[define-return] library provides definition forms with an -explicit early return. This is useful in small defensive functions, and -especially around FFI bindings, where null pointers, error codes, unsupported -states, or failed preconditions should leave the function immediately. +The @racketmodname[define-return/contract] library provides +@racket[define/contract/return], a contracted definition form with an +explicit early return. It is useful in small defensive functions, and +especially around FFI bindings, where null pointers, error codes, +unsupported states, or failed preconditions should leave the function +immediately. -The early return is implemented with an internal exception. A @racket[return] -raises that exception, and the definition forms catch it around the function -body. The contracted form additionally checks early-returned values against -the result contract. +The return mechanism is based on @racket[call/cc]. A function definition +captures the continuation around the function body and binds it to the +return identifier supplied by the programmer. Calling that identifier +leaves the body and returns the supplied value as the result of the +function. No exception is raised and no exception handler is installed. -This module provides the contracted version of @racket{define/return}. -The module re-exports @racketmodname[racket/contract], so contracts such as -@racket[->], @racket[->*], @racket[any/c], @racket[or/c], +The module re-exports @racketmodname[racket/contract], so contracts such +as @racket[->], @racket[->*], @racket[any/c], @racket[or/c], @racket[and/c], and @racket[listof] are available from the same -@racket[require]. +@racket[require]. It also re-exports @racketmodname[define-return]. -Note. This can lead to clashes of symbol @racket[->] with module @racketmodname[ffi/unsafe]. +Note. This can lead to a clash between @racket[->] from +@racketmodname[racket/contract] and @racket[->] from +@racketmodname[ffi/unsafe]. @section{Contracted definitions} @defform*[ -((define/contract/return (name . formals) +((define/contract/return (name . formals) return-id (contract-part ... result-contract) body ...) (define/contract/return name @@ -38,19 +43,27 @@ Note. This can lead to clashes of symbol @racket[->] with module @racketmodname[ value)) ]{ -Like @racket[define/contract], but the body may use @racket[return]. +Like @racket[define/contract], but the function form gives the body a +local early-return identifier. -The first form defines a contracted function. Ordinary results are checked by -@racket[define/contract]. Early-returned values are checked separately -against @racket[result-contract]. The implementation does this by defining a -small local contracted returner with the same result contract, so the result -contract is passed through Racket's contract machinery. +The first form defines a contracted function. The @racket[return-id] +identifier is part of the surface syntax. It is not a predefined binding +exported by this module. This keeps the form hygienic: the programmer +chooses the name of the local escape continuation, and the body uses that +same name explicitly. + +Ordinary results are checked by @racket[define/contract]. Early-returned +values are checked separately against @racket[result-contract]. The +implementation does this by defining a small local contracted returner +with contract @racket[(-> any/c result-contract)]. The early return value +therefore passes through Racket's contract machinery before it leaves the +function body. The contract must be written inline as a parenthesized contract form. The last element is used as the early-return result contract. @racketblock[ -(define/contract/return (h x) +(define/contract/return (h x) return (-> number? (or/c symbol? number?)) (when (< x 0) (return 'x-not-positive)) (let ((y (* x x))) @@ -60,8 +73,8 @@ last element is used as the early-return result contract. ] Here the result contract is @racket[(or/c symbol? number?)]. The symbol -returns are accepted, ordinary numeric results are accepted, and the string -@racket["Wrong answer!"] is rejected. +returns are accepted, ordinary numeric results are accepted, and the +string @racket["Wrong answer!"] is rejected. @codeblock|{ (h -1) ; => 'x-not-positive @@ -73,23 +86,23 @@ returns are accepted, ordinary numeric results are accepted, and the string A zero-argument function works in the same way: @racketblock[ -(define/contract/return (z) +(define/contract/return (z) return (-> symbol?) (let ((cs (current-seconds))) - (when (= (remainder cs 3) 0) (return "deelbaar door 3")) - (when (= (remainder cs 2) 0) (return 'dividable-by-2)) + (when (= (remainder cs 3) 0) (return "divisible by 3")) + (when (= (remainder cs 2) 0) (return 'divisible-by-2)) 'yes)) ] The result contract is @racket[symbol?]. Returning -@racket['dividable-by-2] or @racket['yes] is accepted. Returning the string -@racket["deelbaar door 3"] is rejected. +@racket['divisible-by-2] or @racket['yes] is accepted. Returning the +string @racket["divisible by 3"] is rejected. -Rest arguments can be used when the corresponding contract form is accepted -by @racket[define/contract]: +Rest arguments can be used when the corresponding contract form is +accepted by @racket[define/contract]: @racketblock[ -(define/contract/return (sum . xs) +(define/contract/return (sum . xs) return (->* () #:rest (listof number?) number?) (when (null? xs) (return 0)) (apply + xs)) @@ -101,18 +114,65 @@ by @racket[define/contract]: (sum 1 'x) ; contract violation }| -The second form defines a contracted value. The value expression may use -@racket[return], and the returned value is checked against -@racket[result-contract]. +Since @racket[return-id] is a continuation wrapped in a local contracted +procedure, it is called with one value. The continuation should normally +be used only to escape from the dynamic extent of the function body. + +The second form defines a contracted value. The value is checked against +@racket[result-contract]. This form has no @racket[return-id] position, +so it does not expose a local early-return identifier. Use the function +form when a body needs an explicit early return. @racketblock[ -(define/contract/return v +(define/contract/return answer number? - (return 'ss)) + 42) ] -This definition raises a contract violation, because @racket['ss] does not -satisfy @racket[number?]. +This definition succeeds because @racket[42] satisfies +@racket[number?]. +@racketblock[ +(define/contract/return bad-answer + number? + 'not-a-number) +] + +This definition raises a contract violation, because +@racket['not-a-number] does not satisfy @racket[number?]. } +@section{FFI-style null pointers} + +Many FFI bindings represent a native null pointer as @racket[#f]. The +explicit return identifier makes it easy to leave a wrapper as soon as a +required pointer is missing, while still checking the final result against +the function contract. + +@racketblock[ +(define/contract/return (make-stream-codec-context stream) return + (-> cpointer? (or/c cpointer? symbol?)) + (define codecpar (AVStream-codecpar stream)) + (unless codecpar + (return 'missing-codec-parameters)) + + (define codec + (avcodec-find-decoder + (AVCodecParameters-codec-id codecpar))) + (unless codec + (return 'decoder-not-found)) + + (define ctx (avcodec-alloc-context3 codec)) + (unless ctx + (return 'alloc-codec-context-failed)) + + ctx) +] + +The checks are sequential. The decoder lookup is evaluated only when +@racket[codecpar] is a valid pointer, and the context allocation is +evaluated only when @racket[codec] is a valid pointer. A failed pointer +check returns the corresponding symbol immediately. A successful path +returns @racket[ctx], and both the ordinary result and early-returned +symbols are covered by the result contract. + diff --git a/scrbl/define-return.scrbl b/scrbl/define-return.scrbl index 8d79c57..3ec13f2 100644 --- a/scrbl/define-return.scrbl +++ b/scrbl/define-return.scrbl @@ -1,7 +1,6 @@ #lang scribble/manual @(require (for-label racket/base - racket/contract "../main.rkt")) @title{define/return} @@ -9,41 +8,32 @@ @defmodule[define-return] -The @racketmodname[define-return] library provides definition forms with an -explicit early return. This is useful in small defensive functions, and -especially around FFI bindings, where null pointers, error codes, unsupported -states, or failed preconditions should leave the function immediately. +The @racketmodname[define-return] library provides @racket[define/return], +a small definition form with an explicit early return. It is useful in +compact defensive functions, and especially around FFI bindings, where a +null pointer, an error code, an unsupported state, or a failed precondition +should leave the function immediately. -The early return is implemented with an internal exception. A @racket[return] -raises that exception, and the definition forms catch it around the function -body. The contracted form additionally checks early-returned values against -the result contract. +The return mechanism is based on @racket[call/cc]. A @racket[define/return] +form captures the current continuation of the function body and binds it to +the return identifier supplied by the programmer. Calling that identifier +leaves the body and returns the supplied value as the result of the function. +No exception is raised and no exception handler is installed. -See @racketmodname[define-return/contract] for the contracted version of -this module. +@section{Definitions with an early return} -@section{Return} +@defform[(define/return def return body ...)]{ -@defform[(return val)]{ +Like @racket[define], but @racket[body] may call @racket[return] to leave +the definition early. -Returns @racket[val] from the nearest dynamically enclosing -@racket[define/return] or @racket[define/contract/return] body. - -The form is not a general escape continuation. It raises an internal return -exception. When used outside a body installed by this library, that exception -escapes. - -} - -@section{Uncontracted definitions} - -@defform[(define/return def body ...)]{ - -Like @racket[define], but @racket[body] may use @racket[return] to leave the -definition early. +The @racket[return-id] identifier is part of the surface syntax. It is not a +predefined binding exported by this module. This keeps the form hygienic: the +programmer chooses the name of the local escape continuation, and the body +uses that same name explicitly. @racketblock[ -(define/return (status->symbol code) +(define/return (status->symbol code) return (unless (number? code) (return 'not-a-number)) (when (= code 0) (return 'ok)) @@ -51,9 +41,7 @@ definition early. (when (> code 5) (return 'out-of-range)) (cond ((= code 1) 'normal) - ((>= code 2) (string->symbol (format "code-~a" (* code code)))) - ) - ) + ((>= code 2) (string->symbol (format "code-~a" (* code code)))))) ] The final expression is used when no early return is taken. @@ -69,24 +57,37 @@ The final expression is used when no early return is taken. } -@section{Contracted definitions} +@section{FFI-style checks} -See @racketmodname[define-return/contract]. +The form is deliberately small, but it fits the style of many C libraries. +For example, a wrapper can leave immediately when an FFI call returns a null +pointer: + +@racketblock[ +(define/return (make-codec-context codec-id) return + (define codec (avcodec-find-decoder codec-id)) + (unless codec + (return 'decoder-not-found)) + + (define ctx (avcodec-alloc-context3 codec)) + (unless ctx + (return 'alloc-codec-context-failed)) + + ctx) +] + +The second call is evaluated only when the first one succeeded. If either +call returns @racket[#f], the function returns the corresponding symbol. +Otherwise the final expression returns the allocated context. @section{Notes} -The mechanism is intentionally small. It uses @racket[with-handlers] and an -internal exception type; it does not introduce prompts, continuations, or a -new calling convention. +The mechanism is intentionally just a local escape. It uses @racket[call/cc] +to capture the continuation around the function body. The captured +continuation is bound to @racket[return] and can be called as a normal +procedure with one value. -For @racket[define/contract/return], the normal function contract remains the -job of @racket[define/contract]. The extra returner only exists for the path -where @racket[return] leaves the body through an exception handler. That path -would otherwise bypass the ordinary result position of the function body. +Since @racket[return] is a continuation, not a syntax form, it can also be +passed to local helper procedures when that is useful. It should normally be +used only as an escape from the dynamic extent of the function body. -The contracted form does not try to parse arbitrary contract syntax. It -splits the inline contract form syntactically and reuses its last element as -the early-return result contract. This works well for ordinary result -contracts such as @racket[number?], @racket[symbol?], -@racket[(or/c symbol? number?)], and the result position of @racket[->*] -contracts.