# Parser-shape corpus for the 2.13 <-> 2.14 parity jig.
#
# Format:  NAME|expression
#
# One expression per line, sent as `think <expression>` wrapped in markers.
# Keep each on a single line and avoid the marker text (ZZ) inside it.
#
# These shapes exist because MUSH function-call recognition is context
# sensitive in ways a tokenizer cannot decide: `add(` is a call and
# `foo(` is not, and only a function-table lookup separates them.  The
# corpus therefore probes POSITION (where in the text the call sits),
# ARMING (what precedes it), and NESTING, rather than trying to describe
# a grammar.
#
# Comment lines start with #.  Blank lines are ignored.

# --- position of a call within a function argument ---
POS_START|[strcat(add(1,2))]
POS_START_TAIL|[strcat(add(1,2) tail)]
POS_MID|[strcat(x add(1,2) y)]
POS_END|[strcat(head add(1,2))]
POS_MID_COLON|[strcat(Total: add(1,2) items)]
POS_MID_NOSP|[strcat(xadd(1,2)y)]
POS_MID_COMMA_SEP|[strcat(a,b add(1,2) c)]
POS_ARG2_START|[strcat(zz,add(1,2))]
POS_ARG2_MID|[strcat(zz,q add(1,2) r)]

# --- known vs unknown callee name (the function-table dependency) ---
UNKNOWN_START|[strcat(foo(1,2))]
UNKNOWN_MID|[strcat(x foo(1,2) y)]

# --- escaped brackets: a literal '[' places the call mid-text ---
ESC_BOTH|[strcat(\[add(1,2)\])]
ESC_OPEN|[strcat(\[add(1,2))]
ESC_CLOSE|[strcat(add(1,2)\])]
ESC_BOTH_TEXT|[strcat(q\[add(1,2)\]r)]
REALBRK_IN_ARG|[strcat(q[add(1,2)]r)]
REALBRK_ALONE|[strcat([add(1,2)])]

# --- brace groups ---
BRACE_MID|[strcat({x add(1,2) y})]
BRACE_START|[strcat({add(1,2)})]

# --- %-substitution adjacency ---
PCT_BEFORE|[strcat(%b add(1,2))]
PCT_ADJACENT|[strcat(%badd(1,2))]
PAREN_ESC|[strcat(add%(1,2%))]

# --- more than one call in the same text (the one-shot rule) ---
DOUBLE_CALL|[strcat(add(1,2) mul(3,4))]
DOUBLE_MID|[strcat(z add(1,2) mul(3,4) w)]

# --- nesting depth ---
NESTED_OUTER|[strcat(strcat(add(1,2)))]
NESTED_MID|[strcat(strcat(x add(1,2) y))]
DEEP_MID|[strcat(strcat(strcat(q add(1,2) r)))]

