Option sets: a .o target for shared parameters

A named group of optional parameters, declared once and used by several
klammers, so a writer learns one vocabulary instead of a spelling per
klammer.  The "o" target is a pseudo-target beside "k": "k" declares a
klammer's interface and documents it, "o" declares an option interface and
documents it, and neither produces output for any target.

    @@caption_args.o :caption :number.bool true :caption_side.side
    : Arguments that define a caption for a block element @@

    @@code.k :filename @hpos_args :hpos left @ @caption_args :caption_side top @
    | text.literal : A source file displayed verbatim @@

A set is used only in the parameter list of a ".k" declaration -- the one
place a klammer's interface is declared once for all of its targets -- and
is resolved as that list is read.  Names and types come from the set; a
default may be overridden where it is used.  A klammer application in a
parameter list is now a definition-time error.

The SKS gains the sets caption_args and hpos_args (:hpos and :offset), and
@table, @image, @image_grid, @reference and @show gain .k declarations.  A
distance is no longer written as a position: :hpos 4em is rejected, and the
same layout is :hpos left :offset 4em.  Code listings are numbered by
default, like tables and figures.

New engine sources mac/option_set{,_registry}.{h,cpp}; tst/ ships two more
suites, option_set_test.sh and signature_test.sh (twelve in all).

 (from dev 34e536cb0329)
This commit is contained in:
2026-08-06 13:11:37 +02:00
parent 4306dcd490
commit 6e7596ab2e
37 changed files with 2198 additions and 267 deletions

View File

@@ -17,4 +17,6 @@ test:
./alone_test.sh
./modulepath_test.sh
./klammerset_test.sh
./option_set_test.sh
./signature_test.sh
./editor_test.sh

307
tst/option_set_test.sh Executable file
View File

