Files
antelope/source/antelope/evaluator/expansion.d
T
2026-07-27 23:01:30 -04:00

882 lines
30 KiB
D

/// Variable expansion and substitution at evaluation time.
///
/// This module implements GNU Make-compatible recursive variable expansion.
/// It is the core string-processing engine responsible for resolving
/// `$(VAR)`, `$$`, automatic variables ($@, $<, etc.), D/F suffix extraction,
/// and `$(call ...)` substitution — all with circular-reference detection.
module antelope.evaluator.expansion;
import antelope.shell.environment;
import antelope.diagnostics.errors;
import antelope.parser.functions;
import std.string : lastIndexOf, strip, indexOf;
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Expand all variable references in a string.
///
/// The expansion is recursive: when a variable's value itself contains
/// variable references, those are expanded in turn. Undefined variables
/// expand to the empty string (standard GNU Make behaviour). Circular
/// references (A → B → A) are detected and reported rather than looping.
///
/// Supported syntax:
/// * `$$` → literal `$`
/// * `$@` → current target name
/// * `$<` → first prerequisite
/// * `$^` → all prerequisites (unique, space-separated)
/// * `$+` → all prerequisites (duplicates preserved)
/// * `$?` → prerequisites newer than target (all prereqs for now)
/// * `$*` → pattern stem
/// * `$%` → archive member (not yet implemented → "")
/// * `$|` → order-only prerequisites (not yet implemented → "")
/// * `$(VAR)` → environment variable lookup + recursive expansion
/// * `${VAR}` → same as `$(VAR)`
/// * `$(@D)` / `$(@F)` → directory / file part of target
/// * `$(<D)` / `$(<F)` → directory / file part of first prerequisite
/// * `$(^D)` / `$(^F)` → directory / file parts of all prerequisites
/// * `$(*D)` / `$(*F)` → directory / file part of stem
/// * `$(call FUNC,arg1,arg2,…)` → call-style expansion
///
/// Params:
/// input = String to expand.
/// env = Pointer to the variable environment (may be null).
/// currentTarget = Value for `$@` (current target name).
/// currentPrereqs = Values for `$<`, `$^`, `$+`, `$?`.
/// stem = Value for `$*` (pattern stem).
///
/// Returns: The fully-expanded string.
string expand(string input, Environment* env, string currentTarget = "",
string[] currentPrereqs = [], string stem = "")
{
string[] chain;
return expandImpl(input, env, currentTarget, currentPrereqs, stem, chain);
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/// Directory part of a path (up to and including the last `/`, or `./`).
private string dirPart(string path)
{
auto idx = lastIndexOf(path, "/");
if (idx == size_t.max) // D's lastIndexOf returns size_t.max on no-match
return "./";
return path[0 .. idx + 1];
}
/// File part of a path (everything after the last `/`).
private string filePart(string path)
{
auto idx = lastIndexOf(path, "/");
if (idx == size_t.max)
return path;
return path[idx + 1 .. $];
}
/// Apply `dirPart` or `filePart` to each string in `values` and join with space.
private string applyDirFile(string[] values, bool wantDir)
{
if (values.length == 0)
return "";
import std.array : appender;
auto buf = appender!string();
bool first = true;
foreach (v; values)
{
if (!first) buf.put(" ");
first = false;
buf.put(wantDir ? dirPart(v) : filePart(v));
}
return buf.data;
}
/// Resolve a single-character automatic variable.
/// Never returns null — always returns a string.
private string resolveAutoVar(char c, string currentTarget, string[] currentPrereqs,
string stem)
{
final switch (c)
{
case '@': return currentTarget;
case '<': return currentPrereqs.length > 0 ? currentPrereqs[0] : "";
case '^':
{
// Unique prerequisites (order preserved, first occurrence kept).
if (currentPrereqs.length == 0)
return "";
import std.array : appender;
auto buf = appender!string();
bool[string] seen;
bool first = true;
foreach (p; currentPrereqs)
{
if (p in seen) continue;
seen[p] = true;
if (!first) buf.put(" ");
first = false;
buf.put(p);
}
return buf.data;
}
case '+': // All prereqs, duplicates preserved, space-separated.
{
if (currentPrereqs.length == 0)
return "";
import std.array : appender;
auto buf = appender!string();
foreach (i, p; currentPrereqs)
{
if (i > 0) buf.put(" ");
buf.put(p);
}
return buf.data;
}
case '?':
{
// "Prereqs newer than target" — compare timestamps and
// return only those prerequisites that are newer than the
// target file. This matches GNU Make's $? semantics.
if (currentPrereqs.length == 0) return "";
import antelope.filesystem.timestamps;
import std.array : appender;
auto buf = appender!string();
bool first = true;
long targetTime = getTimestamp(currentTarget);
foreach (p; currentPrereqs)
{
long pt = getTimestamp(p);
if (pt > targetTime || targetTime == -1)
{
if (!first) buf.put(" ");
first = false;
buf.put(p);
}
}
return buf.data;
}
case '*': return stem;
case '%': return ""; // Archive member — not yet implemented.
case '|': return ""; // Order-only prereqs — not yet implemented.
}
}
/// Resolve content found between `(` / `)` or `{` / `}`.
///
/// The content may be:
/// * `call FUNC,arg1,arg2,…` → call-style expansion
/// * `@D`, `<F`, `^D`, `*F`, etc. → automatic var D/F suffix
/// * `VAR` or `VAR_$(NESTED)` → regular variable lookup
///
/// `closer` is `)` or `}` (unused except for potential error messages).
private string resolveParenContent(string content, char closer, Environment* env,
string currentTarget, string[] currentPrereqs,
string stem, ref string[] chain)
{
// --- $(call FUNC,arg1,arg2,…) ------------------------------------------
if (content.length >= 5 && content[0 .. 5] == "call ")
{
return expandCall(content[5 .. $], env, currentTarget, currentPrereqs, stem, chain);
}
// --- Built-in function calls: $(shell ...), $(subst ...), etc. -----------
// Detect if the first word is a known GNU Make function name.
import std.string : indexOf;
auto firstSpace = indexOf(content, ' ');
string firstWord = firstSpace >= 0 ? content[0 .. firstSpace] : content;
if (isBuiltinFunction(firstWord))
{
return evaluateBuiltinCall(firstWord, content, env, currentTarget, currentPrereqs, stem);
}
// --- Automatic var with D/F suffix ($(@D), $(<F), $(^D), etc.) ---------
if (content.length == 2 &&
(content[0] == '@' || content[0] == '<' || content[0] == '^' ||
content[0] == '+' || content[0] == '?' || content[0] == '*') &&
(content[1] == 'D' || content[1] == 'F'))
{
bool wantDir = (content[1] == 'D');
char av = content[0];
// For multi-value automatic vars ($^D, $+D, $?D) we apply
// dirPart / filePart to each element.
if (av == '^' || av == '+' || av == '?')
{
string joined = resolveAutoVar(av, currentTarget, currentPrereqs, stem);
if (joined.length == 0) return "";
import std.array : split;
auto parts = split(joined, " ");
return applyDirFile(parts, wantDir);
}
// Single-value automatic vars ($@, $<, $*)
string single = resolveAutoVar(av, currentTarget, currentPrereqs, stem);
if (single.length == 0) return "";
return wantDir ? dirPart(single) : filePart(single);
}
// --- Substitution reference: $(VAR:old=new) → $(patsubst old,new,$(VAR)) ---
auto colonIdx = indexOf(content, ':');
if (colonIdx >= 0 && colonIdx < content.length - 1 &&
content[colonIdx + 1] != '=' && indexOf(content[colonIdx + 1 .. $], '=') >= 0)
{
// Syntax: VAR:old=new
string varName = content[0 .. colonIdx];
auto eqIdx = indexOf(content[colonIdx + 1 .. $], '=');
string oldPat = content[colonIdx + 1 .. colonIdx + 1 + eqIdx];
string newPat = content[colonIdx + 1 + eqIdx + 1 .. $];
// Equivalent to $(patsubst oldPat,newPat,$(varName))
string varValue = env ? env.get(varName) : "";
if (varValue.length > 0)
{
import antelope.evaluator.functions;
import antelope.parser.functions;
return evaluateFunction(BuiltinFunction.patsubst, [oldPat, newPat, varValue], env);
}
return "";
}
// --- Regular variable lookup -------------------------------------------
// First, recursively expand any nested `$` references in the content.
// This handles $(VAR_$(NESTED)) — expand $(NESTED) first, then look up.
string expandedName = expandImpl(content, env, currentTarget, currentPrereqs, stem, chain);
if (expandedName.length == 0)
return "";
// Environment lookup — consult target-scoped variables first.
string value = env ? env.getScoped(expandedName, currentTarget) : "";
if (value.length == 0)
return "";
// Circular-reference guard.
foreach (c; chain)
{
if (c == expandedName)
{
// Found a cycle — report and bail out.
// (In the future this could use a diagnostic emitter.)
return "";
}
}
chain ~= expandedName;
scope (exit) chain = chain[0 .. $ - 1];
return expandImpl(value, env, currentTarget, currentPrereqs, stem, chain);
}
/// Expand a `$(call FUNC,arg1,arg2,…)` reference.
///
/// `rest` is everything after `"call "` — i.e. `"FUNC,arg1,arg2,…"`.
/// Steps:
/// 1. Split on top-level commas to get variable name + args.
/// 2. Expand the variable name to find which variable to invoke.
/// 3. Look up its value from the environment.
/// 4. Substitute `$1`, `$2`, … in the value with the expanded args.
/// 5. Recursively expand the result.
private string expandCall(string rest, Environment* env,
string currentTarget, string[] currentPrereqs,
string stem, ref string[] chain)
{
// Split `FUNC,arg1,arg2,…` on top-level commas.
string[] parts;
{
import std.array : appender;
auto buf = appender!(string[])();
size_t pos = 0;
size_t depth = 0;
size_t start = 0;
while (pos < rest.length)
{
char ch = rest[pos];
if (ch == '(' || ch == '{') depth++;
else if (ch == ')' || ch == '}') depth--;
else if (ch == ',' && depth == 0)
{
buf.put(rest[start .. pos]);
start = pos + 1;
}
pos++;
}
// Last segment.
buf.put(rest[start .. $]);
parts = buf.data;
}
if (parts.length == 0)
return "";
// Parts[0] is the function name — expand it to get the variable name.
string funcName = expandImpl(parts[0], env, currentTarget, currentPrereqs, stem, chain);
if (funcName.length == 0)
return "";
// Look up the function body.
string body = env ? env.get(funcName) : "";
if (body.length == 0)
return "";
// Expand each argument.
string[] expandedArgs;
foreach (i; 1 .. parts.length)
{
expandedArgs ~= expandImpl(parts[i], env, currentTarget, currentPrereqs, stem, chain);
}
// Substitute $1, $2, … in the body.
// We scan the body character by character for $N patterns.
string bodyWithSubs = substituteCallArgs(body, expandedArgs);
// Recursively expand the result (it may contain further variable refs).
return expandImpl(bodyWithSubs, env, currentTarget, currentPrereqs, stem, chain);
}
/// Substitute `$1`, `$2`, …, `$0` (the function name) in `body` with the
/// corresponding values from `args`.
///
/// `args[0]` corresponds to `$1`, `args[1]` to `$2`, etc.
/// `$0` is set to the function name (already resolved).
///
/// `$$` in the body is left as a literal `$` (it was already quoted).
/// Unknown `$N` is replaced by the empty string.
private string substituteCallArgs(string body, string[] args)
{
import std.array : appender;
auto buf = appender!string();
size_t i = 0;
while (i < body.length)
{
if (body[i] == '$')
{
i++;
if (i >= body.length)
{
buf.put('$');
break;
}
char c = body[i];
if (c == '$')
{
buf.put('$');
i++;
continue;
}
// Is it a digit? $0, $1, …, $9
if (c >= '0' && c <= '9')
{
uint n = c - '0';
if (n == 0)
{
// $0 is replaced by empty string (or function name — but
// GNU Make doesn't use $0 in call; it's the function name
// and is not substituted). Leave as empty for now.
buf.put("");
}
else if (n <= args.length)
{
buf.put(args[n - 1]);
}
else
{
// Unknown $N → ""
}
i++;
continue;
}
// Not a digit — output $ and the char (e.g. $( inside body).
buf.put('$');
buf.put(c);
i++;
}
else
{
buf.put(body[i]);
i++;
}
}
return buf.data;
}
/// Core expansion implementation — recursive, with chain tracking for
/// circular-reference detection.
private string expandImpl(string input, Environment* env, string currentTarget,
string[] currentPrereqs, string stem, ref string[] chain)
{
import std.array : appender;
auto buf = appender!string();
size_t i = 0;
while (i < input.length)
{
if (input[i] == '$')
{
i++;
if (i >= input.length)
{
buf.put('$');
break;
}
char c = input[i];
// $$ → literal $
if (c == '$')
{
buf.put('$');
i++;
continue;
}
// Single-character $X variable references.
// $@, $<, $^, etc. → automatic variables
if (c == '@' || c == '<' || c == '^' || c == '+' ||
c == '?' || c == '*' || c == '%' || c == '|')
{
buf.put(resolveAutoVar(c, currentTarget, currentPrereqs, stem));
i++;
continue;
}
// $1..$9, $0 → positional parameters (call, foreach, etc.)
// and any other single-char variable reference
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_')
{
string varName = [c];
string val = env ? env.get(varName) : "";
buf.put(val);
i++;
continue;
}
// $(…) or ${…}
if (c == '(' || c == '{')
{
char openChar = c;
char closeChar = (c == '(') ? ')' : '}';
i++; // skip opening delimiter
size_t contentStart = i;
int depth = 1;
while (i < input.length && depth > 0)
{
if (input[i] == openChar)
depth++;
else if (input[i] == closeChar)
depth--;
if (depth > 0)
i++;
}
if (i >= input.length)
{
// Unterminated $('… or ${… — output literally.
buf.put('$');
buf.put(openChar);
buf.put(input[contentStart .. $]);
break;
}
string content = input[contentStart .. i];
i++; // skip closing delimiter
buf.put(resolveParenContent(content, closeChar, env, currentTarget,
currentPrereqs, stem, chain));
continue;
}
// Unknown $X — output literally.
buf.put('$');
buf.put(c);
i++;
}
else
{
buf.put(input[i]);
i++;
}
}
return buf.data;
}
// ---------------------------------------------------------------------------
// Unittests
// ---------------------------------------------------------------------------
///
unittest
{
// --- Simple $(VAR) expansion ---
{
Environment env;
env.set("FOO", "bar");
string result = expand("$(FOO)", &env);
assert(result == "bar", "Simple $(FOO) should expand to 'bar', got: " ~ result);
}
// --- ${VAR} syntax ---
{
Environment env;
env.set("FOO", "baz");
string result = expand("${FOO}", &env);
assert(result == "baz", "${FOO} should expand to 'baz', got: " ~ result);
}
// --- $$ → literal $ ---
{
Environment env;
string result = expand("prefix $$ suffix", &env);
assert(result == "prefix $ suffix", "$$ should become '$', got: " ~ result);
result = expand("$$$$", &env);
assert(result == "$$", "$$$$ should become '$$', got: " ~ result);
}
// --- $@ → current target ---
{
Environment env;
string result = expand("$@", &env, "myfile.o");
assert(result == "myfile.o", "$@ should be target name, got: " ~ result);
}
// --- $< → first prerequisite ---
{
Environment env;
string result = expand("$<", &env, "", ["a.c", "b.c"]);
assert(result == "a.c", "$< should be first prereq, got: " ~ result);
result = expand("$<", &env, "", []);
assert(result == "", "$< with no prereqs should be empty, got: " ~ result);
}
// --- $^ → all prereqs, unique ---
{
Environment env;
string result = expand("$^", &env, "", ["a.c", "b.c", "a.c"]);
assert(result == "a.c b.c", "$^ should dedupe prereqs, got: " ~ result);
}
// --- $+ → all prereqs, duplicates preserved ---
{
Environment env;
string result = expand("$+", &env, "", ["a.c", "b.c", "a.c"]);
assert(result == "a.c b.c a.c", "$+ should preserve duplicates, got: " ~ result);
}
// --- $* → stem ---
{
Environment env;
string result = expand("$*", &env, "", [], "foo");
assert(result == "foo", "$* should be stem, got: " ~ result);
}
// --- $% → "" (not implemented) ---
{
Environment env;
string result = expand("$%", &env);
assert(result == "", "$% should be empty (not implemented), got: " ~ result);
}
// --- $| → "" (not implemented) ---
{
Environment env;
string result = expand("$|", &env);
assert(result == "", "$| should be empty (not implemented), got: " ~ result);
}
// --- $(@D) → directory part of target ---
{
Environment env;
string result = expand("$(@D)", &env, "src/sub/file.o");
assert(result == "src/sub/", "$(@D) should be dir part, got: " ~ result);
result = expand("$(@D)", &env, "file.o");
assert(result == "./", "$(@D) with no slash should be './', got: " ~ result);
}
// --- $(@F) → file part of target ---
{
Environment env;
string result = expand("$(@F)", &env, "src/sub/file.o");
assert(result == "file.o", "$(@F) should be file part, got: " ~ result);
result = expand("$(@F)", &env, "file.o");
assert(result == "file.o", "$(@F) with no slash should be full name, got: " ~ result);
}
// --- $(<D) and $(<F) ---
{
Environment env;
string result = expand("$(<D)", &env, "", ["src/a.c"]);
assert(result == "src/", "$(<D) should be dir part of first prereq, got: " ~ result);
result = expand("$(<F)", &env, "", ["src/a.c"]);
assert(result == "a.c", "$(<F) should be file part of first prereq, got: " ~ result);
}
// --- $(^D) and $(^F) ---
{
Environment env;
string result = expand("$(^D)", &env, "", ["src/a.c", "inc/b.h"]);
assert(result == "src/ inc/", "$(^D) with multi prereqs, got: " ~ result);
result = expand("$(^F)", &env, "", ["src/a.c", "inc/b.h"]);
assert(result == "a.c b.h", "$(^F) with multi prereqs, got: " ~ result);
}
// --- Nested expansion: $(VAR_$(NESTED)) ---
{
Environment env;
env.set("SUFFIX", "FLAGS");
env.set("CXXFLAGS", "-O2 -Wall");
string result = expand("$(CXX$(SUFFIX))", &env);
assert(result == "-O2 -Wall", "Nested expansion failed, got: " ~ result);
}
// --- Undefined variable → "" ---
{
Environment env;
string result = expand("$(UNDEFINED_VAR)", &env);
assert(result == "", "Undefined var should be empty, got: " ~ result);
}
// --- $(call ...) basic ---
{
Environment env;
env.set("reverse", "$2 $1");
string result = expand("$(call reverse,a,b)", &env);
assert(result == "b a", "call reverse failed, got: " ~ result);
}
// --- $(call ...) with extra args (unused $N → "") ---
{
Environment env;
env.set("prefix", "[$1]");
string result = expand("$(call prefix,hello)", &env);
assert(result == "[hello]", "call prefix failed, got: " ~ result);
}
// --- $(call ...) with nested $ in args ---
{
Environment env;
env.set("VAL", "xyz");
env.set("wrap", "<$1>");
string result = expand("$(call wrap,$(VAL))", &env);
assert(result == "<xyz>", "call with nested var in arg failed, got: " ~ result);
}
// --- Circular reference detection ---
{
Environment env;
env.set("A", "$(B)");
env.set("B", "$(A)");
string result = expand("$(A)", &env);
assert(result == "", "Circular A→B→A should return empty, got: " ~ result);
}
// --- Circular self-reference ---
{
Environment env;
env.set("X", "$(X)");
string result = expand("$(X)", &env);
assert(result == "", "Self-circular X→X should return empty, got: " ~ result);
}
// --- Deeply nested expansion ---
{
Environment env;
env.set("A", "1$(B)");
env.set("B", "2$(C)");
env.set("C", "3");
string result = expand("$(A)", &env);
assert(result == "123", "Deeply nested A→B→C failed, got: " ~ result);
}
// --- Multiple $ in same string ---
{
Environment env;
env.set("NAME", "Antelope");
env.set("VER", "1.0");
string result = expand("$(NAME) v$(VER) $$HOME", &env);
assert(result == "Antelope v1.0 $HOME",
"Multiple $ expansion failed, got: " ~ result);
}
// --- Literal string passthrough (no $) ---
{
Environment env;
string result = expand("hello world", &env);
assert(result == "hello world", "Plain string should pass through, got: " ~ result);
}
// --- $? (newer prereqs) — all prereqs returned when target doesn't exist ---
{
Environment env;
// Both target ("") and prereqs ("a.c", "b.c") are non-existent files,
// so getTimestamp returns -1 for all. Since targetTime == -1,
// all prereqs are considered newer.
string result = expand("$?", &env, "", ["a.c", "b.c", "a.c"]);
assert(result == "a.c b.c a.c", "$? should return all when target absent, got: " ~ result);
}
// --- Null env pointer — all vars → "" ---
{
string result = expand("$(ANYTHING)", null);
assert(result == "", "Null env should treat all vars as undefined, got: " ~ result);
}
// --- call with $0 → empty ---
{
Environment env;
env.set("showall", "$0 $1 $2");
string result = expand("$(call showall,foo,bar)", &env);
// $0 is empty, $1=foo, $2=bar
assert(result == " foo bar", "call with $0, got: " ~ result);
}
// --- call with nested function name expansion ---
{
Environment env;
env.set("WHICH", "upper");
env.set("upper", "[$1]");
string result = expand("$(call $(WHICH),text)", &env);
assert(result == "[text]", "call with nested func name, got: " ~ result);
}
// --- Unclosed $( → output literally ---
{
Environment env;
env.set("FOO", "bar");
string result = expand("start $(FOO", &env);
assert(result == "start $(FOO", "Unclosed $( should be literal, got: " ~ result);
}
}
/// Check if a word is a known GNU Make built-in function name.
bool isBuiltinFunction(string word)
{
switch (word)
{
case "subst": case "patsubst": case "strip": case "findstring":
case "filter": case "filter-out": case "sort": case "word":
case "words": case "wordlist": case "firstword": case "lastword":
case "dir": case "notdir": case "suffix": case "basename":
case "addsuffix": case "addprefix": case "join":
case "wildcard": case "realpath": case "abspath":
case "shell": case "error": case "warning": case "info":
case "foreach": case "call": case "value": case "origin": case "flavor":
return true;
default:
return false;
}
}
/// Evaluate a built-in function call from expansion context.
private string evaluateBuiltinCall(string funcName, string content, Environment* env,
string currentTarget = "", string[] currentPrereqs = [], string stem = "")
{
import antelope.evaluator.functions;
import antelope.parser.functions;
// Parse arguments with nesting-aware comma splitting.
// Commas inside nested $(...) or ${...} are NOT argument separators.
string[] args;
size_t start = funcName.length;
if (start < content.length && content[start] == ' ')
start++;
string rest = content[start .. $];
size_t segStart = 0;
int depth = 0; // paren/brace nesting depth
for (size_t i = 0; i < rest.length; i++)
{
char c = rest[i];
if (c == '(' || c == '{') depth++;
else if (c == ')' || c == '}') { if (depth > 0) depth--; }
else if (c == ',' && depth == 0)
{
args ~= rest[segStart .. i].strip;
segStart = i + 1;
}
}
// Last segment
if (segStart < rest.length)
args ~= rest[segStart .. $].strip;
// Recursively expand each argument before dispatching to the function.
// Pass currentTarget/prereqs/stem so $@, $<, $^ work inside function args.
// Functions that manage their own expansion (foreach, if, or, and)
// receive raw (unexpanded) arguments.
import antelope.parser.functions;
BuiltinFunction bf = builtinFromName(funcName);
bool preExpand = !isSelfExpanding(bf);
string[] expandedArgs;
foreach (arg; args)
{
if (preExpand)
expandedArgs ~= expand(arg, env, currentTarget, currentPrereqs, stem);
else
expandedArgs ~= arg;
}
return evaluateFunction(bf, expandedArgs, env);
}
/// Returns true for functions that manage their own argument expansion
/// (matching GNU Make's `expand_args=0` flag).
private bool isSelfExpanding(BuiltinFunction bf)
{
import antelope.parser.functions;
switch (bf)
{
case BuiltinFunction.foreach_:
return true;
default:
return false;
}
}
/// Map function name string to BuiltinFunction enum.
BuiltinFunction builtinFromName(string name)
{
import antelope.parser.functions;
switch (name)
{
case "subst": return BuiltinFunction.subst;
case "patsubst": return BuiltinFunction.patsubst;
case "strip": return BuiltinFunction.strip;
case "findstring": return BuiltinFunction.findstring;
case "filter": return BuiltinFunction.filter;
case "filter-out": return BuiltinFunction.filter_out;
case "sort": return BuiltinFunction.sort;
case "word": return BuiltinFunction.word;
case "words": return BuiltinFunction.words;
case "wordlist": return BuiltinFunction.wordlist;
case "firstword": return BuiltinFunction.firstword;
case "lastword": return BuiltinFunction.lastword;
case "dir": return BuiltinFunction.dir;
case "notdir": return BuiltinFunction.notdir;
case "suffix": return BuiltinFunction.suffix;
case "basename": return BuiltinFunction.basename;
case "addsuffix": return BuiltinFunction.addsuffix;
case "addprefix": return BuiltinFunction.addprefix;
case "join": return BuiltinFunction.join;
case "wildcard": return BuiltinFunction.wildcard;
case "realpath": return BuiltinFunction.realpath;
case "abspath": return BuiltinFunction.abspath;
case "shell": return BuiltinFunction.shell;
case "error": return BuiltinFunction.error;
case "warning": return BuiltinFunction.warning;
case "info": return BuiltinFunction.info;
case "foreach": return BuiltinFunction.foreach_;
case "call": return BuiltinFunction.call;
case "value": return BuiltinFunction.value;
case "origin": return BuiltinFunction.origin;
case "flavor": return BuiltinFunction.flavor;
default: return BuiltinFunction.info;
}
}