# --- what precedes the callee name ---
SPACE_BEFORE_PAREN|[strcat(x add (1,2) y)]|2.13
DIGIT_PREFIX|[strcat(x 1add(1,2) y)]
UNDERSCORE_PREFIX|[strcat(x _add(1,2) y)]
PUNCT_PREFIX|[strcat(x .add(1,2) y)]
DASH_PREFIX|[strcat(x -add(1,2) y)]
BANG_PREFIX|[strcat(x !add(1,2) y)]
EQ_PREFIX|[strcat(x =add(1,2) y)]
SLASH_PREFIX|[strcat(x /add(1,2) y)]
QUOTE_PREFIX|[strcat(x "add(1,2) y)]
COLON_PREFIX|[strcat(x :add(1,2) y)]
SEMI_PREFIX|[strcat(x ;add(1,2) y)]
OPENPAREN_PREFIX|[strcat(x (add(1,2) y)]

# --- top level (no enclosing function argument) ---
TOPLEVEL_MID|x add(1,2) y
TOPLEVEL_START|add(1,2) y
TOPLEVEL_BRK|x [add(1,2)] y

# --- control-flow function arguments ---
SWITCH_ARG_MID|[switch(1,1,x add(1,2) y,zz)]
IF_ARG_MID|[if(1,x add(1,2) y)]
ITER_MID|[iter(1,x add(1,2) y)]

# --- callee arity and shape ---
U_ARG_MID|[strcat(x u(1,2) y)]
LIT_LOOKALIKE|[strcat(x add (1,2) y)]|2.13
EMPTY_ARGS|[strcat(x add() y)]
ONE_ARG|[strcat(x abs(-5) y)]

# --- player-facing prose (the reason the rule exists) ---
SAY_PROSE|say Hey, I tried add(1).
SAY_MIXED|say Hey, I tried add(1) and I got [add(1)].
POSE_PROSE|pose tried add(1) today.
PEMIT_PROSE|@pemit me=Hey, I tried add(1).

# ===================================================================
# Nesting map (#1219).  2.13 kept one stack where '[' pushed ']' and
# '(' pushed ')', unwinding on a match; 2.14 tracks bracket and brace
# depth but not paren depth.  These shapes chart the consequences.
# ===================================================================

# --- paren depth, balanced, no commas ---
PN_D1|[strcat(a (b) c)]|2.13
PN_D2|[strcat(a (b (c) d) e)]|2.13
PN_D3|[strcat(a (b (c (d) e) f) g)]|2.13
PN_ADJACENT|[strcat(a (b)(c) d)]|2.13
PN_EMPTY|[strcat(a () b)]|2.13
PN_ONLY|[strcat((a))]

# --- paren depth with commas at each level ---
PNC_D1|[strcat(a (b,c) d)]|2.13
PNC_D2|[strcat(a (b,(c,d),e) f)]|2.13
PNC_LEAD|[strcat((a,b) tail)]|2.13
PNC_TRAIL|[strcat(head (a,b))]|2.13
PNC_TWOARG|[strcat(x (a,b) y,z)]|2.13

# --- unbalanced parens ---
PU_EXTRA_OPEN|[strcat(a (b c)]|neither
PU_EXTRA_CLOSE|[strcat(a b) c)]
PU_CLOSE_FIRST|[strcat(a ) b)]
PU_OPEN_ONLY|[strcat(a ( b)]|neither

# --- brackets inside arguments ---
BK_LITERAL|[strcat(a [b] c)]
BK_CALL|[strcat(a [add(1,2)] c)]
BK_ESC|[strcat(a \[b\] c)]
BK_NESTED_PAREN|[strcat(a [add(1,2)] (x,y) c)]|2.13

# --- braces inside arguments ---
BR_LITERAL|[strcat(a {b} c)]
BR_COMMA|[strcat(a {b,c} d)]
BR_PAREN|[strcat(a {b (c,d) e} f)]

# --- mixed nesting ---
MX_BP|[strcat(a [b (c,d) e] f)]|2.13
MX_PB|[strcat(a (b [c] d) e)]|2.13
MX_PBR|[strcat(a (b {c,d} e) f)]|2.13
MX_ALL|[strcat(a [b (c {d,e} f) g] h)]|2.13

# --- escaped delimiters ---
ES_PCT_PAREN|[strcat(a %(b,c%) d)]
ES_BSL_PAREN|[strcat(a \(b,c\) d)]
ES_PCT_BRACK|[strcat(a %[b%] c)]
ES_MIXED|[strcat(a %(b [c] d%) e)]

# --- depth stress (2.13 stops pushing past stacklim 32) ---
DEEP_08|[strcat(((((((((x))))))))))]
DEEP_16|[strcat((((((((((((((((x))))))))))))))))]
DEEP_34|[strcat((((((((((((((((((((((((((((((((((x))))))))))))))))))))))))))))))))))]

# --- realistic prose through formatting functions ---
RP_TIME|[strcat(Meet me (Tue, 5pm) downtown)]|2.13
RP_HELP|[ansi(r,Warning (see help, page 2))]|2.13
RP_LJUST|[ljust(Hello (world) ok,30)]|2.13
RP_STRLEN|[strlen(Hello (world) ok)]|2.13
RP_NESTED_FN|[strcat(Result: [add(1,2)] (of 3, total))]|2.13
RP_APOSTROPHE|[strcat(It's a (big, red) ball)]|2.13

# --- space between callee name and '(' — PINNED (see #1219 discussion) ---
#
# 2.13 treats `name (args)` with a space as a call; 2.14 requires the
# paren to be adjacent.  So [add (1,2)] evaluates on 2.13 and becomes
# literal text on 2.14 — working softcode silently ceasing to function.
# Flagged as something to support, decision deferred.
#
SPACE_CALL_KNOWN|[add (1,2)]|pin
SPACE_CALL_UNKNOWN|[b (c,d)]|pin
SPACE_CALL_NESTED|[strcat([add (1,2)])]|pin

# ===================================================================
# Global user functions (@function).  2.13 resolves a global on the same
# path as a builtin: the candidate name is looked up in the builtin table
# first, then in ufunc_htab (eval.cpp:1487).  2.14's AST route does the
# same; the JIT's ECALL fallback consults only the builtin table, so a
# global is unreachable there — #1231.
#
# The two setup lines define one.  Lines beginning with & or @ are sent
# as commands rather than wrapped in `think`.
# ===================================================================
UF_SETUP_ATTR|&UFADD me=[add(%0,%1)]
UF_SETUP_FN|@function ufadd=me/UFADD
UF_ADJACENT|[ufadd(2,3)]|2.13
UF_IN_ARG|[strcat(ufadd(2,3))]|2.13
UF_DISARM|[ufadd(2,3) ufadd(4,5)]|2.13
UF_AFTER_BUILTIN|[add(1,2) ufadd(4,5)]
UF_MIDTEXT|[strcat(x ufadd(2,3) y)]
UF_SPACE|[ufadd (2,3)]|pin
UF_TWOSPACE|[ufadd  (2,3)]|pin
UF_IN_ARG_SPACE|[strcat(ufadd (2,3))]
UF_UNKNOWN_SPACE|[nosuchfn (1,2)]|pin

# ===================================================================
# Disarm across nested regions.  After a successful call 2.13 clears
# EV_FCHECK (eval.cpp:1677), so a later '(' in the same region is
# literal; an eval bracket starts a fresh region with FCHECK|FMAND set
# (eval.cpp:2334).  These shapes chart where a region begins and ends.
#
# Most already agree.  They are kept as a guard: a change to the arming
# model would move them, and they say so immediately.
# ===================================================================
DA_TWO_CALLS|[add(1,2) mul(3,4)]
DA_CALL_BRACKET|[add(1,2) [mul(3,4)]]
DA_BRACKET_CALL|[[add(1,2)] mul(3,4)]|2.14
DA_BRACKET_BOTH|[[add(1,2)] [mul(3,4)]]
DA_NESTED_ARG|[strcat(add(1,2) mul(3,4))]
DA_ARG_THEN_OUTER|[strcat(add(1,2)) mul(3,4)]
DA_INNER_RESETS|[strcat(add(1,2),mul(3,4))]
DA_ITER|[iter(1 2,add(%i0,1) mul(2,3))]
DA_SWITCH|[switch(1,1,add(1,2) mul(3,4),no)]
DA_DEEP|[strcat(strcat(add(1,2) mul(3,4)) sub(9,1))]
DA_BRACKET_IN_ARG|[strcat(add(1,2) [mul(3,4)])]
DA_THREE|[add(1,2) mul(3,4) sub(9,1)]

# ===================================================================
# space_compress interaction.  In 2.13 that setting does two unrelated
# jobs: ordinary output whitespace compression, and gating the
# trailing-space trim on the candidate function name (eval.cpp:1434).
# With space_compress 0, [add (1,2)] is an error carrying a name with
# the space still in it — so `name (args)` is not a stable 2.13
# behaviour, it depends on a formatting option.  See the README.
#
# Measured here under the default (space_compress on).  The pure
# compression shapes are included so the two effects stay separable if
# one of them changes.
# ===================================================================
SC_TRIM|[add (1,2)]|pin
SC_TRIM2|[add  (1,2)]|pin
SC_LEADSPACE|[ add (1,2)]|pin
SC_ARG_SP|[strcat(add (1,2))]
SC_DOUBLESPACE|[strcat(a  b)]
SC_LEADTRAIL|[strcat(  a  )]
SC_PLAINTEXT|a  b  c
SC_SPAN|[zz mul(2,3)]|2.14

# --- text before a call in an FMAND region ---
# 2.13's candidate name there is the accumulated OUTPUT, so leading text
# is folded into the name rather than spending the recognition
# opportunity: [x add(1,2) y] -> "#-1 FUNCTION (X ADD) NOT FOUND".
# 2.14 tokenizes instead and cannot express that.
#
# Settled as 2.14 (#1246): the span-name error is not the target and
# literal text is the better answer, so both routes must agree with each
# other and divergence from 2.13 is accepted.  Same family as SC_SPAN
# and DA_BRACKET_CALL, which carry the same verdict for the same reason.
FM_LIT_CALL|[x add(1,2) y]|2.14
FM_LIT_CALL2|[x add(1,2)]|2.14

# --- crossed nesting: closer stack vs depth counter (#1248) ---
# 2.13's parse_to_lite keeps a STACK of expected closers; a closer that
# matches an entry unwinds to it, past any intervening entries.  #1219
# gave 2.14 a paren depth COUNTER instead, which is simpler and fixed the
# balanced shapes, but cannot unwind.  The two structures can only differ
# when a closer arrives whose opener is not the most recent one -- so the
# balanced controls below must agree, and the crossed ones are the test.
#
# Measured 2026-07-26: 6 of 11 diverge, JIT and AST agreeing throughout.
# Two distinct outcomes, adjudicated by the debuggability ranking in
# README.md rather than by compatibility.
#
# Balanced controls -- a counter and a stack agree here by construction.
XN_BAL_PAREN|[strcat((a b) c)]|both
XN_BAL_BRACKET|[strcat([a b] c)]|both
XN_BAL_NEST|[strcat((a [b] c))]|both
#
# 2.13 attempts a lookup 2.14 never attempts, answering with a #-1 FUNCTION
# NOT FOUND that names a real problem where 2.14 silently emits
# plausible-looking text -- rank 3 versus rank 1, the same trade as MX_BP,
# hence the same verdict.
#
# The CROSSING is not what causes it, though the shapes are crossed.  See
# the XN_TWIN_* controls below: remove the crossing entirely and the
# divergence is unchanged.  What arms 2.13 is entering the bracket region,
# where it accepts `a (` as a call by accumulating the name before the
# paren and tolerating the space.  That is the #1247 accumulated-name
# model, not the closer-stack question #1248 asks about -- replacing the
# paren counter with a stack would leave all three of these exactly as
# they are (measured 2026-07-26, both 2.14 routes identical).
#
# So these are VIOLATED by design, but the work they name is #1247's, and
# their verdict is worth re-confirming alongside FM_FAIL_NAME rather than
# inherited from a causal story that did not hold.
XN_BRACKET_FIRST|[strcat([a (b] c))]|2.13
XN_COMMA_BRACKET|[strcat([a, (b], c))]|2.13
XN_DEEP|[strcat((a [b (c] d) e))]|2.13
#
# The controls that separate "crossed" from "inside a bracket region".
# Both twins are perfectly balanced -- `[a (b)]` closes its paren before
# its bracket -- and both diverge exactly as the crossed shapes above do:
#
#   XN_TWIN_NESTED   2.13 `#-1 FUNCTION (A) NOT FOUND c`   2.14 `a (b) c`
#   XN_TWIN_PLAIN    2.13 `#-1 FUNCTION (A) NOT FOUND`     2.14 `a (b)`
#
# XN_TOP_SPACE is the same text at argument top level rather than inside a
# bracket region, and there the two engines agree -- which is what pins the
# trigger to the region, not to the parentheses.
#
# Left UNADJUDICATED deliberately: these are the same defect as the three
# above, so they should take whatever verdict that family settles on once
# it is re-confirmed, rather than having one copied across now.
XN_TWIN_NESTED|[strcat([a (b)] c)]
XN_TWIN_PLAIN|[strcat([a (b)])]
XN_TOP_SPACE|[strcat(a (b) c)]|both
#
# Crossed the other way, and neither engine errors: 2.13 echoes a stray
# trailing ']' and 2.14 quietly drops it.  Both leave the author guessing,
# which is the PU_* situation -- "unbalanced bracket" would beat either.
XN_PAREN_FIRST|[strcat((a [b) c])]|neither
XN_COMMA_CROSS|[strcat((a, [b), c])]|neither
XN_FN_CROSS|[strcat((add(1,2) [x) y])]|neither
#
# ---- does the paren *counter* differ from 2.13's closer stack? (#1248) ----
#
# #1219 replaced 2.13's parse_to_lite stack of expected closers with a paren
# depth counter, noting the stack also unwound crossed nesting like
# '( [ ) ]' and that no corpus shape then distinguished the two.  These
# shapes were written to distinguish them, and they do not: every crossed
# form below agrees.
#
# The XN_* shapes above do not settle it, because two other divergences sit
# on top of the crossing and mask it:
#
#   a space before '(' makes 2.13 read a call (SPACE_CALL_*, pinned), and
#   an unterminated call to an *unknown* function diverges on its own
#   (UT_* below) whatever the nesting is.
#
# So these use top level rather than an eval bracket -- an inner ']' cannot
# then close the region early -- and only known functions.  A literal comma
# surviving in the output means the argument did not split; add() makes the
# same boundary numeric.
#
XN_CROSS_TOP|strcat((a[b)c],d)|both
XN_CROSS_TOP3|strcat((a[b)c],d,e)|both
XN_BRACE_TOP|strcat((a{b)c},d)|both
XN_CROSS_NUM|add(1[2)3],4)|both
XN_NEST_TOP|strcat((a[b]c),d)|both

# ---- unterminated call: known vs unknown function ----
#
# This is what the crossed XN_* shapes above are actually measuring, and
# nesting is not involved: the one-token-deep form below diverges on its own.
#
# When a call's ')' never arrives before the ']':
#
#   2.13  emits '#-1 FUNCTION (X) NOT FOUND' if the name is *not* a
#         function, and falls back to literal text if it is;
#   2.14  falls back to literal text either way.
#
# 2.13 is the inconsistent one.  Its TRACE shows why: it looks the name up
# as soon as it sees 'name(' and reports a failed lookup immediately, but a
# successful lookup still needs an argument list it cannot finish, so that
# path punts.  2.14 defers the lookup until it has a whole call, so it
# never reaches the diagnostic.  Verified with TRACE on both engines:
#
#   [add(1,2]      2.13 'add(1,2'                      2.14 'add(1,2'
#   [nosuchfn(1,2] 2.13 '#-1 FUNCTION (NOSUCHFN) NOT FOUND'  2.14 'nosuchfn(1,2'
#
UT_KNOWN_UNTERM|[add(1,2]|both
UT_UNKNOWN_UNTERM|[a(b]
UT_KNOWN_TOP|add(1,2|both
UT_UNKNOWN_TOP|a(b|both

# ===================================================================
# EV_FMAND failed lookup (#1247).  When a function lookup FAILS at
# eval-bracket top level, 2.13 emits the diagnostic and stops
# evaluating the rest of the region; 2.14 emits it and continues.
#
# Measured 2026-07-26 on Windows (2.13.0.15 win32 binary distribution
# against 2.14 at e19db6b5e): 5 of 8 diverge, the two 2.14 routes
# byte-identical throughout.  Route participation was verified with
# jitstats() rather than assumed -- eval_handled is nonzero for every
# shape here with jit_eval_brackets=1, and eval_attempts is 0 with it
# off, so both routes really do run these.
#
# Left UNADJUDICATED per the issue: the choice is between 2.13's abort
# (better debuggability -- one clear error, no plausible-looking tail)
# and 2.14's continue (both routes already agree, and it is what
# ships).  See the README ranking; this is rank 3 versus rank 2.
#
# The dummy callee is deliberately Z-free: the harness marker is ZZ and
# the diagnostic uppercases the name into the output line.
#
# --- the core shape: does the error stop the region? ---
#
# Adjudicated 2.13 and implemented (#1247).  The measurement that settled
# it: 2.14 never evaluated the tail either -- it emitted it as LITERAL
# text, because a call already spent the region's recognition
# opportunity.  FM_OK_TWICE below shows the same rule after a SUCCESSFUL
# call, and all three engines agree there.  So the difference was only
# whether the literal tail is emitted or dropped, not continue-vs-abort,
# and dropping it costs no evaluation while stopping the diagnostic from
# reading as though part of the region had succeeded.
FM_FAIL_ABORT|[qqnofn(1) add(1,2)]|2.13
FM_FAIL_ABORT_TAIL|[qqnofn(1) add(1,2) tail]|2.13
FM_FAIL_TEXT|[qqnofn(1) hello]|2.13
FM_FAIL_TWICE|[qqnofn(1) qqnofn2(1)]|2.13
#
# Containment.  The abort must end ONE region and nothing else -- these
# are the shapes that fail if the signal leaks (#1247).
#
#   FM_ABORT_TWO_REGIONS   a later bracket still evaluates
#   FM_ABORT_MIDDLE        text and regions on both sides survive
#   FM_ABORT_INNER         an inner [...] must not truncate its parent
#   FM_ABORT_IN_ARG        EV_FMAND is cleared inside arguments, so the
#                          call is literal there and nothing aborts
#
# FM_ABORT_INNER puts the failing call FIRST in the inner region on
# purpose.  Written as `[a [b qqnofn(1)] c]` it tests nothing: the text
# before the call makes it literal on 2.14 (#1246), so no lookup is ever
# attempted and the abort cannot fire either way.  That shape is the
# FM_FAIL_NAME family and is already covered there.
FM_ABORT_TWO_REGIONS|[qqnofn(1) x][add(1,2)]|both
FM_ABORT_MIDDLE|[add(1,2)] [qqnofn(1) z] [mul(2,3)]|both
FM_ABORT_INNER|[a [qqnofn(1) x] c]|both
FM_ABORT_IN_ARG|[strcat(qqnofn(1) x)]|both
#
# FM_FAIL_NAME is NOT continue-vs-abort, and is the reason this family
# cannot be fixed by teaching 2.14 to stop.  2.13's name is the whole
# accumulated output before the '(' -- here "HELLO QQNOFN" -- so it
# reports a failed lookup that 2.14 never attempts at all (2.14 passes
# the text through literally).  An abort rule alone would still not
# reproduce 2.13 here; matching it needs the accumulated-name model,
# which is what the tokenizer cannot express.
FM_FAIL_NAME|[hello qqnofn(1)]
#
# Controls -- these agree on all three engines and must keep agreeing.
# FM_OK_THEN_FAIL and FM_OK_TWICE both show the one-recognition-
# opportunity-per-region rule already matching: the second call is
# literal on every engine.
FM_FAIL_ONLY|[qqnofn(1)]|both
FM_OK_THEN_FAIL|[add(1,2) qqnofn(1)]|both
FM_OK_TWICE|[add(1,2) mul(2,3)]|both
