diff --git a/README.md b/README.md index 649cf70..14c4145 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ are regenerated on each release — patches cannot be merged directly. Report problems (or send patches) to the author; accepted changes are applied to the development tree and appear in a following snapshot. -This snapshot was assembled from development commit `bc7cd62b6f68`. +This snapshot was assembled from development commit `6024f49c2859`. ## License diff --git a/mac/argtype.cpp b/mac/argtype.cpp index f930ba2..877e373 100644 --- a/mac/argtype.cpp +++ b/mac/argtype.cpp @@ -7,13 +7,15 @@ #include "util.h" Argtype::Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern, - std::string default_value, std::string python_cast, modify_string_f python_format, + std::string default_value, std::string alone_value, + std::string python_cast, modify_string_f python_format, const Locator& loc) : m_name(name) , m_desc(desc) , m_symbolic_pattern(symbolic_pattern) , m_pattern(pattern) , m_default(default_value) + , m_alone(alone_value) , m_python_cast(python_cast) , m_python_format(python_format) , m_regex(std::regex(pattern)) diff --git a/mac/argtype.h b/mac/argtype.h index 8e77360..00e5913 100644 --- a/mac/argtype.h +++ b/mac/argtype.h @@ -26,7 +26,8 @@ public: {}; Argtype(std::string name, std::string desc, std::string symbolic_pattern, std::string pattern, - std::string default_value, std::string python_cast, modify_string_f python_format, + std::string default_value, std::string alone_value, + std::string python_cast, modify_string_f python_format, const Locator& loc); std::string python_value(const std::string& var_name, std::vector value, size_t name_size); @@ -48,6 +49,18 @@ public: // (cell_hpos, column_width); general types (bool, float) have no // sensible universal default and leave it empty. std::string m_default {}; + // Value for an optional argument whose name is written alone, with no + // value after it (:number rather than :number 10). Declared by the + // argument type, never by a klammer's parameter declaration: the + // default is what a klammer means by silence and is properly + // per-klammer, but a bare option name must read the same way in every + // klammer or the writer cannot know what it means without consulting + // each signature. bool declares "true", which is where the + // presence-means-true convention comes from; a type whose pattern + // matches everything cannot declare it (the value would swallow the + // following text instead). Empty means the type has no alone value, + // and a bare option name yields the empty string as before. + std::string m_alone {}; // Type parameter for parameterized types like rest(2): the value N // is bound around the python cast as (lambda N: )(2)(...). // Empty means unparameterized; a type with a default parameter diff --git a/mac/argtype_set.cpp b/mac/argtype_set.cpp index 7277c09..71d0afd 100644 --- a/mac/argtype_set.cpp +++ b/mac/argtype_set.cpp @@ -13,7 +13,7 @@ Parameter_set& Argtype_set::parameters() { - static Parameter_set instance("name | desc :pattern .* :python_cast str :default"); + static Parameter_set instance("name | desc :pattern .* :python_cast str :default :alone"); return instance; } @@ -22,11 +22,15 @@ Argtype_set::Argtype_set() (void)(void)K::log(2); Locator loc = current_locator(); for (auto [name, desc, pattern, python_cast, python_format] : base_argtypes) { - add(name, desc, pattern, "", python_cast, python_format, loc); + add(name, desc, pattern, "", "", python_cast, python_format, loc); } // rest is a parameterized type (rest(N)); an unparameterized use is // one-dimensional. m_types["rest"].m_parameter = "1"; + // A bool option written alone is true: :number means :number true. + // This is a declaration like any other type's :alone, not an engine + // special case for booleans. + m_types["bool"].m_alone = "true"; } std::string Argtype_set::replace_symbols(const std::string& pattern, const Locator& loc) @@ -63,6 +67,7 @@ void Argtype_set::check_for_existing_definition( void Argtype_set::add(const std::string& name, const std::string& desc, const std::string& pattern, const std::string& default_value, + const std::string& alone_value, const std::string& python_cast, modify_string_f python_format, const Locator& loc) { @@ -73,7 +78,8 @@ void Argtype_set::add(const std::string& name, const std::string& desc, m_pattern_size = std::max(m_pattern_size, expanded_pattern.size()); try { m_types[name] = Argtype(name, desc, pattern, expanded_pattern, - default_value, python_cast, python_format, loc); + default_value, alone_value, + python_cast, python_format, loc); } catch (const std::regex_error& e) { std::stringstream ss {}; ss << "The pattern for argument type \"" << name @@ -92,6 +98,35 @@ void Argtype_set::add(const std::string& name, const std::string& desc, << " " << pattern << "\n"; throw Definition_error(ss.str(), loc, false); } + if (!alone_value.empty()) { + // A pattern that matches running text cannot delimit a bare option + // name from the text after it: an option's value runs to the next + // bar or option name, so ":opt some words" would silently take + // "some words" as the value and the alone value would never be + // reached. A pattern that rejects multi-word text raises a clean + // argument error there instead. The test is behavioral rather + // than a comparison against matches_all()'s one literal pattern, + // because an @@@argtype written without a :pattern gets ".*", + // which is equally unable to delimit. + if (std::regex_match(std::string("one two"), m_types[name].m_regex)) { + std::stringstream ss {}; + ss << "The argument type \"" << name << "\" cannot declare an :alone value " + << "because its pattern matches running text:\n" + << " " << pattern << "\n\n" + << "An :alone value is used when an option name is written without a " + << "value. A type that matches running text cannot tell a bare option " + << "name from one whose value follows it, so the text after the name " + << "would be taken as the value instead.\n"; + throw Definition_error(ss.str(), loc, false); + } + if (!std::regex_match(alone_value, m_types[name].m_regex)) { + std::stringstream ss {}; + ss << "The alone value \"" << alone_value << "\" for argument type \"" + << name << "\" does not match its own pattern:\n" + << " " << pattern << "\n"; + throw Definition_error(ss.str(), loc, false); + } + } m_names.push_back(name); } @@ -108,7 +143,7 @@ void Argtype_set::add(std::vector::iterator begin, std::vector::it // std::for_each(begin, end+1, [](Katom& k) { k.m_type = katom_t::replaced; }); add(values["name"], values["desc"], values["pattern"], values["default"], - values["python_cast"], modify_string_f{}, + values["alone"], values["python_cast"], modify_string_f{}, begin->m_loc); modify_type(katom_t::replaced, begin, end); @@ -143,6 +178,21 @@ std::string Argtype_set::eval( } } +// The values a type supplies when an argument does not give one: the +// default (the option was not written at all) and the alone value (the +// option name was written without a value). Shown only when declared. +static std::string values_note(const Argtype& type) +{ + std::stringstream note {}; + if (!type.m_default.empty()) { + note << " [default: " << type.m_default << "]"; + } + if (!type.m_alone.empty()) { + note << " [alone: " << type.m_alone << "]"; + } + return note.str(); +} + std::string Argtype_set::describe(bool long_form, int indent_width) const { std::string indent(' ', indent_width); @@ -161,8 +211,9 @@ std::string Argtype_set::describe(bool long_form, int indent_width) const result << sp_arrow << m_types.at(name).m_pattern; result << "\n"; } else { - // result << abbrev(m_types.at(name).m_desc) << "\n"; - result << regex_split(m_types.at(name).m_desc, std::regex("\\n"), true)[0] << "\n"; + // result << abbrev(m_types.at(name).m_desc) << "\n"; + result << regex_split(m_types.at(name).m_desc, std::regex("\\n"), true)[0] + << values_note(m_types.at(name)) << "\n"; } } if (long_form) { diff --git a/mac/argtype_set.h b/mac/argtype_set.h index e0aed62..95954e9 100644 --- a/mac/argtype_set.h +++ b/mac/argtype_set.h @@ -19,6 +19,7 @@ public: void check_for_existing_definition(const std::string& name, const Locator& loc); void add(const std::string& name, const std::string& desc, const std::string& pattern, const std::string& default_value, + const std::string& alone_value, const std::string& python_cast, modify_string_f python_format, const Locator& loc); void add(std::vector::iterator begin, std::vector::iterator end, std::vector& katoms); diff --git a/mac/argument_set.cpp b/mac/argument_set.cpp index 68b21aa..7d39234 100644 --- a/mac/argument_set.cpp +++ b/mac/argument_set.cpp @@ -417,6 +417,16 @@ Parameter_set::value_map( } auto optional_values = check_optional(optional, loc); for (auto [key, value] : optional_values) { + // check_optional returns only the options that were actually + // written, so an empty value here means the name was written alone + // (":number" rather than ":number 10") — distinct from the option + // being absent, which is filled from the default below. The + // argument type supplies the alone value; bool declares "true", + // which is what makes a bare boolean option mean true. + const Parameter* parameter = find(key); + if (value.empty() && parameter && !parameter->m_argtype.m_alone.empty()) { + value = parameter->m_argtype.m_alone; + } values[key] = value; } for (auto opt : m_optional) { diff --git a/sks/code/code.k b/sks/code/code.k index ac6aaab..878c6c0 100644 --- a/sks/code/code.k +++ b/sks/code/code.k @@ -1,5 +1,5 @@ -@@code.k :filename :pattern :caption :number | text.literal : +@@code.k :filename :pattern :caption :number.bool | text.literal : A source file displayed verbatim @@ diff --git a/sks/list/list.py b/sks/list/list.py index 28710bf..96a8a60 100644 --- a/sks/list/list.py +++ b/sks/list/list.py @@ -158,7 +158,7 @@ def tex_list(K, list_type, items): body = "\n\n".join([f"\\item {e}" for e in items]) command = {'ol' : 'enumerate', 'ul' : 'itemize'}[list_type] topsep = '[topsep=0pt]' - listsep = '\\setlist{nolistsep}' if K.cmp != 'false' else '' + listsep = '\\setlist{nolistsep}' if K.cmp else '' result = f'{listsep}\n\\begin{{{command}}}{topsep}\n{body}\n\\end{{{command}}}\n' return result diff --git a/sks/table/table.k b/sks/table/table.k index c58f209..73ba2fe 100644 --- a/sks/table/table.k +++ b/sks/table/table.k @@ -75,6 +75,7 @@ :pattern ((?^:top^|head^|inner^|bottom^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+ :python_cast (lambda s : s.split()) + :alone all @@@ @@@argtype table_vline | @@ -94,6 +95,7 @@ :pattern ((?^:outer^|inner^|all^|none)(?^:'index_subsets')?^|'indexed_range'^|\s+)+ :python_cast (lambda s : s.split()) + :alone all @@@ @@@argtype table_span | @@ -157,6 +159,7 @@ calculated values. :pattern period^|comma :default period + :alone comma @@@ @@@argtype table_justify | diff --git a/tst/Makefile b/tst/Makefile index b3fad84..2e12c6c 100644 --- a/tst/Makefile +++ b/tst/Makefile @@ -1,10 +1,12 @@ # Klammertext distribution test suite (subset). # -# Runs the six shell regression suites: +# Runs the seven shell regression suites: # cond_test.sh — @cond argument delimitation # deftype_test.sh — the four klammer definition modes + redefinition table # escape_test.sh — target character escaping and quoted specials # filename_test.sh — filenames with spaces (quoting, " / " lists, rescue) +# alone_test.sh — an optional argument's three values (default, the +# argument type's :alone value, a written value) # modulepath_test.sh — @eval finds modules beside the file that names them # editor_test.sh — editor support (doc/edit): indentation and table # alignment; needs python3, uses Emacs when installed @@ -17,5 +19,6 @@ test: ./deftype_test.sh ./escape_test.sh ./filename_test.sh + ./alone_test.sh ./modulepath_test.sh ./editor_test.sh diff --git a/tst/alone_test.sh b/tst/alone_test.sh new file mode 100755 index 0000000..ed797b3 --- /dev/null +++ b/tst/alone_test.sh @@ -0,0 +1,240 @@ +#!/bin/bash +# +# alone_test.sh — Regression tests for an argument type's :alone value. +# +# An optional argument has three possible values, not two: +# +# 1. the option name is not written at all -> the default +# 2. the name is written alone, with no value -> the type's :alone value +# 3. the name is written with a value -> that value +# +# The :alone value is declared by the argument type (@@@argtype ... :alone), +# never by a klammer's parameter declaration. A default is what a klammer +# means by silence and is properly per-klammer; a bare option name must read +# the same way in every klammer, or a writer cannot know what it means +# without consulting each signature. +# +# The bool type declares :alone true, which is where the convention that a +# bare boolean option means true comes from. This is a declaration like any +# other type's, not an engine special case for booleans. +# +# A type whose pattern matches running text cannot declare an :alone value: +# an option's value runs to the next bar or option name, so ":opt some words" +# 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 +# and klammers inline, so they do not depend on the Standard Klammer Set. +# +# Usage: ./alone_test.sh (LSan suppressions come from env/runtime.env) +# 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' + +# strip leading/trailing blank lines and surrounding whitespace +trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; } + +# check_eq TEST_NAME EXPECTED KTEXT_ARGS... +# Runs ktext, expects exit status 0, and compares trimmed stdout to EXPECTED. +check_eq() { + local test_name="$1" + local expected="$2" + shift 2 + + local output status + output=$("$KTEXT" "$@" 2>/tmp/alone_test_err.$$) + status=$? + output=$(printf '%s' "$output" | trim) + + if [ $status -ne 0 ]; then + echo "${red}FAIL${reset} $test_name — ktext exited $status" + echo " stderr: $(head -3 /tmp/alone_test_err.$$)" + FAIL=$((FAIL + 1)) + return + fi + if [ "$output" = "$expected" ]; then + echo "${green}PASS${reset} $test_name" + PASS=$((PASS + 1)) + else + echo "${red}FAIL${reset} $test_name" + echo " expected: [$expected]" + echo " got: [$output]" + FAIL=$((FAIL + 1)) + fi +} + +# check_error TEST_NAME PATTERN KTEXT_ARGS... +# Runs ktext, expects a NONZERO exit status and PATTERN in the message. +check_error() { + local test_name="$1" + local pattern="$2" + shift 2 + + local output status + output=$("$KTEXT" "$@" 2>&1) + status=$? + + if [ $status -eq 0 ]; then + echo "${red}FAIL${reset} $test_name — expected an error but ktext succeeded" + FAIL=$((FAIL + 1)) + return + fi + if echo "$output" | grep -qF "$pattern"; then + echo "${green}PASS${reset} $test_name" + PASS=$((PASS + 1)) + else + echo "${red}FAIL${reset} $test_name — expected error to contain [$pattern]" + echo " output: $(echo "$output" | head -5)" + FAIL=$((FAIL + 1)) + fi +} + +# A type with both a default and an alone value, and a klammer using it. +DEPTH='@@@argtype depth | a table of contents depth :pattern \d+ :default 0 :alone 3 @@@' +DK='@@d :n.depth : [*n*] @@' + +# A type with an alone value but no default. +MARK='@@@argtype mark | a mark character :pattern [-*+] :alone * @@@' +MK='@@m :c.mark : [*c*] @@' + +echo "${bold}Argument type :alone value tests${reset}" +echo "================================" +echo + +# --- The three values of an optional argument --- + +check_eq \ + " 1. name absent — the default" \ + "[0]" \ + -k 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 + +check_eq \ + " 3. name written with a value — that value" \ + "[7]" \ + -k 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 + +check_eq \ + " 5. no default declared — alone still applies" \ + "[*]" \ + -k 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 --- + +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 + +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 + +# --- 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 + +check_eq \ + " 9. bool written with false stays false" \ + "[false]" \ + -k 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 + +check_eq \ + "11. bool absent with a true parameter default" \ + "[true]" \ + -k 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 + +# --- 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 + +check_eq \ + "14. python value of an absent bool" \ + "None" \ + -k 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 + +# --- 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 + +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 + +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 + +# --- 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 +# by the type. This is what makes the guard above necessary. + +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 + +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 + +# --- 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 + +echo +echo "================================" +echo "Passed: $PASS Failed: $FAIL" +rm -f /tmp/alone_test_err.$$ +[ $FAIL -eq 0 ]