An output policy for the three commands, and @cond as a true special form

A snapshot of the development tree.  The substantial changes since the last one:

COMMAND OUTPUT POLICY.  The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT.  For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document.  A bare command prints
its usage and succeeds rather than failing.  Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.

"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded.  Higher levels are the trace.

The commands no longer warn and continue: an anomaly is an error, described with
its location.  Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.

@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read.  Two consequences for a writer:

  * a state variable reaches the predicate.  "@@@state Flag :value true @@@
    @cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
    silently take the false branch.  The document now behaves like a klammer
    body, whose arguments are bound before its conditionals are decided.
  * nothing in a discarded branch happens -- it is not read, not evaluated, not
    expanded.  An @eval in the branch not taken used to run anyway.

Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.

@eval REACHING OUTSIDE.  ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported.  A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".

KLAMMER SETS.  Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions.  "none" means none and may not be combined with other symbols.  A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.

TESTS.  Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers.  Three suites
that could not run on macOS at all now do.

Assembled from dev commit 6c8ee6c22fca.
This commit is contained in:
2026-08-16 01:37:59 +02:00
parent 59c1599bc9
commit 240cff4278
84 changed files with 2523 additions and 494 deletions

View File

@@ -23,7 +23,7 @@
# would take "some words" as the value and the alone value would never be
# reached. That is a definition-time error.
#
# Engine tier: these tests run with -k none and define their own argtypes
# Engine tier: these tests run with --klammersets none and define their own argtypes
# and klammers inline, so they do not depend on the Standard Klammer Set.
#
# Usage: ./alone_test.sh (LSan suppressions come from env/runtime.env)
@@ -114,29 +114,29 @@ echo
check_eq \
" 1. name absent — the default" \
"[0]" \
-k none -s "$DEPTH $DK @d@" -d
--klammersets none -s "$DEPTH $DK @d@" -d
check_eq \
" 2. name written alone — the type's alone value" \
"[3]" \
-k none -s "$DEPTH $DK @d :n @" -d
--klammersets none -s "$DEPTH $DK @d :n @" -d
check_eq \
" 3. name written with a value — that value" \
"[7]" \
-k none -s "$DEPTH $DK @d :n 7 @" -d
--klammersets none -s "$DEPTH $DK @d :n 7 @" -d
# --- A type with an alone value but no default ---
check_eq \
" 4. no default declared — absent is empty" \
"[]" \
-k none -s "$MARK $MK @m@" -d
--klammersets none -s "$MARK $MK @m@" -d
check_eq \
" 5. no default declared — alone still applies" \
"[*]" \
-k none -s "$MARK $MK @m :c @" -d
--klammersets none -s "$MARK $MK @m :c @" -d
# --- The alone value belongs to the type, so every parameter of that
# type gets it, and a parameter default does not disturb it ---
@@ -144,73 +144,73 @@ check_eq \
check_eq \
" 6. two parameters of one type share the alone value" \
"[3][3]" \
-k none -s "$DEPTH @@d2 :a.depth :b.depth : [*a*][*b*] @@ @d2 :a :b @" -d
--klammersets none -s "$DEPTH @@d2 :a.depth :b.depth : [*a*][*b*] @@ @d2 :a :b @" -d
check_eq \
" 7. a parameter default overrides the type default, not the alone value" \
"[5]|[3]" \
-k none -s "$DEPTH @@d3 :n.depth 5 : [*n*] @@ @d3@|@d3 :n @" -d
--klammersets none -s "$DEPTH @@d3 :n.depth 5 : [*n*] @@ @d3@|@d3 :n @" -d
# --- bool: the convention that a bare boolean option means true ---
check_eq \
" 8. bool written alone is true" \
"[true]" \
-k none -s '@@b :f.bool : [*f*] @@ @b :f @' -d
--klammersets none -s '@@b :f.bool : [*f*] @@ @b :f @' -d
check_eq \
" 9. bool written with false stays false" \
"[false]" \
-k none -s '@@b :f.bool : [*f*] @@ @b :f false @' -d
--klammersets none -s '@@b :f.bool : [*f*] @@ @b :f false @' -d
check_eq \
"10. bool absent with no default is empty" \
"[]" \
-k none -s '@@b :f.bool : [*f*] @@ @b@' -d
--klammersets none -s '@@b :f.bool : [*f*] @@ @b@' -d
check_eq \
"11. bool absent with a true parameter default" \
"[true]" \
-k none -s '@@b :f.bool true : [*f*] @@ @b@' -d
--klammersets none -s '@@b :f.bool true : [*f*] @@ @b@' -d
check_eq \
"12. bool false explicitly against a true default" \
"[false]" \
-k none -s '@@b :f.bool true : [*f*] @@ @b :f false @' -d
--klammersets none -s '@@b :f.bool true : [*f*] @@ @b :f false @' -d
# --- The value reaching @eval ---
check_eq \
"13. python value of a bool written alone" \
"True" \
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b :f @' -d
--klammersets none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b :f @' -d
check_eq \
"14. python value of an absent bool" \
"None" \
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b@' -d
--klammersets none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b@' -d
check_eq \
"15. python value of a user type written alone" \
"'3'" \
-k none -s "$DEPTH @@d4 :n.depth : @eval repr(K.n) @ @@ @d4 :n @" -d
--klammersets none -s "$DEPTH @@d4 :n.depth : @eval repr(K.n) @ @@ @d4 :n @" -d
# --- Types that cannot delimit a bare option name ---
check_error \
"16. :alone refused on a type matching running text" \
"cannot declare an :alone value" \
-k none -s '@@@argtype loose | anything at all :alone x @@@' -d
--klammersets none -s '@@@argtype loose | anything at all :alone x @@@' -d
check_error \
"17. :alone refused on an explicit match-everything pattern" \
"cannot declare an :alone value" \
-k none -s '@@@argtype loose | anything :pattern (?:.^|\n)* :alone x @@@' -d
--klammersets none -s '@@@argtype loose | anything :pattern (?:.^|\n)* :alone x @@@' -d
check_error \
"18. an alone value must match its own type's pattern" \
"does not match its own pattern" \
-k none -s '@@@argtype depth | a depth :pattern \d+ :alone many @@@' -d
--klammersets none -s '@@@argtype depth | a depth :pattern \d+ :alone many @@@' -d
# --- Delimitation: an option value still runs to the next bar or option
# name, so text after a bare name is taken as the value and rejected
@@ -219,19 +219,19 @@ check_error \
check_error \
"19. text after a bare option name is taken as its value" \
"does not match" \
-k none -s "$DEPTH $DK @d :n some words @" -d
--klammersets none -s "$DEPTH $DK @d :n some words @" -d
check_eq \
"20. a bar separates a bare option name from following text" \
"[3]two" \
-k none -s "$DEPTH @@d5 :n.depth | t : [*n*]*t* @@ @d5 :n | two @" -d
--klammersets none -s "$DEPTH @@d5 :n.depth | t : [*n*]*t* @@ @d5 :n | two @" -d
# --- A type with no alone value is unchanged: a bare name is empty ---
check_eq \
"21. bare option of a type with no alone value is empty" \
"[]" \
-k none -s '@@s :t.word : [*t*] @@ @s :t @' -d
--klammersets none -s '@@s :t.word : [*t*] @@ @s :t @' -d
echo
echo "================================"