Compile backtick commands as __command() calls with ${} interpolation

Backtick expressions now compile as calls to a global __command()
function instead of being treated as plain strings. Interpolation
via ${expr} is supported, with each interpolated value wrapped in
tostring() for type safety. Commands are parsed in primaryexp so
suffix operations like `.stdout` work directly.

Adds a stub __command that returns {code, stdout, stderr} for testing
until the real implementation (issue #03).
This commit is contained in:
Cormac Shannon
2026-02-19 20:00:15 +00:00
parent db6f39a2a4
commit a773398cab
4 changed files with 239 additions and 5 deletions

115
lparser.c
View File

@@ -540,6 +540,31 @@ static void singlevar (LexState *ls, expdesc *var) {
}
/*
** Compile a command expression: emit code equivalent to
** __command(cmdstring)
** where cmdstring is already built (possibly via concatenation
** from interpolation fragments).
*/
static void codecommand (LexState *ls, expdesc *v, expdesc *cmdstr) {
FuncState *fs = ls->fs;
int base, line;
expdesc func;
TString *cmdname = luaX_newstring(ls, "__command", 9);
line = ls->linenumber;
/* look up __command as _ENV["__command"] */
buildglobal(ls, cmdname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
/* push the command string argument */
luaK_exp2nextreg(fs, cmdstr);
/* emit OP_CALL: 1 arg, 1 result */
init_exp(v, VCALL, luaK_codeABC(fs, OP_CALL, base, 2, 2));
luaK_fixline(fs, line);
fs->freereg = cast_byte(base + 1);
}
/*
** Adjust the number of results from an expression list 'e' with 'nexps'
** expressions to 'nvars' values.
@@ -1194,8 +1219,91 @@ static void funcargs (LexState *ls, expdesc *f) {
*/
/*
** Parse a backtick command expression (TK_COMMAND or TK_COMMAND_INTERP).
** Compiles `cmd` as __command("cmd") and `cmd ${expr}` as
** __command("cmd " .. tostring(expr) .. "").
** On entry, ls->t.token is TK_COMMAND or TK_COMMAND_INTERP.
** On exit, the token has been consumed (ls->t has the next token).
*/
static void commandexp (LexState *ls, expdesc *v) {
if (ls->t.token == TK_COMMAND) {
/* simple command with no interpolation */
expdesc cmdstr;
codestring(&cmdstr, ls->t.seminfo.ts);
luaX_next(ls);
codecommand(ls, v, &cmdstr);
}
else {
/* command with ${expr} interpolation */
FuncState *fs = ls->fs;
expdesc func, cmdstr;
int base, nconcat = 0;
int line = ls->linenumber;
TString *cmdname = luaX_newstring(ls, "__command", 9);
TString *tsname = luaX_newstring(ls, "tostring", 8);
/* load __command function first so it occupies the base register */
buildglobal(ls, cmdname, &func);
luaK_exp2nextreg(fs, &func);
base = func.u.info;
/* load the first fragment */
codestring(&cmdstr, ls->t.seminfo.ts);
luaK_exp2nextreg(fs, &cmdstr);
nconcat++;
for (;;) {
expdesc interp, tostrfn;
int tostr_base;
/* parse the interpolated expression */
luaX_next(ls); /* advance past TK_COMMAND_INTERP */
/* emit tostring(expr): load tostring function, then the expr,
** then OP_CALL — result occupies one register in the concat chain */
buildglobal(ls, tsname, &tostrfn);
luaK_exp2nextreg(fs, &tostrfn);
tostr_base = tostrfn.u.info;
expr(ls, &interp);
check(ls, '}');
/* don't call luaX_next — luaX_readcommandcont reads directly
from ls->current, which is already past the '}' */
luaK_exp2nextreg(fs, &interp);
luaK_codeABC(fs, OP_CALL, tostr_base, 2, 2);
fs->freereg = cast_byte(tostr_base + 1);
nconcat++;
/* continue reading the command string */
if (luaX_readcommandcont(ls) == TK_COMMAND_INTERP) {
/* another fragment + more interpolation coming */
expdesc frag;
codestring(&frag, ls->t.seminfo.ts);
luaK_exp2nextreg(fs, &frag);
nconcat++;
/* loop continues */
}
else {
/* TK_COMMAND: final fragment */
expdesc frag;
codestring(&frag, ls->t.seminfo.ts);
luaK_exp2nextreg(fs, &frag);
nconcat++;
break;
}
}
/* advance past the final TK_COMMAND token */
luaX_next(ls);
/* emit OP_CONCAT for all fragments (result goes into base+1) */
if (nconcat > 1) {
int first = base + 1;
luaK_codeABC(fs, OP_CONCAT, first, nconcat, 0);
fs->freereg = cast_byte(first + 1);
}
/* emit OP_CALL: 1 arg (the concatenated string), 1 result */
init_exp(v, VCALL, luaK_codeABC(fs, OP_CALL, base, 2, 2));
luaK_fixline(fs, line);
fs->freereg = cast_byte(base + 1);
}
}
static void primaryexp (LexState *ls, expdesc *v) {
/* primaryexp -> NAME | '(' expr ')' */
/* primaryexp -> NAME | '(' expr ')' | COMMAND */
switch (ls->t.token) {
case '(': {
int line = ls->linenumber;
@@ -1209,6 +1317,10 @@ static void primaryexp (LexState *ls, expdesc *v) {
singlevar(ls, v);
return;
}
case TK_COMMAND: case TK_COMMAND_INTERP: {
commandexp(ls, v);
return;
}
default: {
luaX_syntaxerror(ls, "unexpected symbol");
}
@@ -2192,4 +2304,3 @@ LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff,
L->top.p--; /* remove scanner's table */
return cl; /* closure is on the stack, too */
}