mirror of
https://github.com/brazilofmux/tinymux
synced 2026-08-13 00:23:11 -04:00
A bail_noop memo that outlives a softcode/builtin registration can refuse forever without re-fetch. Invalidate with the #2068 gate epoch (#2140 review).
3247 lines
102 KiB
C++
3247 lines
102 KiB
C++
/*! \file ast.cpp
|
|
* \brief AST-based expression parser and evaluator.
|
|
*
|
|
* See ast.h for design constraints and public API.
|
|
*/
|
|
|
|
#include "copyright.h"
|
|
#include "autoconf.h"
|
|
#include "config.h"
|
|
#include "externs.h"
|
|
|
|
#include "ast.h"
|
|
#include "functions.h"
|
|
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <algorithm>
|
|
#include <list>
|
|
#include <unordered_map>
|
|
|
|
#include "jit_tier1_stamp.h"
|
|
|
|
// Tier 1 build stamp for this unit (#2061). Folded into the persisted
|
|
// code_cache's staleness key so a codegen change here invalidates entries
|
|
// compiled by the previous build. Updates when THIS unit is recompiled,
|
|
// which is what makes it work under incremental make.
|
|
TIER1_STAMP_DEFINE(TIER1_STAMP_AST);
|
|
|
|
// ---------------------------------------------------------------
|
|
// Tokenizer — implemented in ast_scan.cpp (generated by Ragel
|
|
// from ast_scan.rl).
|
|
// ---------------------------------------------------------------
|
|
|
|
// ---------------------------------------------------------------
|
|
// Parser
|
|
// ---------------------------------------------------------------
|
|
|
|
static std::string ast_upper_builtin_name(std::string_view funcName)
|
|
{
|
|
LBuf TempFun = LBuf_Src("ast_upper_builtin");
|
|
size_t nName = funcName.size();
|
|
if (nName >= LBUF_SIZE)
|
|
{
|
|
nName = LBUF_SIZE - 1;
|
|
}
|
|
memcpy(TempFun, funcName.data(), nName);
|
|
TempFun[nName] = '\0';
|
|
|
|
size_t nUpper;
|
|
UTF8 *pUpper = mux_strupr(TempFun, nUpper);
|
|
if (nUpper >= LBUF_SIZE)
|
|
{
|
|
nUpper = LBUF_SIZE - 1;
|
|
}
|
|
return std::string(reinterpret_cast<const char *>(pUpper), nUpper);
|
|
}
|
|
|
|
static ASTNoevalKind ast_noeval_kind(std::string_view funcName)
|
|
{
|
|
const std::string name = ast_upper_builtin_name(funcName);
|
|
|
|
if (name == "IF" || name == "IFELSE")
|
|
{
|
|
return ASTNOEVAL_IFELSE;
|
|
}
|
|
if (name == "ITER")
|
|
{
|
|
return ASTNOEVAL_ITER;
|
|
}
|
|
if (name == "CAND" || name == "CANDBOOL")
|
|
{
|
|
return (name == "CANDBOOL")
|
|
? ASTNOEVAL_CANDBOOL
|
|
: ASTNOEVAL_CAND;
|
|
}
|
|
if (name == "COR" || name == "CORBOOL")
|
|
{
|
|
return (name == "CORBOOL")
|
|
? ASTNOEVAL_CORBOOL
|
|
: ASTNOEVAL_COR;
|
|
}
|
|
if (name == "SWITCH")
|
|
{
|
|
return ASTNOEVAL_SWITCH;
|
|
}
|
|
if (name == "CASE")
|
|
{
|
|
return ASTNOEVAL_CASE;
|
|
}
|
|
if (name == "SWITCHALL")
|
|
{
|
|
return ASTNOEVAL_SWITCHALL;
|
|
}
|
|
if (name == "CASEALL")
|
|
{
|
|
return ASTNOEVAL_CASEALL;
|
|
}
|
|
if (name == "ULAMBDA")
|
|
{
|
|
return ASTNOEVAL_ULAMBDA;
|
|
}
|
|
return ASTNOEVAL_NONE;
|
|
}
|
|
|
|
static bool ast_noeval_arg_is_deferred(ASTNoevalKind kind, int argIndex, int nfargs)
|
|
{
|
|
switch (kind)
|
|
{
|
|
case ASTNOEVAL_IFELSE:
|
|
return argIndex >= 1;
|
|
|
|
case ASTNOEVAL_ITER:
|
|
return argIndex == 1;
|
|
|
|
case ASTNOEVAL_CAND:
|
|
case ASTNOEVAL_CANDBOOL:
|
|
case ASTNOEVAL_COR:
|
|
case ASTNOEVAL_CORBOOL:
|
|
return true;
|
|
|
|
case ASTNOEVAL_SWITCH:
|
|
case ASTNOEVAL_CASE:
|
|
case ASTNOEVAL_SWITCHALL:
|
|
case ASTNOEVAL_CASEALL:
|
|
if (argIndex == 0)
|
|
{
|
|
return false;
|
|
}
|
|
if ((nfargs % 2) == 0 && argIndex == nfargs - 1)
|
|
{
|
|
return true;
|
|
}
|
|
return (argIndex % 2) == 0;
|
|
|
|
case ASTNOEVAL_ULAMBDA:
|
|
return argIndex == 0; // only the body/reference is deferred
|
|
|
|
case ASTNOEVAL_NONE:
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool ast_call_arg_is_deferred(const ASTNode *call, int argIndex)
|
|
{
|
|
if (!call || argIndex < 0)
|
|
{
|
|
return false;
|
|
}
|
|
if (argIndex < static_cast<int>(call->deferred_args.size()))
|
|
{
|
|
return call->deferred_args[argIndex].is_deferred;
|
|
}
|
|
if (!call->parser_known_noeval)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const int nfargs = static_cast<int>(call->children.size());
|
|
return ast_noeval_arg_is_deferred(call->noeval_kind, argIndex, nfargs);
|
|
}
|
|
|
|
static const std::string *ast_call_raw_arg(const ASTNode *call, int argIndex)
|
|
{
|
|
if (!call || argIndex < 0)
|
|
{
|
|
return nullptr;
|
|
}
|
|
if (argIndex < static_cast<int>(call->deferred_args.size()))
|
|
{
|
|
return &call->deferred_args[argIndex].raw_text;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// Hard cap on AST *parser* recursion depth, mirroring the evaluator's
|
|
// AST_EVAL_MAX_DEPTH (below). parseSequence/parseEvalBracket/parseBraceGroup/
|
|
// parseFunctionCall are mutually recursive on the nesting of [], {}, and () in
|
|
// the (LBUF-bounded) input. On a default 8 MiB stack the LBUF cap already keeps
|
|
// even maximally nested input ("[[[[...]]]]") from overflowing, but — like the
|
|
// evaluator's cap — this bounds adversarial deep nesting independently of stack
|
|
// size (the evaluator's comment notes platforms with smaller stack defaults).
|
|
// The counter is thread_local so it also bounds the nested re-parse of NOEVAL
|
|
// structural arguments (ast_parse_region in parser_apply_structural_arg_policy).
|
|
//
|
|
// Over the cap, parseSequence returns its (empty) node without recursing; the
|
|
// enclosing parseEvalBracket/etc. still consumed their opening token, so m_pos
|
|
// always advances and parsing terminates. Real softcode nests a few levels;
|
|
// 1000 is far beyond legitimate use and well under the evaluator's reach.
|
|
//
|
|
static constexpr int AST_PARSE_MAX_DEPTH = 1000;
|
|
static thread_local int s_ast_parse_depth = 0;
|
|
|
|
namespace
|
|
{
|
|
class AstParseDepthGuard
|
|
{
|
|
public:
|
|
AstParseDepthGuard() { ++s_ast_parse_depth; }
|
|
~AstParseDepthGuard() { --s_ast_parse_depth; }
|
|
bool overflow() const { return s_ast_parse_depth > AST_PARSE_MAX_DEPTH; }
|
|
};
|
|
}
|
|
|
|
class ASTParser {
|
|
public:
|
|
ASTParser(const std::vector<ASTToken> &tokens)
|
|
: m_tokens(tokens), m_pos(0), m_bracketDepth(0), m_braceDepth(0) {}
|
|
|
|
std::unique_ptr<ASTNode> parse()
|
|
{
|
|
return parseSequence(false, false, false, false);
|
|
}
|
|
|
|
private:
|
|
const std::vector<ASTToken> &m_tokens;
|
|
size_t m_pos;
|
|
int m_bracketDepth;
|
|
int m_braceDepth;
|
|
|
|
const ASTToken &peek() const { return m_tokens[m_pos]; }
|
|
ASTToken advance() { return m_tokens[m_pos++]; }
|
|
|
|
bool atEnd() const
|
|
{
|
|
return m_pos >= m_tokens.size()
|
|
|| m_tokens[m_pos].type == ASTTOK_EOF;
|
|
}
|
|
|
|
static bool parser_lookup_builtin_noeval(std::string_view funcName)
|
|
{
|
|
LBuf TempFun = LBuf_Src("lookup_noeval");
|
|
size_t nName = funcName.size();
|
|
if (nName >= LBUF_SIZE)
|
|
{
|
|
nName = LBUF_SIZE - 1;
|
|
}
|
|
memcpy(TempFun, funcName.data(), nName);
|
|
TempFun[nName] = '\0';
|
|
|
|
size_t nUpper;
|
|
UTF8 *pUpper = mux_strupr(TempFun, nUpper);
|
|
if (nUpper >= LBUF_SIZE)
|
|
{
|
|
nUpper = LBUF_SIZE - 1;
|
|
}
|
|
memcpy(TempFun, pUpper, nUpper);
|
|
TempFun[nUpper] = '\0';
|
|
|
|
std::vector<UTF8> name_key(TempFun.get(), TempFun.get() + nUpper);
|
|
const auto it = mudstate.builtin_functions.find(name_key);
|
|
return it != mudstate.builtin_functions.end()
|
|
&& (it->second->flags & FN_NOEVAL) != 0;
|
|
}
|
|
|
|
static bool parser_should_structuralize_arg(const ASTNode *call, int argIndex)
|
|
{
|
|
return ast_call_arg_is_deferred(call, argIndex);
|
|
}
|
|
|
|
void parser_capture_raw_arg(ASTNode *call, size_t start, size_t end)
|
|
{
|
|
if (!call || !call->parser_known_noeval)
|
|
{
|
|
return;
|
|
}
|
|
call->deferred_args.emplace_back(rawTextFromTokens(start, end), false);
|
|
}
|
|
|
|
void parser_apply_structural_arg_policy(ASTNode *call)
|
|
{
|
|
if (!call || !call->parser_known_noeval)
|
|
{
|
|
return;
|
|
}
|
|
|
|
call->deferred_args.resize(call->children.size());
|
|
for (int i = 0; i < static_cast<int>(call->children.size()); i++)
|
|
{
|
|
if (!parser_should_structuralize_arg(call, i))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
call->deferred_args[i].is_deferred = true;
|
|
const std::string *raw = ast_call_raw_arg(call, i);
|
|
if (!raw)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
auto structural = ast_parse_region(
|
|
ASTSourceSpan(reinterpret_cast<const UTF8 *>(raw->c_str()),
|
|
raw->size()),
|
|
ASTLEX_STRUCTURAL);
|
|
if (structural)
|
|
{
|
|
call->children[i] = std::move(structural);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::string rawTextFromTokens(size_t start, size_t end) const
|
|
{
|
|
std::string raw;
|
|
for (size_t i = start; i < end && i < m_tokens.size(); i++)
|
|
{
|
|
raw.append(m_tokens[i].text.data(), m_tokens[i].text.size());
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
std::unique_ptr<ASTNode> parseSequence(
|
|
bool stopRP, bool stopRB, bool stopRC, bool stopCM)
|
|
{
|
|
// Bound parser recursion: every []/{}/() nesting level re-enters here.
|
|
//
|
|
AstParseDepthGuard depth_guard;
|
|
auto seq = std::make_unique<ASTNode>(AST_SEQUENCE);
|
|
if (depth_guard.overflow())
|
|
{
|
|
return seq;
|
|
}
|
|
// Depth of bare parentheses opened inside this sequence (#1219).
|
|
//
|
|
// A '(' that follows a name is consumed by parseFuncCall, which
|
|
// recurses and accounts for its own parens; the only thing counted
|
|
// here is a parenthesis appearing as literal text. While one is
|
|
// open, a ')' or ',' belongs to it rather than to the enclosing
|
|
// call, so neither terminates the sequence.
|
|
//
|
|
// 2.13 did this with a stack of expected closers in parse_to_lite:
|
|
// '(' pushed ')', and a closer found on the stack unwound to it
|
|
// rather than ending the argument. Without the equivalent here,
|
|
// `strcat(Meet me (Tue, 5pm) downtown)` ended its argument at the
|
|
// first ')' and the comma split it, giving
|
|
// `Meet me (Tue5pm downtown)`.
|
|
//
|
|
int parenDepth = 0;
|
|
|
|
while (!atEnd())
|
|
{
|
|
ASTTokenType t = peek().type;
|
|
if (stopRP && t == ASTTOK_RPAREN && 0 == parenDepth) break;
|
|
if (stopRB && t == ASTTOK_RBRACK) break;
|
|
if (stopRC && t == ASTTOK_RBRACE) break;
|
|
if (stopCM && t == ASTTOK_COMMA && 0 == parenDepth) break;
|
|
|
|
if (ASTTOK_LPAREN == t)
|
|
{
|
|
parenDepth++;
|
|
}
|
|
else if (ASTTOK_RPAREN == t && 0 < parenDepth)
|
|
{
|
|
parenDepth--;
|
|
}
|
|
|
|
auto node = parseOne();
|
|
if (node)
|
|
{
|
|
seq->addChild(std::move(node));
|
|
}
|
|
}
|
|
if (seq->children.size() == 1)
|
|
{
|
|
return std::move(seq->children[0]);
|
|
}
|
|
return seq;
|
|
}
|
|
|
|
std::unique_ptr<ASTNode> parseOne()
|
|
{
|
|
const ASTToken &tok = peek();
|
|
switch (tok.type)
|
|
{
|
|
case ASTTOK_LIT:
|
|
{
|
|
auto n = std::make_unique<ASTNode>(AST_LITERAL, tok.text);
|
|
advance();
|
|
return n;
|
|
}
|
|
|
|
case ASTTOK_SPACE:
|
|
{
|
|
auto n = std::make_unique<ASTNode>(AST_SPACE, tok.text);
|
|
advance();
|
|
return n;
|
|
}
|
|
|
|
case ASTTOK_PCT:
|
|
{
|
|
auto n = std::make_unique<ASTNode>(AST_SUBST, tok.text);
|
|
advance();
|
|
// No DynCall support — if ( follows a substitution,
|
|
// the ( is just a literal parenthesis.
|
|
return n;
|
|
}
|
|
|
|
case ASTTOK_ESC:
|
|
{
|
|
auto n = std::make_unique<ASTNode>(AST_ESCAPE, tok.text);
|
|
advance();
|
|
return n;
|
|
}
|
|
|
|
case ASTTOK_SEMI:
|
|
{
|
|
auto n = std::make_unique<ASTNode>(AST_SEMICOLON, tok.text);
|
|
advance();
|
|
return n;
|
|
}
|
|
|
|
case ASTTOK_FUNC:
|
|
return parseFuncCall();
|
|
|
|
case ASTTOK_LBRACK:
|
|
return parseEvalBracket();
|
|
|
|
case ASTTOK_LBRACE:
|
|
return parseBraceGroup();
|
|
|
|
case ASTTOK_RPAREN:
|
|
case ASTTOK_RBRACK:
|
|
case ASTTOK_RBRACE:
|
|
case ASTTOK_COMMA:
|
|
case ASTTOK_LPAREN:
|
|
{
|
|
auto n = std::make_unique<ASTNode>(AST_LITERAL, tok.text);
|
|
advance();
|
|
return n;
|
|
}
|
|
|
|
case ASTTOK_EOF:
|
|
return nullptr;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::unique_ptr<ASTNode> parseFuncCall()
|
|
{
|
|
ASTToken funcTok = advance();
|
|
auto call = std::make_unique<ASTNode>(AST_FUNCCALL, funcTok.text);
|
|
call->noeval_kind = ast_noeval_kind(funcTok.text);
|
|
call->parser_known_noeval = parser_lookup_builtin_noeval(funcTok.text);
|
|
|
|
if (atEnd() || peek().type != ASTTOK_LPAREN)
|
|
{
|
|
call->type = AST_LITERAL;
|
|
return call;
|
|
}
|
|
advance(); // consume LPAREN
|
|
|
|
parseArgList(call.get());
|
|
return call;
|
|
}
|
|
|
|
void parseArgList(ASTNode *call)
|
|
{
|
|
// Handle zero-argument function calls: foo()
|
|
//
|
|
if (!atEnd() && peek().type == ASTTOK_RPAREN)
|
|
{
|
|
advance();
|
|
call->has_close_paren = true;
|
|
return;
|
|
}
|
|
|
|
call->has_close_paren = false;
|
|
|
|
// When inside an eval bracket, ] terminates the argument
|
|
// list (matching mux_exec behavior where ] closes the bracket
|
|
// even with unclosed parentheses). Outside brackets, ] is
|
|
// literal text in function arguments.
|
|
//
|
|
bool inBracket = (m_bracketDepth > 0);
|
|
bool inBrace = (m_braceDepth > 0);
|
|
size_t argStart = m_pos;
|
|
auto arg = parseSequence(true, inBracket, inBrace, true);
|
|
parser_capture_raw_arg(call, argStart, m_pos);
|
|
call->addChild(std::move(arg));
|
|
|
|
while (!atEnd() && peek().type == ASTTOK_COMMA)
|
|
{
|
|
advance();
|
|
argStart = m_pos;
|
|
arg = parseSequence(true, inBracket, inBrace, true);
|
|
parser_capture_raw_arg(call, argStart, m_pos);
|
|
call->addChild(std::move(arg));
|
|
}
|
|
|
|
if (!atEnd() && peek().type == ASTTOK_RPAREN)
|
|
{
|
|
advance();
|
|
call->has_close_paren = true;
|
|
}
|
|
|
|
parser_apply_structural_arg_policy(call);
|
|
}
|
|
|
|
std::unique_ptr<ASTNode> parseEvalBracket()
|
|
{
|
|
advance(); // consume LBRACK
|
|
m_bracketDepth++;
|
|
|
|
auto bracket = std::make_unique<ASTNode>(AST_EVALBRACKET);
|
|
auto contents = parseSequence(false, true, false, false);
|
|
bracket->addChild(std::move(contents));
|
|
|
|
if (!atEnd() && peek().type == ASTTOK_RBRACK)
|
|
{
|
|
advance();
|
|
}
|
|
else
|
|
{
|
|
bracket->has_close_bracket = false;
|
|
}
|
|
m_bracketDepth--;
|
|
return bracket;
|
|
}
|
|
|
|
std::unique_ptr<ASTNode> parseBraceGroup()
|
|
{
|
|
advance(); // consume LBRACE
|
|
m_braceDepth++;
|
|
|
|
auto group = std::make_unique<ASTNode>(AST_BRACEGROUP);
|
|
auto contents = parseSequence(false, false, true, false);
|
|
group->addChild(std::move(contents));
|
|
|
|
if (!atEnd() && peek().type == ASTTOK_RBRACE)
|
|
{
|
|
advance();
|
|
}
|
|
else
|
|
{
|
|
group->has_close_brace = false;
|
|
}
|
|
m_braceDepth--;
|
|
return group;
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------
|
|
// Public parse API
|
|
// ---------------------------------------------------------------
|
|
|
|
std::unique_ptr<ASTNode> ast_parse(const std::vector<ASTToken> &tokens)
|
|
{
|
|
ASTParser parser(tokens);
|
|
return parser.parse();
|
|
}
|
|
|
|
std::unique_ptr<ASTNode> ast_parse_region(ASTSourceSpan span,
|
|
ASTLexMode mode)
|
|
{
|
|
auto tokens = ast_tokenize_mode(span.input, span.nLen, mode);
|
|
return ast_parse(tokens);
|
|
}
|
|
|
|
std::unique_ptr<ASTNode> ast_parse_string(const UTF8 *input, size_t nLen)
|
|
{
|
|
return ast_parse_region(ASTSourceSpan(input, nLen), ASTLEX_EVAL);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Utility functions
|
|
// ---------------------------------------------------------------
|
|
|
|
std::string ast_raw_text(const ASTNode *n)
|
|
{
|
|
if (!n)
|
|
{
|
|
return "";
|
|
}
|
|
|
|
switch (n->type)
|
|
{
|
|
case AST_LITERAL:
|
|
case AST_SPACE:
|
|
case AST_SUBST:
|
|
case AST_ESCAPE:
|
|
return n->text;
|
|
|
|
case AST_SEMICOLON:
|
|
return ";";
|
|
|
|
case AST_FUNCCALL:
|
|
{
|
|
std::string r = n->text + "(";
|
|
for (size_t i = 0; i < n->children.size(); i++)
|
|
{
|
|
if (i > 0) r += ",";
|
|
r += ast_raw_text(n->children[i].get());
|
|
}
|
|
if (n->has_close_paren)
|
|
{
|
|
r += ")";
|
|
}
|
|
return r;
|
|
}
|
|
|
|
case AST_EVALBRACKET:
|
|
{
|
|
std::string r = "[";
|
|
for (const auto &c : n->children)
|
|
{
|
|
r += ast_raw_text(c.get());
|
|
}
|
|
return r + "]";
|
|
}
|
|
|
|
case AST_BRACEGROUP:
|
|
{
|
|
std::string r = "{";
|
|
for (const auto &c : n->children)
|
|
{
|
|
r += ast_raw_text(c.get());
|
|
}
|
|
return r + "}";
|
|
}
|
|
|
|
case AST_SEQUENCE:
|
|
{
|
|
std::string r;
|
|
for (const auto &c : n->children)
|
|
{
|
|
r += ast_raw_text(c.get());
|
|
}
|
|
return r;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
static const char *ast_node_name(ASTNodeType t)
|
|
{
|
|
switch (t)
|
|
{
|
|
case AST_SEQUENCE: return "Seq";
|
|
case AST_LITERAL: return "Lit";
|
|
case AST_SPACE: return "Sp";
|
|
case AST_SUBST: return "Sub";
|
|
case AST_ESCAPE: return "Esc";
|
|
case AST_FUNCCALL: return "Call";
|
|
case AST_EVALBRACKET: return "Eval";
|
|
case AST_BRACEGROUP: return "Brace";
|
|
case AST_SEMICOLON: return "Semi";
|
|
}
|
|
return "???";
|
|
}
|
|
|
|
void ast_dump(const ASTNode *node, int indent)
|
|
{
|
|
if (!node)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Bound dump recursion so debug logging on a pathological AST
|
|
// can't overflow the stack. Use a fixed cap rather than the
|
|
// evaluator's depth counter since dump is diagnostic.
|
|
//
|
|
// Indent by hand rather than with "%*s". mux_vsnprintf implements the
|
|
// '-' and '0' flags and literal digit widths only -- it has no '*' width,
|
|
// so "%*s" reached mux_assert(0) and abort()ed the process (#1429 covers
|
|
// the general case; this was the one live instance in the tree). Nothing
|
|
// calls ast_dump today, which is the only reason it never fired, but it
|
|
// is declared in ast.h as a debug helper and is meant to be callable.
|
|
//
|
|
UTF8 pad[81];
|
|
size_t nPad = (indent < 0) ? 0 : static_cast<size_t>(indent);
|
|
if (sizeof(pad) - 1 < nPad)
|
|
{
|
|
nPad = sizeof(pad) - 1;
|
|
}
|
|
memset(pad, ' ', nPad);
|
|
pad[nPad] = '\0';
|
|
|
|
if (indent > 2 * 400)
|
|
{
|
|
STARTLOG(LOG_DEBUG, "AST", "DUMP");
|
|
Log.tinyprintf(T("%s... (truncated)"), pad);
|
|
ENDLOG;
|
|
return;
|
|
}
|
|
|
|
STARTLOG(LOG_DEBUG, "AST", "DUMP");
|
|
Log.tinyprintf(T("%s%s"), pad, ast_node_name(node->type));
|
|
if (!node->text.empty())
|
|
{
|
|
Log.tinyprintf(T(" \"%s\""), node->text.c_str());
|
|
}
|
|
ENDLOG;
|
|
|
|
for (const auto &child : node->children)
|
|
{
|
|
ast_dump(child.get(), indent + 2);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// AST Evaluator (Phase 2)
|
|
// ---------------------------------------------------------------
|
|
//
|
|
// Walks the AST tree and produces evaluated output into buff/bufc.
|
|
// Uses the same runtime context as mux_exec (mudstate, mudconf,
|
|
// builtin function table, registers, iterator stack, etc.)
|
|
//
|
|
// For %-substitutions, delegates to mux_exec on the short
|
|
// substitution text (2-6 bytes). This gives exact compatibility
|
|
// with all L2 dispatch table entries without reimplementing them.
|
|
//
|
|
// For function calls, looks up in mudstate.builtin_functions and
|
|
// dispatches through the existing FUN handler. NOEVAL functions
|
|
// receive raw text via ast_raw_text() — the handler calls mux_exec
|
|
// internally as before.
|
|
//
|
|
|
|
// 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 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.
|
|
//
|
|
// Forward declaration.
|
|
//
|
|
static void ast_eval_node(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs);
|
|
|
|
// Evaluate a selected argument from a FN_NOEVAL function using the
|
|
// 2.13-style noeval pass followed by reparse/re-eval.
|
|
//
|
|
// This replicates the two-pass behavior observed in 2.13:
|
|
// Pass 1: noeval -- parse/evaluate the deferred region under ASTLEX_NOEVAL
|
|
// Pass 2: eval -- re-tokenize the result and evaluate it
|
|
//
|
|
static void ast_eval_deferred_region(const ASTNode *node,
|
|
const ASTNode *noevalNode, const std::string *rawText, UTF8 *buff,
|
|
UTF8 **bufc, dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
if (!node)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::string text;
|
|
|
|
if (noevalNode)
|
|
{
|
|
LBuf temp = LBuf_Src("ast_noeval_region");
|
|
UTF8 *tp = temp;
|
|
ast_eval_node(noevalNode, temp, &tp,
|
|
executor, caller, enactor,
|
|
((eval & ~(EV_EVAL | EV_TOP | EV_FMAND | EV_STRIP_CURLY | EV_FCHECK))
|
|
| EV_NOFCHECK),
|
|
cargs, ncargs);
|
|
*tp = '\0';
|
|
text.assign(reinterpret_cast<const char *>(temp.get()), tp - temp);
|
|
}
|
|
else if (rawText)
|
|
{
|
|
auto noevalAst = ast_parse_region(
|
|
ASTSourceSpan(reinterpret_cast<const UTF8 *>(rawText->c_str()),
|
|
rawText->size()),
|
|
ASTLEX_NOEVAL);
|
|
if (!noevalAst)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LBuf temp = LBuf_Src("ast_noeval_region");
|
|
UTF8 *tp = temp;
|
|
ast_eval_node(noevalAst.get(), temp, &tp,
|
|
executor, caller, enactor,
|
|
((eval & ~(EV_EVAL | EV_TOP | EV_FMAND | EV_STRIP_CURLY | EV_FCHECK))
|
|
| EV_NOFCHECK),
|
|
cargs, ncargs);
|
|
*tp = '\0';
|
|
text.assign(reinterpret_cast<const char *>(temp.get()), tp - temp);
|
|
}
|
|
else
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The selected branch/body of a FN_NOEVAL function is evaluated
|
|
// with one outer brace layer stripped. Nested inner braces remain
|
|
// part of the deferred text.
|
|
if ( node->type == AST_BRACEGROUP
|
|
&& text.size() >= 2
|
|
&& text.front() == '{'
|
|
&& text.back() == '}')
|
|
{
|
|
text = text.substr(1, text.size() - 2);
|
|
}
|
|
|
|
// Pass 2: re-tokenize the selected region in EVAL mode and
|
|
// evaluate the resulting subtree directly. This is the first
|
|
// concrete use of the parser-controlled region-parse API.
|
|
//
|
|
auto reparsed = ast_parse_region(
|
|
ASTSourceSpan(reinterpret_cast<const UTF8 *>(text.c_str()), text.size()),
|
|
ASTLEX_EVAL);
|
|
if (!reparsed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ast_eval_node(reparsed.get(), buff, bufc,
|
|
executor, caller, enactor,
|
|
(eval & ~(EV_TOP | EV_FMAND | EV_STRIP_CURLY))
|
|
| EV_EVAL | EV_FCHECK,
|
|
cargs, ncargs);
|
|
}
|
|
|
|
static bool ast_is_malformed_qsubst(const ASTNode *node)
|
|
{
|
|
if ( !node
|
|
|| node->type != AST_SUBST)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const std::string &txt = node->text;
|
|
return txt.size() >= 3
|
|
&& txt[0] == '%'
|
|
&& (txt[1] == 'q' || txt[1] == 'Q')
|
|
&& txt[2] == '<'
|
|
&& txt.find('>', 3) == std::string::npos;
|
|
}
|
|
|
|
// Evaluate a function argument with the same top-level space trimming
|
|
// that parse_arglist()/parse_to() applies around comma-separated args.
|
|
// Evaluate children [first, last) of a SEQUENCE, applying 2.13's
|
|
// one-call-per-region rule.
|
|
//
|
|
// EV_FCHECK without EV_FMAND means "the first '(' in this region may be
|
|
// a function call". 2.13 clears EV_FCHECK once that opportunity is used
|
|
// (mux/src/eval.cpp:1677, `eval &= ~EV_FCHECK`), so a later call in the
|
|
// same region emits as literal text:
|
|
//
|
|
// [strcat(x add(1,2) y)] -> x add(1,2) y (not "x 3 y")
|
|
//
|
|
// Two places evaluate such a run — the AST_SEQUENCE case in
|
|
// ast_eval_node and the space-compressed argument path in
|
|
// ast_eval_argument — and both have to apply the rule. Only the former
|
|
// did, so the rule was dead for exactly the case that matters, a call
|
|
// sitting mid-argument (#1214). Shared here so the two cannot drift.
|
|
//
|
|
// Set when a lookup fails under EV_FMAND, to stop the rest of the region
|
|
// from being emitted (#1247). 2.13 does this with `*bufc = oldp; break;`
|
|
// in its single eval loop (mux/src/eval.cpp:1497); 2.14 evaluates a tree,
|
|
// so the failure has to be signalled from the funccall node up to the
|
|
// sequence that contains it.
|
|
//
|
|
// Scoped the same way as the FCHECK opportunity: saved and cleared when
|
|
// entering an eval-bracket region, consumed by the nearest enclosing
|
|
// EV_FMAND sequence, restored on the way out. That keeps an inner
|
|
// region's failure from aborting an outer one, and keeps a failure inside
|
|
// a u()'d attribute from escaping into its caller.
|
|
//
|
|
static bool s_fmand_abort = false;
|
|
|
|
static void ast_eval_sequence_children(const ASTNode *node,
|
|
size_t first, size_t last, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
// Whether a call in this region can still be recognised.
|
|
//
|
|
bool armed = (eval & EV_FCHECK) != 0;
|
|
|
|
// Any non-space child spends it. 2.13 checks only the first '('
|
|
// it reaches, and a dispatched call clears EV_FCHECK for whatever
|
|
// follows (mux/src/eval.cpp:1677):
|
|
//
|
|
// [strcat(x add(1,2) y)] -> x add(1,2) y
|
|
// [add(1,2) mul(3,4)] -> 3 mul(3,4)
|
|
// [add(1,2) zz mul(3,4)] -> 3 zz mul(3,4)
|
|
//
|
|
// The rule is uniform across region kinds. #1238 briefly made
|
|
// EV_FMAND regions special so that text before a call would not
|
|
// spend the opportunity: 2.13's candidate name there is the
|
|
// accumulated OUTPUT, and it folds preceding text into the name
|
|
// ([x add(1,2) y] -> "#-1 FUNCTION (X ADD) NOT FOUND"). That
|
|
// existed to avoid settling an unadjudicated shape by accident.
|
|
//
|
|
// It has since been settled the other way (#1246): the span-name
|
|
// error is not the target, literal text is the better answer, and
|
|
// the compiled route — which never modelled the span — is the
|
|
// reference. With the distinction gone both routes agree:
|
|
//
|
|
// [x add(1,2) y] -> x add(1,2) y
|
|
// [zz mul(2,3)] -> zz mul(2,3)
|
|
//
|
|
for (size_t i = first; i < last; i++)
|
|
{
|
|
const ASTNode *child = node->children[i].get();
|
|
|
|
int childEval = eval;
|
|
if ( !armed
|
|
|| child->type != AST_FUNCCALL)
|
|
{
|
|
childEval = eval & ~EV_FCHECK;
|
|
}
|
|
|
|
ast_eval_node(child, buff, bufc,
|
|
executor, caller, enactor, childEval, cargs, ncargs);
|
|
|
|
// A failed mandatory lookup ends the region. Everything after it
|
|
// would be literal text by both engines' rules anyway (a dispatched
|
|
// call already spends the recognition opportunity), so nothing
|
|
// evaluable is lost -- but emitting it makes the diagnostic read as
|
|
// though part of the region had succeeded (#1247).
|
|
//
|
|
if ( (eval & EV_FMAND)
|
|
&& s_fmand_abort)
|
|
{
|
|
s_fmand_abort = false;
|
|
break;
|
|
}
|
|
|
|
if ( armed
|
|
&& child->type != AST_SPACE)
|
|
{
|
|
armed = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ast_eval_argument(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
if (!node)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( node->type == AST_SEQUENCE
|
|
&& !node->children.empty()
|
|
&& mudconf.space_compress
|
|
&& !(eval & EV_NO_COMPRESS))
|
|
{
|
|
size_t first = 0;
|
|
size_t last = node->children.size();
|
|
while ( first < last
|
|
&& node->children[first]->type == AST_SPACE)
|
|
{
|
|
first++;
|
|
}
|
|
while ( last > first
|
|
&& node->children[last - 1]->type == AST_SPACE)
|
|
{
|
|
last--;
|
|
}
|
|
|
|
ast_eval_sequence_children(node, first, last, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
return;
|
|
}
|
|
|
|
ast_eval_node(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Native %-substitution handler
|
|
// ---------------------------------------------------------------
|
|
//
|
|
// Handles all L2 dispatch table substitutions natively instead of
|
|
// delegating to mux_exec. The node->text is the full %-sequence
|
|
// as gathered by gather_pct (e.g. "%0", "%qa", "%q<name>", "%xn",
|
|
// "%c<rgb>", "%va", "%i0", "%=<attr>", "%%", "%r", "%b", etc.)
|
|
//
|
|
static void ast_eval_subst(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
const std::string &txt = node->text;
|
|
if (txt.size() < 2)
|
|
{
|
|
// Bare '%' at end of string — output literally.
|
|
//
|
|
safe_chr('%', buff, bufc);
|
|
return;
|
|
}
|
|
|
|
// ## / #@ / #$ — iterator/switch substitutions.
|
|
//
|
|
if (txt[0] == '#')
|
|
{
|
|
switch (txt[1])
|
|
{
|
|
case '#':
|
|
// ## — bound variable (same as %i0).
|
|
//
|
|
{
|
|
int i = mudstate.in_loop - 1;
|
|
if (0 <= i && i < MAX_ITEXT && mudstate.itext[i])
|
|
{
|
|
safe_str(mudstate.itext[i], buff, bufc);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case '@':
|
|
// #@ — list place number (same as inum()).
|
|
//
|
|
{
|
|
int i = mudstate.in_loop - 1;
|
|
if (0 <= i && i < MAX_ITEXT)
|
|
{
|
|
safe_ltoa(mudstate.inum[i], buff, bufc);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case '$':
|
|
// #$ — switch value (same as switch() matched value).
|
|
// Resolved from mudstate.switch_token if available.
|
|
//
|
|
if (mudstate.switch_token)
|
|
{
|
|
safe_str(mudstate.switch_token, buff, bufc);
|
|
}
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
|
|
unsigned char ch = static_cast<unsigned char>(txt[1]);
|
|
unsigned char upper = static_cast<unsigned char>(mux_toupper_ascii(ch));
|
|
|
|
// The L2 table sets flag 0x80 on uppercase A, M, N, O, P, Q, S, V
|
|
// to trigger mux_toupper_first on the substituted value.
|
|
//
|
|
bool bUpperCase = false;
|
|
if ('A' <= ch && ch <= 'Z')
|
|
{
|
|
switch (ch)
|
|
{
|
|
case 'A': case 'M': case 'N': case 'O':
|
|
case 'P': case 'Q': case 'S': case 'V':
|
|
bUpperCase = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
LBuf scratch = LBuf_Src("ast_eval_subst");
|
|
UTF8 *TempPtr = *bufc;
|
|
|
|
// %0-%9 — command argument substitution.
|
|
//
|
|
if (ch >= '0' && ch <= '9')
|
|
{
|
|
int i = ch - '0';
|
|
if (i < ncargs && cargs[i])
|
|
{
|
|
safe_str(cargs[i], buff, bufc);
|
|
}
|
|
return;
|
|
}
|
|
|
|
switch (upper)
|
|
{
|
|
case 'Q':
|
|
// %q0-%q9, %qa-%qz, %q<name> — register substitution.
|
|
//
|
|
if (txt.size() >= 3)
|
|
{
|
|
if (txt[2] == '<')
|
|
{
|
|
// Named register: %q<name>
|
|
//
|
|
size_t close = txt.find('>', 3);
|
|
if (close != std::string::npos)
|
|
{
|
|
size_t nName = close - 3;
|
|
const UTF8 *pName = reinterpret_cast<const UTF8 *>(txt.c_str() + 3);
|
|
int regnum = -1;
|
|
if ( 1 == nName
|
|
&& (regnum = mux_RegisterSet[pName[0]]) >= 0
|
|
&& regnum < MAX_GLOBAL_REGS)
|
|
{
|
|
if ( mudstate.global_regs[regnum]
|
|
&& mudstate.global_regs[regnum]->reg_len > 0)
|
|
{
|
|
safe_copy_buf(mudstate.global_regs[regnum]->reg_ptr,
|
|
mudstate.global_regs[regnum]->reg_len, buff, bufc);
|
|
}
|
|
}
|
|
else if (IsValidNamedReg(pName, nName))
|
|
{
|
|
reg_ref *rr = NamedRegRead(mudstate.named_regs, pName, nName);
|
|
if (rr && rr->reg_len > 0)
|
|
{
|
|
safe_copy_buf(rr->reg_ptr, rr->reg_len, buff, bufc);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Malformed %q<name with no closing > — output literally.
|
|
//
|
|
safe_str(reinterpret_cast<const UTF8 *>(txt.c_str()), buff, bufc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Traditional single-char: %q0-%q9, %qa-%qz
|
|
//
|
|
int i = mux_RegisterSet[static_cast<unsigned char>(txt[2])];
|
|
if ( 0 <= i
|
|
&& i < MAX_GLOBAL_REGS)
|
|
{
|
|
if ( mudstate.global_regs[i]
|
|
&& mudstate.global_regs[i]->reg_len > 0)
|
|
{
|
|
safe_copy_buf(mudstate.global_regs[i]->reg_ptr,
|
|
mudstate.global_regs[i]->reg_len, buff, bufc);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case '#':
|
|
// %# — enactor dbref.
|
|
//
|
|
{
|
|
scratch[0] = '#';
|
|
size_t n = mux_ltoa(enactor, scratch + 1);
|
|
safe_copy_buf(scratch, n + 1, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case '!':
|
|
// %! — executor dbref.
|
|
//
|
|
{
|
|
scratch[0] = '#';
|
|
size_t n = mux_ltoa(executor, scratch + 1);
|
|
safe_copy_buf(scratch, n + 1, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case '@':
|
|
// %@ — caller dbref.
|
|
//
|
|
{
|
|
scratch[0] = '#';
|
|
size_t n = mux_ltoa(caller, scratch + 1);
|
|
safe_copy_buf(scratch, n + 1, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case '%':
|
|
// %% — literal percent.
|
|
//
|
|
safe_chr('%', buff, bufc);
|
|
break;
|
|
|
|
case 'R':
|
|
// %r — carriage return.
|
|
//
|
|
safe_copy_buf(T("\r\n"), 2, buff, bufc);
|
|
break;
|
|
|
|
case 'B':
|
|
// %b — blank (space).
|
|
//
|
|
safe_chr(' ', buff, bufc);
|
|
break;
|
|
|
|
case 'T':
|
|
// %t — tab.
|
|
//
|
|
safe_chr('\t', buff, bufc);
|
|
break;
|
|
|
|
case 'N':
|
|
// %n/%N — enactor name.
|
|
//
|
|
safe_str(Name(enactor), buff, bufc);
|
|
break;
|
|
|
|
case 'L':
|
|
// %l — enactor location dbref.
|
|
//
|
|
if (!(eval & EV_NO_LOCATION))
|
|
{
|
|
scratch[0] = '#';
|
|
size_t n = mux_ltoa(where_is(enactor), scratch + 1);
|
|
safe_copy_buf(scratch, n + 1, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case 'S':
|
|
// %s/%S — subjective pronoun.
|
|
//
|
|
{
|
|
const PRONOUN_SET *ps = get_pronoun_set(enactor);
|
|
safe_str(ps->subjective, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case 'P':
|
|
// %p/%P — possessive pronoun.
|
|
//
|
|
{
|
|
const PRONOUN_SET *ps = get_pronoun_set(enactor);
|
|
safe_str(ps->possessive, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case 'O':
|
|
// %o/%O — objective pronoun.
|
|
//
|
|
{
|
|
const PRONOUN_SET *ps = get_pronoun_set(enactor);
|
|
safe_str(ps->objective, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case 'A':
|
|
// %a/%A — absolute possessive pronoun.
|
|
//
|
|
{
|
|
const PRONOUN_SET *ps = get_pronoun_set(enactor);
|
|
safe_str(ps->absolute, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case 'M':
|
|
// %m — last command.
|
|
//
|
|
safe_str(mudstate.curr_cmd, buff, bufc);
|
|
break;
|
|
|
|
case 'K':
|
|
// %k — moniker.
|
|
//
|
|
safe_str(Moniker(enactor), buff, bufc);
|
|
break;
|
|
|
|
case '|':
|
|
// %| — piped command output.
|
|
//
|
|
safe_str(mudstate.pout, buff, bufc);
|
|
break;
|
|
|
|
case '+':
|
|
// %+ — number of command args.
|
|
//
|
|
safe_i64toa(ncargs, buff, bufc);
|
|
break;
|
|
|
|
case ':':
|
|
// %: — enactor objid (#dbref:creation_seconds).
|
|
//
|
|
{
|
|
scratch[0] = '#';
|
|
size_t n = mux_ltoa(enactor, scratch + 1);
|
|
int64_t csecs = creation_seconds(enactor);
|
|
if (0 != csecs)
|
|
{
|
|
scratch[n + 1] = ':';
|
|
mux_i64toa(csecs, scratch + n + 2);
|
|
}
|
|
safe_str(scratch, buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case 'V':
|
|
// %va-%vz — variable attribute.
|
|
//
|
|
if (txt.size() >= 3 && mux_isazAZ(txt[2]))
|
|
{
|
|
int i = A_VA + mux_toupper_ascii(txt[2]) - 'A';
|
|
dbref aowner;
|
|
int aflags;
|
|
size_t nAttrGotten;
|
|
atr_pget_str_LEN(scratch, executor, i, &aowner, &aflags, &nAttrGotten);
|
|
if (0 < nAttrGotten)
|
|
{
|
|
safe_copy_buf(scratch, nAttrGotten, buff, bufc);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'I':
|
|
// %i0-%i9 — itext() substitution.
|
|
//
|
|
if (txt.size() >= 3 && mux_isdigit(txt[2]))
|
|
{
|
|
int depth = txt[2] - '0';
|
|
int i = mudstate.in_loop - depth - 1;
|
|
if (0 <= i && i < MAX_ITEXT)
|
|
{
|
|
safe_str(mudstate.itext[i], buff, bufc);
|
|
}
|
|
}
|
|
else if (txt.size() >= 3)
|
|
{
|
|
// %i followed by non-digit — output the char after %i literally.
|
|
//
|
|
safe_chr(txt[2], buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case '=':
|
|
// %= — plain equals sign.
|
|
// %=<name> — attribute or numbered arg substitution.
|
|
//
|
|
if (txt.size() >= 4 && txt[2] == '<')
|
|
{
|
|
size_t close = txt.find('>', 3);
|
|
if (close != std::string::npos)
|
|
{
|
|
size_t nName = close - 3;
|
|
memcpy(scratch, txt.c_str() + 3, nName);
|
|
scratch[nName] = '\0';
|
|
|
|
if (mux_isdigit(scratch[0]))
|
|
{
|
|
// Numeric arg reference: %=<0> through %=<999>
|
|
//
|
|
int i;
|
|
if (!mux_isdigit(scratch[1]))
|
|
{
|
|
i = scratch[0] - '0';
|
|
}
|
|
else if (!mux_isdigit(scratch[2]))
|
|
{
|
|
i = TableATOI(scratch[0] - '0', scratch[1] - '0');
|
|
}
|
|
else if (!mux_isdigit(scratch[3]))
|
|
{
|
|
i = 10 * TableATOI(scratch[0] - '0', scratch[1] - '0')
|
|
+ scratch[2] - '0';
|
|
}
|
|
else
|
|
{
|
|
i = MAX_ARG;
|
|
}
|
|
if (i < ncargs && nullptr != cargs[i])
|
|
{
|
|
safe_str(cargs[i], buff, bufc);
|
|
}
|
|
}
|
|
else if (mux_isattrnameinitial(scratch))
|
|
{
|
|
ATTR *ap = atr_str(scratch);
|
|
if (ap && See_attr(executor, executor, ap))
|
|
{
|
|
dbref aowner;
|
|
int aflags;
|
|
size_t nLen;
|
|
atr_pget_str_LEN(scratch, executor, ap->number,
|
|
&aowner, &aflags, &nLen);
|
|
safe_copy_buf(scratch, nLen, buff, bufc);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Malformed %=<name with no closing > — output literally.
|
|
//
|
|
safe_str(reinterpret_cast<const UTF8 *>(txt.c_str()), buff, bufc);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'C':
|
|
case 'X':
|
|
// %c/%x — color codes.
|
|
// Uppercase C/X → background (0x40 flag in L2 table).
|
|
// Lowercase c/x → foreground.
|
|
//
|
|
if (txt.size() >= 3)
|
|
{
|
|
bool bBackground = ('A' <= ch && ch <= 'Z');
|
|
|
|
if (txt[2] == '<')
|
|
{
|
|
// Extended color: %c<rgb>, %x<name>, etc.
|
|
//
|
|
size_t close = txt.find('>', 3);
|
|
if (close != std::string::npos)
|
|
{
|
|
size_t nColor = close - 3;
|
|
const UTF8 *pColor = reinterpret_cast<const UTF8 *>(txt.c_str() + 3);
|
|
|
|
RGB rgb;
|
|
if (parse_rgb(nColor, pColor, rgb))
|
|
{
|
|
// Emit via LettersToBinary — the same
|
|
// ColorTransitionBinary / EmitSMPColor (v5 two-
|
|
// codepoint) path ansi() uses. The previous
|
|
// hand-rolled 0xF0x00+channel scheme was the
|
|
// retired v4 three-codepoint delta encoding;
|
|
// the live decoder is v5, so non-palette-exact
|
|
// truecolor was corrupted (#1933).
|
|
//
|
|
// Letter form: "<body>" FG, "/<body>" BG.
|
|
// Body length is tiny (#RRGGBB or "R G B").
|
|
//
|
|
UTF8 letters[32];
|
|
size_t li = 0;
|
|
if (bBackground)
|
|
{
|
|
letters[li++] = '/';
|
|
}
|
|
letters[li++] = '<';
|
|
if (li + nColor + 1 < sizeof(letters))
|
|
{
|
|
memcpy(letters + li, pColor, nColor);
|
|
li += nColor;
|
|
letters[li++] = '>';
|
|
letters[li] = '\0';
|
|
safe_str(LettersToBinary(letters), buff, bufc);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Malformed %c< color with no closing > — output literally.
|
|
//
|
|
safe_str(reinterpret_cast<const UTF8 *>(txt.c_str()), buff, bufc);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Simple color code: %xn, %ch, etc.
|
|
//
|
|
unsigned int iColor = ColorTable[static_cast<unsigned char>(txt[2])];
|
|
if (iColor)
|
|
{
|
|
safe_str(aColors[iColor].pUTF, buff, bufc);
|
|
}
|
|
else
|
|
{
|
|
// Unknown color letter — output it literally.
|
|
//
|
|
safe_chr(txt[2], buff, bufc);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
// Unknown substitution — output the character literally
|
|
// (matches iCode == 0 in mux_exec).
|
|
//
|
|
safe_chr(ch, buff, bufc);
|
|
break;
|
|
}
|
|
|
|
// For uppercase escape letters (%S, %N, %P, %O, %A, %K),
|
|
// uppercase the first character of the substituted value.
|
|
//
|
|
if (bUpperCase)
|
|
{
|
|
mux_toupper_first(TempPtr, bufc, LBUF_SIZE);
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// Native NOEVAL handlers
|
|
// ---------------------------------------------------------------
|
|
//
|
|
// These functions handle specific NOEVAL built-in functions by
|
|
// evaluating AST subtrees directly instead of serializing back
|
|
// to text and re-parsing through mux_exec.
|
|
//
|
|
|
|
// Helper: evaluate an AST subtree for a NOEVAL branch (if/switch/iter).
|
|
//
|
|
// ##/#@/#$ are resolved natively as AST_SUBST nodes at eval time
|
|
// (from mudstate.itext/inum/switch_token).
|
|
//
|
|
static void ast_eval_branch(const ASTNode *callNode, int childIndex,
|
|
const ASTNode *child, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
const ASTNode *noevalNode = nullptr;
|
|
const std::string *rawText = nullptr;
|
|
std::string fallbackRaw;
|
|
if (ast_call_arg_is_deferred(callNode, childIndex))
|
|
{
|
|
noevalNode = child;
|
|
rawText = ast_call_raw_arg(callNode, childIndex);
|
|
}
|
|
else if (child)
|
|
{
|
|
fallbackRaw = ast_raw_text(child);
|
|
rawText = &fallbackRaw;
|
|
}
|
|
|
|
ast_eval_deferred_region(child, noevalNode, rawText, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
|
|
// Native cand/candbool: short-circuit AND.
|
|
//
|
|
static void ast_noeval_cand(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs,
|
|
bool bBool)
|
|
{
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
bool val = true;
|
|
LBuf temp = LBuf_Src("ast_noeval_cand");
|
|
for (int i = 0; i < nfargs && val && !alarm_clock.alarmed; i++)
|
|
{
|
|
UTF8 *bp = temp;
|
|
ast_eval_node(node->children[i].get(), temp, &bp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
val = bBool ? xlate(temp) : isTRUE(mux_atoi64(temp));
|
|
}
|
|
safe_bool(val, buff, bufc);
|
|
}
|
|
|
|
// Native cor/corbool: short-circuit OR.
|
|
//
|
|
static void ast_noeval_cor(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs,
|
|
bool bBool)
|
|
{
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
bool val = false;
|
|
LBuf temp = LBuf_Src("ast_noeval_cor");
|
|
for (int i = 0; i < nfargs && !val && !alarm_clock.alarmed; i++)
|
|
{
|
|
UTF8 *bp = temp;
|
|
ast_eval_node(node->children[i].get(), temp, &bp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
val = bBool ? xlate(temp) : isTRUE(mux_atoi64(temp));
|
|
}
|
|
safe_bool(val, buff, bufc);
|
|
}
|
|
|
|
// Native if/ifelse: conditional branch selection.
|
|
//
|
|
static void ast_noeval_ifelse(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
|
|
// Evaluate the condition.
|
|
//
|
|
LBuf lbuff = LBuf_Src("ast_noeval_if");
|
|
UTF8 *bp = lbuff;
|
|
ast_eval_node(node->children[0].get(), lbuff, &bp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
|
|
const UTF8 *saved_switch = mudstate.switch_token;
|
|
mudstate.switch_token = lbuff;
|
|
|
|
if (xlate(lbuff))
|
|
{
|
|
ast_eval_branch(node, 1, node->children[1].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
else if (nfargs >= 3)
|
|
{
|
|
ast_eval_branch(node, 2, node->children[2].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
|
|
mudstate.switch_token = saved_switch;
|
|
}
|
|
|
|
// Native switch/case: first-match pattern dispatch.
|
|
//
|
|
static void ast_noeval_switch(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs,
|
|
bool bWild)
|
|
{
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
|
|
// Evaluate the target in child[0].
|
|
//
|
|
LBuf mbuff = LBuf_Src("ast_noeval_switch");
|
|
UTF8 *bp = mbuff;
|
|
ast_eval_node(node->children[0].get(), mbuff, &bp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
|
|
LBuf tbuff = LBuf_Src("ast_noeval_switch.2");
|
|
|
|
// Loop through patterns looking for a match. #$ is bound to the
|
|
// match target only while a matched <result> (or the default) is
|
|
// evaluated, never while a pattern is evaluated (#857; mirrors
|
|
// switch_handler in functions.cpp).
|
|
//
|
|
int i;
|
|
for (i = 1; i < nfargs - 1 && !alarm_clock.alarmed; i += 2)
|
|
{
|
|
bp = tbuff;
|
|
ast_eval_node(node->children[i].get(), tbuff, &bp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
|
|
if ( bWild
|
|
? wild_match(tbuff, mbuff)
|
|
: strcmp(reinterpret_cast<char *>(tbuff.get()),
|
|
reinterpret_cast<char *>(mbuff.get())) == 0)
|
|
{
|
|
const UTF8 *saved_switch = mudstate.switch_token;
|
|
mudstate.switch_token = mbuff;
|
|
ast_eval_branch(node, i + 1, node->children[i + 1].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
mudstate.switch_token = saved_switch;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// No match — evaluate default if present.
|
|
//
|
|
if (i < nfargs)
|
|
{
|
|
const UTF8 *saved_switch = mudstate.switch_token;
|
|
mudstate.switch_token = mbuff;
|
|
ast_eval_branch(node, i, node->children[i].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
mudstate.switch_token = saved_switch;
|
|
}
|
|
}
|
|
|
|
// Native switchall/caseall: all-match pattern dispatch.
|
|
//
|
|
static void ast_noeval_switchall(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs,
|
|
bool bWild)
|
|
{
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
|
|
// Evaluate the target in child[0].
|
|
//
|
|
LBuf mbuff = LBuf_Src("ast_noeval_switchall");
|
|
UTF8 *bp = mbuff;
|
|
ast_eval_node(node->children[0].get(), mbuff, &bp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
|
|
LBuf tbuff = LBuf_Src("ast_noeval_switchall.2");
|
|
|
|
// Loop through all patterns, evaluating every match. As in
|
|
// ast_noeval_switch, #$ is bound only around <result>/default
|
|
// evaluation, never pattern evaluation (#857).
|
|
//
|
|
bool bMatched = false;
|
|
int i;
|
|
for (i = 1; i < nfargs - 1 && !alarm_clock.alarmed; i += 2)
|
|
{
|
|
bp = tbuff;
|
|
ast_eval_node(node->children[i].get(), tbuff, &bp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
|
|
if ( bWild
|
|
? wild_match(tbuff, mbuff)
|
|
: strcmp(reinterpret_cast<char *>(tbuff.get()),
|
|
reinterpret_cast<char *>(mbuff.get())) == 0)
|
|
{
|
|
bMatched = true;
|
|
const UTF8 *saved_switch = mudstate.switch_token;
|
|
mudstate.switch_token = mbuff;
|
|
ast_eval_branch(node, i + 1, node->children[i + 1].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
mudstate.switch_token = saved_switch;
|
|
}
|
|
}
|
|
|
|
// If nothing matched, evaluate the default.
|
|
//
|
|
if (!bMatched && i < nfargs)
|
|
{
|
|
const UTF8 *saved_switch = mudstate.switch_token;
|
|
mudstate.switch_token = mbuff;
|
|
ast_eval_branch(node, i, node->children[i].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
mudstate.switch_token = saved_switch;
|
|
}
|
|
}
|
|
|
|
// Native iter: list iteration.
|
|
//
|
|
static void ast_noeval_iter(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
|
|
// Handle optional delimiters (args 3 and 4) by serializing
|
|
// and evaluating them, then parsing into SEP structures.
|
|
// For the common case (no delimiters), use space defaults.
|
|
//
|
|
SEP sep;
|
|
sep.n = 1;
|
|
memcpy(sep.str, " ", 2);
|
|
|
|
SEP osep;
|
|
osep.n = 1;
|
|
memcpy(osep.str, " ", 2);
|
|
|
|
if (nfargs >= 3)
|
|
{
|
|
// Evaluate input delimiter.
|
|
//
|
|
LBuf dbuf = LBuf_Src("ast_noeval_iter.sep");
|
|
UTF8 *dp = dbuf;
|
|
ast_eval_node(node->children[2].get(), dbuf, &dp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*dp = '\0';
|
|
size_t dlen = dp - dbuf;
|
|
if (dlen == 1)
|
|
{
|
|
sep.n = 1;
|
|
memcpy(sep.str, dbuf.get(), 2);
|
|
}
|
|
else if (dlen > 1 && dlen <= MAX_SEP_LEN)
|
|
{
|
|
sep.n = dlen;
|
|
memcpy(sep.str, dbuf.get(), dlen);
|
|
sep.str[dlen] = '\0';
|
|
}
|
|
}
|
|
|
|
if (nfargs >= 4)
|
|
{
|
|
// Evaluate output delimiter.
|
|
//
|
|
LBuf dbuf = LBuf_Src("ast_noeval_iter.osep");
|
|
UTF8 *dp = dbuf;
|
|
ast_eval_node(node->children[3].get(), dbuf, &dp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*dp = '\0';
|
|
size_t dlen = dp - dbuf;
|
|
if (dlen == 0)
|
|
{
|
|
osep.n = 1;
|
|
memcpy(osep.str, " ", 2);
|
|
}
|
|
else if (dlen == 2 && memcmp(dbuf.get(), "@@", 2) == 0)
|
|
{
|
|
osep.n = 0;
|
|
osep.str[0] = '\0';
|
|
}
|
|
else if (dlen == 2 && memcmp(dbuf.get(), "\r\n", 2) == 0)
|
|
{
|
|
osep.n = 2;
|
|
memcpy(osep.str, "\r\n", 3);
|
|
}
|
|
else if (dlen == 1)
|
|
{
|
|
osep.n = 1;
|
|
memcpy(osep.str, dbuf.get(), 2);
|
|
}
|
|
else if (dlen <= MAX_SEP_LEN)
|
|
{
|
|
osep.n = dlen;
|
|
memcpy(osep.str, dbuf.get(), dlen);
|
|
osep.str[dlen] = '\0';
|
|
}
|
|
}
|
|
|
|
// Evaluate the list (child[0]).
|
|
//
|
|
LBuf curr = LBuf_Src("ast_noeval_iter");
|
|
UTF8 *dp = curr;
|
|
ast_eval_node(node->children[0].get(), curr, &dp,
|
|
executor, caller, enactor,
|
|
eval|EV_STRIP_CURLY|EV_FCHECK|EV_EVAL, cargs, ncargs);
|
|
*dp = '\0';
|
|
|
|
size_t ncp;
|
|
UTF8 *cp = trim_space_sep_LEN(curr, dp - curr, sep, &ncp);
|
|
if (!*cp)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool first = true;
|
|
int number = 0;
|
|
bool bLoopInBounds = ( 0 <= mudstate.in_loop
|
|
&& mudstate.in_loop < MAX_ITEXT);
|
|
if (bLoopInBounds)
|
|
{
|
|
mudstate.itext[mudstate.in_loop] = nullptr;
|
|
mudstate.inum[mudstate.in_loop] = number;
|
|
}
|
|
mudstate.in_loop++;
|
|
|
|
while ( cp
|
|
&& mudstate.func_invk_ctr < mudconf.func_invk_lim
|
|
&& !alarm_clock.alarmed)
|
|
{
|
|
if (!first)
|
|
{
|
|
print_sep(osep, buff, bufc);
|
|
}
|
|
first = false;
|
|
number++;
|
|
UTF8 *objstring = split_token(&cp, sep);
|
|
if (bLoopInBounds)
|
|
{
|
|
mudstate.itext[mudstate.in_loop - 1] = objstring;
|
|
mudstate.inum[mudstate.in_loop - 1] = number;
|
|
}
|
|
|
|
// iter() body is collected through the FN_NOEVAL arg path and
|
|
// then re-evaluated per item. Preserve that boundary by
|
|
// routing through the deferred branch helper as well.
|
|
ast_eval_branch(node, 1, node->children[1].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
|
|
mudstate.in_loop--;
|
|
if (bLoopInBounds)
|
|
{
|
|
mudstate.itext[mudstate.in_loop] = nullptr;
|
|
mudstate.inum[mudstate.in_loop] = 0;
|
|
}
|
|
}
|
|
|
|
// ulambda(body, arg0, arg1, ...): anonymous function evaluation.
|
|
//
|
|
// The first argument (body) is received UNEVALUATED — the parser's
|
|
// deferred-eval mechanism prevents inner function calls from being
|
|
// dispatched. We reconstruct the raw text, then evaluate it with
|
|
// the remaining args as %0-%9 substitutions.
|
|
//
|
|
// The body can be:
|
|
// - #lambda/code → extract code after "#lambda/"
|
|
// - #apply[N]/func → synthesize func(%0,...,%N-1)
|
|
// - obj/attr → resolve attribute from object
|
|
// - attrname → resolve attribute from executor
|
|
//
|
|
static void ast_noeval_ulambda(const ASTNode *node,
|
|
UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
UNUSED_PARAMETER(caller);
|
|
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
if (nfargs < 1) return;
|
|
|
|
// Reconstruct the first argument from the raw deferred text.
|
|
// The deferred mechanism preserves the raw token text, bypassing
|
|
// function-call parsing of the body. We evaluate it with
|
|
// EV_STRIP_CURLY|EV_EVAL but WITHOUT EV_FCHECK so inner function
|
|
// names like mul(...) are emitted as literal text, not dispatched.
|
|
//
|
|
LBuf raw_arg0 = LBuf_Src("ulambda.arg0");
|
|
UTF8 *rp = raw_arg0;
|
|
|
|
const std::string *rawText = ast_call_raw_arg(node, 0);
|
|
if (rawText && !rawText->empty()) {
|
|
UTF8 *rawCopy = alloc_lbuf("ulambda.raw");
|
|
size_t rawLen = rawText->size();
|
|
if (rawLen > LBUF_SIZE - 1) rawLen = LBUF_SIZE - 1;
|
|
memcpy(rawCopy, rawText->c_str(), rawLen);
|
|
rawCopy[rawLen] = '\0';
|
|
|
|
mux_exec(rawCopy, rawLen, raw_arg0, &rp,
|
|
executor, executor, enactor,
|
|
EV_STRIP_CURLY | EV_EVAL,
|
|
cargs, ncargs);
|
|
free_lbuf(rawCopy);
|
|
}
|
|
*rp = '\0';
|
|
|
|
// Evaluate the remaining arguments (these are NOT deferred).
|
|
//
|
|
UTF8 *fargs[MAX_ARG];
|
|
memset(fargs, 0, sizeof(fargs));
|
|
fargs[0] = raw_arg0;
|
|
int real_nfargs = nfargs;
|
|
if (real_nfargs > MAX_ARG) real_nfargs = MAX_ARG;
|
|
for (int i = 1; i < real_nfargs; i++) {
|
|
fargs[i] = alloc_lbuf("ulambda.arg");
|
|
UTF8 *bp = fargs[i];
|
|
ast_eval_node(node->children[i].get(), fargs[i], &bp,
|
|
executor, executor, enactor,
|
|
eval | EV_FCHECK | EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
}
|
|
|
|
// Use parse_and_get_attrib to handle #lambda/, #apply/, obj/attr.
|
|
//
|
|
UTF8 *atext;
|
|
dbref thing;
|
|
dbref aowner;
|
|
int aflags;
|
|
if (!parse_and_get_attrib(executor, fargs, &atext, &thing,
|
|
&aowner, &aflags, buff, bufc))
|
|
{
|
|
for (int i = 1; i < real_nfargs; i++) free_lbuf(fargs[i]);
|
|
return;
|
|
}
|
|
|
|
// Mirror do_ufun (fun_u): a NO_EVAL attribute or a NO_EVAL object
|
|
// returns the attribute text literally instead of evaluating it
|
|
// (#786). This branch was the one piece of do_ufun the ulambda
|
|
// route was missing.
|
|
//
|
|
if ((aflags & AF_NOEVAL) || NoEval(thing))
|
|
{
|
|
size_t nLen = strlen(reinterpret_cast<const char *>(atext));
|
|
safe_copy_buf(atext, nLen, buff, bufc);
|
|
}
|
|
else
|
|
{
|
|
// Evaluate the body with fargs[1]... as %0, %1, ...
|
|
//
|
|
// Use mux_exec so the body JIT-compiles, mirroring do_ufun
|
|
// (fun_u). The cargs we pass here (&fargs[1]) populate %0-%9
|
|
// correctly: a depth-1 run_cached_program reads them from
|
|
// CARGS_BASE. This was previously routed through ast_exec to
|
|
// dodge a blob-internal float intrinsic bug (#778); now that
|
|
// #778 is fixed, the JIT path is safe.
|
|
//
|
|
mux_exec(atext, LBUF_SIZE - 1, buff, bufc, thing, executor, enactor,
|
|
AttrTrace(aflags, EV_FCHECK | EV_EVAL),
|
|
const_cast<const UTF8 **>(&fargs[1]), real_nfargs - 1);
|
|
}
|
|
free_lbuf(atext);
|
|
for (int i = 1; i < real_nfargs; i++) free_lbuf(fargs[i]);
|
|
}
|
|
|
|
// Dispatch table for native NOEVAL handling. Returns true if the
|
|
// function was handled natively (caller should skip generic dispatch).
|
|
//
|
|
static bool ast_try_native_noeval(const ASTNode *node,
|
|
UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
if (!node)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
switch (node->noeval_kind)
|
|
{
|
|
case ASTNOEVAL_IFELSE:
|
|
ast_noeval_ifelse(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs);
|
|
return true;
|
|
case ASTNOEVAL_SWITCH:
|
|
ast_noeval_switch(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, true);
|
|
return true;
|
|
case ASTNOEVAL_CASE:
|
|
ast_noeval_switch(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, false);
|
|
return true;
|
|
case ASTNOEVAL_SWITCHALL:
|
|
ast_noeval_switchall(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, true);
|
|
return true;
|
|
case ASTNOEVAL_CASEALL:
|
|
ast_noeval_switchall(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, false);
|
|
return true;
|
|
case ASTNOEVAL_ITER:
|
|
ast_noeval_iter(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs);
|
|
return true;
|
|
case ASTNOEVAL_CAND:
|
|
ast_noeval_cand(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, false);
|
|
return true;
|
|
case ASTNOEVAL_CANDBOOL:
|
|
ast_noeval_cand(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, true);
|
|
return true;
|
|
case ASTNOEVAL_COR:
|
|
ast_noeval_cor(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, false);
|
|
return true;
|
|
case ASTNOEVAL_CORBOOL:
|
|
ast_noeval_cor(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs, true);
|
|
return true;
|
|
case ASTNOEVAL_ULAMBDA:
|
|
ast_noeval_ulambda(node, buff, bufc, executor, caller, enactor,
|
|
eval, cargs, ncargs);
|
|
return true;
|
|
case ASTNOEVAL_NONE:
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static std::string ast_raw_arg_text(const ASTNode *callNode, int argIndex)
|
|
{
|
|
if (!callNode || argIndex < 0)
|
|
{
|
|
return "";
|
|
}
|
|
if (const std::string *raw = ast_call_raw_arg(callNode, argIndex))
|
|
{
|
|
return *raw;
|
|
}
|
|
if (argIndex < static_cast<int>(callNode->children.size()))
|
|
{
|
|
return ast_raw_text(callNode->children[argIndex].get());
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// Output a function call node as literal text: name(arg,arg,...).
|
|
//
|
|
static void ast_emit_literal_funccall(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
safe_str(reinterpret_cast<const UTF8 *>(node->text.c_str()), buff, bufc);
|
|
safe_chr('(', buff, bufc);
|
|
|
|
// The function name did not resolve — this is not a real function call.
|
|
// Strip EV_FCHECK so that nested FUNCCALL children are emitted as
|
|
// literal text, matching the classic evaluator's behavior where a
|
|
// failed function-call check consumes the FCHECK opportunity.
|
|
//
|
|
int childEval = eval & ~EV_FCHECK;
|
|
for (size_t i = 0; i < node->children.size(); i++)
|
|
{
|
|
if (i > 0) safe_chr(',', buff, bufc);
|
|
ast_eval_node(node->children[i].get(), buff, bufc,
|
|
executor, caller, enactor, childEval, cargs, ncargs);
|
|
}
|
|
safe_chr(')', buff, bufc);
|
|
}
|
|
|
|
// Evaluate a function call node (AST_FUNCCALL).
|
|
//
|
|
static void ast_eval_funccall(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
// EV_FCHECK without EV_FMAND means "check if the first ( is a
|
|
// function call." If EV_FCHECK has been stripped (by the SEQUENCE
|
|
// handler after the first child), this function call is not at
|
|
// the start of the expression — output as literal text.
|
|
//
|
|
if (!(eval & EV_FCHECK) && !(eval & EV_FMAND))
|
|
{
|
|
ast_emit_literal_funccall(node, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
return;
|
|
}
|
|
|
|
|
|
// Missing closing ')' — the function is NOT dispatched. Output
|
|
// the function name and '(' literally, then evaluate the argument
|
|
// children (%-substitutions must be resolved, matching the classic
|
|
// parser which evaluates as it scans).
|
|
//
|
|
if (!node->has_close_paren)
|
|
{
|
|
safe_str(reinterpret_cast<const UTF8 *>(node->text.c_str()), buff, bufc);
|
|
safe_chr('(', buff, bufc);
|
|
for (size_t i = 0; i < node->children.size(); i++)
|
|
{
|
|
if (i > 0) safe_chr(',', buff, bufc);
|
|
ast_eval_node(node->children[i].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Uppercase the function name for lookup.
|
|
//
|
|
size_t nName = node->text.size();
|
|
if (nName == 0 || nName > MAX_UFUN_NAME_LEN)
|
|
{
|
|
ast_emit_literal_funccall(node, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
return;
|
|
}
|
|
|
|
LBuf TempFun = LBuf_Src("ast_eval_funccall");
|
|
memcpy(TempFun, node->text.c_str(), nName);
|
|
TempFun[nName] = '\0';
|
|
size_t nUpper;
|
|
UTF8 *pUpper = mux_strupr(TempFun, nUpper);
|
|
if (nUpper >= LBUF_SIZE)
|
|
{
|
|
nUpper = LBUF_SIZE - 1;
|
|
}
|
|
memcpy(TempFun, pUpper, nUpper);
|
|
TempFun[nUpper] = '\0';
|
|
|
|
std::vector<UTF8> name_key(TempFun.get(), TempFun.get() + nUpper);
|
|
FUN *fp = nullptr;
|
|
UFUN *ufp = nullptr;
|
|
|
|
const auto it = mudstate.builtin_functions.find(name_key);
|
|
if (it != mudstate.builtin_functions.end())
|
|
{
|
|
fp = it->second;
|
|
}
|
|
|
|
if (!fp)
|
|
{
|
|
auto it_ufunc = mudstate.ufunc_htab.find(name_key);
|
|
ufp = (it_ufunc != mudstate.ufunc_htab.end())
|
|
? static_cast<UFUN*>(it_ufunc->second) : nullptr;
|
|
}
|
|
|
|
if (!fp && !ufp)
|
|
{
|
|
if (eval & EV_FMAND)
|
|
{
|
|
safe_str(S_("#-1 FUNCTION ("), buff, bufc);
|
|
safe_str(TempFun, buff, bufc);
|
|
safe_str(T(") NOT FOUND"), buff, bufc);
|
|
s_fmand_abort = true;
|
|
}
|
|
else
|
|
{
|
|
ast_emit_literal_funccall(node, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check invocation limits.
|
|
//
|
|
mudstate.func_nest_lev++;
|
|
mudstate.func_invk_ctr++;
|
|
|
|
UTF8 *oldp = *bufc;
|
|
|
|
if (mudconf.func_nest_lim <= mudstate.func_nest_lev)
|
|
{
|
|
safe_str(S_("#-1 FUNCTION RECURSION LIMIT EXCEEDED"), buff, bufc);
|
|
}
|
|
else if (mudconf.func_invk_lim <= mudstate.func_invk_ctr)
|
|
{
|
|
safe_str(S_("#-1 FUNCTION INVOCATION LIMIT EXCEEDED"), buff, bufc);
|
|
}
|
|
else if (Going(executor))
|
|
{
|
|
safe_str(S_("#-1 BAD EXECUTOR"), buff, bufc);
|
|
}
|
|
else if (!check_access(executor, ufp ? ufp->perms : fp->perms))
|
|
{
|
|
safe_noperm(buff, bufc);
|
|
}
|
|
else if (ufp && (ufp->flags & FN_RESTRICT) && !Wizard(executor))
|
|
{
|
|
safe_noperm(buff, bufc);
|
|
}
|
|
else if (alarm_clock.alarmed)
|
|
{
|
|
safe_str(S_("#-1 CPU LIMITED"), buff, bufc);
|
|
}
|
|
else if (ufp)
|
|
{
|
|
// User-defined function — fetch attribute and evaluate.
|
|
//
|
|
dbref aowner;
|
|
int aflags;
|
|
UTF8 *tbuf = atr_get("ast_eval.ufun", ufp->obj, ufp->atr,
|
|
&aowner, &aflags);
|
|
|
|
dbref obj = (ufp->flags & FN_PRIV) ? ufp->obj : executor;
|
|
|
|
int nfargs = static_cast<int>(node->children.size());
|
|
if (nfargs > MAX_ARG)
|
|
{
|
|
nfargs = MAX_ARG;
|
|
}
|
|
|
|
// Evaluate arguments for UFUN.
|
|
//
|
|
UTF8 *fargs[MAX_ARG];
|
|
memset(fargs, 0, sizeof(fargs));
|
|
for (int i = 0; i < nfargs; i++)
|
|
{
|
|
fargs[i] = alloc_lbuf("ast_eval.ufun.arg");
|
|
UTF8 *bp = fargs[i];
|
|
ast_eval_node(node->children[i].get(), fargs[i], &bp,
|
|
executor, caller, enactor,
|
|
eval | EV_FCHECK | EV_EVAL, cargs, ncargs);
|
|
*bp = '\0';
|
|
}
|
|
|
|
if ((aflags & AF_NOEVAL) || NoEval(ufp->obj))
|
|
{
|
|
size_t nLen = strlen(reinterpret_cast<const char *>(tbuf));
|
|
safe_copy_buf(tbuf, nLen, buff, bufc);
|
|
}
|
|
else
|
|
{
|
|
reg_ref **preserve = nullptr;
|
|
if (ufp->flags & FN_PRES)
|
|
{
|
|
preserve = PushRegisters(MAX_GLOBAL_REGS);
|
|
save_global_regs(preserve);
|
|
}
|
|
|
|
int feval = eval & ~(EV_TOP | EV_FMAND);
|
|
mux_exec(tbuf, LBUF_SIZE-1, buff, bufc, obj, executor, enactor,
|
|
AttrTrace(aflags, feval),
|
|
const_cast<const UTF8 **>(fargs), nfargs);
|
|
|
|
if (ufp->flags & FN_PRES)
|
|
{
|
|
restore_global_regs(preserve);
|
|
PopRegisters(preserve, MAX_GLOBAL_REGS);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < nfargs; i++)
|
|
{
|
|
free_lbuf(fargs[i]);
|
|
}
|
|
free_lbuf(tbuf);
|
|
}
|
|
else
|
|
{
|
|
// Built-in function.
|
|
//
|
|
// The AST parser always splits on commas, but mux_exec uses
|
|
// maxArgsParsed to limit splitting — excess args are catenated
|
|
// (with commas) into the last slot. Apply the same logic here.
|
|
//
|
|
int nParsed = static_cast<int>(node->children.size());
|
|
int nfargs = nParsed;
|
|
if (nfargs > fp->maxArgsParsed && fp->maxArgsParsed > 0)
|
|
{
|
|
nfargs = fp->maxArgsParsed;
|
|
}
|
|
if (nfargs > MAX_ARG)
|
|
{
|
|
nfargs = MAX_ARG;
|
|
}
|
|
|
|
if ( fp->minArgs <= nfargs
|
|
&& nfargs <= fp->maxArgs
|
|
&& !alarm_clock.alarmed)
|
|
{
|
|
// Try native NOEVAL handlers first.
|
|
//
|
|
if ( (fp->flags & FN_NOEVAL)
|
|
&& ast_try_native_noeval(node, buff, bufc,
|
|
executor, caller, enactor,
|
|
eval, cargs, ncargs))
|
|
{
|
|
mudstate.func_nest_lev--;
|
|
return;
|
|
}
|
|
|
|
UTF8 *fargs[MAX_ARG];
|
|
memset(fargs, 0, sizeof(fargs));
|
|
int feval;
|
|
|
|
if (fp->flags & FN_NOEVAL)
|
|
{
|
|
// NOEVAL functions receive raw text. The handler
|
|
// calls mux_exec internally on args it wants to
|
|
// evaluate.
|
|
//
|
|
feval = eval & ~(EV_EVAL | EV_TOP | EV_FMAND | EV_STRIP_CURLY);
|
|
for (int i = 0; i < nfargs; i++)
|
|
{
|
|
fargs[i] = alloc_lbuf("ast_eval.noeval");
|
|
|
|
if (i < nfargs - 1 || nParsed <= nfargs)
|
|
{
|
|
std::string raw = ast_raw_arg_text(node, i);
|
|
size_t len = raw.size();
|
|
if (len >= LBUF_SIZE) len = LBUF_SIZE - 1;
|
|
memcpy(fargs[i], raw.c_str(), len);
|
|
fargs[i][len] = '\0';
|
|
}
|
|
else
|
|
{
|
|
// Catenate remaining children with commas.
|
|
//
|
|
UTF8 *bp = fargs[i];
|
|
for (int j = i; j < nParsed; j++)
|
|
{
|
|
if (j > i) safe_chr(',', fargs[i], &bp);
|
|
std::string raw = ast_raw_arg_text(node, j);
|
|
safe_str(reinterpret_cast<const UTF8 *>(raw.c_str()),
|
|
fargs[i], &bp);
|
|
}
|
|
*bp = '\0';
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Normal functions: evaluate each argument.
|
|
//
|
|
feval = eval & ~(EV_TOP | EV_FMAND);
|
|
for (int i = 0; i < nfargs; i++)
|
|
{
|
|
fargs[i] = alloc_lbuf("ast_eval.arg");
|
|
UTF8 *bp = fargs[i];
|
|
|
|
if (i < nfargs - 1 || nParsed <= nfargs)
|
|
{
|
|
ast_eval_argument(node->children[i].get(), fargs[i], &bp,
|
|
executor, caller, enactor,
|
|
feval | EV_FCHECK | EV_EVAL, cargs, ncargs);
|
|
}
|
|
else
|
|
{
|
|
// Catenate remaining children with commas.
|
|
//
|
|
for (int j = i; j < nParsed; j++)
|
|
{
|
|
if (j > i) safe_chr(',', fargs[i], &bp);
|
|
ast_eval_argument(node->children[j].get(), fargs[i], &bp,
|
|
executor, caller, enactor,
|
|
feval | EV_FCHECK | EV_EVAL, cargs, ncargs);
|
|
}
|
|
}
|
|
*bp = '\0';
|
|
}
|
|
}
|
|
|
|
fp->fun(fp, buff, &oldp, executor, caller, enactor,
|
|
feval & EV_TRACE, fargs, nfargs, cargs, ncargs);
|
|
*bufc = oldp;
|
|
|
|
for (int i = 0; i < nfargs; i++)
|
|
{
|
|
free_lbuf(fargs[i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Wrong argument count.
|
|
//
|
|
if (fp->minArgs == fp->maxArgs)
|
|
{
|
|
safe_tprintf_str(buff, bufc,
|
|
S_("#-1 FUNCTION (%s) EXPECTS %d ARGUMENTS"),
|
|
fp->name, fp->minArgs);
|
|
}
|
|
else if (fp->minArgs + 1 == fp->maxArgs)
|
|
{
|
|
safe_tprintf_str(buff, bufc,
|
|
S_("#-1 FUNCTION (%s) EXPECTS %d OR %d ARGUMENTS"),
|
|
fp->name, fp->minArgs, fp->maxArgs);
|
|
}
|
|
else
|
|
{
|
|
safe_tprintf_str(buff, bufc,
|
|
S_("#-1 FUNCTION (%s) EXPECTS BETWEEN %d AND %d ARGUMENTS"),
|
|
fp->name, fp->minArgs, fp->maxArgs);
|
|
}
|
|
}
|
|
}
|
|
|
|
mudstate.func_nest_lev--;
|
|
}
|
|
|
|
// Hard cap on AST evaluator C-stack recursion. This is independent of
|
|
// mudconf.func_nest_lim (function call depth) and mudconf.nStackLimit
|
|
// (softcode bracket nesting). It exists to keep adversarial or
|
|
// pathologically deep ASTs — e.g., `[[[[...x]]]]` with thousands of
|
|
// bracket layers, or deeply chained `if()`/`switch()` constructs —
|
|
// from blowing the native C stack before the soft limits trip on a
|
|
// subsequent re-entry into mux_exec.
|
|
//
|
|
// Each AST_EVALBRACKET/AST_BRACEGROUP layer adds ~2 frames (bracket →
|
|
// sequence → inner bracket), so a cap of 400 bounds the stack around
|
|
// 800 frames — well under a default 8 MiB Linux stack and safe on
|
|
// platforms with smaller defaults.
|
|
//
|
|
static constexpr int AST_EVAL_MAX_DEPTH = 400;
|
|
static thread_local int s_ast_eval_depth = 0;
|
|
|
|
class AstEvalDepthGuard
|
|
{
|
|
public:
|
|
AstEvalDepthGuard() { s_ast_eval_depth++; }
|
|
~AstEvalDepthGuard() { s_ast_eval_depth--; }
|
|
bool overflow() const { return s_ast_eval_depth > AST_EVAL_MAX_DEPTH; }
|
|
};
|
|
|
|
// Evaluate a single AST node into buff/bufc.
|
|
//
|
|
static void ast_eval_node(const ASTNode *node, UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
if (!node || alarm_clock.alarmed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AstEvalDepthGuard depth_guard;
|
|
if (depth_guard.overflow())
|
|
{
|
|
mudstate.bStackLimitReached = true;
|
|
return;
|
|
}
|
|
|
|
// Softcode execution trace (#1023). Record (source -> result) for the
|
|
// call boundaries -- function calls and eval brackets -- when tracing is
|
|
// armed. These are the subexpressions a builder debugging code cares
|
|
// about; the change-filter in tcache_add drops no-op nodes. Off-path
|
|
// cost is a single bit test, since EV_TRACE is only set while tracing.
|
|
//
|
|
const bool bTrace =
|
|
(eval & EV_TRACE) && !(eval & EV_NOTRACE)
|
|
&& ( AST_FUNCCALL == node->type
|
|
|| AST_EVALBRACKET == node->type);
|
|
UTF8 *pTraceStart = bTrace ? *bufc : nullptr;
|
|
|
|
switch (node->type)
|
|
{
|
|
case AST_LITERAL:
|
|
safe_str(reinterpret_cast<const UTF8 *>(node->text.c_str()), buff, bufc);
|
|
break;
|
|
|
|
case AST_SPACE:
|
|
if ( mudconf.space_compress
|
|
&& !(eval & EV_NO_COMPRESS))
|
|
{
|
|
// Space compression: emit a single space regardless of
|
|
// how many whitespace characters are in the source.
|
|
//
|
|
safe_chr(' ', buff, bufc);
|
|
}
|
|
else
|
|
{
|
|
safe_str(reinterpret_cast<const UTF8 *>(node->text.c_str()), buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case AST_SEMICOLON:
|
|
safe_chr(';', buff, bufc);
|
|
break;
|
|
|
|
case AST_SUBST:
|
|
if (eval & EV_EVAL)
|
|
{
|
|
ast_eval_subst(node, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
else
|
|
{
|
|
// Without EV_EVAL, pass through literally.
|
|
//
|
|
safe_str(reinterpret_cast<const UTF8 *>(node->text.c_str()), buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case AST_ESCAPE:
|
|
// Output the escaped character (skip the backslash).
|
|
//
|
|
if (node->text.size() > 1)
|
|
{
|
|
safe_chr(node->text[1], buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case AST_FUNCCALL:
|
|
if (eval & EV_FCHECK)
|
|
{
|
|
ast_eval_funccall(node, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
else
|
|
{
|
|
// Without EV_FCHECK, output as literal text.
|
|
//
|
|
safe_str(reinterpret_cast<const UTF8 *>(node->text.c_str()), buff, bufc);
|
|
safe_chr('(', buff, bufc);
|
|
for (size_t i = 0; i < node->children.size(); i++)
|
|
{
|
|
if (i > 0) safe_chr(',', buff, bufc);
|
|
ast_eval_node(node->children[i].get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
safe_chr(')', buff, bufc);
|
|
}
|
|
break;
|
|
|
|
case AST_EVALBRACKET:
|
|
if (eval & EV_NOFCHECK)
|
|
{
|
|
// Brackets suppressed — pass through as literal text.
|
|
//
|
|
safe_chr('[', buff, bufc);
|
|
if (!node->children.empty())
|
|
{
|
|
std::string raw = ast_raw_text(node->children[0].get());
|
|
safe_str(reinterpret_cast<const UTF8 *>(raw.c_str()), buff, bufc);
|
|
}
|
|
safe_chr(']', buff, bufc);
|
|
}
|
|
else
|
|
{
|
|
// Evaluate contents with function checking enabled.
|
|
//
|
|
mudstate.nStackNest++;
|
|
if (!node->children.empty())
|
|
{
|
|
// Confine the abort signal to this region (#1247): an
|
|
// inner [...] that fails must not truncate the region
|
|
// containing it, and neither must a failure inside an
|
|
// attribute reached from here.
|
|
//
|
|
bool saved_abort = s_fmand_abort;
|
|
s_fmand_abort = false;
|
|
ast_eval_node(node->children[0].get(), buff, bufc,
|
|
executor, caller, enactor,
|
|
eval | EV_FCHECK | EV_FMAND, cargs, ncargs);
|
|
s_fmand_abort = saved_abort;
|
|
}
|
|
mudstate.nStackNest--;
|
|
}
|
|
break;
|
|
|
|
case AST_BRACEGROUP:
|
|
mudstate.nStackNest++;
|
|
if (eval & EV_STRIP_CURLY)
|
|
{
|
|
// Strip braces and evaluate contents without
|
|
// function checking.
|
|
//
|
|
int innerEval = eval & ~(EV_STRIP_CURLY | EV_FCHECK | EV_FMAND);
|
|
if (!node->children.empty())
|
|
{
|
|
ast_eval_node(node->children[0].get(), buff, bufc,
|
|
executor, caller, enactor, innerEval, cargs, ncargs);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Pass through as literal braces.
|
|
//
|
|
safe_chr('{', buff, bufc);
|
|
if (!node->children.empty())
|
|
{
|
|
int innerEval = eval & ~(EV_TOP | EV_FMAND);
|
|
if (eval & EV_EVAL)
|
|
{
|
|
innerEval = innerEval & ~(EV_STRIP_CURLY | EV_FCHECK | EV_FMAND);
|
|
ast_eval_node(node->children[0].get(), buff, bufc,
|
|
executor, caller, enactor, innerEval, cargs, ncargs);
|
|
}
|
|
else
|
|
{
|
|
innerEval = (innerEval & ~EV_FCHECK) | EV_NOFCHECK;
|
|
ast_eval_node(node->children[0].get(), buff, bufc,
|
|
executor, caller, enactor, innerEval, cargs, ncargs);
|
|
}
|
|
}
|
|
safe_chr('}', buff, bufc);
|
|
}
|
|
mudstate.nStackNest--;
|
|
break;
|
|
|
|
case AST_SEQUENCE:
|
|
{
|
|
size_t first = 0;
|
|
size_t count = node->children.size();
|
|
size_t last = count;
|
|
|
|
if ( mudconf.space_compress
|
|
&& !(eval & EV_NO_COMPRESS))
|
|
{
|
|
while ( last > first
|
|
&& ast_is_malformed_qsubst(node->children[last - 1].get()))
|
|
{
|
|
last--;
|
|
}
|
|
// Skip leading and trailing AST_SPACE children.
|
|
// This matches mux_exec's at_space=1 (suppress leading)
|
|
// and trailing-space strip behavior, without touching
|
|
// spaces generated by function output.
|
|
//
|
|
while (first < count && node->children[first]->type == AST_SPACE)
|
|
{
|
|
first++;
|
|
}
|
|
while (last > first && node->children[last - 1]->type == AST_SPACE)
|
|
{
|
|
last--;
|
|
}
|
|
}
|
|
|
|
ast_eval_sequence_children(node, first, last, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Trace emit (#1023): record this call boundary's (source -> result).
|
|
// tcache_add takes ownership of pOrig and applies the change-filter.
|
|
//
|
|
if (bTrace)
|
|
{
|
|
std::string src = ast_raw_text(node);
|
|
UTF8 *pOrig = alloc_lbuf("ast_trace.orig");
|
|
size_t nSrc = src.size();
|
|
if (nSrc > LBUF_SIZE - 1)
|
|
{
|
|
nSrc = LBUF_SIZE - 1;
|
|
}
|
|
memcpy(pOrig, src.data(), nSrc);
|
|
pOrig[nSrc] = '\0';
|
|
|
|
// The result span [pTraceStart, *bufc) is not NUL-terminated yet.
|
|
//
|
|
UTF8 chSaved = **bufc;
|
|
**bufc = '\0';
|
|
tcache_add(executor, pOrig, pTraceStart);
|
|
**bufc = chSaved;
|
|
|
|
// Bottom-up mode flushes each line as its subexpression completes
|
|
// (innermost first); top-down accumulates and flushes at the outermost
|
|
// eval (mux_exec).
|
|
//
|
|
if (!mudconf.trace_topdown)
|
|
{
|
|
tcache_finish();
|
|
}
|
|
}
|
|
}
|
|
|
|
void ast_exec(const UTF8 *pStr, size_t nStr,
|
|
UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
if ( nullptr == pStr
|
|
|| '\0' == pStr[0]
|
|
|| alarm_clock.alarmed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
size_t nLen = strlen(reinterpret_cast<const char *>(pStr));
|
|
if (nLen > nStr)
|
|
{
|
|
nLen = nStr;
|
|
}
|
|
|
|
auto ast = ast_parse_string(pStr, nLen);
|
|
if (!ast)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ast_eval_node(ast.get(), buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// AST parse cache
|
|
// ---------------------------------------------------------------
|
|
//
|
|
// LRU cache of parsed AST trees, keyed by expression text.
|
|
// Parsing is pure (no side effects), so cached ASTs are safe
|
|
// to share across evaluations with different contexts.
|
|
//
|
|
|
|
struct ASTCacheEntry
|
|
{
|
|
std::shared_ptr<ASTNode> ast;
|
|
std::list<std::string>::iterator lru_it;
|
|
};
|
|
|
|
static std::unordered_map<std::string, ASTCacheEntry> s_astCache;
|
|
static std::list<std::string> s_astLru;
|
|
static const size_t AST_CACHE_MAX = 1024;
|
|
static const size_t AST_CACHE_MIN_LEN = 16;
|
|
|
|
// ---------------------------------------------------------------
|
|
// Stamp for the memoized jit_can_handle() verdict (#2068).
|
|
//
|
|
// The verdict is NOT a pure function of the AST, which is the trap in
|
|
// "the tree is cached, so cache the answer". It also depends on:
|
|
//
|
|
// 1. mudconf.jit_eval_brackets -- a runtime toggle, and one the smoke
|
|
// suite deliberately runs both ways. Carried in the low bit rather
|
|
// than hooked, since it is a plain read.
|
|
// 2. mudstate.ufunc_htab -- @function and @function/delete.
|
|
// 3. mudstate.builtin_functions -- module (de)registration, and the
|
|
// function_alias config directive.
|
|
//
|
|
// (2) and (3) bump the epoch. Getting this wrong would fail silently and
|
|
// in the dangerous direction: a name that did not resolve when the verdict
|
|
// was taken, and does now, would leave a cached "decline" -- or worse, a
|
|
// cached "accept" for a name that has since gone away.
|
|
//
|
|
// The epoch moves in steps of two so the low bit stays the toggle's. A
|
|
// stamp of 0 means "never computed", which the epoch never produces.
|
|
//
|
|
static uint32_t s_jit_gate_epoch = 2;
|
|
|
|
void jit_gate_note_function_table_change(void)
|
|
{
|
|
s_jit_gate_epoch += 2;
|
|
if (0 == s_jit_gate_epoch)
|
|
{
|
|
// Wrapped. Skip the reserved "never computed" value; every live
|
|
// stamp simply misses once and recomputes.
|
|
//
|
|
s_jit_gate_epoch = 2;
|
|
}
|
|
#if defined(TINYMUX_JIT)
|
|
// #2130 decline memo: a bail_noop verdict is pure in the compiled
|
|
// shape, but registration can change what that shape is on the next
|
|
// compile. Clear here with the gate epoch so both memos move together
|
|
// (#2140 review).
|
|
//
|
|
jit_decline_memo_invalidate();
|
|
#endif
|
|
}
|
|
|
|
static inline uint32_t jit_gate_stamp_now(void)
|
|
{
|
|
return s_jit_gate_epoch | (mudconf.jit_eval_brackets ? 1u : 0u);
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------
|
|
// mux_exec — drop-in replacement for mux_exec
|
|
// ---------------------------------------------------------------
|
|
//
|
|
// Parse into AST (with caching), then evaluate via ast_eval_node.
|
|
//
|
|
void mux_exec(const UTF8 *pStr, size_t nStr,
|
|
UTF8 *buff, UTF8 **bufc,
|
|
dbref executor, dbref caller, dbref enactor,
|
|
int eval, const UTF8 *cargs[], int ncargs)
|
|
{
|
|
if ( nullptr == pStr
|
|
|| '\0' == pStr[0]
|
|
|| alarm_clock.alarmed)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Stack limit checking.
|
|
//
|
|
if (mudconf.nStackLimit < mudstate.nStackNest)
|
|
{
|
|
mudstate.bStackLimitReached = true;
|
|
return;
|
|
}
|
|
|
|
// nStr is a buffer-size limit, not the string length.
|
|
// Use the actual string length for caching and parsing.
|
|
//
|
|
size_t nLen = strlen(reinterpret_cast<const char *>(pStr));
|
|
if (nLen > nStr)
|
|
{
|
|
nLen = nStr;
|
|
}
|
|
|
|
// Look up in the parse cache.
|
|
//
|
|
const ASTNode *ast_ptr;
|
|
std::shared_ptr<ASTNode> cache_holder;
|
|
std::unique_ptr<ASTNode> parse_holder;
|
|
|
|
if (nLen >= AST_CACHE_MIN_LEN)
|
|
{
|
|
std::string key(reinterpret_cast<const char *>(pStr), nLen);
|
|
auto it = s_astCache.find(key);
|
|
if (it != s_astCache.end())
|
|
{
|
|
// Cache hit — move to front of LRU.
|
|
//
|
|
cache_holder = it->second.ast;
|
|
s_astLru.splice(s_astLru.begin(), s_astLru, it->second.lru_it);
|
|
ast_ptr = cache_holder.get();
|
|
}
|
|
else
|
|
{
|
|
// Cache miss — parse and insert.
|
|
//
|
|
cache_holder = std::shared_ptr<ASTNode>(
|
|
ast_parse_string(pStr, nLen).release());
|
|
ast_ptr = cache_holder.get();
|
|
|
|
// Evict LRU entries if cache is full.
|
|
//
|
|
while (s_astCache.size() >= AST_CACHE_MAX)
|
|
{
|
|
s_astCache.erase(s_astLru.back());
|
|
s_astLru.pop_back();
|
|
}
|
|
|
|
s_astLru.push_front(key);
|
|
s_astCache[key] = {cache_holder, s_astLru.begin()};
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Short expressions — parse without caching.
|
|
//
|
|
parse_holder = ast_parse_string(pStr, nLen);
|
|
ast_ptr = parse_holder.get();
|
|
}
|
|
|
|
// Softcode execution trace (TRACE flag / EV_TRACE). Reconnects the
|
|
// trace emitter that the AST/JIT evaluator migration orphaned (#1023).
|
|
// Two things follow from tracing being armed: the accumulated
|
|
// (input -> output) lines are bracketed and flushed to the owner at the
|
|
// outermost evaluation, and the JIT is bypassed for this call -- a
|
|
// compiled blob has no per-node frames for ast_eval_node's hook to
|
|
// observe, so traced evaluation must run on the AST interpreter.
|
|
//
|
|
const bool is_trace =
|
|
(Trace(executor) || (eval & EV_TRACE)) && !(eval & EV_NOTRACE);
|
|
bool is_top = false;
|
|
if (is_trace)
|
|
{
|
|
is_top = tcache_empty();
|
|
eval |= EV_TRACE; // propagate to the whole subtree
|
|
}
|
|
|
|
// Evaluate the AST.
|
|
//
|
|
#if defined(TINYMUX_JIT)
|
|
// Only JIT expressions whose %-substitutions are fully supported.
|
|
// Currently: %0-%9, %b, %r, %t. Unsupported: %#, %!, %n, %l, %q,
|
|
// %c, %x, etc. Quick scan: if any % is followed by something
|
|
// we don't handle, fall back to AST.
|
|
//
|
|
// Check if the AST is a pure expression the JIT can compile:
|
|
// - Must be a single function call, eval-bracket, or sequence of
|
|
// function calls. NOT mixed literal+command text.
|
|
// - No unsupported %-substitutions.
|
|
//
|
|
auto jit_can_handle_compute = [&]() -> bool {
|
|
if (!ast_ptr) return false;
|
|
|
|
// The AST root must be a function call, eval bracket, or
|
|
// sequence of function calls/literals. If it's just a literal
|
|
// (plain text), the JIT adds no value.
|
|
if (ast_ptr->type == AST_LITERAL || ast_ptr->type == AST_SPACE)
|
|
return false;
|
|
|
|
// Conservative parser-parity guard:
|
|
// - eval brackets have subtle re-evaluation behavior
|
|
// - unterminated calls must match legacy literal/parsing behavior
|
|
//
|
|
// Keep these on the classic evaluator until JIT semantics match
|
|
// exactly.
|
|
//
|
|
// Also require at least one function call somewhere in the tree.
|
|
// A multi-token expression with no calls (e.g. plain command text
|
|
// such as "+jobs/select monitor") parses as a SEQUENCE of literal
|
|
// and space nodes whose root is neither AST_LITERAL nor AST_SPACE,
|
|
// so the single-node check above does not catch it. Compiling such
|
|
// pure-literal text adds no value, yet it populates the compile
|
|
// cache with a constant-folded passthrough whose result does not
|
|
// round-trip through SQLite persistence (the folded output can
|
|
// reference the source buffer rather than the persisted string
|
|
// pool), reloading later as an empty string. Keep all
|
|
// function-free text on the classic evaluator.
|
|
bool saw_funccall = false;
|
|
std::vector<const ASTNode *> work;
|
|
work.push_back(ast_ptr);
|
|
while (!work.empty()) {
|
|
const ASTNode *node = work.back();
|
|
work.pop_back();
|
|
|
|
if (node->type == AST_EVALBRACKET) {
|
|
// Terminated brackets are JITtable when the toggle is
|
|
// on: hir_lower evaluates their contents in FMAND
|
|
// context, and the Phase 2-3 q-register coherence work
|
|
// (slot resync, tracking save/restore, read
|
|
// materialization) covers the scoping semantics that
|
|
// made the original unconditional bail load-bearing.
|
|
// Unterminated brackets keep legacy literal parsing —
|
|
// always bail. The EV_NOFCHECK literal-passthrough
|
|
// mode is gated at the call site
|
|
// (docs/plan-jit-evalbracket-lift.md, Phase 4).
|
|
if (!mudconf.jit_eval_brackets
|
|
|| !node->has_close_bracket) {
|
|
return false;
|
|
}
|
|
}
|
|
if (node->type == AST_FUNCCALL) {
|
|
saw_funccall = true;
|
|
if (!node->has_close_paren) {
|
|
return false;
|
|
}
|
|
|
|
// Bail if the function name doesn't resolve. Unresolved
|
|
// names (e.g., "eobject=strmatch" from search eval args)
|
|
// require the AST's EV_FCHECK stripping in
|
|
// ast_emit_literal_funccall; the JIT doesn't replicate
|
|
// that behavior.
|
|
//
|
|
std::string upper = node->text;
|
|
for (auto &ch : upper) {
|
|
ch = static_cast<char>(toupper(
|
|
static_cast<unsigned char>(ch)));
|
|
}
|
|
std::vector<UTF8> name_key(upper.begin(), upper.end());
|
|
if (mudstate.builtin_functions.find(name_key)
|
|
== mudstate.builtin_functions.end()
|
|
&& mudstate.ufunc_htab.find(name_key)
|
|
== mudstate.ufunc_htab.end())
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
for (const auto &child : node->children) {
|
|
if (child) {
|
|
work.push_back(child.get());
|
|
}
|
|
}
|
|
}
|
|
|
|
// setq/setr: now supported via ECALL_SETQ write-through
|
|
// (writes to both SUBST slot and mudstate.global_regs).
|
|
|
|
// All standard %-substitutions and # references are now handled
|
|
// by the compiler. No scanning needed — the AST parser creates
|
|
// the correct node types, and the compiler resolves each one
|
|
// at compile time or via ECALL/SUBST slots at runtime.
|
|
|
|
// Pure literal/space text (no function calls anywhere): leave it on
|
|
// the classic evaluator. See the saw_funccall note above.
|
|
if (!saw_funccall) {
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Memoized gate (#2068). The walk above is the flat tax this removes:
|
|
// a worklist allocation per evaluation, plus a std::string, a
|
|
// std::vector<UTF8> and two hash lookups per call node -- paid on every
|
|
// evaluation, including ones the JIT declines and ones where it wins.
|
|
//
|
|
// The stamp covers everything the verdict depends on beyond the tree
|
|
// itself; see jit_gate_stamp_now().
|
|
//
|
|
auto jit_can_handle = [&]() -> bool {
|
|
if (!ast_ptr) {
|
|
return false;
|
|
}
|
|
const uint32_t stamp = jit_gate_stamp_now();
|
|
if (ast_ptr->jit_gate_stamp == stamp) {
|
|
return ast_ptr->jit_gate_verdict;
|
|
}
|
|
const bool bVerdict = jit_can_handle_compute();
|
|
ast_ptr->jit_gate_stamp = stamp;
|
|
ast_ptr->jit_gate_verdict = bVerdict;
|
|
return bVerdict;
|
|
};
|
|
|
|
if (nLen >= 8
|
|
&& (eval & EV_EVAL)
|
|
// Under EV_NOFCHECK, eval brackets pass through as literal
|
|
// [...] text (the ast.cpp literal path); the JIT lowerer
|
|
// always evaluates bracket contents, so NOFCHECK text must
|
|
// stay on the AST evaluator.
|
|
&& !(eval & EV_NOFCHECK)
|
|
&& !alarm_clock.alarmed
|
|
&& !is_trace // traced evaluation must run on the AST interpreter
|
|
&& jit_can_handle())
|
|
{
|
|
if (jit_eval(pStr, nLen, buff, bufc,
|
|
executor, caller, enactor,
|
|
eval, cargs, ncargs))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
#endif
|
|
ast_eval_node(ast_ptr, buff, bufc,
|
|
executor, caller, enactor, eval, cargs, ncargs);
|
|
|
|
// Flush accumulated trace lines to the owner at the outermost eval.
|
|
// ast_eval_node returns normally on alarm/stack-limit, so this is
|
|
// always reached for the top-level call.
|
|
//
|
|
if (is_top)
|
|
{
|
|
// trace_limit caps stored lines in top-down mode; tell the owner how
|
|
// many were dropped (2.13 parity). Read the overflow before finish
|
|
// resets the counter.
|
|
//
|
|
const int nDropped =
|
|
mudconf.trace_topdown ? tcache_dropped_count() : 0;
|
|
tcache_finish();
|
|
if (0 < nDropped)
|
|
{
|
|
notify(executor,
|
|
tprintf(T("%d lines of trace output discarded."), nDropped));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------
|
|
// asteval(): softcode function — evaluate via AST evaluator only.
|
|
//
|
|
// Bypasses the JIT entirely. Useful for:
|
|
// - Benchmarking AST vs JIT performance
|
|
// - Fallback if users find a JIT parity bug
|
|
// - Verifying parser correctness
|
|
//
|
|
// Usage: think asteval(add(1,2)) → 3
|
|
// ---------------------------------------------------------------
|
|
|
|
FUNCTION(fun_asteval)
|
|
{
|
|
UNUSED_PARAMETER(fp);
|
|
UNUSED_PARAMETER(cargs);
|
|
UNUSED_PARAMETER(ncargs);
|
|
|
|
if (nfargs < 1) {
|
|
safe_str(S_("#-1 TOO FEW ARGUMENTS"), buff, bufc);
|
|
return;
|
|
}
|
|
|
|
const UTF8 *expr = fargs[0];
|
|
size_t nLen = strlen(reinterpret_cast<const char *>(expr));
|
|
if (nLen == 0) return;
|
|
|
|
auto ast = ast_parse_string(expr, nLen);
|
|
if (!ast) return;
|
|
|
|
ast_eval_node(ast.get(), buff, bufc,
|
|
executor, caller, enactor,
|
|
eval | EV_FCHECK | EV_EVAL, cargs, ncargs);
|
|
}
|
|
|
|
// jiteval() — the Phase 0 forced-JIT debug oracle — was retired here
|
|
// after the Phase 5 default flip (docs/plan-jit-evalbracket-lift.md):
|
|
// with jit_eval_brackets on by default, the production route reaches
|
|
// everything the gate bypass existed for, and the q-register oracle
|
|
// compares production evaluation across two workspaces (toggle on vs
|
|
// explicitly off) instead.
|
|
|
|
// ---------------------------------------------------------------
|
|
// astbench(): head-to-head AST vs JIT benchmark.
|
|
//
|
|
// astbench(<expr>, <iterations>)
|
|
//
|
|
// Runs the expression through both the AST evaluator and the JIT,
|
|
// reports microseconds per call for each. Output format:
|
|
// ast=X.XXus jit=Y.YYus ratio=Z.Zx result=<value>
|
|
// ---------------------------------------------------------------
|
|
|
|
FUNCTION(fun_astbench)
|
|
{
|
|
UNUSED_PARAMETER(fp);
|
|
UNUSED_PARAMETER(cargs);
|
|
UNUSED_PARAMETER(ncargs);
|
|
|
|
if (nfargs < 2) {
|
|
safe_str(S_("#-1 TOO FEW ARGUMENTS"), buff, bufc);
|
|
return;
|
|
}
|
|
|
|
const UTF8 *expr = fargs[0];
|
|
size_t nLen = strlen(reinterpret_cast<const char *>(expr));
|
|
// Clamp in 64-bit BEFORE narrowing. Narrowing first truncates, so
|
|
// astbench(expr, 4294967296) became 0 iterations and returned nothing,
|
|
// and 4294967297 became exactly 1 -- while the cap below claims to be
|
|
// limiting the caller to 100000. A request far above the cap has to
|
|
// land ON the cap, not wrap past it (#1402).
|
|
//
|
|
int64_t iRequested = mux_atoi64(fargs[1]);
|
|
if (nLen == 0 || iRequested < 1) return;
|
|
if (iRequested > 100000) iRequested = 100000;
|
|
int iterations = static_cast<int>(iRequested);
|
|
|
|
// Parse once (shared by both paths).
|
|
auto ast = ast_parse_string(expr, nLen);
|
|
if (!ast) {
|
|
safe_str(S_("#-1 PARSE FAILED"), buff, bufc);
|
|
return;
|
|
}
|
|
|
|
// --- AST benchmark ---
|
|
#ifdef WIN32
|
|
LARGE_INTEGER freq, pc0, pc1;
|
|
QueryPerformanceFrequency(&freq);
|
|
QueryPerformanceCounter(&pc0);
|
|
#else
|
|
struct timespec t0, t1;
|
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
#endif
|
|
for (int i = 0; i < iterations; i++) {
|
|
LBuf temp = LBuf_Src("astbench_ast");
|
|
UTF8 *tp = temp;
|
|
ast_eval_node(ast.get(), temp, &tp,
|
|
executor, caller, enactor,
|
|
eval | EV_FCHECK | EV_EVAL, nullptr, 0);
|
|
*tp = '\0';
|
|
}
|
|
#ifdef WIN32
|
|
QueryPerformanceCounter(&pc1);
|
|
double ast_us = (double)(pc1.QuadPart - pc0.QuadPart) * 1e6
|
|
/ ((double)freq.QuadPart * iterations);
|
|
#else
|
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
|
double ast_us = ((t1.tv_sec - t0.tv_sec) * 1e6
|
|
+ (t1.tv_nsec - t0.tv_nsec) / 1e3) / iterations;
|
|
#endif
|
|
|
|
// --- JIT benchmark ---
|
|
#if defined(TINYMUX_JIT)
|
|
// Warm the compile cache, then discard what the warm-up emitted.
|
|
//
|
|
// Rewind to where THIS call started writing, not to the base of the
|
|
// buffer. `buff` belongs to the caller, and whatever is already in it is
|
|
// not ours to throw away: `think TAG [astbench(add(1,2),50)]` silently
|
|
// lost TAG, because the rewind landed in front of it (#2060).
|
|
//
|
|
UTF8 *entry = *bufc;
|
|
jit_eval(expr, nLen, buff, bufc, executor, caller, enactor,
|
|
eval | EV_FMAND | EV_EVAL, nullptr, 0);
|
|
*bufc = entry;
|
|
|
|
// #2133: jit_eval() returns whether it HANDLED the expression, and this
|
|
// discarded it. A decline is fast -- the gate walks the tree, finds a
|
|
// shape it cannot lower, and returns -- so an unhandled expression
|
|
// reported a spectacular jit= time that was the cost of saying no.
|
|
// citer() read a flat 2.7us at every N, which is absence rendered as
|
|
// speed, and the field has a standing warning in CLAUDE.md and
|
|
// tests/growth/README.md telling people not to trust it.
|
|
//
|
|
// Count what actually happened instead. A field that cannot answer
|
|
// should say so rather than return a number that will be read as one.
|
|
int nHandled = 0;
|
|
|
|
#ifdef WIN32
|
|
QueryPerformanceCounter(&pc0);
|
|
#else
|
|
clock_gettime(CLOCK_MONOTONIC, &t0);
|
|
#endif
|
|
for (int i = 0; i < iterations; i++) {
|
|
LBuf temp = LBuf_Src("astbench_jit");
|
|
UTF8 *tp = temp;
|
|
if (jit_eval(expr, nLen, temp, &tp, executor, caller, enactor,
|
|
eval | EV_FMAND | EV_EVAL, nullptr, 0)) {
|
|
nHandled++;
|
|
}
|
|
}
|
|
#ifdef WIN32
|
|
QueryPerformanceCounter(&pc1);
|
|
double jit_us = (double)(pc1.QuadPart - pc0.QuadPart) * 1e6
|
|
/ ((double)freq.QuadPart * iterations);
|
|
#else
|
|
clock_gettime(CLOCK_MONOTONIC, &t1);
|
|
double jit_us = ((t1.tv_sec - t0.tv_sec) * 1e6
|
|
+ (t1.tv_nsec - t0.tv_nsec) / 1e3) / iterations;
|
|
#endif
|
|
#else
|
|
double jit_us = 0.0;
|
|
int nHandled = 0;
|
|
#endif
|
|
|
|
// Get the result value.
|
|
LBuf result = LBuf_Src("astbench_result");
|
|
UTF8 *rp = result;
|
|
ast_eval_node(ast.get(), result, &rp,
|
|
executor, caller, enactor,
|
|
eval | EV_FCHECK | EV_EVAL, nullptr, 0);
|
|
*rp = '\0';
|
|
|
|
// The JIT leg only reports a time if the JIT actually ran the expression
|
|
// every time it was asked (#2133). Three distinct outcomes, three
|
|
// distinct words -- "declined" and "0.03us" must not look alike:
|
|
//
|
|
// handled == iterations a real measurement
|
|
// handled == 0 declined; jit_us is the cost of the decline
|
|
// 0 < handled < iters mixed, which is a finding in itself: the
|
|
// verdict changed mid-run, so neither the time
|
|
// nor the decline describes the whole loop
|
|
//
|
|
const bool bAllHandled = (iterations > 0 && nHandled == iterations);
|
|
const bool bNoneHandled = (nHandled == 0);
|
|
double ratio = (bAllHandled && jit_us > 0.001) ? ast_us / jit_us : 0.0;
|
|
|
|
// Format floating-point with libc snprintf. mux_vsnprintf / safe_tprintf
|
|
// only know integer and string conversions — "%.2f" falls through to
|
|
// mux_assert(0) and aborts the process (#1382). rvbench() already uses
|
|
// this pattern for the same reason.
|
|
//
|
|
char ast_buf[32];
|
|
char jit_buf[32];
|
|
char ratio_buf[32];
|
|
mux_sprintf(reinterpret_cast<UTF8 *>(ast_buf), sizeof(ast_buf),
|
|
T("%.2f"), ast_us);
|
|
if (bAllHandled) {
|
|
mux_sprintf(reinterpret_cast<UTF8 *>(jit_buf), sizeof(jit_buf),
|
|
T("%.2fus"), jit_us);
|
|
} else if (bNoneHandled) {
|
|
mux_strncpy(reinterpret_cast<UTF8 *>(jit_buf), T("declined"),
|
|
sizeof(jit_buf) - 1);
|
|
} else {
|
|
mux_sprintf(reinterpret_cast<UTF8 *>(jit_buf), sizeof(jit_buf),
|
|
T("mixed(%d/%d)"), nHandled, iterations);
|
|
}
|
|
if (bAllHandled) {
|
|
mux_sprintf(reinterpret_cast<UTF8 *>(ratio_buf), sizeof(ratio_buf),
|
|
T("%.1fx"), ratio);
|
|
} else {
|
|
mux_strncpy(reinterpret_cast<UTF8 *>(ratio_buf), T("n/a"),
|
|
sizeof(ratio_buf) - 1);
|
|
}
|
|
|
|
safe_tprintf_str(buff, bufc,
|
|
T("ast=%sus jit=%s ratio=%s result=%s"),
|
|
ast_buf, jit_buf, ratio_buf, result.get());
|
|
}
|