@@ -0,0 +1,307 @@
#!/bin/bash
#
# option_set_test.sh — Regression tests for option sets (the ".o" target).
#
# An option set is a named group of OPTIONAL parameters, declared once and
# used by several klammers, so that a writer learns one vocabulary instead of
# a spelling per klammer:
#
# @@caption_args.o :caption :number.bool true : A caption @@
# @@table.k rows.rest(2) @caption_args@ : A table of rows of cells @@
#
# The rules this suite holds to:
#
# * "o" is a pseudo-target beside "k". A ".o" declaration defines no
# klammer and produces no output for any target.
# * A set declares optional parameters only. A positional is not
# writer-facing, so there is nothing for a set to standardize.
# * Names and types come from the set; a DEFAULT may be overridden where
# the set is used, because what varies between klammers is only what
# silence means for that one klammer.
# * A set may be used only in the parameter list of a ".k" declaration --
# the one place a klammer's interface is declared once for all of its
# targets. Not in a per-target definition, and not in another set.
# * A klammer application in a parameter list is an error. Splicing a
# constant klammer there used to be the way to share parameters, and in
# a ".k" declaration it silently destroyed the whole parameter list.
#
# Engine tier: no SKS. Every fixture defines its own target inline.
#
# Usage: ./option_set_test.sh
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# The fixture prelude: a target, and two sets to use in declarations.
PRELUDE='@@@target fix | a fixture target @@@
@@cap.o :caption :number.bool true :side bottom : A caption for an element @@
@@pos.o :hpos left : Where an element sits @@'
# check_eq NAME EXPECTED SOURCE [TARGET] — render SOURCE (for the fix target
# unless TARGET says otherwise) and compare the trimmed output with EXPECTED.
check_eq() {
local name="$1" expected="$2" source="$3" target="${4:-fix}"
local output status
output=$("$KTEXT" -k none -s "$PRELUDE
$source" -t "$target" -d 2>&1)
status=$?
output=$(printf '%s' "$output" | tr -d '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — ktext exited $status"
echo " output: $(printf '%s' "$output" | head -3)"
FAIL=$((FAIL + 1))
return
fi
if [ "$output" = "$expected" ]; then
echo "${green}PASS${reset} $name"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $name"
echo " expected: [$expected]"
echo " got: [$output]"
FAIL=$((FAIL + 1))
fi
}
# check_fails NAME SUBSTRING SOURCE — SOURCE must be rejected, with a
# message containing SUBSTRING.
check_fails() {
local name="$1" needle="$2" source="$3"
local output status
output=$("$KTEXT" -k none -s "$PRELUDE
$source" -t fix -d 2>&1)
status=$?
if [ $status -eq 0 ]; then
echo "${red}FAIL${reset} $name — expected an error, ktext exited 0"
FAIL=$((FAIL + 1))
return
fi
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
echo "${green}PASS${reset} $name"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $name"
echo " expected error containing: [$needle]"
echo " got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
FAIL=$((FAIL + 1))
fi
}
# check_warns NAME SUBSTRING SOURCE — SOURCE must be accepted, and its
# combined output must contain SUBSTRING (used where a warning is expected
# beside the result, so an exact comparison would test the warning's wording).
check_warns() {
local name="$1" needle="$2" source="$3"
local output status
output=$("$KTEXT" -k none -s "$PRELUDE
$source" -t fix -d 2>&1)
status=$?
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — ktext exited $status"
FAIL=$((FAIL + 1))
return
fi
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
echo "${green}PASS${reset} $name"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $name"
echo " expected a warning containing: [$needle]"
echo " got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
FAIL=$((FAIL + 1))
fi
}
echo "${bold}Option set (.o) tests${reset}"
echo "====================="
echo
# --- The members become the klammer's parameters --------------------------
# A declaration using a set accepts the set's options, and the writer's
# arguments reach the body under the names the set declares.
check_eq " 1. a set's parameters are the klammer's" \
"[A|top]" \
'@@e.k x @cap@ : an element @@
@@e.fix :: [*caption*|*side*] @@
@e X :caption A :side top @'
# Absent argument, declared default: the set supplies it.
check_eq " 2. a member's default applies when the argument is absent" \
"[|true|bottom]" \
'@@e.k x @cap@ : an element @@
@@e.fix :: [*caption*|*number*|*side*] @@
@e X @'
# The use site may override a default -- and only the default.
check_eq " 3. a use-site override changes the default" \
"[|true|top]" \
'@@e.k x @cap :side top @ : an element @@
@@e.fix :: [*caption*|*number*|*side*] @@
@e X @'
check_eq " 4. a boolean default can be overridden to false" \
"[false]" \
'@@e.k x @cap :number false @ : an element @@
@@e.fix :: [*number*] @@
@e X @'
# An option written alone at the use site takes the argument type's :alone
# value, exactly as it does where an argument is written.
check_eq " 5. an override written alone takes the argtype's alone value" \
"[true]" \
'@@n.o :number.bool false : A number @@
@@e.k x @n :number @ : an element @@
@@e.fix :: [*number*] @@
@e X @'
# Three levels: argument type, option set, use site -- and the writer's
# argument still wins over all of them.
check_eq " 6. a written argument wins over the overridden default" \
"[maybe]" \
'@@e.k x @cap :side top @ : an element @@
@@e.fix :: [*side*] @@
@e X :side maybe @'
check_eq " 7. two sets in one declaration" \
"[bottom|left]" \
'@@e.k x @cap@ @pos@ : an element @@
@@e.fix :: [*side*|*hpos*] @@
@e X @'
check_eq " 8. a set's parameters mix with the klammer's own" \
"[own|bottom]" \
'@@e.k x :mine own @cap@ : an element @@
@@e.fix :: [*mine*|*side*] @@
@e X @'
# Every target definition is an instance, so all of them get the expanded
# list: the set is resolved once, in the declaration.
check_eq " 9. a second target's instance inherits the same parameters" \
"<bottom>" \
'@@@target fix2 | another fixture target @@@
@@e.k x @cap@ : an element @@
@@e.fix :: [*side*] @@
@@e.fix2 :: <*side*> @@
@e X @' \
fix2
# --- The declaration is not a klammer -------------------------------------
check_fails "10. a set defines no klammer" \
'The klammer "cap" is not defined' \
'@cap@'
# --- Where a set may be used ----------------------------------------------
check_fails "11. not in a per-target definition" \
'may be used only in the parameter list of a ".k" declaration' \
'@@e.fix x @cap@ : [*caption*] @@'
check_fails "12. not in a general definition" \
'may be used only in the parameter list of a ".k" declaration' \
'@@e x @cap@ : [*caption*] @@'
check_fails "13. not in another set" \
'a set does not include another set' \
'@@both.o :extra @cap@ : two vocabularies @@'
# --- A klammer application in a parameter list ----------------------------
check_fails "14. a klammer application in a parameter list is rejected" \
'A klammer application in a parameter list is not allowed' \
'@@c : :spliced @@
@@e.k x @c@ : an element @@'
check_fails "15. ... and the message names the declared sets" \
'Declared option sets: cap, pos' \
'@@e.k x @nosuch@ : an element @@'
# --- Use-site overrides ---------------------------------------------------
check_fails "16. an override must name a member of the set" \
'The option set "cap" has no parameter ":nope"' \
'@@e.k x @cap :nope 1 @ : an element @@'
check_fails "17. an override value is validated at definition time" \
'does not match the "bool" argument type' \
'@@e.k x @cap :number perhaps @ : an element @@'
check_fails "18. an override may not restate the type" \
'only a default may be given where it is used' \
'@@e.k x @cap :number.bool false @ : an element @@'
check_fails "19. an override may not give a positional value" \
'gives a value that is not an option' \
'@@e.k x @cap here @ : an element @@'
# --- What a set may declare -----------------------------------------------
check_fails "20. a positional parameter is rejected" \
'An option set declares only optional parameters' \
'@@bad.o p :q : oops @@'
check_fails "21. a rest parameter is rejected" \
'An option set declares only optional parameters' \
'@@bad.o r.rest :q : oops @@'
check_fails "22. a set with no parameters is rejected" \
'declares no parameters' \
'@@bad.o : nothing at all @@'
check_fails '23. "::" has no meaning for a set' \
'An option set IS a declaration' \
'@@cap.o :: nope @@'
# --- Collisions -----------------------------------------------------------
check_fails "24. two sets declaring the same name name both sets" \
'from the option set "cap"' \
'@@other.o :side right : another side @@
@@e.k x @cap@ @other@ : an element @@'
check_fails "25. a set colliding with a declared parameter" \
'declared in the parameter list' \
'@@e.k x :side own @cap@ : an element @@'
# --- Redefinition ---------------------------------------------------------
check_fails "26. declaring a set twice is an error" \
'Option set "cap.o" already defined' \
'@@cap.o :caption : a second caption @@'
# A set is its own declaration, so an override restates what it declares --
# there is no ".k" for it to inherit a parameter list from.
check_warns '27. ":::" overrides a set, with a warning' \
'overridden' \
'@@cap.o :caption :number.bool true :side top ::: a replaced caption @@'
check_warns "28. ... and the overriding declaration is what a klammer gets" \
"[top]" \
'@@cap.o :caption :number.bool true :side top ::: a replaced caption @@
@@e.k x @cap@ : an element @@
@@e.fix :: [*side*] @@
@e X @'
# A "::::" default is silently superseded by a later create, and the create
# is what the using declaration gets.
check_eq "29. a default set is superseded by a later declaration" \
"[right]" \
'@@d.o :where left :::: a default @@
@@d.o :where right : the real one @@
@@e.k x @d@ : an element @@
@@e.fix :: [*where*] @@
@e X @'
echo
echo "====================="
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
[ $FAIL -eq 0 ]

143
tst/signature_test.sh Executable file
View File

@@ -0,0 +1,143 @@
#!/bin/bash
#
# signature_test.sh — one klammer, one interface (engine suite, tst/).
#
# A klammer may be defined separately for each target. When it is, every
# target's definition carries its own parameter list, and those lists must
# agree: a klammer's interface is a property of the KLAMMER, not of the
# target it is being rendered to. If they disagree, the same document would
# bind arguments differently — or fail — depending only on the target, which
# is exactly the thing an author must be able to rely on not happening.
#
# The engine enforces this rather than warning about it, and the remedy it
# names is the .k declaration: declare the parameters once, and give each
# target a "::" instance with no parameter list of its own. That is the
# migration from repeating an argument list per target to declaring it once.
#
# These are engine tests, so they use -k none and define their own targets
# inline: a target is a Machine construct (@@@target), not owned by any
# klammer set.
#
# Usage: ./signature_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS+1)); }
fail() {
echo "${red}FAIL${reset} $1"
echo " expected: $2"
echo " got: $3"
FAIL=$((FAIL+1))
}
TARGETS='@@@target ta | Target A @@@
@@@target tb | Target B @@@
'
# accepted NAME SRC — the definitions are consistent and the klammer applies.
accepted() {
local name="$1" src="$2" out
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
if echo "$out" | grep -qi "error"; then
fail "$name" "no error" "$(echo "$out" | grep -i -A1 error | tail -1)"
else
pass "$name"
fi
}
# rejected NAME SRC PATTERN — the drift is caught, and the message says how.
rejected() {
local name="$1" src="$2" want="$3" out
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
if ! echo "$out" | grep -qi "error"; then
fail "$name" "a definition error" "accepted"
elif ! echo "$out" | grep -qF "$want"; then
fail "$name" "message containing: $want" "$(echo "$out" | head -6 | tail -3)"
else
pass "$name"
fi
}
echo "${bold}Klammer signature consistency${reset}"
echo "============================="
echo
accepted " 1. identical parameter lists" \
'@@k1.ta s :n : [*s*] @@
@@k1.tb s :n : [*s*] @@
@k1 x @'
rejected " 2. positional names differ" \
'@@k2.ta s : [*s*] @@
@@k2.tb t : [*t*] @@
@k2 x @' 'are not the same for every target'
rejected " 3. option names differ" \
'@@k3.ta s :one : [*s*] @@
@@k3.tb s :two : [*s*] @@
@k3 x @' 'are not the same for every target'
rejected " 4. defaults differ" \
'@@k4.ta s :n abc : [*s*] @@
@@k4.tb s :n xyz : [*s*] @@
@k4 x @' 'are not the same for every target'
rejected " 5. argument types differ" \
'@@k5.ta s :n.int : [*s*] @@
@@k5.tb s :n.word : [*s*] @@
@k5 x @' 'are not the same for every target'
echo
echo "-- the message must show HOW they differ, not just where"
# With more than two targets the designer would otherwise have to diff the
# definitions by hand; each target's own signature is listed beside its name.
name=" 6. each target's signature is shown"
out=$("$KTEXT" -k none -s "$TARGETS"'@@k6.ta s :one : [*s*] @@
@@k6.tb s :two : [*s*] @@
@k6 x @' -t ta -d 2>&1)
if echo "$out" | grep -q "ta .*s :one" && echo "$out" | grep -q "tb .*s :two"; then
pass "$name"
else
fail "$name" "both signatures listed by target" "$(echo "$out" | head -8 | tail -4)"
fi
# The error path once printed internal katom detail through msg(), which is
# debugging scaffolding and must never reach a user-facing message.
name=" 7. no internal debug output on the error path"
out=$("$KTEXT" -k none -s "$TARGETS"'@@k7.k s : a declaration @@
@@k7.ta s2 :other : [*s2*] @@
@k7 x @' -t ta -d 2>&1)
if echo "$out" | grep -q "klammer-definition"; then
fail "$name" "no internal dump" "$(echo "$out" | head -2)"
else
pass "$name"
fi
echo
echo "-- the remedy the message names"
accepted " 8. a .k declaration with :: instances" \
'@@k8.k s :n : a declaration @@
@@k8.ta :: [*s*] @@
@@k8.tb :: [*s*] @@
@k8 x @'
rejected " 9. a .k declaration plus a parameterized definition" \
'@@k9.k s : a declaration @@
@@k9.ta s2 :other : [*s2*] @@
@k9 x @' 'both a declaration'
echo
echo "============================="
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
[ $FAIL -eq 0 ]