xo-umbrella2/xo-reader2/utest/printable_render.test.cpp

1369 lines
64 KiB
C++

/* @file printable_render.test.cpp
*
* author: Roland Conybeare, Aug 2026
*
* Phase C verification for xo-reader2's printers, bottom-up.
*
* Follows the template in xo-expression2/utest/printable_render.test.cpp --
* see .xo-backlog/xo-printable2/issues/01-aprintable-pretty-ppsink.md for why
* both renderings are pinned rather than only asserted equal.
*
* DIFFERENT FROM the expression2 file in one way that matters. There each
* printer got its own conversion cycle, because each taught something new.
* Here 24 printers remain and most are two-field structs over an enum and a
* string accessor; they are converted in BATCHES by shape, with one pinned
* table per batch. RC's call 2026-08-11. The batches, in order:
*
* 1. flat leaves -- no facet children (this file, s_leaf_v)
* 2. facet-nesting -- DProgressSsm, DDefineSsm, ... , DExpectQDictSsm
* 3. DExpectFormalArglistSsm, ParserStack -- the only two hand-rolled
* two-pass printers in the subsystem
* 4. DSchematikaParser -- last, it renders :stack
*
* The one thing this batch is really testing, uniformly: reader2's printers
* are full of ENUMS (parenstate, quote_xst, deftypestate, fstate, seqtype),
* none of which has a Prettifier<>. They reach ppsink through its leaf
* FALLBACK to operator<< -- the same silent path DVarRef's Binding field took.
* Whether that fallback agrees with legacy for an enum is one question with
* twelve answers, which is the argument for doing them together.
*
* Expectations are OBSERVED, never predicted.
*/
#include <xo/reader2/DeftypeSsm.hpp>
#include <xo/reader2/ExpectExprSsm.hpp>
#include <xo/reader2/ExpectFormalArgSsm.hpp>
#include <xo/reader2/ExpectListTypeSsm.hpp>
#include <xo/reader2/ExpectQLiteralSsm.hpp>
#include <xo/reader2/ExpectSymbolSsm.hpp>
#include <xo/reader2/ExpectTypeSsm.hpp>
#include <xo/reader2/ParenSsm.hpp>
#include <xo/reader2/QuoteSsm.hpp>
#include <xo/reader2/SequenceSsm.hpp>
#include <xo/reader2/ToplevelSeqSsm.hpp>
#include <xo/reader2/GlobalEnv.hpp>
#include <xo/reader2/ApplySsm.hpp>
#include <xo/reader2/DefineSsm.hpp>
#include <xo/reader2/IfElseSsm.hpp>
#include <xo/reader2/LambdaSsm.hpp>
#include <xo/reader2/ProgressSsm.hpp>
#include <xo/reader2/ExpectQListSsm.hpp>
#include <xo/reader2/ExpectQArraySsm.hpp>
#include <xo/reader2/ExpectQDictSsm.hpp>
#include <xo/reader2/ParserResult.hpp>
#include <xo/reader2/SchematikaParser.hpp>
#include <xo/reader2/init_reader2.hpp>
#include <xo/expression2/DefineExpr.hpp>
#include <xo/expression2/IfElseExpr.hpp>
#include <xo/expression2/Constant.hpp>
#include <xo/object2/Integer.hpp>
#include <xo/object2/List.hpp>
#include <xo/object2/Array.hpp>
#include <xo/object2/Dictionary.hpp>
#include <xo/stringtable2/String.hpp>
#include <xo/expression2/GlobalSymtab.hpp>
#include <xo/arena/ArenaHashMapConfig.hpp>
#include <xo/gc/X1Collector.hpp>
#include <xo/gc/init_gc.hpp>
#include <xo/alloc2/Arena.hpp>
#include <xo/alloc2/CollectorTypeRegistry.hpp>
#include <xo/printable2/Printable.hpp>
#include <xo/indentlog2/print/toppstr.hpp>
#include <xo/facet/FacetRegistry.hpp>
#include <xo/testutil/UtestRehearser.hpp>
#include <xo/ppsink/PpStyle.hpp>
#include <xo/ppsink/scope.hpp>
#include <xo/ppsink/scope_macros.hpp>
#include <catch2/catch.hpp>
#include <iostream>
#include <cstdint>
#include <string>
#include <vector>
namespace xo {
using xo::mm::ArenaConfig;
using xo::mm::AAllocator;
using xo::mm::ACollector;
using xo::mm::AGCObject;
using xo::mm::DArena;
using xo::mm::DX1Collector;
using xo::mm::X1CollectorConfig;
using xo::mm::CollectorTypeRegistry;
using xo::scm::DParenSsm;
using xo::scm::DQuoteSsm;
using xo::scm::DDeftypeSsm;
using xo::scm::DExpectSymbolSsm;
using xo::scm::DExpectListTypeSsm;
using xo::scm::DExpectFormalArgSsm;
using xo::scm::DExpectTypeSsm;
using xo::scm::DExpectQLiteralSsm;
using xo::scm::DExpectExprSsm;
using xo::scm::DSequenceSsm;
using xo::scm::DToplevelSeqSsm;
using xo::scm::DGlobalEnv;
using xo::scm::DGlobalSymtab;
using xo::scm::DSchematikaParser;
using xo::scm::ParserConfig;
using xo::mm::dp;
using xo::map::ArenaHashMapConfig;
using xo::print::APrintable;
using xo::facet::obj;
using xo::facet::with_facet;
static InitEvidence s_init = (InitSubsys<S_reader2_tag>::require()
^ InitSubsys<S_gc_tag>::require());
namespace ut {
using xo::pp::PpConfig;
using xo::pp::toppstr;
using xo::pp::scope;
using xo::pp::xtag;
namespace {
/** render @p x through pretty(PpSink&) **/
template <typename T>
std::string
render_pretty(const T & x, std::uint32_t margin) {
return toppstr(PpConfig::scratch_plain(margin), x);
}
/* THREE process-wide counters leak into these renderings, exactly
* as they do in xo-expression2's phase-C table, and for the same
* reasons. Copied verbatim from
* xo-expression2/utest/printable_render.test.cpp rather than
* shared: they are test scaffolding for observed output, not API.
*/
/** replace the digits of ":id <n>" with "N". See the identical
* helper in xo-procedure2/utest/printable_render.test.cpp: TypeId
* is a process-wide counter handed out in reflection order, so it
* moves when an unrelated test reflects a new type first.
*
* This scrubs the NESTED TypeDescr's :id. TypeRef's own :id is a
* type-variable NAME, is quoted, and stays pinned exactly.
**/
std::string scrub_type_id(std::string s) {
const std::string key = ":id ";
for (std::size_t i = s.find(key); i != std::string::npos; i = s.find(key, i+1)) {
std::size_t b = i + key.size();
std::size_t e = b;
while (e < s.size() && ::isdigit((unsigned char)s[e]))
++e;
if (e > b)
s.replace(b, e - b, "N");
}
return s;
}
/** index of the digits belonging to the ".tseq" at/after @p from,
* or npos. @p n_digit receives their count.
*
* NB the separator is whitespace, not necessarily a SPACE: at a
* narrow margin the value breaks onto its own line, so ".tseq" is
* followed by "\n" plus indent. Keying on ".tseq " silently
* scrubbed nothing in exactly those cases -- caught because the
* broken-layout expectations then failed.
**/
std::size_t find_tseq_digits(const std::string & s, std::size_t from,
std::size_t * n_digit) {
const std::string key = ".tseq";
for (std::size_t i = s.find(key, from); i != std::string::npos;
i = s.find(key, i+1))
{
std::size_t b = i + key.size();
while (b < s.size() && ::isspace((unsigned char)s[b]))
++b;
std::size_t e = b;
while (e < s.size() && ::isdigit((unsigned char)s[e]))
++e;
if (e > b) {
*n_digit = e - b;
return b;
}
}
return std::string::npos;
}
/** replace the digits in a quoted GENERATED type-variable name:
* :id "if:12" -> :id "if:N".
*
* A third counter, distinct from TypeDescr's :id (scrub_type_id)
* and from typeseq (scrub_tseq). DIfElseExpr::_make_empty() builds
* its TypeRef through TypeRef::generate_unique(), which draws on a
* process-wide counter -- so these move with how many TypeRefs the
* run happened to make before this one, including from unrelated
* tests. The PREFIX is kept: "if:" is a property of the printer
* under test, the number is not.
**/
std::string scrub_typevar(std::string s) {
const std::string key = ":id \"";
for (std::size_t i = s.find(key); i != std::string::npos; i = s.find(key, i+1)) {
std::size_t b = i + key.size();
/* skip the prefix, up to and including its ':' */
while (b < s.size() && s[b] != ':' && s[b] != '"')
++b;
if (b >= s.size() || s[b] != ':')
continue; /* no prefix separator -- not a generated name */
++b;
std::size_t e = b;
while (e < s.size() && ::isdigit((unsigned char)s[e]))
++e;
if (e > b)
s.replace(b, e - b, "N");
}
return s;
}
/** replace each ".tseq" value with "N".
*
* Same reasoning as scrub_type_id, different counter: a typeseq is
* handed out by xo::reflect::typeseq::id<T>() on first use, and in
* practice that is subsystem registration order
* (SetupObject2::register_facets etc). Stable today -- DInteger
* is 9 and DFloat 10 on every run -- but it is registration order,
* not a property of DConstant, so pinning the digits would make an
* unrelated object2 registration break this test.
*
* What the digits actually SAY is checked separately, in
* DConstant-tseq-fields below.
**/
std::string scrub_tseq(std::string s) {
std::size_t n_digit = 0;
for (std::size_t i = find_tseq_digits(s, 0, &n_digit);
i != std::string::npos;
i = find_tseq_digits(s, i+1, &n_digit))
{
s.replace(i, n_digit, "N");
}
return s;
}
/** arena for parser-owned state machines, plus a collector for the
* two leaves that need expression memory.
*
* Deliberately NOT reusing SchematikaParser.test.cpp's
* ParserFixture: that builds a whole DSchematikaParser to drive
* source text through. Every printer in this batch is reachable
* from a bare `_make(parser_mm)`, and a fixture that cannot fail
* for parser reasons is a better witness for a printer test.
**/
struct SsmFixture {
explicit SsmFixture(const std::string & testname)
: parser_arena_{ArenaConfig()
.with_name(ArenaNameStr::concat("printable_render.", testname))
.with_size(64 * 1024)
.with_store_header_flag(true)},
gc_{X1CollectorConfig{
.name_ = "printable_render.gc." + testname,
.arena_config_ = ArenaConfig{
.size_ = 8192,
.store_header_flag_ = true},
.object_types_z_ = 16384,
.gc_trigger_v_{{4096, 4096}},
.debug_flag_ = false}}
{
CollectorTypeRegistry::instance()
.install_types(with_facet<ACollector>::mkobj(&gc_));
}
DArena & parser_mm() { return parser_arena_; }
/** DGlobalEnv is the one leaf needing more than an arena: it
* renders symtab_->n_vars(), so it needs a real symtab. The
* hash-map superstructure lives outside GC space, hence the
* parser arena as fixed_mm -- DGlobalSymtab::make's own
* mm / fixed_mm split, not a testing convenience.
**/
DGlobalEnv * make_global_env() {
auto cfg = ArenaHashMapConfig()
.with_name("printable_render.symtab")
.with_hint_max_capacity(16);
DGlobalSymtab * symtab
= DGlobalSymtab::_make(this->expr_mm(),
with_facet<AAllocator>::mkobj(&parser_arena_),
cfg, cfg);
return DGlobalEnv::_make(this->expr_mm(), symtab);
}
obj<AAllocator> expr_mm() { return with_facet<AAllocator>::mkobj(&gc_); }
DArena parser_arena_;
DX1Collector gc_;
};
/** BATCH 2 needs a real parser and batch 1 did not. Four of
* batch 2's printers hold children that only exist mid-parse,
* and for three of them the EMPTY case is not renderable at all
* -- DExpectQList/QArraySsm throw, DExpectQDictSsm aborts. So
* stepping a parser one token at a time and reading top_ssm()
* between tokens is the only way to pin them. Batch 3's
* ParserStack will want the same machinery.
*
* Structure copied from SchematikaParser.test.cpp's
* ParserFixture rather than bolted onto SsmFixture: the
* collector must be allocated FROM the aux arena and the parser
* registered as a GC root. A version that skipped both
* segfaulted before the first token.
**/
struct ParseFixture {
explicit ParseFixture(const std::string & testname) {
aux_arena_ = std::move(DArena(ArenaConfig()
.with_name(ArenaNameStr::concat(testname))
.with_size(64 * 1024)
.with_store_header_flag(true)));
obj<AAllocator,DArena> aux_mm(&aux_arena_);
X1CollectorConfig x1_config
= (X1CollectorConfig()
.with_name("printable_render.gc." + testname)
.with_size(32 * 1024)
.with_debug_flag(false)
.with_sanitize_flag(true));
dp<DX1Collector> expr_x1_dp
= dp<DX1Collector>::make(aux_mm, x1_config);
this->expr_mm_
= obj<AAllocator,DX1Collector>(expr_x1_dp.release());
obj<ACollector> gc = expr_mm_.to_facet<ACollector>();
CollectorTypeRegistry::instance().install_types(gc);
ParserConfig cfg;
cfg.parser_arena_config_.size_ = 16 * 1024;
cfg.symtab_var_config_.hint_max_capacity_ = 128;
cfg.symtab_types_config_.hint_max_capacity_ = 64;
cfg.max_stringtable_capacity_ = 512;
cfg.debug_flag_ = false;
this->parser_
= DSchematikaParser::make(aux_mm, cfg, expr_mm_, aux_mm);
this->parser_gco_ = parser_;
gc.add_gc_root_poly(&parser_gco_);
}
ParseFixture(const ParseFixture &) = delete;
~ParseFixture() {
expr_mm_._drop();
parser_._drop();
}
DArena aux_arena_;
obj<AAllocator> expr_mm_;
obj<AGCObject,DSchematikaParser> parser_;
obj<AGCObject> parser_gco_;
};
} /*namespace*/
/** BATCH 1: the flat leaves -- every xo-reader2 printer with no facet
* child. Thirteen classes, fifteen cases (DExpectTypeSsm and
* DExpectExprSsm each get two, for arguments that do and do not reach
* the output), at margins 200 and 30.
*
* Batched rather than one cycle per printer: RC's call 2026-08-11.
* See the file header for why, and for the batch order.
*
* WHAT THIS BATCH ESTABLISHED, and the reason it is worth doing as
* one unit: reader2's printers are full of enums -- parenstate,
* quote_xst, deftypestate, fstate, seqtype -- and NONE has a
* Prettifier<>. Each reaches ppsink through its leaf FALLBACK to
* operator<<, the same silent path DVarRef's Binding field took. The
* fallback agrees with legacy EXACTLY, in every case: lparen_0,
* quote_0, def_0, formal_0, toplevel-interactive, toplevel-batch.
*
* The only divergence anywhere in the batch is the familiar
* field-value column -- legacy indent+2, ppsink indent+1 -- visible
* on the wrapped :expect values at margin 30 and nowhere else.
*
* Three of these render BARE, with no fields, and each for its own
* reason worth not "fixing" silently: DExpectSymbolSsm's :member
* field is commented out in legacy; DExpectListTypeSsm prints nothing
* though it holds an elt_type_; DExpectTypeSsm ignores its
* `corrected` constructor argument (both cases below pin that it
* makes no difference).
*
* Expectations are OBSERVED, never predicted, and were generated from
* the observed bytes rather than transcribed.
**/
TEST_CASE("reader2-leaf-render", "[printable][reader2]")
{
REQUIRE(s_init.evidence());
UtestRehearser rh;
for (auto _ : rh) {
scope log(XO_DEBUG2_(rh.enable_debug(), "reader2-leaf-render"));
SsmFixture fx("leaves");
DArena & mm = fx.parser_mm();
DToplevelSeqSsm tl_i(xo::scm::exprseqtype::toplevel_interactive);
DToplevelSeqSsm tl_b(xo::scm::exprseqtype::toplevel_batch);
auto check = [&rh, &log](const char * label, auto * p,
std::uint32_t margin,
const char * expect_pretty)
{
auto pr = with_facet<APrintable>::mkobj(p);
std::string pretty = render_pretty(pr, margin);
log && log(xtag("label", label), xtag("margin", margin), xtag("pretty", pretty));
REHEARSE(rh, pretty == std::string(expect_pretty));
};
check("DParenSsm", DParenSsm::_make(mm), 200,
"<DParenSsm :parenstate lparen_0 :expect leftparen>");
check("DParenSsm", DParenSsm::_make(mm), 30,
"<DParenSsm\n"
" :parenstate lparen_0\n"
" :expect leftparen>");
check("DQuoteSsm", DQuoteSsm::_make(mm), 200,
"<DQuoteSsm :quote_xst quote_0 :expect #q>");
check("DQuoteSsm", DQuoteSsm::_make(mm), 30,
"<DQuoteSsm\n"
" :quote_xst quote_0\n"
" :expect #q>");
check("DDeftypeSsm", DDeftypeSsm::_make(mm), 200,
"<DDeftypeSsm :deftypestate def_0 :expect deftype>");
check("DDeftypeSsm", DDeftypeSsm::_make(mm), 30,
"<DDeftypeSsm\n"
" :deftypestate def_0\n"
" :expect deftype>");
check("DExpectSymbolSsm", DExpectSymbolSsm::_make(mm), 200,
"<DExpectSymbolSsm>");
check("DExpectSymbolSsm", DExpectSymbolSsm::_make(mm), 30,
"<DExpectSymbolSsm>");
check("DExpectListTypeSsm", DExpectListTypeSsm::_make(mm), 200,
"<DExpectListTypeSsm>");
check("DExpectListTypeSsm", DExpectListTypeSsm::_make(mm), 30,
"<DExpectListTypeSsm>");
check("DExpectFormalArgSsm", DExpectFormalArgSsm::_make(mm), 200,
"<DExpectFormalArgSsm :fstate formal_0 :expect formal-name>");
check("DExpectFormalArgSsm", DExpectFormalArgSsm::_make(mm), 30,
"<DExpectFormalArgSsm\n"
" :fstate formal_0\n"
" :expect formal-name>");
check("DExpectTypeSsm.f", DExpectTypeSsm::_make(mm, false), 200,
"<DExpectTypeSsm>");
check("DExpectTypeSsm.f", DExpectTypeSsm::_make(mm, false), 30,
"<DExpectTypeSsm>");
check("DExpectTypeSsm.t", DExpectTypeSsm::_make(mm, true), 200,
"<DExpectTypeSsm>");
check("DExpectTypeSsm.t", DExpectTypeSsm::_make(mm, true), 30,
"<DExpectTypeSsm>");
check("DExpectQLiteralSsm", DExpectQLiteralSsm::_make(mm, false, false), 200,
"<DExpectQLiteralSsm :expect leftparen|leftbracket|leftbrace|string|f64|i64|bool>");
check("DExpectQLiteralSsm", DExpectQLiteralSsm::_make(mm, false, false), 30,
"<DExpectQLiteralSsm\n"
" :expect\n"
" leftparen|leftbracket|leftbrace|string|f64|i64|bool>");
check("DExpectExprSsm.fff", DExpectExprSsm::_make(mm, false, false, false), 200,
"<DExpectExprSsm :allow_defs false :cxl_on_rightbrace false :cxl_on_rightparen false :expect if|lambda|lparen|lbrace|literal|var>");
check("DExpectExprSsm.fff", DExpectExprSsm::_make(mm, false, false, false), 30,
"<DExpectExprSsm\n"
" :allow_defs false\n"
" :cxl_on_rightbrace false\n"
" :cxl_on_rightparen false\n"
" :expect\n"
" if|lambda|lparen|lbrace|literal|var>");
check("DExpectExprSsm.ttt", DExpectExprSsm::_make(mm, true, true, true), 200,
"<DExpectExprSsm :allow_defs true :cxl_on_rightbrace true :cxl_on_rightparen true :expect def|if|lambda|lparen|lbrace|literal|var>");
check("DExpectExprSsm.ttt", DExpectExprSsm::_make(mm, true, true, true), 30,
"<DExpectExprSsm\n"
" :allow_defs true\n"
" :cxl_on_rightbrace true\n"
" :cxl_on_rightparen true\n"
" :expect\n"
" def|if|lambda|lparen|lbrace|literal|var>");
check("DSequenceSsm", DSequenceSsm::_make(mm, fx.expr_mm()), 200,
"<DSequenceSsm :seq_expr.size 0 :expect expr|semicolon|rightbrace>");
check("DSequenceSsm", DSequenceSsm::_make(mm, fx.expr_mm()), 30,
"<DSequenceSsm\n"
" :seq_expr.size 0\n"
" :expect\n"
" expr|semicolon|rightbrace>");
check("DToplevelSeqSsm.i", &tl_i, 200,
"<DToplevelSeqSsm :seqtype toplevel-interactive>");
check("DToplevelSeqSsm.i", &tl_i, 30,
"<DToplevelSeqSsm\n"
" :seqtype\n"
" toplevel-interactive>");
check("DToplevelSeqSsm.b", &tl_b, 200,
"<DToplevelSeqSsm :seqtype toplevel-batch>");
check("DToplevelSeqSsm.b", &tl_b, 30,
"<DToplevelSeqSsm\n"
" :seqtype toplevel-batch>");
check("DGlobalEnv", fx.make_global_env(), 200,
"<DGlobalEnv :n_vars 0>");
check("DGlobalEnv", fx.make_global_env(), 30,
"<DGlobalEnv :n_vars 0>");
}
}
/** BATCH 2: the facet-nesting printers -- those whose fields include
* another printer reached through APrintable.
*
* SEVEN of the nine planned, not nine. DExpectQListSsm and
* DExpectQArraySsm moved OUT of this batch on 2026-08-11, after
* observation rather than by plan: neither can be pinned yet.
* Their empty state is not renderable on the legacy protocol at all
* (`variant<APrintable,AGCObject>` on a null child throws), and
* their populated state never reaches the top of the parser stack --
* each element of a quoted list pushes its own DExpectQLiteralSsm on
* top of them, so top_ssm() shows the element, never the container.
* They are visible only inside a whole-parser rendering, at :[1] of
* :stack, which on the ppsink side still stops at STUB:ParserStack.
* So they convert AFTER ParserStack and DSchematikaParser, where the
* observation window exists. See the ticket.
*
* WHAT THIS BATCH ESTABLISHED:
*
* 1. field()'s `present` flag absorbs every optional in the batch.
* DProgressSsm has THREE (:lhs :op :rhs), DApplySsm and
* DLambdaSsm one each. DLambdaSsm's legacy if/else COLLAPSED --
* both arms rendered :lmstate and :expect and only :body differed
* -- which is the DExpectFormalArgSsm shape, not DLambdaExpr's.
*
* 2. ParserResult's switch does NOT collapse, and is the one place
* in the batch where a branch had to stay. Its three arms have
* three different ARITIES, and folding them onto present-flags
* would mean calling variant<APrintable,AExpression> on the none
* and error paths, where result_expr_ is null and that throws.
*
* 3. ParserResult also needed a Prettifier<> added, not just a
* pretty() body: it is not a facet type, so before this it fell
* through ppsink's leaf fallback to operator<< and rendered
* ParserResult::print(ostream&) -- a DIFFERENT struct (:expr and
* :src_fn always present, :error quoted, never wrapping). Its
* phase-B stub was dead code that nothing ever called. Two more
* printers are in that position: ParserStack* and
* DSchematikaParser*, both still ppdetail-only.
*
* 4. Two printers have states in which they CANNOT be printed, both
* pre-existing and both reproduced rather than fixed.
* DDefineSsm::get_expect_str() asserts for defstate_ == def_0,
* which is exactly what _make() leaves behind -- hence the
* parser-driven cases below. DExpectQDictSsm builds its :dict
* handle by DIRECT construction rather than a registry lookup, so
* a null dict_ yields an empty obj<> that ABORTS rather than
* throwing. Tickets for both.
*
* The only divergence between the two protocols anywhere in the
* batch is inherited, never introduced here:
* - the familiar field-value column, legacy indent+2 vs ppsink
* indent+1, compounding once per nesting level; and
* - DDictionary's empty/closing padding ({ } vs {}), already
* pinned deliberately at xo-object2/utest/printable_render.test.cpp
* as Testcase_Dict(80, {}, "{ }", "{}").
*
* Expectations are OBSERVED, never predicted, and were generated
* from the observed bytes rather than transcribed.
**/
TEST_CASE("reader2-nesting-render", "[printable][reader2]")
{
using xo::scm::DApplySsm;
using xo::scm::DIfElseSsm;
using xo::scm::DLambdaSsm;
using xo::scm::DProgressSsm;
using xo::scm::ParserResult;
using xo::scm::DIfElseExpr;
using xo::scm::DConstant;
using xo::scm::DInteger;
using xo::scm::DString;
using xo::scm::AExpression;
using xo::scm::optype;
REQUIRE(s_init.evidence());
UtestRehearser rh;
for (auto _ : rh) {
scope log(XO_DEBUG2_(rh.enable_debug(), "reader2-nesting-render"));
SsmFixture fx("nesting");
DArena & mm = fx.parser_mm();
obj<AAllocator> emm = fx.expr_mm();
auto konst = [&emm](int i) {
return obj<AExpression>(
DConstant::make(emm,
DInteger::box<AGCObject>(emm, i)));
};
auto check = [&rh, &log](const char * label, auto * p,
std::uint32_t margin,
const char * expect_pretty)
{
auto pr = with_facet<APrintable>::mkobj(p);
std::string pretty
= scrub_tseq(scrub_type_id(
scrub_typevar(render_pretty(pr, margin))));
log && log(xtag("label", label), xtag("margin", margin),
xtag("pretty", pretty));
REHEARSE(rh, pretty == std::string(expect_pretty));
};
/** ParserResult is not a facet type: no with_facet<>. **/
auto check_pr = [&rh, &log](const char * label,
const ParserResult & x,
std::uint32_t margin,
const char * expect_pretty)
{
std::string pretty
= scrub_tseq(scrub_type_id(
scrub_typevar(render_pretty(x, margin))));
log && log(xtag("label", label), xtag("margin", margin),
xtag("pretty", pretty));
REHEARSE(rh, pretty == std::string(expect_pretty));
};
ParserResult r_none;
ParserResult r_expr = ParserResult::expression("some_ssm", konst(5));
ParserResult r_err
= ParserResult::error("some_ssm",
DString::from_view(
emm, std::string_view("bad juju")));
/* :fn_expr absent -- the present-flag path. */
check("Apply.null", DApplySsm::_make(mm, obj<AExpression>()), 200,
"<DApplySsm :applystate apply_0 :expect expr>");
check("Apply.null", DApplySsm::_make(mm, obj<AExpression>()), 30,
"<DApplySsm\n"
" :applystate apply_0\n"
" :expect expr>");
/* :fn_expr present. Note _make advances applystate to
* apply_1, so the two cases differ in the enum too.
*/
check("Apply.fn", DApplySsm::_make(mm, konst(7)), 200,
"<DApplySsm :applystate apply_1 :expect lparen :fn_expr <DConstant :value_.tseq N :value.tseq N :value 7>>");
check("Apply.fn", DApplySsm::_make(mm, konst(7)), 30,
"<DApplySsm\n"
" :applystate apply_1\n"
" :expect lparen\n"
" :fn_expr\n"
" <DConstant\n"
" :value_.tseq N\n"
" :value.tseq N\n"
" :value 7>>");
/* the one batch-2 printer with NO :expect field. */
check("IfElse", DIfElseSsm::_make(mm, DIfElseExpr::_make_empty(emm)), 200,
"<DIfElseSsm :ifstate if_0 :if_expr <DIfElseExpr :typeref <TypeRef :id \"if:N\" :td null>>>");
check("IfElse", DIfElseSsm::_make(mm, DIfElseExpr::_make_empty(emm)), 30,
"<DIfElseSsm\n"
" :ifstate if_0\n"
" :if_expr\n"
" <DIfElseExpr\n"
" :typeref\n"
" <TypeRef\n"
" :id \"if:N\"\n"
" :td null>>>");
/* :body absent. The present FLAG is pinned -- forcing it
* true is caught by this case. What is NOT pinned, and
* cannot be, is the rendering of a non-null body: body_ is
* assigned only immediately before this ssm pops, so it is
* null at every token boundary.
*/
check("Lambda.bare", DLambdaSsm::_make(mm), 200,
"<DLambdaSsm :lmstate lm_0 :expect lambda>");
check("Lambda.bare", DLambdaSsm::_make(mm), 30,
"<DLambdaSsm\n"
" :lmstate lm_0\n"
" :expect lambda>");
/* all three optionals absent: only :expect survives. */
check("Progress.bare", DProgressSsm::_make(mm, obj<AExpression>(), optype::invalid), 200,
"<DProgressSsm :expect expr1|leftparen>");
check("Progress.bare", DProgressSsm::_make(mm, obj<AExpression>(), optype::invalid), 30,
"<DProgressSsm\n"
" :expect expr1|leftparen>");
/* :lhs and :op present, :rhs absent -- rhs_ is set during
* parsing and _make has no argument for it.
*/
check("Progress.lhs", DProgressSsm::_make(mm, konst(3), optype::op_add), 200,
"<DProgressSsm :lhs <DConstant :value_.tseq N :value.tseq N :value 3> :op op+ :expect expr2|leftparen>");
check("Progress.lhs", DProgressSsm::_make(mm, konst(3), optype::op_add), 30,
"<DProgressSsm\n"
" :lhs\n"
" <DConstant\n"
" :value_.tseq N\n"
" :value.tseq N\n"
" :value 3>\n"
" :op op+\n"
" :expect expr2|leftparen>");
/* ParserResult arm 1 of 3: :type alone. */
check_pr("Result.none", r_none, 200,
"<ParserResult :type none>");
check_pr("Result.none", r_none, 30,
"<ParserResult :type none>");
/* arm 2: :type :expr. */
check_pr("Result.expr", r_expr, 200,
"<ParserResult :type expression :expr <DConstant :value_.tseq N :value.tseq N :value 5>>");
check_pr("Result.expr", r_expr, 30,
"<ParserResult\n"
" :type expression\n"
" :expr\n"
" <DConstant\n"
" :value_.tseq N\n"
" :value.tseq N\n"
" :value 5>>");
/* arm 3: :type :src_fn :error. Three arities, which is why
* the switch could not collapse.
*/
check_pr("Result.err", r_err, 200,
"<ParserResult :type error :src_fn some_ssm :error bad juju>");
check_pr("Result.err", r_err, 30,
"<ParserResult\n"
" :type error\n"
" :src_fn some_ssm\n"
" :error bad juju>");
}
}
/** BATCH 2, part 2: the cases only a RUNNING PARSER can reach.
*
* DDefineSsm and DExpectQDictSsm cannot be pinned from a bare
* _make(): DDefineSsm::get_expect_str() asserts for the def_0 state
* _make leaves behind, and DExpectQDictSsm's dict_ is null until the
* opening brace arrives (and a null dict_ ABORTS, it does not throw).
* So both are driven from real token streams and read off the top of
* the parser stack between tokens.
*
* The token index is part of the case: it names the SSM STATE being
* pinned, and those states are what the printers' fields report.
**/
TEST_CASE("reader2-parser-render", "[printable][reader2]")
{
using xo::scm::Token;
REQUIRE(s_init.evidence());
UtestRehearser rh;
/* def foo : f64 = 3.5 ;
* ^ ^
* tk1 tk3 -- DDefineSsm at def_2 and def_4
*/
const std::vector<Token> define_v = {
Token::def_token(), Token::symbol_token("foo"),
Token::colon_token(), Token::symbol_token("f64"),
Token::singleassign_token(), Token::f64_token("3.5"),
Token::semicolon_token()
};
/* #q { { a : 1 } }
* ^ ^ ^
* | | tk5 -- qdict_1d, :key cleared, :dict populated
* | tk3 -- qdict_1b, :key present, :dict empty
* tk2 -- qdict_1a, :key absent, :dict empty
*/
const std::vector<Token> qdict_v = {
Token::quote_token(), Token::leftbrace_token(),
Token::leftbrace_token(), Token::symbol_token("a"),
Token::colon_token(), Token::i64_token("1"),
Token::rightbrace_token(), Token::rightbrace_token()
};
/* lambda ( ) { 1.5 }
* ^
* tk0 -- DExpectFormalArglistSsm at argl_0, :n_args 0
*/
const std::vector<Token> arg0_v = {
Token::lambda_token(), Token::leftparen_token(),
Token::rightparen_token(), Token::leftbrace_token(),
Token::f64_token("1.5"), Token::rightbrace_token()
};
/* lambda ( x : f64 , y : f64 ) { 1.5 }
* ^ ^
* tk4 tk8
* -- :n_args 1 and 2: the arg[i] loop at one and two iterations.
* tk0 covers zero, above.
*/
const std::vector<Token> arg2_v = {
Token::lambda_token(), Token::leftparen_token(),
Token::symbol_token("x"), Token::colon_token(),
Token::symbol_token("f64"), Token::comma_token(),
Token::symbol_token("y"), Token::colon_token(),
Token::symbol_token("f64"), Token::rightparen_token(),
Token::leftbrace_token(), Token::f64_token("1.5"),
Token::rightbrace_token()
};
for (auto _ : rh) {
scope log(XO_DEBUG2_(rh.enable_debug(), "reader2-parser-render"));
/** step a fresh parser through @p tk_v, and render the top of
* its stack after token @p i_tk.
**/
auto check_step = [&rh, &log](const std::vector<Token> & tk_v,
const char * name,
std::size_t i_tk,
std::uint32_t margin,
const char * expect_pretty)
{
ParseFixture fx(std::string(name) + "." + std::to_string(i_tk)
+ "." + std::to_string(margin));
fx.parser_->begin_interactive_session();
for (std::size_t i = 0; i <= i_tk; ++i)
fx.parser_->on_token(tk_v[i]);
auto top = fx.parser_->top_ssm();
REQUIRE(bool(top));
auto pr = top.to_facet<APrintable>();
std::string pretty
= scrub_tseq(scrub_type_id(
scrub_typevar(render_pretty(pr, margin))));
log && log(xtag("name", name), xtag("i_tk", i_tk),
xtag("margin", margin),
xtag("pretty", pretty));
REHEARSE(rh, pretty == std::string(expect_pretty));
};
check_step(define_v, "define", 1, 200,
"<DDefineSsm :defstate def_2 :expect singleassign|colon :def_expr <DDefineExpr :lhs <DVariable :name \"foo\" :typeref <TypeRef :id \"\" :td null>>>>");
check_step(define_v, "define", 1, 60,
"<DDefineSsm\n"
" :defstate def_2\n"
" :expect singleassign|colon\n"
" :def_expr\n"
" <DDefineExpr\n"
" :lhs\n"
" <DVariable\n"
" :name \"foo\"\n"
" :typeref <TypeRef :id \"\" :td null>>>>");
check_step(define_v, "define", 3, 200,
"<DDefineSsm\n"
" :defstate def_4\n"
" :expect singleassign\n"
" :def_expr <DDefineExpr :lhs <DVariable :name \"foo\" :typeref <TypeRef :id \"\" :td <TypeDescr :id N :canonical_name double :complete true :metatype atomic>>>>>");
check_step(define_v, "define", 3, 60,
"<DDefineSsm\n"
" :defstate def_4\n"
" :expect singleassign\n"
" :def_expr\n"
" <DDefineExpr\n"
" :lhs\n"
" <DVariable\n"
" :name \"foo\"\n"
" :typeref\n"
" <TypeRef\n"
" :id \"\"\n"
" :td\n"
" <TypeDescr\n"
" :id N\n"
" :canonical_name double\n"
" :complete true\n"
" :metatype atomic>>>>>");
check_step(qdict_v, "qdict", 2, 200,
"<DExpectQDictSsm :state qdict_1a :expect symbol|rightbrace :dict {}>");
check_step(qdict_v, "qdict", 2, 60,
"<DExpectQDictSsm\n"
" :state qdict_1a\n"
" :expect symbol|rightbrace\n"
" :dict {}>");
check_step(qdict_v, "qdict", 3, 200,
"<DExpectQDictSsm :state qdict_1b :expect colon :key a :dict {}>");
check_step(qdict_v, "qdict", 3, 60,
"<DExpectQDictSsm\n"
" :state qdict_1b\n"
" :expect colon\n"
" :key a\n"
" :dict {}>");
check_step(qdict_v, "qdict", 5, 200,
"<DExpectQDictSsm :state qdict_1d :expect semicolon|rightbrace :dict {a: 1;}>");
check_step(qdict_v, "qdict", 5, 60,
"<DExpectQDictSsm\n"
" :state qdict_1d\n"
" :expect semicolon|rightbrace\n"
" :dict {a: 1;}>");
/* DExpectFormalArglistSsm: a HAND-ROLLED two-pass printer,
* collapsed to struct_open() for its runtime 3 + n_args
* arity. Its own framing is byte-identical on both
* protocols in every case here; the divergences at margins
* 60 and 30 all begin INSIDE <DVariable, i.e. in children
* converted earlier.
*/
check_step(arg0_v, "arg0", 0, 200,
"<DExpectFormalArglistSsm :fastate argl_0 :expect leftparen :n_args 0>");
check_step(arg0_v, "arg0", 0, 30,
"<DExpectFormalArglistSsm\n"
" :fastate argl_0\n"
" :expect leftparen\n"
" :n_args 0>");
check_step(arg2_v, "arg2", 4, 200,
"<DExpectFormalArglistSsm\n"
" :fastate argl_1b\n"
" :expect comma|rightparen\n"
" :n_args 1\n"
" :arg[0] <DVariable :name \"x\" :typeref <TypeRef :id \"\" :td <TypeDescr :id N :canonical_name double :complete true :metatype atomic>>>>");
check_step(arg2_v, "arg2", 4, 60,
"<DExpectFormalArglistSsm\n"
" :fastate argl_1b\n"
" :expect comma|rightparen\n"
" :n_args 1\n"
" :arg[0]\n"
" <DVariable\n"
" :name \"x\"\n"
" :typeref\n"
" <TypeRef\n"
" :id \"\"\n"
" :td\n"
" <TypeDescr\n"
" :id N\n"
" :canonical_name double\n"
" :complete true\n"
" :metatype atomic>>>>");
check_step(arg2_v, "arg2", 8, 200,
"<DExpectFormalArglistSsm\n"
" :fastate argl_1b\n"
" :expect comma|rightparen\n"
" :n_args 2\n"
" :arg[0] <DVariable :name \"x\" :typeref <TypeRef :id \"\" :td <TypeDescr :id N :canonical_name double :complete true :metatype atomic>>>\n"
" :arg[1] <DVariable :name \"y\" :typeref <TypeRef :id \"\" :td <TypeDescr :id N :canonical_name double :complete true :metatype atomic>>>>");
check_step(arg2_v, "arg2", 8, 60,
"<DExpectFormalArglistSsm\n"
" :fastate argl_1b\n"
" :expect comma|rightparen\n"
" :n_args 2\n"
" :arg[0]\n"
" <DVariable\n"
" :name \"x\"\n"
" :typeref\n"
" <TypeRef\n"
" :id \"\"\n"
" :td\n"
" <TypeDescr\n"
" :id N\n"
" :canonical_name double\n"
" :complete true\n"
" :metatype atomic>>>\n"
" :arg[1]\n"
" <DVariable\n"
" :name \"y\"\n"
" :typeref\n"
" <TypeRef\n"
" :id \"\"\n"
" :td\n"
" <TypeDescr\n"
" :id N\n"
" :canonical_name double\n"
" :complete true\n"
" :metatype atomic>>>>");
}
}
/** ParserStack + DSchematikaParser -- converted TOGETHER, because
* neither is observable without the other. DSchematikaParser::psm_
* is private with no accessor, so no test can obtain a ParserStack*;
* it is only ever reached through the parser's own :stack field.
*
* Both are non-facet types reached by ppdetail<T*> on the POINTER,
* so both needed a Prettifier<T*> added, not just a pretty() body --
* the same situation as ParserResult, and the last of the three.
* Before this, a parser on the ppsink path fell through to
* operator<< / DSchematikaParser::print(ostream&), which renders a
* DIFFERENT struct (:debug and :has_stack, not :stack), ends with
* std::endl, and colours its tags via legacy xo::xtag.
*
* Both use force_break, reproducing legacy's unconditional
* `if (ppii.upto()) return false;`. The "fresh" case below is what
* pins that decision: <SchematikaParser :stack nullptr> is short
* enough to fit at margin 200, and BOTH protocols break it anyway.
*
* Every stack pinned here is made only of ALREADY-CONVERTED ssms.
* A quoted-literal stack would show :[1] STUB:DExpectQListSsm today
* and change when that converts; keeping those out means this table
* does not churn when the last two printers land. Those two get
* their own cases then -- this conversion is what opened their
* observation window.
**/
TEST_CASE("reader2-stack-render", "[printable][reader2]")
{
using xo::scm::Token;
REQUIRE(s_init.evidence());
UtestRehearser rh;
/* -1: render before begin_interactive_session(), so psm_.stack()
* is null -- the Prettifier's nullptr branch, which is reachable
* only in this window.
*/
const int fresh_upto = -2;
/* -1: begin_interactive_session() and no tokens */
const int rest_upto = -1;
const int d2_upto = 0;
const int lam_upto = 3;
const std::vector<Token> tk_v = {
Token::lambda_token(), Token::leftparen_token(),
Token::symbol_token("x"), Token::colon_token()
};
const std::vector<Token> d2_tk_v = { Token::i64_token("1") };
for (auto _ : rh) {
scope log(XO_DEBUG2_(rh.enable_debug(), "reader2-stack-render"));
auto check_parser = [&rh, &log, &tk_v, &d2_tk_v]
(const char * label, int upto, std::uint32_t margin, const char * expect_pretty)
{
ParseFixture fx(std::string(label) + "."
+ std::to_string(margin));
if (upto >= -1)
fx.parser_->begin_interactive_session();
const std::vector<Token> & src
= (std::string(label) == "d2") ? d2_tk_v : tk_v;
for (int i = 0; i <= upto; ++i)
fx.parser_->on_token(src[i]);
std::string pretty
= scrub_tseq(scrub_type_id(
scrub_typevar(render_pretty(fx.parser_.data(),
margin))));
log && log(xtag("label", label), xtag("margin", margin),
xtag("pretty", pretty));
REHEARSE(rh, pretty == std::string(expect_pretty));
};
check_parser("fresh", fresh_upto, 200,
"<SchematikaParser\n"
" :stack nullptr>");
check_parser("rest", rest_upto, 200,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
check_parser("d2", d2_upto, 200,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DProgressSsm :lhs <DConstant :value_.tseq N :value.tseq N :value 1> :expect oper|semicolon|leftparen|rightparen|rightbrace>\n"
" :[1] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
check_parser("d2", d2_upto, 60,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0]\n"
" <DProgressSsm\n"
" :lhs\n"
" <DConstant :value_.tseq N :value.tseq N :value 1>\n"
" :expect\n"
" oper|semicolon|leftparen|rightparen|rightbrace>\n"
" :[1] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
check_parser("lam", lam_upto, 200,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DExpectTypeSsm>\n"
" :[1] <DExpectFormalArgSsm :fstate formal_2 :expect typename :name x>\n"
" :[2] <DExpectFormalArglistSsm :fastate argl_1a :expect formal-name :n_args 0>\n"
" :[3] <DLambdaSsm :lmstate lm_1 :expect lambda-params>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
check_parser("lam", lam_upto, 60,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DExpectTypeSsm>\n"
" :[1]\n"
" <DExpectFormalArgSsm\n"
" :fstate formal_2\n"
" :expect typename\n"
" :name x>\n"
" :[2]\n"
" <DExpectFormalArglistSsm\n"
" :fastate argl_1a\n"
" :expect formal-name\n"
" :n_args 0>\n"
" :[3] <DLambdaSsm :lmstate lm_1 :expect lambda-params>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
/* Prettifier<DSchematikaParser*>'s OWN null branch. NOT the
* same thing as "fresh" above: that pins :stack nullptr,
* which comes from Prettifier<ParserStack*>. This one is a
* null PARSER pointer, and without a case for it a mutation
* of its nullptr text goes unnoticed -- found by exactly such
* a mutation, 2026-08-11.
*/
{
xo::scm::DSchematikaParser * null_parser = nullptr;
REHEARSE(rh, render_pretty(null_parser, 200) == "nullptr");
}
}
}
/** DExpectQListSsm + DExpectQArraySsm -- the last two reader2
* printers, and the ones the ParserStack/DSchematikaParser
* conversion above made observable at all. Neither is reachable
* from a test any other way: both are pushed and popped inside a
* single quoted literal, so the only window on them is a parser
* rendered mid-literal.
*
* `#q` takes a LEFTBRACE; the bracket INSIDE it selects the
* container -- `#q { ( .. ) }` a list, `#q { [ .. ] }` an array.
*
* These renderings embed DList/DArray output, where DDictionary has
* a deliberate legacy-vs-ppsink divergence pinned in xo-object2
* (`{ }` vs `{}`, at xo-object2/utest/printable_render.test.cpp:314).
* Measured 2026-08-11: DList and DArray have no such divergence --
* converting these two printers changed the STUB: lines and nothing
* else in the observed table. Hence one expectation string per case
* below would have sufficed; two are kept anyway, since
* the deprecated expectation was scaffolding, deleted at phase E.
*
* State qlist_0 / qarray_0 is NOT pinned: start_/array_ are null
* there and the printer throws. Pre-existing, and tracked as
* .xo-backlog/xo-reader2/issues/01-ssm-printer-null-children.md.
**/
TEST_CASE("reader2-qliteral-stack-render", "[printable][reader2]")
{
using xo::scm::Token;
REQUIRE(s_init.evidence());
UtestRehearser rh;
/* `#q { ( 1 2` -- stops before the rightparen that pops the ssm */
const std::vector<Token> qlist_tk_v = {
Token::quote_token(), Token::leftbrace_token(),
Token::leftparen_token(),
Token::i64_token("1"), Token::i64_token("2")
};
/* `#q { [ 1 , 2` -- likewise before the rightbracket */
const std::vector<Token> qarray_tk_v = {
Token::quote_token(), Token::leftbrace_token(),
Token::leftbracket_token(),
Token::i64_token("1"), Token::comma_token(),
Token::i64_token("2")
};
for (auto _ : rh) {
scope log(XO_DEBUG2_(rh.enable_debug(),
"reader2-qliteral-stack-render"));
auto check_parser = [&rh, &log]
(const char * label, const std::vector<Token> & src,
std::size_t upto, std::uint32_t margin, const char * expect_pretty)
{
ParseFixture fx(std::string(label) + "."
+ std::to_string(margin));
fx.parser_->begin_interactive_session();
for (std::size_t i = 0; i < upto; ++i)
fx.parser_->on_token(src[i]);
std::string pretty
= scrub_tseq(scrub_type_id(
scrub_typevar(render_pretty(fx.parser_.data(),
margin))));
log && log(xtag("label", label), xtag("margin", margin),
xtag("pretty", pretty));
REHEARSE(rh, pretty == std::string(expect_pretty));
};
/* the EMPTY list: `()`, not `( )`. DList::_nil() is already
* assigned here, so this pins the empty-container rendering
* without touching the null-start_ throw.
*/
check_parser("qlist-empty", qlist_tk_v, 3, 200,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DExpectQLiteralSsm :expect leftparen|leftbracket|leftbrace|string|f64|i64|bool>\n"
" :[1] <DExpectQListSsm :state qlist_1a :expect qliteral|rightparen :list ()>\n"
" :[2] <DQuoteSsm :quote_xst quote_2 :expect qliteral>\n"
" :[3] <DProgressSsm :expect expr1|leftparen>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
check_parser("qlist", qlist_tk_v, 5, 200,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DExpectQLiteralSsm :expect leftparen|leftbracket|leftbrace|string|f64|i64|bool>\n"
" :[1] <DExpectQListSsm :state qlist_1a :expect qliteral|rightparen :list (1 2)>\n"
" :[2] <DQuoteSsm :quote_xst quote_2 :expect qliteral>\n"
" :[3] <DProgressSsm :expect expr1|leftparen>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
/* margin 60: the ssm itself breaks, so this is what pins the
* per-field indent -- legacy indent_width 2 against ppsink's
* tag_value_offset 1, compounding with nesting depth.
*/
check_parser("qlist", qlist_tk_v, 5, 60,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0]\n"
" <DExpectQLiteralSsm\n"
" :expect\n"
" leftparen|leftbracket|leftbrace|string|f64|i64|bool>\n"
" :[1]\n"
" <DExpectQListSsm\n"
" :state qlist_1a\n"
" :expect qliteral|rightparen\n"
" :list (1 2)>\n"
" :[2] <DQuoteSsm :quote_xst quote_2 :expect qliteral>\n"
" :[3] <DProgressSsm :expect expr1|leftparen>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
/* the empty ARRAY: `[]`. NB :expect still reads
* qliteral|rightPAREN in state qarray_1a -- a mnemonic the
* ssm shares with the list version. Pinned as observed;
* whether it should say rightbracket is not this refactor's
* question.
*/
check_parser("qarray-empty", qarray_tk_v, 3, 200,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DExpectQLiteralSsm :expect leftparen|leftbracket|leftbrace|string|f64|i64|bool>\n"
" :[1] <DExpectQArraySsm :state qarray_1a :expect qliteral|rightparen :array []>\n"
" :[2] <DQuoteSsm :quote_xst quote_2 :expect qliteral>\n"
" :[3] <DProgressSsm :expect expr1|leftparen>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
check_parser("qarray", qarray_tk_v, 6, 200,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0] <DExpectQLiteralSsm :expect leftparen|leftbracket|leftbrace|string|f64|i64|bool>\n"
" :[1] <DExpectQArraySsm :state qarray_1a :expect qliteral|rightparen :array [1 2]>\n"
" :[2] <DQuoteSsm :quote_xst quote_2 :expect qliteral>\n"
" :[3] <DProgressSsm :expect expr1|leftparen>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
check_parser("qarray", qarray_tk_v, 6, 60,
"<SchematikaParser\n"
" :stack\n"
" <ParserStack\n"
" :[0]\n"
" <DExpectQLiteralSsm\n"
" :expect\n"
" leftparen|leftbracket|leftbrace|string|f64|i64|bool>\n"
" :[1]\n"
" <DExpectQArraySsm\n"
" :state qarray_1a\n"
" :expect qliteral|rightparen\n"
" :array [1 2]>\n"
" :[2] <DQuoteSsm :quote_xst quote_2 :expect qliteral>\n"
" :[3] <DProgressSsm :expect expr1|leftparen>\n"
" :[4] <DToplevelSeqSsm :seqtype toplevel-interactive>>>");
}
}
} /*namespace ut*/
} /*namespace xo*/
/* end printable_render.test.cpp */