2026-03-07 08:03:26 -07:00
|
|
|
|
/*
|
|
|
|
|
|
* eval.cpp - MUX expression AST evaluator study tool.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Stage 3 of the parser study: walk the AST and evaluate it, producing
|
|
|
|
|
|
* output equivalent to mux_exec for the subset of expressions that
|
|
|
|
|
|
* don't require database access.
|
|
|
|
|
|
*
|
2026-03-07 08:45:01 -07:00
|
|
|
|
* Two-tier function dispatch:
|
|
|
|
|
|
* - Normal functions: arguments pre-evaluated, handler gets strings
|
|
|
|
|
|
* - FN_NOEVAL functions: arguments NOT pre-evaluated, handler gets
|
|
|
|
|
|
* AST subtrees and calls eval() selectively
|
2026-03-07 08:03:26 -07:00
|
|
|
|
*
|
2026-03-07 08:45:01 -07:00
|
|
|
|
* This demonstrates that MUX softcode CAN be evaluated from an AST
|
|
|
|
|
|
* with proper deferred evaluation for control flow and iteration.
|
2026-03-07 08:03:26 -07:00
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
#include "mux_parse.h"
|
|
|
|
|
|
|
2026-03-07 08:03:26 -07:00
|
|
|
|
#include <cstdlib>
|
|
|
|
|
|
#include <cmath>
|
|
|
|
|
|
#include <map>
|
|
|
|
|
|
#include <functional>
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
// Evaluation context
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
2026-03-07 08:49:18 -07:00
|
|
|
|
// Eval flags — mirror the defines in mux/src/externs.h.
|
|
|
|
|
|
// These control what the evaluator does with each node type.
|
|
|
|
|
|
//
|
|
|
|
|
|
enum {
|
|
|
|
|
|
EV_EVAL = 0x0004, // Evaluate %-substitutions
|
|
|
|
|
|
EV_STRIP_CURLY = 0x0008, // Strip outer {} from args
|
|
|
|
|
|
EV_FCHECK = 0x0010, // Check for function calls on (
|
|
|
|
|
|
EV_FMAND = 0x0020, // Require valid function name
|
|
|
|
|
|
EV_NOFCHECK = 0x0040, // Suppress [ evaluation
|
|
|
|
|
|
EV_NO_COMPRESS = 0x0080, // Don't compress spaces
|
|
|
|
|
|
EV_STRIP_LS = 0x1000, // Strip leading spaces
|
|
|
|
|
|
EV_STRIP_TS = 0x2000, // Strip trailing spaces
|
|
|
|
|
|
EV_TOP = 0x0800, // Top-level evaluation
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Default eval flags for top-level evaluation.
|
|
|
|
|
|
static constexpr int EV_DEFAULT = EV_EVAL | EV_FCHECK | EV_TOP;
|
|
|
|
|
|
|
2026-03-07 08:03:26 -07:00
|
|
|
|
struct EvalContext {
|
|
|
|
|
|
// Registers %q0-%q9, %qa-%qz, and named %q<name>
|
|
|
|
|
|
std::map<std::string, std::string> registers;
|
|
|
|
|
|
|
|
|
|
|
|
// Command arguments %0-%9
|
|
|
|
|
|
std::string args[10];
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
int nargs = 2; // number of valid args
|
2026-03-07 08:03:26 -07:00
|
|
|
|
|
|
|
|
|
|
// Iterator state for iter/list
|
|
|
|
|
|
struct IterFrame {
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::string itext; // current item (itext(n))
|
|
|
|
|
|
int inum; // current index (inum(n), 1-based)
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
std::vector<IterFrame> iterStack;
|
|
|
|
|
|
|
|
|
|
|
|
// Special substitutions
|
|
|
|
|
|
std::string enactorName; // %n
|
|
|
|
|
|
std::string enactorDbref; // %#
|
|
|
|
|
|
std::string executorDbref; // %!
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
std::string callerDbref; // %@
|
|
|
|
|
|
std::string enactorObjid; // %: (dbref:creation_time)
|
|
|
|
|
|
std::string lastCommand; // %m
|
|
|
|
|
|
std::string moniker; // %k
|
|
|
|
|
|
std::string pipedOutput; // %|
|
|
|
|
|
|
std::string switchToken; // #$
|
|
|
|
|
|
|
|
|
|
|
|
// Pronoun substitutions — subjective, possessive, objective, abs-possessive
|
|
|
|
|
|
std::string pronSub; // %s (he/she/they)
|
|
|
|
|
|
std::string pronPos; // %p (his/her/their)
|
|
|
|
|
|
std::string pronObj; // %o (him/her/them)
|
|
|
|
|
|
std::string pronAbs; // %a (his/hers/theirs)
|
|
|
|
|
|
|
|
|
|
|
|
// Variable attributes %va–%vz (26 slots)
|
|
|
|
|
|
std::string varAttrs[26];
|
|
|
|
|
|
|
|
|
|
|
|
// Penn-specific
|
|
|
|
|
|
std::string rawCommand; // Penn %c — raw command line
|
|
|
|
|
|
std::string evaledCommand; // Penn %u — evaluated command line
|
|
|
|
|
|
std::string accentedName; // Penn %~ — accented name
|
|
|
|
|
|
std::string attrName; // Penn %= — current attribute name
|
2026-03-07 08:49:18 -07:00
|
|
|
|
|
|
|
|
|
|
// Current eval flags
|
|
|
|
|
|
int evalFlags = EV_DEFAULT;
|
2026-03-21 03:08:02 -06:00
|
|
|
|
|
|
|
|
|
|
// Parser/evaluator compatibility profile.
|
|
|
|
|
|
ParserProfile profile = PROFILE_MUX214_AST;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
// Evaluator
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class Evaluator {
|
|
|
|
|
|
public:
|
|
|
|
|
|
Evaluator(EvalContext &ctx) : m_ctx(ctx) {
|
|
|
|
|
|
registerBuiltins();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string eval(const ASTNode *node) {
|
2026-03-07 08:49:18 -07:00
|
|
|
|
return evalWithFlags(node, m_ctx.evalFlags);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Evaluate with specific flags (used for recursive contexts).
|
|
|
|
|
|
//
|
|
|
|
|
|
std::string evalWithFlags(const ASTNode *node, int flags) {
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (!node) return "";
|
|
|
|
|
|
|
2026-03-07 08:49:18 -07:00
|
|
|
|
// Save and restore flags for this scope.
|
|
|
|
|
|
int savedFlags = m_ctx.evalFlags;
|
|
|
|
|
|
m_ctx.evalFlags = flags;
|
|
|
|
|
|
|
|
|
|
|
|
std::string result;
|
|
|
|
|
|
|
2026-03-07 08:03:26 -07:00
|
|
|
|
switch (node->type) {
|
|
|
|
|
|
case NODE_SEQUENCE:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
result = evalSequence(node);
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_LITERAL:
|
|
|
|
|
|
case NODE_SPACE:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
result = node->text;
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_SUBST:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
// Only resolve substitutions if EV_EVAL is set.
|
|
|
|
|
|
result = (flags & EV_EVAL) ? evalSubst(node) : node->text;
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_ESCAPE:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
result = (flags & EV_EVAL) ? evalEscape(node) : node->text;
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_FUNCCALL:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
// Only dispatch functions if EV_FCHECK is set.
|
|
|
|
|
|
if (flags & EV_FCHECK) {
|
|
|
|
|
|
result = evalFuncCall(node);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No function checking — treat as literal text + parens.
|
|
|
|
|
|
result = node->text + "(";
|
|
|
|
|
|
for (size_t i = 0; i < node->children.size(); i++) {
|
|
|
|
|
|
if (i > 0) result += ",";
|
|
|
|
|
|
result += evalWithFlags(node->children[i].get(), flags);
|
|
|
|
|
|
}
|
|
|
|
|
|
result += ")";
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_DYNCALL:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
result = "#-1 DYNAMIC CALL NOT SUPPORTED";
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_EVALBRACKET:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
// Only recurse into [...] if EV_NOFCHECK is NOT set.
|
|
|
|
|
|
if (!(flags & EV_NOFCHECK)) {
|
|
|
|
|
|
result = evalEvalBracket(node);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Pass through as literal brackets.
|
|
|
|
|
|
result = "[";
|
|
|
|
|
|
if (!node->children.empty())
|
|
|
|
|
|
result += evalWithFlags(node->children[0].get(), flags);
|
|
|
|
|
|
result += "]";
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_BRACEGROUP:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
result = evalBraceGroup(node);
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
case NODE_SEMICOLON:
|
2026-03-07 08:49:18 -07:00
|
|
|
|
result = "";
|
|
|
|
|
|
break;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
2026-03-07 08:49:18 -07:00
|
|
|
|
|
|
|
|
|
|
m_ctx.evalFlags = savedFlags;
|
|
|
|
|
|
return result;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
|
EvalContext &m_ctx;
|
|
|
|
|
|
|
2026-03-07 08:39:34 -07:00
|
|
|
|
// Two dispatch tables:
|
|
|
|
|
|
// - m_funcs: normal functions, receive pre-evaluated string args
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// - m_noeval_funcs: FN_NOEVAL functions, receive unevaluated AST
|
|
|
|
|
|
// children and call eval() selectively (deferred evaluation)
|
2026-03-07 08:39:34 -07:00
|
|
|
|
//
|
2026-03-07 08:03:26 -07:00
|
|
|
|
using FuncHandler = std::function<std::string(const std::vector<std::string>&)>;
|
|
|
|
|
|
std::map<std::string, FuncHandler> m_funcs;
|
|
|
|
|
|
|
2026-03-07 08:39:34 -07:00
|
|
|
|
using NoevalHandler = std::function<std::string(const std::vector<std::unique_ptr<ASTNode>>&)>;
|
|
|
|
|
|
std::map<std::string, NoevalHandler> m_noeval_funcs;
|
|
|
|
|
|
|
2026-03-07 08:03:26 -07:00
|
|
|
|
static std::string toUpper(const std::string &s) {
|
|
|
|
|
|
std::string r = s;
|
|
|
|
|
|
for (auto &c : r) c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
|
|
|
|
|
|
return r;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static long toLong(const std::string &s) {
|
|
|
|
|
|
if (s.empty()) return 0;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
return strtol(s.c_str(), nullptr, 10);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static double toDouble(const std::string &s) {
|
|
|
|
|
|
if (s.empty()) return 0.0;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
return strtod(s.c_str(), nullptr);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static bool toBool(const std::string &s) {
|
|
|
|
|
|
if (s.empty()) return false;
|
|
|
|
|
|
char *end;
|
|
|
|
|
|
double v = strtod(s.c_str(), &end);
|
|
|
|
|
|
if (end != s.c_str()) return v != 0.0;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static std::string fmtNum(double v) {
|
|
|
|
|
|
if (v == floor(v) && fabs(v) < 1e15) {
|
|
|
|
|
|
return std::to_string(static_cast<long long>(v));
|
|
|
|
|
|
}
|
|
|
|
|
|
char buf[64];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%g", v);
|
|
|
|
|
|
return buf;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static std::vector<std::string> splitList(const std::string &s,
|
|
|
|
|
|
const std::string &sep = " ")
|
|
|
|
|
|
{
|
|
|
|
|
|
std::vector<std::string> result;
|
|
|
|
|
|
if (s.empty()) return result;
|
|
|
|
|
|
|
|
|
|
|
|
if (sep == " ") {
|
|
|
|
|
|
size_t i = 0;
|
|
|
|
|
|
while (i < s.size() && s[i] == ' ') i++;
|
|
|
|
|
|
while (i < s.size()) {
|
|
|
|
|
|
size_t j = i;
|
|
|
|
|
|
while (j < s.size() && s[j] != ' ') j++;
|
|
|
|
|
|
result.push_back(s.substr(i, j - i));
|
|
|
|
|
|
while (j < s.size() && s[j] == ' ') j++;
|
|
|
|
|
|
i = j;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
size_t start = 0;
|
|
|
|
|
|
size_t pos;
|
|
|
|
|
|
while ((pos = s.find(sep, start)) != std::string::npos) {
|
|
|
|
|
|
result.push_back(s.substr(start, pos - start));
|
|
|
|
|
|
start = pos + sep.size();
|
|
|
|
|
|
}
|
|
|
|
|
|
result.push_back(s.substr(start));
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string evalSequence(const ASTNode *node) {
|
|
|
|
|
|
std::string result;
|
2026-03-15 22:33:58 -06:00
|
|
|
|
int flags = m_ctx.evalFlags;
|
|
|
|
|
|
bool bFCheckPending = (flags & EV_FCHECK) != 0 && (flags & EV_FMAND) == 0;
|
|
|
|
|
|
|
2026-03-21 03:08:02 -06:00
|
|
|
|
for (size_t i = 0; i < node->children.size(); i++) {
|
|
|
|
|
|
const auto &child = node->children[i];
|
2026-03-21 05:23:57 -06:00
|
|
|
|
if (child->type == NODE_ESCAPE
|
|
|
|
|
|
&& child->text == "\\\\"
|
|
|
|
|
|
&& i + 1 < node->children.size()
|
|
|
|
|
|
&& node->children[i + 1]->type == NODE_SUBST) {
|
|
|
|
|
|
result += node->children[i + 1]->text;
|
|
|
|
|
|
i++;
|
|
|
|
|
|
if (bFCheckPending) {
|
|
|
|
|
|
bFCheckPending = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-15 22:33:58 -06:00
|
|
|
|
int childFlags = flags;
|
|
|
|
|
|
if (bFCheckPending) {
|
|
|
|
|
|
if (child->type != NODE_FUNCCALL) {
|
|
|
|
|
|
childFlags &= ~EV_FCHECK;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (child->type != NODE_SPACE) {
|
|
|
|
|
|
bFCheckPending = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if ((flags & EV_FCHECK) != 0 && (flags & EV_FMAND) == 0) {
|
|
|
|
|
|
childFlags &= ~EV_FCHECK;
|
|
|
|
|
|
}
|
2026-03-21 03:08:02 -06:00
|
|
|
|
|
2026-03-15 22:33:58 -06:00
|
|
|
|
result += evalWithFlags(child.get(), childFlags);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
// Apply first-character uppercasing when the substitution letter
|
|
|
|
|
|
// was uppercase (%N, %S, %P, %O, %A, %K, %M, %Q, %V).
|
|
|
|
|
|
//
|
|
|
|
|
|
static std::string maybeCapitalize(const std::string &s, char origLetter) {
|
|
|
|
|
|
if (s.empty()) return s;
|
|
|
|
|
|
if (isupper(static_cast<unsigned char>(origLetter))) {
|
|
|
|
|
|
std::string r = s;
|
|
|
|
|
|
r[0] = static_cast<char>(toupper(static_cast<unsigned char>(r[0])));
|
|
|
|
|
|
return r;
|
|
|
|
|
|
}
|
|
|
|
|
|
return s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 08:03:26 -07:00
|
|
|
|
std::string evalSubst(const ASTNode *node) {
|
|
|
|
|
|
const std::string &sub = node->text;
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
|
|
|
|
|
|
// Hash forms: ##, #@, #$
|
|
|
|
|
|
if (sub.size() == 2 && sub[0] == '#') {
|
|
|
|
|
|
switch (sub[1]) {
|
|
|
|
|
|
case '#': {
|
|
|
|
|
|
// ## — same as %i0 (current loop item)
|
|
|
|
|
|
int idx = static_cast<int>(m_ctx.iterStack.size()) - 1;
|
|
|
|
|
|
return (idx >= 0) ? m_ctx.iterStack[idx].itext : "";
|
|
|
|
|
|
}
|
|
|
|
|
|
case '@': {
|
|
|
|
|
|
// #@ — same as inum(0) (current loop position)
|
|
|
|
|
|
int idx = static_cast<int>(m_ctx.iterStack.size()) - 1;
|
|
|
|
|
|
return (idx >= 0) ? std::to_string(m_ctx.iterStack[idx].inum) : "";
|
|
|
|
|
|
}
|
|
|
|
|
|
case '$':
|
|
|
|
|
|
// #$ — switch matched value
|
|
|
|
|
|
return m_ctx.switchToken;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (sub.size() < 2) return "%";
|
|
|
|
|
|
|
|
|
|
|
|
char ch = sub[1];
|
2026-03-07 08:45:01 -07:00
|
|
|
|
char upper = static_cast<char>(toupper(static_cast<unsigned char>(ch)));
|
2026-03-07 08:03:26 -07:00
|
|
|
|
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
// %0–%9: command arguments
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (ch >= '0' && ch <= '9') {
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
int idx = ch - '0';
|
|
|
|
|
|
return (idx < m_ctx.nargs) ? m_ctx.args[idx] : "";
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
|
|
|
|
|
|
// %q/%Q: registers
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (upper == 'Q') {
|
|
|
|
|
|
std::string regname;
|
|
|
|
|
|
if (sub.size() >= 4 && sub[2] == '<') {
|
|
|
|
|
|
regname = sub.substr(3, sub.size() - 4);
|
|
|
|
|
|
} else if (sub.size() >= 3) {
|
|
|
|
|
|
regname = std::string(1, sub[2]);
|
|
|
|
|
|
}
|
|
|
|
|
|
auto it = m_ctx.registers.find(regname);
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
std::string val = (it != m_ctx.registers.end()) ? it->second : "";
|
|
|
|
|
|
return maybeCapitalize(val, ch);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
|
|
|
|
|
|
// %v/%V: variable attributes A–Z
|
|
|
|
|
|
if (upper == 'V' && sub.size() >= 3) {
|
|
|
|
|
|
char attrCh = static_cast<char>(toupper(static_cast<unsigned char>(sub[2])));
|
|
|
|
|
|
if (attrCh >= 'A' && attrCh <= 'Z') {
|
|
|
|
|
|
std::string val = m_ctx.varAttrs[attrCh - 'A'];
|
|
|
|
|
|
return maybeCapitalize(val, ch);
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// %w/%W: Penn W-attributes (profile-dependent)
|
|
|
|
|
|
if (upper == 'W' && sub.size() >= 3) {
|
|
|
|
|
|
if (m_ctx.profile == PROFILE_PENN) {
|
2026-03-21 05:23:57 -06:00
|
|
|
|
return "";
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
}
|
2026-03-21 04:28:44 -06:00
|
|
|
|
// MUX: unknown → literal. In MUX profiles the tokenizer
|
|
|
|
|
|
// should not have consumed the trailing attribute letter.
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
return std::string(1, ch);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
|
|
|
|
|
|
// %c/%C, %x/%X: color codes (MUX) or command/X-attr (Penn)
|
|
|
|
|
|
if (upper == 'C' || upper == 'X') {
|
|
|
|
|
|
if (m_ctx.profile == PROFILE_PENN) {
|
|
|
|
|
|
if (upper == 'C') {
|
|
|
|
|
|
// Penn: %c = raw command line
|
|
|
|
|
|
return m_ctx.rawCommand;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Penn: %x = X-attribute (if sub.size() >= 3)
|
|
|
|
|
|
if (sub.size() >= 3) {
|
2026-03-21 05:23:57 -06:00
|
|
|
|
return "thing";
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
// MUX: color codes — simplified for study tool
|
|
|
|
|
|
if (sub.size() >= 3) {
|
|
|
|
|
|
return "[color:" + sub.substr(1) + "]";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// %i/%I: loop itext at depth
|
2026-03-21 05:23:57 -06:00
|
|
|
|
if (upper == 'I' && sub.size() == 2 && m_ctx.profile == PROFILE_MUX214_AST) {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (upper == 'I' && sub.size() >= 3) {
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
char depthCh = sub[2];
|
|
|
|
|
|
if (depthCh >= '0' && depthCh <= '9') {
|
|
|
|
|
|
int depth = depthCh - '0';
|
|
|
|
|
|
int idx = static_cast<int>(m_ctx.iterStack.size()) - 1 - depth;
|
|
|
|
|
|
if (idx >= 0 && idx < static_cast<int>(m_ctx.iterStack.size())) {
|
|
|
|
|
|
return m_ctx.iterStack[idx].itext;
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (toupper(static_cast<unsigned char>(depthCh)) == 'L'
|
|
|
|
|
|
&& m_ctx.profile == PROFILE_PENN) {
|
2026-03-21 05:23:57 -06:00
|
|
|
|
if (m_ctx.iterStack.empty()) {
|
|
|
|
|
|
return "#-1 ARGUMENT OUT OF RANGE";
|
|
|
|
|
|
}
|
|
|
|
|
|
return m_ctx.iterStack.back().itext;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
|
|
|
|
|
|
// %=: attribute name (Penn) or extended arg/attr lookup (MUX)
|
|
|
|
|
|
if (ch == '=') {
|
|
|
|
|
|
if (sub.size() == 2) {
|
|
|
|
|
|
// Bare %=
|
|
|
|
|
|
if (m_ctx.profile == PROFILE_PENN) {
|
|
|
|
|
|
return m_ctx.attrName;
|
|
|
|
|
|
}
|
|
|
|
|
|
return "=";
|
|
|
|
|
|
}
|
|
|
|
|
|
// %=<...> — extended form (MUX only)
|
|
|
|
|
|
if (sub.size() >= 4 && sub[2] == '<') {
|
|
|
|
|
|
std::string inner = sub.substr(3, sub.size() - 4);
|
|
|
|
|
|
// Check if numeric → extended arg
|
|
|
|
|
|
bool isNum = !inner.empty();
|
|
|
|
|
|
for (char c : inner) {
|
|
|
|
|
|
if (!isdigit(static_cast<unsigned char>(c))) {
|
|
|
|
|
|
isNum = false;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isNum) {
|
|
|
|
|
|
int idx = atoi(inner.c_str());
|
|
|
|
|
|
return (idx < m_ctx.nargs) ? m_ctx.args[idx] : "";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "[attr:" + inner + "]";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "=";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// %$: Penn stack variables
|
|
|
|
|
|
if (ch == '$' && m_ctx.profile == PROFILE_PENN) {
|
|
|
|
|
|
return "[stack]";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Simple single-character forms
|
|
|
|
|
|
switch (upper) {
|
|
|
|
|
|
case 'R':
|
|
|
|
|
|
// MUX: \r\n. Penn: \n only.
|
|
|
|
|
|
return (m_ctx.profile == PROFILE_PENN) ? "\n" : "\r\n";
|
|
|
|
|
|
case 'B': return " ";
|
|
|
|
|
|
case 'T': return "\t";
|
|
|
|
|
|
case 'N': return maybeCapitalize(m_ctx.enactorName, ch);
|
|
|
|
|
|
case 'S': return maybeCapitalize(m_ctx.pronSub, ch);
|
|
|
|
|
|
case 'P': return maybeCapitalize(m_ctx.pronPos, ch);
|
|
|
|
|
|
case 'O': return maybeCapitalize(m_ctx.pronObj, ch);
|
|
|
|
|
|
case 'A': return maybeCapitalize(m_ctx.pronAbs, ch);
|
|
|
|
|
|
case 'K': return maybeCapitalize(m_ctx.moniker, ch);
|
|
|
|
|
|
case 'M': return maybeCapitalize(m_ctx.lastCommand, ch);
|
|
|
|
|
|
case 'L': return "[location]";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (ch) {
|
|
|
|
|
|
case '%': return "%";
|
|
|
|
|
|
case '#': return m_ctx.enactorDbref;
|
|
|
|
|
|
case '!': return m_ctx.executorDbref;
|
|
|
|
|
|
case '@': return m_ctx.callerDbref;
|
|
|
|
|
|
case ':': return m_ctx.enactorObjid;
|
|
|
|
|
|
case '+': return std::to_string(m_ctx.nargs);
|
|
|
|
|
|
case '|': return m_ctx.pipedOutput;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Penn-specific single-char forms
|
|
|
|
|
|
if (m_ctx.profile == PROFILE_PENN) {
|
|
|
|
|
|
if (ch == ' ') return "% ";
|
|
|
|
|
|
if (ch == '~') return m_ctx.accentedName;
|
|
|
|
|
|
if (ch == '?') return "[limits]";
|
|
|
|
|
|
if (upper == 'U') return m_ctx.evaledCommand;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Unknown substitution — output the character literally
|
|
|
|
|
|
// (matches iCode == 0 fallback in all engines).
|
2026-03-21 03:08:02 -06:00
|
|
|
|
return std::string(1, ch);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string evalEscape(const ASTNode *node) {
|
|
|
|
|
|
if (node->text.size() >= 2) {
|
2026-03-21 05:23:57 -06:00
|
|
|
|
if (node->text == "\\%" && m_ctx.profile != PROFILE_PENN) {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return node->text.substr(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return "\\";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
// Replicate 2.13's noeval pass on an AST subtree.
|
|
|
|
|
|
//
|
|
|
|
|
|
// In 2.13, mux_exec with EV_EVAL off still processes backslash
|
|
|
|
|
|
// escapes (the handler at line 2439 has no EV_EVAL guard).
|
|
|
|
|
|
// Percent substitutions are copied literally (guarded by EV_EVAL).
|
|
|
|
|
|
//
|
|
|
|
|
|
// This produces a string with one layer of backslash stripping,
|
|
|
|
|
|
// which can then be re-tokenized and evaluated.
|
|
|
|
|
|
//
|
|
|
|
|
|
std::string noevalPass(const ASTNode *node) {
|
|
|
|
|
|
if (!node) return "";
|
|
|
|
|
|
|
|
|
|
|
|
switch (node->type) {
|
|
|
|
|
|
case NODE_LITERAL:
|
|
|
|
|
|
case NODE_SPACE:
|
|
|
|
|
|
case NODE_SEMICOLON:
|
|
|
|
|
|
return node->text;
|
|
|
|
|
|
|
|
|
|
|
|
case NODE_SUBST:
|
|
|
|
|
|
// Percent handler IS guarded by EV_EVAL in 2.13.
|
|
|
|
|
|
// With EV_EVAL off, it copies % and following char literally.
|
|
|
|
|
|
return node->text;
|
|
|
|
|
|
|
|
|
|
|
|
case NODE_ESCAPE:
|
|
|
|
|
|
// Backslash handler is NOT guarded by EV_EVAL in 2.13.
|
|
|
|
|
|
// It unconditionally consumes the \ and emits the next char.
|
|
|
|
|
|
if (node->text.size() >= 2) {
|
|
|
|
|
|
return node->text.substr(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
return "\\";
|
|
|
|
|
|
|
|
|
|
|
|
case NODE_FUNCCALL:
|
|
|
|
|
|
// In noeval, function calls aren't dispatched.
|
|
|
|
|
|
// Copy the name and parens literally.
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string r = node->text + "(";
|
|
|
|
|
|
for (size_t i = 0; i < node->children.size(); i++) {
|
|
|
|
|
|
if (i > 0) r += ",";
|
|
|
|
|
|
r += noevalPass(node->children[i].get());
|
|
|
|
|
|
}
|
|
|
|
|
|
return r + ")";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case NODE_EVALBRACKET:
|
|
|
|
|
|
// In noeval with EV_NOFCHECK, [...] is not evaluated.
|
|
|
|
|
|
// Copy brackets and contents literally.
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string r = "[";
|
|
|
|
|
|
for (const auto &c : node->children)
|
|
|
|
|
|
r += noevalPass(c.get());
|
|
|
|
|
|
return r + "]";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case NODE_BRACEGROUP:
|
|
|
|
|
|
// Nested brace groups: copy braces and recurse.
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string r = "{";
|
|
|
|
|
|
for (const auto &c : node->children)
|
|
|
|
|
|
r += noevalPass(c.get());
|
|
|
|
|
|
return r + "}";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case NODE_SEQUENCE:
|
|
|
|
|
|
{
|
|
|
|
|
|
std::string r;
|
2026-03-21 12:01:17 -06:00
|
|
|
|
for (size_t i = 0; i < node->children.size(); i++) {
|
|
|
|
|
|
const auto &c = node->children[i];
|
2026-04-04 17:48:22 -06:00
|
|
|
|
if (c->type == NODE_ESCAPE
|
|
|
|
|
|
&& c->text == "\\\\"
|
|
|
|
|
|
&& i + 1 < node->children.size()
|
|
|
|
|
|
&& node->children[i + 1]->type == NODE_SUBST) {
|
|
|
|
|
|
r += node->children[i + 1]->text;
|
|
|
|
|
|
i++;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
r += noevalPass(c.get());
|
2026-03-21 12:01:17 -06:00
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
return r;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
case NODE_DYNCALL:
|
|
|
|
|
|
return "#-1 DYNAMIC";
|
|
|
|
|
|
}
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 05:23:57 -06:00
|
|
|
|
// Evaluate a selected argument from a FN_NOEVAL function using the
|
|
|
|
|
|
// 2.13-style noeval pass followed by reparse/re-eval.
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
//
|
2026-03-21 05:23:57 -06:00
|
|
|
|
// This replicates the two-pass behavior observed in 2.13:
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
// Pass 1: noeval — strip one layer of backslash (noevalPass)
|
|
|
|
|
|
// Pass 2: eval — re-tokenize the result and evaluate it
|
|
|
|
|
|
//
|
2026-03-21 05:23:57 -06:00
|
|
|
|
std::string evalNoevalLegacyArg(const ASTNode *node) {
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
if (!node) return "";
|
|
|
|
|
|
|
|
|
|
|
|
const ASTNode *inner = node;
|
|
|
|
|
|
if (node->type == NODE_BRACEGROUP && !node->children.empty()) {
|
|
|
|
|
|
inner = node->children[0].get();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Pass 1: produce text with one backslash layer stripped.
|
|
|
|
|
|
std::string text = noevalPass(inner);
|
|
|
|
|
|
|
|
|
|
|
|
// Pass 2: re-tokenize and evaluate.
|
|
|
|
|
|
auto tokens = tokenize(text.c_str(), m_ctx.profile);
|
|
|
|
|
|
Parser parser(tokens);
|
|
|
|
|
|
auto ast = parser.parse();
|
|
|
|
|
|
int flags = (m_ctx.evalFlags & ~(EV_TOP | EV_FMAND))
|
|
|
|
|
|
| EV_EVAL | EV_FCHECK;
|
|
|
|
|
|
return evalWithFlags(ast.get(), flags);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Evaluate an argument to a FN_NOEVAL function.
|
|
|
|
|
|
//
|
|
|
|
|
|
std::string evalNoevalArg(const ASTNode *node) {
|
|
|
|
|
|
if (!node) return "";
|
2026-03-21 05:23:57 -06:00
|
|
|
|
if (m_ctx.profile == PROFILE_MUX213_COMPAT) {
|
|
|
|
|
|
return evalNoevalLegacyArg(node);
|
|
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
if (node->type == NODE_BRACEGROUP) {
|
2026-03-21 04:28:44 -06:00
|
|
|
|
// Baseline AST/Penn study behavior: brace groups selected by
|
2026-03-21 05:23:57 -06:00
|
|
|
|
// FN_NOEVAL functions are evaluated with brace stripping.
|
2026-03-21 04:28:44 -06:00
|
|
|
|
int flags = (m_ctx.evalFlags & ~(EV_TOP | EV_FMAND))
|
|
|
|
|
|
| EV_EVAL | EV_FCHECK | EV_STRIP_CURLY;
|
|
|
|
|
|
return evalWithFlags(node, flags);
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
}
|
|
|
|
|
|
return eval(node);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 08:03:26 -07:00
|
|
|
|
std::string evalFuncCall(const ASTNode *node) {
|
|
|
|
|
|
std::string fname = toUpper(node->text);
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// Check FN_NOEVAL first — deferred evaluation.
|
2026-03-07 08:39:34 -07:00
|
|
|
|
auto nit = m_noeval_funcs.find(fname);
|
|
|
|
|
|
if (nit != m_noeval_funcs.end()) {
|
|
|
|
|
|
return nit->second(node->children);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// Normal — evaluate all arguments first.
|
2026-03-07 08:03:26 -07:00
|
|
|
|
auto it = m_funcs.find(fname);
|
|
|
|
|
|
if (it == m_funcs.end()) {
|
|
|
|
|
|
return "#-1 FUNCTION (" + fname + ") NOT FOUND";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::vector<std::string> args;
|
2026-03-15 22:33:58 -06:00
|
|
|
|
int flags = (m_ctx.evalFlags & ~(EV_TOP | EV_FMAND)) | EV_EVAL | EV_FCHECK;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
for (const auto &child : node->children) {
|
2026-03-15 22:33:58 -06:00
|
|
|
|
args.push_back(evalWithFlags(child.get(), flags));
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
return it->second(args);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string evalEvalBracket(const ASTNode *node) {
|
|
|
|
|
|
if (node->children.empty()) return "";
|
2026-03-07 08:49:18 -07:00
|
|
|
|
// Inside [...], functions are checked and mandatory.
|
2026-03-15 22:33:58 -06:00
|
|
|
|
// This matches mux_exec: eval | EV_FCHECK | EV_FMAND | EV_EVAL
|
|
|
|
|
|
int flags = (m_ctx.evalFlags & ~EV_TOP) | EV_EVAL | EV_FCHECK | EV_FMAND;
|
2026-03-07 08:49:18 -07:00
|
|
|
|
return evalWithFlags(node->children[0].get(), flags);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string evalBraceGroup(const ASTNode *node) {
|
|
|
|
|
|
if (node->children.empty()) return "";
|
2026-03-07 08:49:18 -07:00
|
|
|
|
int flags = m_ctx.evalFlags;
|
|
|
|
|
|
|
|
|
|
|
|
if (flags & EV_STRIP_CURLY) {
|
|
|
|
|
|
// Strip braces, evaluate contents without function checking.
|
|
|
|
|
|
// This matches mux_exec behavior: inside {} with EV_EVAL,
|
|
|
|
|
|
// the flags become eval & ~(EV_STRIP_CURLY|EV_FCHECK|EV_FMAND).
|
|
|
|
|
|
int innerFlags = (flags & ~(EV_STRIP_CURLY | EV_FCHECK | EV_FMAND));
|
|
|
|
|
|
return evalWithFlags(node->children[0].get(), innerFlags);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// No strip — return the raw text including braces.
|
|
|
|
|
|
return "{" + ast_raw_text(node->children[0].get()) + "}";
|
|
|
|
|
|
}
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
// Builtin function registration
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
void registerBuiltins() {
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// -- Arithmetic --
|
2026-03-07 08:03:26 -07:00
|
|
|
|
m_funcs["ADD"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
double sum = 0;
|
|
|
|
|
|
for (const auto &a : args) sum += toDouble(a);
|
|
|
|
|
|
return fmtNum(sum);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["SUB"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.size() < 2) return "0";
|
|
|
|
|
|
return fmtNum(toDouble(args[0]) - toDouble(args[1]));
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["MUL"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
double prod = 1;
|
|
|
|
|
|
for (const auto &a : args) prod *= toDouble(a);
|
|
|
|
|
|
return fmtNum(prod);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["DIV"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.size() < 2) return "0";
|
|
|
|
|
|
long b = toLong(args[1]);
|
|
|
|
|
|
if (b == 0) return "#-1 DIVIDE BY ZERO";
|
|
|
|
|
|
return std::to_string(toLong(args[0]) / b);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["MOD"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.size() < 2) return "0";
|
|
|
|
|
|
long b = toLong(args[1]);
|
|
|
|
|
|
if (b == 0) return "#-1 DIVIDE BY ZERO";
|
|
|
|
|
|
return std::to_string(toLong(args[0]) % b);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["ABS"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.empty()) return "0";
|
|
|
|
|
|
return fmtNum(fabs(toDouble(args[0])));
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["INC"] = [](const std::vector<std::string> &args) -> std::string {
|
2026-03-07 08:45:01 -07:00
|
|
|
|
return std::to_string((args.empty() ? 0 : toLong(args[0])) + 1);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
m_funcs["DEC"] = [](const std::vector<std::string> &args) -> std::string {
|
2026-03-07 08:45:01 -07:00
|
|
|
|
return std::to_string((args.empty() ? 0 : toLong(args[0])) - 1);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
m_funcs["FLOOR"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.empty()) return "0";
|
|
|
|
|
|
return std::to_string(static_cast<long long>(floor(toDouble(args[0]))));
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["CEIL"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.empty()) return "0";
|
|
|
|
|
|
return std::to_string(static_cast<long long>(ceil(toDouble(args[0]))));
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["ROUND"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.empty()) return "0";
|
|
|
|
|
|
double v = toDouble(args[0]);
|
|
|
|
|
|
int places = args.size() > 1 ? static_cast<int>(toLong(args[1])) : 0;
|
|
|
|
|
|
double factor = pow(10.0, places);
|
|
|
|
|
|
v = round(v * factor) / factor;
|
|
|
|
|
|
if (places <= 0) return std::to_string(static_cast<long long>(v));
|
|
|
|
|
|
char buf[64];
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "%.*f", places, v);
|
|
|
|
|
|
return buf;
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["MAX"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.empty()) return "0";
|
|
|
|
|
|
double m = toDouble(args[0]);
|
|
|
|
|
|
for (size_t i = 1; i < args.size(); i++) {
|
|
|
|
|
|
double v = toDouble(args[i]);
|
|
|
|
|
|
if (v > m) m = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmtNum(m);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["MIN"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.empty()) return "0";
|
|
|
|
|
|
double m = toDouble(args[0]);
|
|
|
|
|
|
for (size_t i = 1; i < args.size(); i++) {
|
|
|
|
|
|
double v = toDouble(args[i]);
|
|
|
|
|
|
if (v < m) m = v;
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmtNum(m);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["POWER"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.size() < 2) return "0";
|
|
|
|
|
|
return fmtNum(pow(toDouble(args[0]), toDouble(args[1])));
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["SQRT"] = [](const std::vector<std::string> &args) -> std::string {
|
|
|
|
|
|
if (args.empty()) return "0";
|
|
|
|
|
|
double v = toDouble(args[0]);
|
|
|
|
|
|
if (v < 0) return "#-1 SQUARE ROOT OF NEGATIVE";
|
|
|
|
|
|
return fmtNum(sqrt(v));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// -- Comparison --
|
|
|
|
|
|
m_funcs["EQ"] = [](const std::vector<std::string> &a) { return a.size() < 2 ? "0" : (toLong(a[0]) == toLong(a[1]) ? "1" : "0"); };
|
|
|
|
|
|
m_funcs["NEQ"] = [](const std::vector<std::string> &a) { return a.size() < 2 ? "0" : (toLong(a[0]) != toLong(a[1]) ? "1" : "0"); };
|
|
|
|
|
|
m_funcs["GT"] = [](const std::vector<std::string> &a) { return a.size() < 2 ? "0" : (toLong(a[0]) > toLong(a[1]) ? "1" : "0"); };
|
|
|
|
|
|
m_funcs["GTE"] = [](const std::vector<std::string> &a) { return a.size() < 2 ? "0" : (toLong(a[0]) >= toLong(a[1]) ? "1" : "0"); };
|
|
|
|
|
|
m_funcs["LT"] = [](const std::vector<std::string> &a) { return a.size() < 2 ? "0" : (toLong(a[0]) < toLong(a[1]) ? "1" : "0"); };
|
|
|
|
|
|
m_funcs["LTE"] = [](const std::vector<std::string> &a) { return a.size() < 2 ? "0" : (toLong(a[0]) <= toLong(a[1]) ? "1" : "0"); };
|
|
|
|
|
|
m_funcs["COMP"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "0";
|
|
|
|
|
|
int r = a[0].compare(a[1]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return std::to_string(r < 0 ? -1 : (r > 0 ? 1 : 0));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// -- Boolean --
|
|
|
|
|
|
m_funcs["AND"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
for (const auto &x : a) if (!toBool(x)) return "0";
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return "1";
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["OR"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
for (const auto &x : a) if (toBool(x)) return "1";
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return "0";
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["NOT"] = [](const std::vector<std::string> &a) { return (a.empty() || !toBool(a[0])) ? "1" : "0"; };
|
|
|
|
|
|
m_funcs["XOR"] = [](const std::vector<std::string> &a) { return a.size() < 2 ? "0" : ((toBool(a[0]) != toBool(a[1])) ? "1" : "0"); };
|
|
|
|
|
|
m_funcs["T"] = [](const std::vector<std::string> &a) { return (a.empty() || !toBool(a[0])) ? "0" : "1"; };
|
|
|
|
|
|
|
|
|
|
|
|
// -- String --
|
|
|
|
|
|
m_funcs["STRLEN"] = [](const std::vector<std::string> &a) { return a.empty() ? "0" : std::to_string(a[0].size()); };
|
|
|
|
|
|
m_funcs["MID"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 3) return "";
|
|
|
|
|
|
long pos = toLong(a[1]), len = toLong(a[2]);
|
|
|
|
|
|
if (pos < 0 || len < 0 || pos >= static_cast<long>(a[0].size())) return "";
|
|
|
|
|
|
return a[0].substr(pos, len);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["LEFT"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
long len = toLong(a[1]);
|
|
|
|
|
|
return len <= 0 ? "" : a[0].substr(0, len);
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["RIGHT"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
long len = toLong(a[1]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (len <= 0) return "";
|
2026-03-07 08:45:01 -07:00
|
|
|
|
if (len >= static_cast<long>(a[0].size())) return a[0];
|
|
|
|
|
|
return a[0].substr(a[0].size() - len);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["CAPSTR"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty() || a[0].empty()) return "";
|
|
|
|
|
|
std::string s = a[0];
|
2026-03-07 08:03:26 -07:00
|
|
|
|
s[0] = static_cast<char>(toupper(static_cast<unsigned char>(s[0])));
|
|
|
|
|
|
return s;
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["LCSTR"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
std::string s = a[0];
|
2026-03-07 08:03:26 -07:00
|
|
|
|
for (auto &c : s) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
return s;
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["UCSTR"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
std::string s = a[0];
|
2026-03-07 08:03:26 -07:00
|
|
|
|
for (auto &c : s) c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
|
|
|
|
|
|
return s;
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["CAT"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
std::string r;
|
|
|
|
|
|
for (size_t i = 0; i < a.size(); i++) { if (i > 0) r += " "; r += a[i]; }
|
|
|
|
|
|
return r;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["STRCAT"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
std::string r;
|
|
|
|
|
|
for (const auto &x : a) r += x;
|
|
|
|
|
|
return r;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["REPEAT"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
long n = toLong(a[1]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (n <= 0) return "";
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::string r;
|
|
|
|
|
|
for (long i = 0; i < n; i++) r += a[0];
|
|
|
|
|
|
return r;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["SPACE"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
long n = a.empty() ? 1 : toLong(a[0]);
|
|
|
|
|
|
return n <= 0 ? "" : std::string(n, ' ');
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["TRIM"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
size_t start = a[0].find_first_not_of(' ');
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (start == std::string::npos) return "";
|
2026-03-07 08:45:01 -07:00
|
|
|
|
size_t end = a[0].find_last_not_of(' ');
|
|
|
|
|
|
return a[0].substr(start, end - start + 1);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// -- List --
|
|
|
|
|
|
m_funcs["WORDS"] = [](const std::vector<std::string> &a) {
|
|
|
|
|
|
return a.empty() ? "0" : std::to_string(splitList(a[0]).size());
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["FIRST"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
auto w = splitList(a[0]);
|
|
|
|
|
|
return w.empty() ? "" : w[0];
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["REST"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
auto w = splitList(a[0]);
|
|
|
|
|
|
if (w.size() <= 1) return "";
|
|
|
|
|
|
std::string r;
|
|
|
|
|
|
for (size_t i = 1; i < w.size(); i++) { if (i > 1) r += " "; r += w[i]; }
|
|
|
|
|
|
return r;
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["LAST"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
auto w = splitList(a[0]);
|
|
|
|
|
|
return w.empty() ? "" : w.back();
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["EXTRACT"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 3) return "";
|
|
|
|
|
|
auto w = splitList(a[0]);
|
|
|
|
|
|
long first = toLong(a[1]) - 1, count = toLong(a[2]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (first < 0) first = 0;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::string r;
|
|
|
|
|
|
for (long i = first; i < first + count && i < static_cast<long>(w.size()); i++) {
|
|
|
|
|
|
if (i > first) r += " ";
|
|
|
|
|
|
r += w[i];
|
|
|
|
|
|
}
|
|
|
|
|
|
return r;
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["LNUM"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
long n = toLong(a[0]);
|
|
|
|
|
|
std::string sep = a.size() > 1 ? a[1] : " ";
|
|
|
|
|
|
std::string r;
|
|
|
|
|
|
for (long i = 0; i < n; i++) { if (i > 0) r += sep; r += std::to_string(i); }
|
|
|
|
|
|
return r;
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["SORT"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
auto w = splitList(a[0]);
|
|
|
|
|
|
bool allNum = true;
|
|
|
|
|
|
for (const auto &x : w) {
|
2026-03-07 08:03:26 -07:00
|
|
|
|
char *end;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
strtod(x.c_str(), &end);
|
|
|
|
|
|
if (end == x.c_str() || *end != '\0') { allNum = false; break; }
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
2026-03-07 08:45:01 -07:00
|
|
|
|
if (allNum) {
|
|
|
|
|
|
std::sort(w.begin(), w.end(), [](const std::string &x, const std::string &y) {
|
|
|
|
|
|
return strtod(x.c_str(), nullptr) < strtod(y.c_str(), nullptr);
|
|
|
|
|
|
});
|
2026-03-07 08:03:26 -07:00
|
|
|
|
} else {
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::sort(w.begin(), w.end());
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::string r;
|
|
|
|
|
|
for (size_t i = 0; i < w.size(); i++) { if (i > 0) r += " "; r += w[i]; }
|
|
|
|
|
|
return r;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["MEMBER"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "0";
|
|
|
|
|
|
auto w = splitList(a[0]);
|
|
|
|
|
|
for (size_t i = 0; i < w.size(); i++) if (w[i] == a[1]) return std::to_string(i + 1);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return "0";
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["INDEX"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 4) return "";
|
|
|
|
|
|
auto items = splitList(a[0], a[1]);
|
|
|
|
|
|
long first = toLong(a[2]) - 1, count = toLong(a[3]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
if (first < 0) first = 0;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::string r;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
for (long i = first; i < first + count && i < static_cast<long>(items.size()); i++) {
|
2026-03-07 08:45:01 -07:00
|
|
|
|
if (i > first) r += a[1];
|
|
|
|
|
|
r += items[i];
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
2026-03-07 08:45:01 -07:00
|
|
|
|
return r;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// -- Set operations --
|
|
|
|
|
|
m_funcs["SETUNION"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
auto x = splitList(a[0]), y = splitList(a[1]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
std::vector<std::string> result;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::sort(x.begin(), x.end()); std::sort(y.begin(), y.end());
|
|
|
|
|
|
std::set_union(x.begin(), x.end(), y.begin(), y.end(), std::back_inserter(result));
|
|
|
|
|
|
std::string r;
|
|
|
|
|
|
for (size_t i = 0; i < result.size(); i++) { if (i > 0) r += " "; r += result[i]; }
|
|
|
|
|
|
return r;
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["SETDIFF"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
auto x = splitList(a[0]), y = splitList(a[1]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
std::vector<std::string> result;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::sort(x.begin(), x.end()); std::sort(y.begin(), y.end());
|
|
|
|
|
|
std::set_difference(x.begin(), x.end(), y.begin(), y.end(), std::back_inserter(result));
|
|
|
|
|
|
std::string r;
|
|
|
|
|
|
for (size_t i = 0; i < result.size(); i++) { if (i > 0) r += " "; r += result[i]; }
|
|
|
|
|
|
return r;
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["SETINTER"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
auto x = splitList(a[0]), y = splitList(a[1]);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
std::vector<std::string> result;
|
2026-03-07 08:45:01 -07:00
|
|
|
|
std::sort(x.begin(), x.end()); std::sort(y.begin(), y.end());
|
|
|
|
|
|
std::set_intersection(x.begin(), x.end(), y.begin(), y.end(), std::back_inserter(result));
|
|
|
|
|
|
std::string r;
|
|
|
|
|
|
for (size_t i = 0; i < result.size(); i++) { if (i > 0) r += " "; r += result[i]; }
|
|
|
|
|
|
return r;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// -- Registers --
|
|
|
|
|
|
m_funcs["SETQ"] = [this](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
m_ctx.registers[a[0]] = a[1];
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return "";
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["SETR"] = [this](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "";
|
|
|
|
|
|
m_ctx.registers[a[0]] = a[1];
|
|
|
|
|
|
return a[1];
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["R"] = [this](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.empty()) return "";
|
|
|
|
|
|
auto it = m_ctx.registers.find(a[0]);
|
|
|
|
|
|
return (it != m_ctx.registers.end()) ? it->second : "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// -- Misc (normal eval) --
|
|
|
|
|
|
m_funcs["NULL"] = [](const std::vector<std::string> &) -> std::string { return ""; };
|
|
|
|
|
|
m_funcs["MATCH"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "0";
|
|
|
|
|
|
auto w = splitList(a[0]);
|
|
|
|
|
|
for (size_t i = 0; i < w.size(); i++) if (w[i] == a[1]) return std::to_string(i + 1);
|
|
|
|
|
|
return "0";
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_funcs["STRMATCH"] = [](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
if (a.size() < 2) return "0";
|
|
|
|
|
|
if (a[1] == "*") return "1";
|
|
|
|
|
|
return (a[0] == a[1]) ? "1" : "0";
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["ITEXT"] = [this](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
int depth = a.empty() ? 0 : static_cast<int>(toLong(a[0]));
|
|
|
|
|
|
int idx = static_cast<int>(m_ctx.iterStack.size()) - 1 - depth;
|
|
|
|
|
|
if (idx >= 0 && idx < static_cast<int>(m_ctx.iterStack.size()))
|
|
|
|
|
|
return m_ctx.iterStack[idx].itext;
|
|
|
|
|
|
return "";
|
|
|
|
|
|
};
|
|
|
|
|
|
m_funcs["INUM"] = [this](const std::vector<std::string> &a) -> std::string {
|
|
|
|
|
|
int depth = a.empty() ? 0 : static_cast<int>(toLong(a[0]));
|
|
|
|
|
|
int idx = static_cast<int>(m_ctx.iterStack.size()) - 1 - depth;
|
|
|
|
|
|
if (idx >= 0 && idx < static_cast<int>(m_ctx.iterStack.size()))
|
|
|
|
|
|
return std::to_string(m_ctx.iterStack[idx].inum);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
// FN_NOEVAL functions — deferred evaluation
|
|
|
|
|
|
// ---------------------------------------------------------------
|
2026-03-07 08:39:34 -07:00
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_noeval_funcs["IFELSE"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
if (c.size() < 3) return "";
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
return toBool(eval(c[0].get())) ? evalNoevalArg(c[1].get()) : evalNoevalArg(c[2].get());
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// switch(val, pat1, result1, ..., default)
|
|
|
|
|
|
m_noeval_funcs["SWITCH"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
if (c.size() < 2) return "";
|
|
|
|
|
|
std::string val = eval(c[0].get());
|
|
|
|
|
|
for (size_t i = 1; i + 1 < c.size(); i += 2) {
|
|
|
|
|
|
std::string pat = eval(c[i].get());
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
if (pat == val || pat == "*") return evalNoevalArg(c[i + 1].get());
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
if (c.size() % 2 == 0) return evalNoevalArg(c.back().get());
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return "";
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_noeval_funcs["CASE"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
if (c.size() < 2) return "";
|
|
|
|
|
|
std::string val = eval(c[0].get());
|
|
|
|
|
|
for (size_t i = 1; i + 1 < c.size(); i += 2) {
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
if (eval(c[i].get()) == val) return evalNoevalArg(c[i + 1].get());
|
2026-03-07 08:39:34 -07:00
|
|
|
|
}
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
if (c.size() % 2 == 0) return evalNoevalArg(c.back().get());
|
2026-03-07 08:03:26 -07:00
|
|
|
|
return "";
|
|
|
|
|
|
};
|
2026-03-07 08:39:34 -07:00
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// Short-circuit boolean
|
|
|
|
|
|
m_noeval_funcs["CAND"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
for (const auto &x : c) if (!toBool(eval(x.get()))) return "0";
|
2026-03-07 08:39:34 -07:00
|
|
|
|
return "1";
|
|
|
|
|
|
};
|
2026-03-07 08:45:01 -07:00
|
|
|
|
m_noeval_funcs["COR"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
for (const auto &x : c) if (toBool(eval(x.get()))) return "1";
|
2026-03-07 08:39:34 -07:00
|
|
|
|
return "0";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// @@(comment) — discard without evaluating
|
|
|
|
|
|
m_noeval_funcs["@@"] = [](const std::vector<std::unique_ptr<ASTNode>> &) -> std::string {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// lit(text) — return unevaluated source text
|
|
|
|
|
|
m_noeval_funcs["LIT"] = [](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
return c.empty() ? "" : ast_raw_text(c[0].get());
|
2026-03-07 08:03:26 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-07 08:45:01 -07:00
|
|
|
|
// iter(list, body, osep, isep) — evaluate body per item
|
|
|
|
|
|
m_noeval_funcs["ITER"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
if (c.size() < 2) return "";
|
|
|
|
|
|
std::string listVal = eval(c[0].get());
|
|
|
|
|
|
std::string sep = c.size() > 3 ? eval(c[3].get()) : " ";
|
|
|
|
|
|
std::string osep = c.size() > 2 ? eval(c[2].get()) : " ";
|
2026-03-07 08:39:34 -07:00
|
|
|
|
auto items = splitList(listVal, sep);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
std::string result;
|
|
|
|
|
|
for (size_t i = 0; i < items.size(); i++) {
|
|
|
|
|
|
if (i > 0) result += osep;
|
2026-03-07 08:39:34 -07:00
|
|
|
|
m_ctx.iterStack.push_back({items[i], static_cast<int>(i + 1)});
|
2026-03-21 05:23:57 -06:00
|
|
|
|
result += evalNoevalArg(c[1].get());
|
2026-03-07 08:39:34 -07:00
|
|
|
|
m_ctx.iterStack.pop_back();
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
};
|
2026-03-15 23:13:25 -06:00
|
|
|
|
|
|
|
|
|
|
m_noeval_funcs["IF"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
if (c.empty()) return "";
|
|
|
|
|
|
std::string cond_str = evalWithFlags(c[0].get(), (m_ctx.evalFlags & ~EV_TOP) | EV_EVAL | EV_FCHECK);
|
|
|
|
|
|
bool cond = !cond_str.empty() && cond_str != "0";
|
|
|
|
|
|
if (cond) {
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
return (c.size() > 1) ? evalNoevalArg(c[1].get()) : "";
|
2026-03-15 23:13:25 -06:00
|
|
|
|
} else {
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
return (c.size() > 2) ? evalNoevalArg(c[2].get()) : "";
|
2026-03-15 23:13:25 -06:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
m_noeval_funcs["EVAL"] = [this](const std::vector<std::unique_ptr<ASTNode>> &c) -> std::string {
|
|
|
|
|
|
if (c.empty()) return "";
|
|
|
|
|
|
std::string text = evalWithFlags(c[0].get(), (m_ctx.evalFlags & ~EV_TOP) | EV_EVAL | EV_FCHECK);
|
2026-03-21 04:28:44 -06:00
|
|
|
|
auto tokens = tokenize(text.c_str(), m_ctx.profile);
|
2026-03-15 23:13:25 -06:00
|
|
|
|
Parser parser(tokens);
|
|
|
|
|
|
auto ast = parser.parse();
|
|
|
|
|
|
return evalWithFlags(ast.get(), (m_ctx.evalFlags & ~EV_TOP) | EV_EVAL | EV_FCHECK);
|
|
|
|
|
|
};
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
// Main
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
int main(int argc, char *argv[])
|
|
|
|
|
|
{
|
|
|
|
|
|
bool showAST = false;
|
2026-03-21 03:08:02 -06:00
|
|
|
|
ParserProfile profile = PROFILE_MUX214_AST;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
for (int i = 1; i < argc; i++) {
|
|
|
|
|
|
if (strcmp(argv[i], "-a") == 0 || strcmp(argv[i], "--ast") == 0) {
|
|
|
|
|
|
showAST = true;
|
2026-03-21 03:08:02 -06:00
|
|
|
|
} else if (0 == strcmp(argv[i], "--profile")) {
|
|
|
|
|
|
if (i + 1 >= argc || !parse_profile_string(argv[i + 1], profile)) {
|
|
|
|
|
|
fprintf(stderr, "usage: %s [--ast] [--profile mux214|mux213|penn]\n", argv[0]);
|
|
|
|
|
|
return 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
i++;
|
|
|
|
|
|
} else if (0 == strncmp(argv[i], "--profile=", 10)) {
|
|
|
|
|
|
if (!parse_profile_string(argv[i] + 10, profile)) {
|
|
|
|
|
|
fprintf(stderr, "usage: %s [--ast] [--profile mux214|mux213|penn]\n", argv[0]);
|
|
|
|
|
|
return 2;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
fprintf(stderr, "usage: %s [--ast] [--profile mux214|mux213|penn]\n", argv[0]);
|
|
|
|
|
|
return 2;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
EvalContext ctx;
|
2026-03-21 03:08:02 -06:00
|
|
|
|
ctx.profile = profile;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
ctx.enactorName = "testplayer";
|
|
|
|
|
|
ctx.enactorDbref = "#1234";
|
|
|
|
|
|
ctx.executorDbref = "#1234";
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
ctx.callerDbref = "#1234";
|
|
|
|
|
|
ctx.enactorObjid = "#1234:1700000000";
|
|
|
|
|
|
ctx.lastCommand = "test";
|
|
|
|
|
|
ctx.moniker = "TestPlayer";
|
|
|
|
|
|
ctx.pronSub = "they";
|
|
|
|
|
|
ctx.pronPos = "their";
|
|
|
|
|
|
ctx.pronObj = "them";
|
|
|
|
|
|
ctx.pronAbs = "theirs";
|
2026-03-21 05:23:57 -06:00
|
|
|
|
ctx.rawCommand = "@pemit me=%cg";
|
|
|
|
|
|
ctx.evaledCommand = "@pemit me=%cg";
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
ctx.accentedName = "testplayer";
|
2026-03-21 05:23:57 -06:00
|
|
|
|
ctx.attrName = "";
|
|
|
|
|
|
ctx.varAttrs['G' - 'A'] = "thing";
|
2026-03-07 08:03:26 -07:00
|
|
|
|
ctx.args[0] = "hello";
|
|
|
|
|
|
ctx.args[1] = "world";
|
Add parser percent-substitution matrix and fix noeval brace-group backslash handling
Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 04:17:43 -06:00
|
|
|
|
ctx.nargs = 2;
|
2026-03-07 08:03:26 -07:00
|
|
|
|
|
|
|
|
|
|
Evaluator evaluator(ctx);
|
|
|
|
|
|
|
|
|
|
|
|
char line[8192];
|
|
|
|
|
|
while (fgets(line, sizeof(line), stdin)) {
|
|
|
|
|
|
size_t len = strlen(line);
|
|
|
|
|
|
if (len > 0 && line[len - 1] == '\n') {
|
|
|
|
|
|
line[len - 1] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 03:08:02 -06:00
|
|
|
|
auto tokens = tokenize(line, profile);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
Parser parser(tokens);
|
|
|
|
|
|
auto ast = parser.parse();
|
|
|
|
|
|
|
|
|
|
|
|
if (showAST) {
|
|
|
|
|
|
printf("INPUT: %s\n", line);
|
2026-03-21 03:08:02 -06:00
|
|
|
|
printf("PROFILE: %s\n", profile_name(profile));
|
2026-03-07 08:03:26 -07:00
|
|
|
|
printf("AST:\n");
|
2026-03-07 08:45:01 -07:00
|
|
|
|
ast_print(ast.get(), 2);
|
2026-03-07 08:03:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
std::string result = evaluator.eval(ast.get());
|
|
|
|
|
|
printf("%s\n", result.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|