641 lines
24 KiB
Racket
641 lines
24 KiB
Racket
#lang at-exp racket/base
|
|
(require "common.rkt"
|
|
scribble/decode
|
|
scribble/struct
|
|
scribble/racket
|
|
racket/list
|
|
racket/pretty
|
|
syntax/docprovide
|
|
(for-syntax racket/base)
|
|
)
|
|
|
|
(provide prim-variables
|
|
prim-forms
|
|
define-forms/normal
|
|
define-form/explicit-lambda
|
|
beginner-abbr-forms
|
|
intermediate-forms
|
|
prim-ops
|
|
prim-op-defns)
|
|
|
|
(define (maybe-make-table l t)
|
|
(if (paragraph? t)
|
|
(make-paragraph
|
|
(append l (cons " "
|
|
(paragraph-content t))))
|
|
(make-table
|
|
"prototype"
|
|
(list (list (make-flow (list (make-paragraph l)))
|
|
(make-flow (list t)))))))
|
|
|
|
|
|
(define (typeset-type type)
|
|
(let-values ([(in out) (make-pipe)])
|
|
(parameterize ([pretty-print-columns 50])
|
|
(pretty-write type out))
|
|
(port-count-lines! in)
|
|
(read-syntax #f in)))
|
|
|
|
(define (sort-category category)
|
|
(sort
|
|
(cadr category)
|
|
(lambda (x y)
|
|
(string<=? (symbol->string (car x))
|
|
(symbol->string (car y))))))
|
|
|
|
|
|
(define (make-proto func ctx-stx)
|
|
(maybe-make-table
|
|
(list
|
|
(hspace 2)
|
|
(to-element (datum->syntax ctx-stx (car func)))
|
|
(hspace 1)
|
|
":"
|
|
(hspace 1))
|
|
(to-paragraph
|
|
(typeset-type (cadr func)))))
|
|
|
|
(define-syntax-rule
|
|
(prim-variables (section-prefix) empty true false ..2 ..3 ..4 ..5 ..6)
|
|
;; ===>
|
|
(make-splice
|
|
(list
|
|
@section[#:tag (string-append section-prefix " Pre-Defined Variables")]{Pre-Defined Variables}
|
|
|
|
@defthing[empty empty?]{
|
|
|
|
The empty list.}
|
|
|
|
@defthing[true boolean?]{
|
|
|
|
The true value.}
|
|
|
|
@defthing[false boolean?]{
|
|
|
|
The false value.}
|
|
|
|
@section[#:tag (string-append section-prefix " Template Variables")]{Template Variables}
|
|
@; MF: I tried abstracting but I failed
|
|
@defidform[..2]{A placeholder for indicating that a definition is a template.}
|
|
@defidform[..3]{A placeholder for indicating that a definition is a template.}
|
|
@defidform[..4]{A placeholder for indicating that a definition is a template.}
|
|
@defidform[..5]{A placeholder for indicating that a definition is a template.}
|
|
@defidform[..6]{A placeholder for indicating that a definition is a template.}
|
|
)))
|
|
|
|
;; ----------------------------------------
|
|
|
|
(define-syntax-rule (define-forms/normal define)
|
|
(gen-define-forms/normal #'define @racket[define]))
|
|
|
|
(define (gen-define-forms/normal define-id define-elem)
|
|
;; Since `define' has a source location different from the use site,
|
|
;; use the `#:id [spec-id bind-id]' form in `defform*':
|
|
(list
|
|
@defform*[#:id [define define-id]
|
|
[(define (name variable variable ...) expression)]]{
|
|
|
|
Defines a function named @racket[name]. The @racket[expression] is the body
|
|
of the function. When the function is called,
|
|
the values of the arguments are inserted into the body in place of the
|
|
@racket[variable]s. The function returns the value of that new expression.
|
|
|
|
The function name's cannot be the same as that of another function or
|
|
variable.}
|
|
|
|
@defform/none[(@#,define-elem name expression)]{
|
|
|
|
Defines a variable called @racket[name] with the the value of
|
|
@racket[expression]. The variable name's cannot be the same as that of
|
|
another function or variable, and @racket[name] itself must not appear in
|
|
@racket[expression].}))
|
|
|
|
;; ----------------------------------------
|
|
|
|
(define-syntax-rule (define-form/explicit-lambda define lambda)
|
|
(gen-define-form/explicit-lambda @racket[define]
|
|
#'lambda @racket[lambda]))
|
|
|
|
(define (gen-define-form/explicit-lambda define-elem lambda-id lambda-elem)
|
|
(list
|
|
@defform/none[(#,define-elem name (#,lambda-elem (variable variable ...) expression))]{
|
|
|
|
An alternate way on defining functions. The @racket[name] is the name of
|
|
the function, which cannot be the same as that of another function or
|
|
variable.
|
|
|
|
A @defidform/inline[#,lambda-id] cannot be used outside of this alternate syntax.}))
|
|
|
|
;; ----------------------------------------
|
|
|
|
(define-syntax-rule (prim-forms
|
|
(section-prefix)
|
|
define
|
|
lambda
|
|
define-struct [ds-extra ...]
|
|
define-wish
|
|
cond
|
|
else
|
|
if
|
|
and
|
|
or
|
|
check-expect
|
|
check-within
|
|
check-error
|
|
check-member-of
|
|
check-range
|
|
require
|
|
true
|
|
false
|
|
#:with-beginner-function-call with-beginner-function-call)
|
|
(gen-prim-forms #'define-struct @racket[define-struct] (list ds-extra ...)
|
|
#'cond @racket[cond]
|
|
#'else @racket[else]
|
|
#'if @racket[if]
|
|
#'or @racket[or]
|
|
#'and @racket[and]
|
|
#'check-expect @racket[check-expect]
|
|
#'check-within @racket[check-within]
|
|
#'check-error @racket[check-error]
|
|
#'check-member-of @racket[check-member-of]
|
|
#'check-range @racket[check-range]
|
|
#'require @racket[require]
|
|
@racket[true] @racket[false]
|
|
with-beginner-function-call))
|
|
|
|
(define (gen-prim-forms define-struct-id define-struct-elem ds-extras
|
|
cond-id cond-elem
|
|
else-id else-elem
|
|
if-id if-elem
|
|
or-id or-elem
|
|
and-id and-elem
|
|
check-expect-id check-expect-elem
|
|
check-within-id check-within-elem
|
|
check-error-id check-error-elem
|
|
check-member-of-id check-member-of-elem
|
|
check-range-id check-range-elem
|
|
require-id require-elem
|
|
true-elem false-elem
|
|
with-beginner-function-call)
|
|
(list
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@defform*[#:id [define-struct define-struct-id]
|
|
[(define-struct structure-name (field-name ...))]]{
|
|
|
|
Defines a new structure called @racket[structure-name]. The structure's fields are
|
|
named by the @racket[field-name]s. After the @define-struct-elem, the following new
|
|
functions are available:
|
|
|
|
@itemize[
|
|
|
|
@item{@racketidfont{make-}@racket[structure-name] : takes a number of
|
|
arguments equal to the number of fields in the structure,
|
|
and creates a new instance of that structure.}
|
|
|
|
@item{@racket[structure-name]@racketidfont{-}@racket[field-name] : takes an
|
|
instance of the structure and returns the value in the field named by
|
|
@racket[field-name].}
|
|
|
|
@item{@racket[structure-name]@racketidfont{?} : takes any value, and returns
|
|
@true-elem if the value is an instance of the structure.}
|
|
]
|
|
|
|
The name of the new functions introduced by @define-struct-elem
|
|
must not be the same as that of other functions or variables,
|
|
otherwise @define-struct-elem reports an error.
|
|
|
|
@ds-extras}
|
|
|
|
#|
|
|
|
|
@defform*[[(define-wish name)]]{
|
|
|
|
Defines a function called @racket[name] that we wish exists but have not
|
|
implemented yet. The wished-for function can be called with one argument, and
|
|
are reported in the test report for the current program.
|
|
|
|
The name of the function cannot be the same as another function or variable.}
|
|
|
|
|
|
@defform/none[#:literals (define-wish)
|
|
(define-wish name expression)]{
|
|
Similar to the above form, defines a wished-for function named @racket[name]. If the
|
|
wished-for function is called with one value, it returns the values of @racket[expression]. }
|
|
|#
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@(if with-beginner-function-call
|
|
@defform/none[(name expression expression ...)]{
|
|
Calls the function named @racket[name]. The value of the call is the
|
|
value of @racket[name]'s body when every one of the function's
|
|
variables are replaced by the values of the corresponding
|
|
@racket[expression]s.
|
|
|
|
The function named @racket[name] must defined before it can be called. The
|
|
number of argument @racket[expression]s must be the same as the number of arguments
|
|
expected by the function.}
|
|
@elem[])
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@defform*[#:id [cond cond-id]
|
|
#:literals (else)
|
|
[(cond [question-expression answer-expression] ...)
|
|
(#,cond-elem [question-expression answer-expression]
|
|
...
|
|
[#,else-elem answer-expression])]]{
|
|
|
|
Chooses a clause based on some condition. @racket[cond] finds the first
|
|
@racket[question-expression] that evaluates to @true-elem, then
|
|
evaluates the corresponding @racket[answer-expression].
|
|
|
|
If none of the @racket[question-expression]s evaluates to @true-elem,
|
|
@cond-elem's value is the @racket[answer-expression] of the
|
|
@else-elem clause. If there is no @else-elem, @cond-elem reports
|
|
an error. If the result of a @racket[question-expression] is neither
|
|
@true-elem nor @false-elem, @cond-elem also reports an error.
|
|
|
|
@defidform/inline[#,else-id] cannot be used outside of @|cond-elem|.}
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@defform*[#:id [if if-id]
|
|
[(if test-expression then-expression else-expression)]]{
|
|
|
|
When the value of the @racket[test-expression] is @true-elem,
|
|
@if-elem evaluates the @racket[then-expression]. When the test is
|
|
@false-elem, @if-elem evaluates the @racket[else-expression].
|
|
|
|
If the @racket[test-expression] is neither @true-elem nor
|
|
@false-elem, @if-elem reports an error.}
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@defform*[#:id [and and-id]
|
|
[(and expression expression expression ...)]]{
|
|
|
|
Evaluates to @true-elem if all the @racket[expression]s are
|
|
@|true-elem|. If any @racket[expression] is @|false-elem|, the @and-elem
|
|
expression evaluates to @false-elem (and the expressions to the
|
|
right of that expression are not evaluated.)
|
|
|
|
If any of the expressions evaluate to a value other than @true-elem or
|
|
@false-elem, @and-elem reports an error.}
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
|
|
@defform*[#:id [or or-id]
|
|
[(or expression expression expression ...)]]{
|
|
|
|
Evaluates to @true-elem as soon as one of the
|
|
@racket[expression]s is @true-elem (and the expressions to the right of that
|
|
expression are not evaluated.) If all of the @racket[expression]s are @|false-elem|,
|
|
the @or-elem expression evaluates to @|false-elem|.
|
|
|
|
If any of the expressions evaluate to a value other than @true-elem or
|
|
@false-elem, @or-elem reports an error.}
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
|
|
@defform*[#:id [check-expect check-expect-id]
|
|
[(check-expect expression expected-expression)]]{
|
|
|
|
Checks that the first @racket[expression] evaluates to the same value as the
|
|
@racket[expected-expression].}
|
|
|
|
|
|
@defform*[#:id [check-within check-within-id]
|
|
[(check-within expression expected-expression delta)]]{
|
|
|
|
Checks whether the value of the @racket[expression] expression is
|
|
structurally equal to the value produced by the
|
|
@racket[expected-expression] expression; every number in the first
|
|
expression must be within @racket[delta] of the corresponding number in
|
|
the second expression.
|
|
|
|
It is an error for @racket[expressions] or @racket[expected-expression]
|
|
to produce a function value.
|
|
|
|
If @racket[delta] is not a number, @check-within-elem reports an error.}
|
|
|
|
|
|
@defform*[#:id [check-error check-error-id]
|
|
[(check-error expression expected-error-message)
|
|
(#,check-error-elem expression)]]{
|
|
|
|
Checks that the @racket[expression] reports an error,
|
|
where the error messages matches the
|
|
value of @racket[expected-error-message], if it is present.}
|
|
|
|
|
|
@defform*[#:id [check-member-of check-member-of-id]
|
|
[(check-member-of expression expression expression ...)]]{
|
|
|
|
Checks that the value of the first @racket[expression] as that of
|
|
one of the following @racket[expression]s.}
|
|
|
|
|
|
@defform*[#:id [check-range check-range-id]
|
|
[(check-range expression low-expression high-expression)]]{
|
|
|
|
Checks that the value of the first @racket[expression] is a number in
|
|
between the value of the @racket[low-expression] and the
|
|
@racket[high-expression], inclusive.}
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@defform*[#:id [require require-id]
|
|
[(require string)]]{
|
|
|
|
Makes the definitions of the module specified by @racket[string]
|
|
available in the current module (i.e., the current file), where
|
|
@racket[string] refers to a file relative to the current file.
|
|
|
|
The @racket[string] is constrained in several ways to avoid
|
|
problems with different path conventions on different platforms: a
|
|
@litchar{/} is a directory separator, @litchar{.} always means the
|
|
current directory, @litchar{..} always means the parent directory,
|
|
path elements can use only @litchar{a} through @litchar{z}
|
|
(uppercase or lowercase), @litchar{0} through @litchar{9},
|
|
@litchar{-}, @litchar{_}, and @litchar{.}, and the string cannot be
|
|
empty or contain a leading or trailing @litchar{/}.}
|
|
|
|
|
|
@defform/none[(#,require-elem module-name)]{
|
|
|
|
Accesses a file in an installed library. The library name is an
|
|
identifier with the same constraints as for a relative-path string
|
|
(though without the quotes), with the additional constraint that it
|
|
must not contain a @litchar{.}.}
|
|
|
|
@defform/none[(#,require-elem (lib string string ...))]{
|
|
|
|
Accesses a file in an installed library, making its definitions
|
|
available in the current module (i.e., the current file). The first
|
|
@racket[string] names the library file, and the remaining
|
|
@racket[string]s name the collection (and sub-collection, and so on)
|
|
where the file is installed. Each string is constrained in the same
|
|
way as for the @racket[(#,require-elem string)] form.}
|
|
|
|
|
|
@deftogether[(
|
|
@defform/none[#:literals (planet)
|
|
(#,require-elem (planet string (string string number number)))]{}
|
|
@defform/none[#:literals (planet) (#,require-elem (planet id))]{}
|
|
@defform/none[#:literals (planet) (#,require-elem (planet string))]{})]{
|
|
|
|
Accesses a library that is distributed on the internet via the
|
|
@|PLaneT| server, making it definitions available in the current module
|
|
(i.e., current file).
|
|
|
|
The full grammar for planet requires is given in
|
|
@secref["require"
|
|
#:doc '(lib "scribblings/reference/reference.scrbl")], but
|
|
the best place to find examples of the syntax is on the
|
|
@link["http://planet.racket-lang.org"]{the @PLaneT server}, in the
|
|
description of a specific package.
|
|
}
|
|
))
|
|
|
|
;; ----------------------------------------
|
|
|
|
(define-syntax-rule
|
|
(beginner-abbr-forms quote quasiquote unquote unquote-splicing)
|
|
(gen-beginner-abbr-forms #'quote @racket[quote]
|
|
#'quasiquote @racket[quasiquote]
|
|
#'unquote @racket[unquote]
|
|
#'unquote-splicing @racket[unquote-splicing]))
|
|
|
|
(define (gen-beginner-abbr-forms quote-id quote-elem
|
|
quasiquote-id quasiquote-elem
|
|
unquote-id unquote-elem
|
|
unquote-splicing-id unquote-splicing-elem)
|
|
|
|
(list
|
|
@deftogether[(
|
|
@defform/none[(unsyntax @elem{@racketvalfont{'}@racket[name]})]
|
|
@defform/none[(unsyntax @elem{@racketvalfont{'}@racket[part]})]
|
|
@defform[#:id [quote quote-id] (quote name)]
|
|
@defform/none[(#,quote-elem part)]
|
|
)]{
|
|
|
|
A quoted name is a symbol. A quoted part is an abbreviation for a nested lists.
|
|
|
|
Normally, this quotation is written with a @litchar{'}, like
|
|
@racket['(apple banana)], but it can also be written with
|
|
@quote-elem, like @racket[(@#,quote-elem (apple banana))].}
|
|
|
|
|
|
@deftogether[(
|
|
@defform/none[(unsyntax @elem{@racketvalfont{`}@racket[name]})]
|
|
@defform/none[(unsyntax @elem{@racketvalfont{`}@racket[part]})]
|
|
@defform[#:id [quasiquote quasiquote-id]
|
|
(quasiquote name)]
|
|
@defform/none[(#,quasiquote-elem part)]
|
|
)]{
|
|
|
|
Like @quote-elem, but also allows escaping to expression
|
|
``unquotes.''
|
|
|
|
Normally, quasi-quotations are written with a backquote,
|
|
@litchar{`}, like @racket[`(apple ,(+ 1 2))], but they can also be
|
|
written with @quasiquote-elem, like
|
|
@racket[(@quasiquote-elem (apple ,(+ 1 2)))].}
|
|
|
|
|
|
@deftogether[(
|
|
@defform/none[(unsyntax @elem{@racketvalfont{,}@racket[expression]})]
|
|
@defform[#:id [unquote unquote-id]
|
|
(unquote expression)]
|
|
)]{
|
|
|
|
Under a single quasiquote, @racketfont{,}@racket[expression]
|
|
escapes from the quote to include an evaluated expression whose
|
|
result is inserted into the abbreviated list.
|
|
|
|
Under multiple quasiquotes, @racketfont{,}@racket[expression] is
|
|
really the literal @racketfont{,}@racket[expression], decrementing
|
|
the quasiquote count by one for @racket[expression].
|
|
|
|
Normally, an unquote is written with @litchar{,}, but it can also be
|
|
written with @|unquote-elem|.}
|
|
|
|
|
|
@deftogether[(
|
|
@defform/none[(unsyntax @elem{@racketvalfont[",@"]@racket[expression]})]
|
|
@defform[#:id [unquote-splicing unquote-splicing-id]
|
|
(unquote-splicing expression)]
|
|
)]{
|
|
|
|
Under a single quasiquote, @racketfont[",@"]@racket[expression]
|
|
escapes from the quote to include an evaluated expression whose
|
|
result is a list to splice into the abbreviated list.
|
|
|
|
Under multiple quasiquotes, a splicing unquote is like an unquote;
|
|
that is, it decrements the quasiquote count by one.
|
|
|
|
Normally, a splicing unquote is written with @litchar{,}, but it
|
|
can also be written with @|unquote-splicing-elem|.}
|
|
|
|
))
|
|
|
|
|
|
(define-syntax-rule
|
|
(intermediate-forms lambda
|
|
local
|
|
letrec
|
|
let*
|
|
let
|
|
time
|
|
define
|
|
define-struct)
|
|
(gen-intermediate-forms #'lambda @racket[lambda]
|
|
#'local @racket[local]
|
|
#'letrec @racket[letrec]
|
|
#'let* @racket[let*]
|
|
#'let @racket[let]
|
|
#'time @racket[time]
|
|
@racket[define]
|
|
@racket[define-struct]))
|
|
|
|
(define (gen-intermediate-forms lambda-id lambda-elem
|
|
local-id local-elem
|
|
letrec-id letrec-elem
|
|
let*-id let*-elem
|
|
let-id let-elem
|
|
time-id time-elem
|
|
define-elem
|
|
define-struct-elem
|
|
)
|
|
(list
|
|
|
|
@defform[#:id [local local-id]
|
|
(local [definition ...] expression)]{
|
|
|
|
Groups related definitions for use in @racket[expression]. Each
|
|
@racket[definition] can be either a @define-elem or a
|
|
@|define-struct-elem|.
|
|
|
|
When evaluating @local-elem, each @racket[definition] is evaluated
|
|
in order, and finally the body @racket[expression] is
|
|
evaluated. Only the expressions within the @local-elem (including
|
|
the right-hand-sides of the @racket[definition]s and the
|
|
@racket[expression]) may refer to the names defined by the
|
|
@racket[definition]s. If a name defined in the @local-elem is the
|
|
same as a top-level binding, the inner one ``shadows'' the outer
|
|
one. That is, inside the @local-elem, any references to that name
|
|
refer to the inner one.}
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@defform[#:id [letrec letrec-id]
|
|
(letrec ([name expr-for-let] ...) expression)]{
|
|
|
|
Like @local-elem, but with a simpler syntax. Each @racket[name]
|
|
defines a variable (or a function) with the value of the
|
|
corresponding @racket[expr-for-let]. If @racket[expr-for-let] is a
|
|
@lambda-elem, @letrec-elem defines a function, otherwise it
|
|
defines a variable.}
|
|
|
|
|
|
@defform[#:id [let* let*-id]
|
|
(let* ([name expr-for-let] ...) expression)]{
|
|
|
|
Like @letrec-elem, but each @racket[name] can only be used in
|
|
@racket[expression], and in @racket[expr-for-let]s occuring after
|
|
that @racket[name].}
|
|
|
|
|
|
@defform[#:id [let let-id]
|
|
(let ([name expr-for-let] ...) expression)]{
|
|
|
|
Like @letrec-elem, but the defined @racket[name]s can be used only
|
|
in the last @racket[expression], not the @racket[expr-for-let]s
|
|
next to the @racket[name]s.}
|
|
|
|
@; ----------------------------------------------------------------------
|
|
|
|
@defform[#:id [time time-id]
|
|
(time expression)]{
|
|
|
|
Measures the time taken to evaluate @racket[expression]. After
|
|
evaluating @racket[expression], @racket[time] prints out the time
|
|
taken by the evaluation (including real time, time taken by the
|
|
CPU, and the time spent collecting free memory). The value of
|
|
@time-elem is the same as that of @racket[expression].}))
|
|
|
|
;; ----------------------------------------
|
|
|
|
(define (prim-ops lib ctx-stx)
|
|
(let ([ops (map (lambda (cat)
|
|
(cons (car cat)
|
|
(list (cdr cat))))
|
|
(lookup-documentation lib 'procedures))])
|
|
(make-table
|
|
#f
|
|
(cons
|
|
(list
|
|
(make-flow
|
|
(list
|
|
(make-paragraph
|
|
(list "In function calls, the function appearing immediatly after the open parenthesis can be any functions
|
|
defined with " (racket define) " or " (racket define-struct) ", or any one of:")))))
|
|
(apply
|
|
append
|
|
(map (lambda (category)
|
|
(cons
|
|
(list (make-flow
|
|
(list
|
|
(make-paragraph (list (hspace 1)
|
|
(bold (car category)))))))
|
|
(map (lambda (func)
|
|
(list
|
|
(make-flow
|
|
(list
|
|
(make-proto func ctx-stx)))))
|
|
(sort-category category))))
|
|
ops))))))
|
|
|
|
|
|
(define (prim-op-defns lib ctx-stx not-in)
|
|
(make-splice
|
|
(let ([ops (map (lambda (cat)
|
|
(cons (car cat)
|
|
(list (cdr cat))))
|
|
(lookup-documentation lib 'procedures))]
|
|
[not-in-ns (map (lambda (not-in-mod)
|
|
(let ([ns (make-base-namespace)])
|
|
(parameterize ([current-namespace ns])
|
|
(namespace-require `(for-label ,not-in-mod)))
|
|
ns))
|
|
not-in)])
|
|
(apply
|
|
append
|
|
(map (lambda (category)
|
|
(cons
|
|
(subsection #:tag-prefix (format "~a" lib) (car category))
|
|
(filter values
|
|
(map
|
|
(lambda (func)
|
|
(let ([id (datum->syntax ctx-stx (car func))])
|
|
(and (not (ormap
|
|
(lambda (ns)
|
|
(free-label-identifier=?
|
|
id
|
|
(parameterize ([current-namespace ns])
|
|
(namespace-syntax-introduce (datum->syntax #f (car func))))))
|
|
not-in-ns))
|
|
(let ([desc-strs (cddr func)])
|
|
(defthing/proc
|
|
(if (pair? (cadr func)) "function" "constant")
|
|
id
|
|
(to-paragraph (typeset-type (cadr func)))
|
|
desc-strs)))))
|
|
(sort-category category)))))
|
|
ops)))))
|
|
|