in Source/CNTK/BrainScript/BrainScriptEvaluator.cpp [554:882]
static ConfigValuePtr Evaluate(const ExpressionPtr &e, const IConfigRecordPtr &scope, wstring exprPath, const wstring &exprId)
{
try // catch clause for this will catch error, inject this tree node's TextLocation, and rethrow
{
// expression names
// Merge exprPath and exprId into one unless one is empty
if (!exprPath.empty() && !exprId.empty())
exprPath.append(L".");
exprPath.append(exprId);
// tracing
if (trace)
TextLocation::Trace(e->location, msra::strfun::wstrprintf(L"eval SP=0x%p", &exprPath).c_str(), e->op.c_str(), exprPath.c_str());
// --- literals
if (e->op == L"d")
return MakePrimitiveConfigValuePtr(e->d, MakeFailFn(e->location), exprPath); // === double literal
else if (e->op == L"s")
return ConfigValuePtr(make_shared<String>(e->s), MakeFailFn(e->location), exprPath); // === string literal
else if (e->op == L"b")
return MakePrimitiveConfigValuePtr(e->b, MakeFailFn(e->location), exprPath); // === bool literal
else if (e->op == L"new") // === 'new' expression: instantiate C++ runtime object right here
{
// find the constructor lambda
let rtInfo = FindRuntimeTypeInfo(e->id);
if (!rtInfo)
Fail(L"unknown runtime type " + e->id, e->location);
// form the config record
let &dictExpr = e->args[0];
let argsExprPath = rtInfo->isConfigRecord ? L"" : exprPath; // reset expr-name path if object exposes a dictionary
let value = ConfigValuePtr(rtInfo->construct(ConfigRecordFromDictExpression(dictExpr, scope, argsExprPath)), MakeFailFn(e->location), exprPath); // this constructs it
// if object has a name, we set it
let valueWithName = dynamic_cast<HasName *>(value.get());
if (valueWithName)
valueWithName->SetName(value.GetExpressionName());
return value; // we return the created but not initialized object as the value, so others can reference it
}
else if (e->op == L"if") // === conditional expression
{
let argValPtr = Evaluate(e->args[0], scope, exprPath, L"if");
if (argValPtr.Is<ComputationNodeObject>()) // if ComputationNode becomes If(c,t,e)
return EvaluateNodeOp(e,
argValPtr,
Evaluate(e->args[1], scope, exprPath, L"then"),
Evaluate(e->args[2], scope, exprPath, L"else"),
scope, exprPath);
else if (ToBoolean(argValPtr, e->args[0]))
return Evaluate(e->args[1], scope, exprPath, L""); // pass exprPath through 'if' since only of the two exists
else
return Evaluate(e->args[2], scope, exprPath, L"");
}
// --- functions
else if (e->op == L"=>") // === lambda (all macros are stored as lambdas)
{
// on scope: The lambda expression remembers the lexical scope of the '=>'; this is how it captures its context.
let &argListExpr = e->args[0]; // [0] = argument list ("()" expression of identifiers, possibly optional args)
if (argListExpr->op != L"()")
LogicError("parameter list expected");
let &fnExpr = e->args[1]; // [1] = expression of the function itself
let f = [argListExpr, fnExpr, scope, exprPath](vector<ConfigValuePtr> &&args, ConfigLambda::NamedParams &&namedArgs, const wstring &callerExprPath) -> ConfigValuePtr
{
// TODO: document namedArgs--does it have a parent scope? Or is it just a dictionary? Should we just use a shared_ptr<map,ConfigValuPtr>> instead for clarity?
// on exprName
// - 'callerExprPath' is the name to which the result of the fn evaluation will be assigned
// - 'exprPath' (outside) is the name of the macro we are defining this lambda under
let &argList = argListExpr->args;
if (args.size() != argList.size())
LogicError("function application with mismatching number of arguments");
// To execute a function body with passed arguments, we
// - create a new scope that contains all positional and named args
// - then evaluate the expression with that scope
// - parent scope for this is the scope of the function definition (captured context)
// Note that the 'scope' variable in here (we are in a lambda) is the scope of the '=>' expression, that is, the macro definition.
// create a ConfigRecord with param names from 'argList' and values from 'args'
let argScope = make_shared<ConfigRecord>(scope, MakeFailFn(argListExpr->location)); // look up in params first; then proceed upwards in lexical scope of '=>' (captured context)
// Note: ^^ The failfn in the ConfigRecord will report unknown variables by pointing to the location of the argList expression.
// However, as long as we run this lambda inside BrainScript, the access will check by itself and instead print the location of the variable.
// create an entry for every argument value
// Note that these values should normally be thunks since we only want to evaluate what's used.
for (size_t i = 0; i < args.size(); i++) // positional arguments
{
let argName = argList[i]; // parameter name
if (argName->op != L"id")
LogicError("function parameter list must consist of identifiers");
auto argVal = move(args[i]); // value of the parameter --TODO: Is this ever unresolved?
let failfn = argVal.GetFailFn();
argScope->Add(argName->id, MakeFailFn(argName->location), move(argVal));
// note: these are expressions for the parameter values; so they must be evaluated in the current scope
}
// also named arguments
for (auto &namedArg : namedArgs)
{
let id = namedArg.first;
auto argVal = move(namedArg.second);
let failfn = argVal.GetFailFn(); // note: do before argVal gets destroyed in the upcoming move()
argScope->Add(id, failfn, move(argVal)); // TODO: is the failfn the right one?
}
// get the macro name for the exprPath
wstring macroId = exprPath;
let pos = macroId.find(L".");
if (pos != wstring::npos)
macroId.erase(0, pos + 1);
// now evaluate the function
return Evaluate(fnExpr, argScope, callerExprPath, L"" /*L"[" + macroId + L"]"*/); // bring args into scope; keep lex scope of '=>' as upwards chain
};
// positional args
vector<wstring> paramNames;
let &argList = argListExpr->args;
for (let &arg : argList)
{
if (arg->op != L"id")
LogicError("function parameter list must consist of identifiers");
paramNames.push_back(arg->id);
}
// named args
// The nammedArgs in the definition lists optional arguments with their default values
ConfigLambda::NamedParams namedParams;
for (let &namedArg : argListExpr->namedArgs)
{
let &id = namedArg.first;
//let & location = namedArg.second.first; // location of identifier
let &expr = namedArg.second.second; // expression to evaluate to get default value
namedParams[id] = move(MakeEvaluateThunkPtr(expr, scope /*evaluate default value in context of definition*/, exprPath /*TODO??*/, id));
//namedParams->Add(id, location/*loc of id*/, ConfigValuePtr(MakeEvaluateThunkPtr(expr, scope/*evaluate default value in context of definition*/, exprPath, id), expr->location, exprPath/*TODO??*/));
// the thunk is called if the default value is ever used
}
return ConfigValuePtr(make_shared<ConfigLambda>(move(paramNames), move(namedParams), f), MakeFailFn(e->location), exprPath);
}
else if (e->op == L"(" || e->op == L"{") // === apply a function to its arguments
{
// Note: "{" is experimental and currently ignored as a distinction. To do it more completely, we need
// - remember how a function was declared (currently not possible for lambdas)
// - make sure the invocation matches declaration
// - disallow calling Parameter() or any other creating functions as "()"
// - disallow calling "{}"-declared functions from inside a "()"
let &lambdaExpr = e->args[0]; // [0] = function
let &argsExpr = e->args[1]; // [1] = arguments passed to the function ("()" expression of expressions)
let lambda = AsPtr<ConfigLambda>(Evaluate(lambdaExpr, scope, exprPath, L"" /*macros are not visible in expression names*/), lambdaExpr, L"function");
if (argsExpr->op != L"()")
LogicError("argument list expected");
// put all args into a vector of values
// Like in an [] expression, we do not evaluate at this point, but pass in a lambda to compute on-demand.
let &args = argsExpr->args;
if (args.size() != lambda->GetNumParams())
Fail(wstrprintf(L"function expects %d positional parameters, %d were provided", (int) lambda->GetNumParams(), (int) args.size()), argsExpr->location);
vector<ConfigValuePtr> argVals(args.size());
//bool onlyOneArg = args.size() == 1 && argsExpr->namedArgs.empty();
for (size_t i = 0; i < args.size(); i++) // positional arguments
{
let argValExpr = args[i]; // expression to evaluate arg [i]
let argName = lambda->GetParamNames()[i];
argVals[i] = move(MakeEvaluateThunkPtr(argValExpr, scope, exprPath /*TODO??*/, /*onlyOneArg ? L"" :*/ argName));
// Make it a thunked value and pass by rvalue ref since unresolved ConfigValuePtrs may not be copied.
/*this wstrprintf should be gone, this is now the exprName*/
// Note on scope: macro arguments form a scope (ConfigRecord), the expression for an arg does not have access to that scope.
// E.g. F(A,B) is used as F(13,A) then that A must come from outside, it is not the function argument.
// This is a little inconsistent with real records, e.g. [ A = 13 ; B = A ] where this A now does refer to this record.
// However, it is still the expected behavior, because in a real record, the user sees all the other names, while when
// passing args to a function, he does not; and also the parameter names can depend on the specific lambda being used.
}
// named args are put into a ConfigRecord
// We could check whether the named ars are actually accepted by the lambda, but we leave that to Apply() so that the check also happens for lambda calls from CNTK C++ code.
let &namedArgs = argsExpr->namedArgs;
ConfigLambda::NamedParams namedArgVals;
// TODO: no scope here? ^^ Where does the scope come in? Maybe not needed since all values are already resolved? Document this!
for (let namedArg : namedArgs)
{
let id = namedArg.first; // id of passed in named argument
let location = namedArg.second.first; // location of expression
let expr = namedArg.second.second; // expression of named argument
namedArgVals[id] = move(MakeEvaluateThunkPtr(expr, scope, exprPath /*TODO??*/, id));
// the thunk is evaluated when/if the passed actual value is ever used the first time
// This array owns the Thunk, and passes it by styd::move() to Apply, since it is not allowed to copy unresolved ConfigValuePtrs.
// Note on scope: same as above.
// E.g. when a function declared as F(A=0,B=0) is called as F(A=13,B=A), then A in B=A is not A=13, but anything from above.
// For named args, it is far less clear whether users would expect this. We still do it for consistency with positional args, which are far more common.
}
// call the function!
return lambda->Apply(move(argVals), move(namedArgVals), exprPath);
}
// --- variable access
else if (e->op == L"[]") // === record (-> ConfigRecord)
{
let newScope = make_shared<ConfigRecord>(scope, MakeFailFn(e->location)); // new scope: inside this record, all symbols from above are also visible
// ^^ The failfn here will be used if C++ code uses operator[] to retrieve a value. It will report the text location where the record was defined.
// create an entry for every dictionary entry.
// We do not evaluate the members at this point.
// Instead, as the value, we keep the ExpressionPtr itself wrapped in a lambda that evaluates that ExpressionPtr to a ConfigValuePtr when called.
// Members are evaluated on demand when they are used.
for (let &entry : e->namedArgs)
{
let &id = entry.first;
let &expr = entry.second.second; // expression to compute the entry
newScope->Add(id, MakeFailFn(entry.second.first /*loc of id*/), MakeEvaluateThunkPtr(expr, newScope /*scope*/, exprPath /*TODO??*/, id));
// Note on scope: record assignments are like a "let rec" in F#/OCAML. That is, all record members are visible to all
// expressions that initialize the record members. E.g. [ A = 13 ; B = A ] assigns B as 13, not to a potentially outer A.
// (To explicitly access an outer A, use the slightly ugly syntax ...A)
}
// BUGBUG: wrong text location passed in. Should be the one of the identifier, not the RHS. NamedArgs store no location for their identifier.
return ConfigValuePtr(newScope, MakeFailFn(e->location), exprPath);
}
else if (e->op == L"id")
return ResolveIdentifier(e->id, e->location, scope); // === variable/macro access within current scope
else if (e->op == L".") // === variable/macro access in given ConfigRecord element
{
let &recordExpr = e->args[0];
return RecordLookup(recordExpr, e->id, e->location, scope /*for evaluating recordExpr*/, exprPath);
}
// --- arrays
else if (e->op == L":") // === array expression (-> ConfigArray)
{
// this returns a flattened list of all members as a ConfigArray type
let arr = make_shared<ConfigArray>(); // note: we could speed this up by keeping the left arg and appending to it
for (size_t i = 0; i < e->args.size(); i++) // concatenate the two args
{
let &expr = e->args[i];
arr->Append(move(MakeEvaluateThunkPtr(expr, scope, msra::strfun::wstrprintf(L"%ls[%d]", exprPath.c_str(), i), L"")));
}
return ConfigValuePtr(arr, MakeFailFn(e->location), exprPath); // location will be that of the first ':', not sure if that is best way
}
else if (e->op == L"array") // === array constructor from lambda function
{
let &firstIndexExpr = e->args[0]; // first index
let &lastIndexExpr = e->args[1]; // last index
let &initLambdaExpr = e->args[2]; // lambda to initialize the values
let firstIndex = ToInt(Evaluate(firstIndexExpr, scope, exprPath, L"array_first"), firstIndexExpr);
let lastIndex = ToInt(Evaluate(lastIndexExpr, scope, exprPath, L"array_last"), lastIndexExpr);
let lambda = AsPtr<ConfigLambda>(Evaluate(initLambdaExpr, scope, exprPath, L"_initializer"), initLambdaExpr, L"function");
if (lambda->GetNumParams() != 1)
Fail(L"'array' requires an initializer function with one argument (the index)", initLambdaExpr->location);
// At this point, we must know the dimensions and the initializer lambda, but we don't need to know all array elements.
// Resolving array members on demand allows recursive access to the array variable, e.g. h[t] <- f(h[t-1]).
// create a vector of Thunks to initialize each value
vector<ConfigValuePtr> elementThunks;
for (int index = firstIndex; index <= lastIndex; index++)
{
let indexValue = MakePrimitiveConfigValuePtr((double) index, MakeFailFn(e->location), exprPath /*never needed*/); // index as a ConfigValuePtr
let elemExprPath = exprPath.empty() ? L"" : wstrprintf(L"%ls[%d]", exprPath.c_str(), index); // expression name shows index lookup
let initExprPath = exprPath.empty() ? L"" : wstrprintf(L"_lambda"); // expression name shows initializer with arg
// create a lambda that realizes this array element
function<ConfigValuePtr()> f = [indexValue, initLambdaExpr, scope, elemExprPath, initExprPath]() // lambda that computes this value of 'expr'
{
if (trace)
TextLocation::PrintIssue(vector<TextLocation>(1, initLambdaExpr->location), L"", wstrprintf(L"index %d", (int) indexValue).c_str(), L"executing array initializer thunk");
// apply initLambdaExpr to indexValue and return the resulting value
let initLambda = AsPtr<ConfigLambda>(Evaluate(initLambdaExpr, scope, initExprPath, L""), initLambdaExpr, L"function"); // get the function itself (most of the time just a simple name)
vector<ConfigValuePtr> argVals(1, indexValue); // create an arg list with indexValue as the one arg
// TODO: where does the current scope come in? Aren't we looking up in namedArgs directly?
let value = initLambda->Apply(move(argVals), ConfigLambda::NamedParams(), elemExprPath);
// TODO: change this ^^ to the const & version of Apply() once it is there
return value; // this is a great place to set a breakpoint!
};
elementThunks.push_back(ConfigValuePtr::MakeThunk(f, MakeFailFn(initLambdaExpr->location), elemExprPath /*TODO??*/));
}
auto arr = make_shared<ConfigArray>(firstIndex, move(elementThunks));
return ConfigValuePtr(arr, MakeFailFn(e->location), exprPath);
}
else if (e->op == L"[") // === access array element by index
{
let arrValue = Evaluate(e->args[0], scope, exprPath, L"_vector");
let &indexExpr = e->args[1];
let arr = AsPtr<ConfigArray>(arrValue, indexExpr, L"array");
let index = ToInt(Evaluate(indexExpr, scope, exprPath, L"_index"), indexExpr);
return arr->At(index, MakeFailFn(indexExpr->location)); // note: the array element may be as of now unresolved; this resolved it
}
// --- unary operators '+' '-' and '!'
else if (e->op == L"+(" || e->op == L"-(") // === unary operators + and -
{
let argValPtr = Evaluate(e->args[0], scope, exprPath, e->op == L"+(" ? L"" : L"_negate");
// note on exprPath: since - has only one argument, we do not include it in the expressionPath --TODO: comment correct?
if (argValPtr.Is<Double>())
if (e->op == L"+(") // +(double) (a no-op)
return argValPtr;
else // -(double)
return MakePrimitiveConfigValuePtr(-ToDouble(argValPtr, e->args[0]), MakeFailFn(e->location), exprPath);
else if (argValPtr.Is<ComputationNodeObject>()) // -ComputationNode becomes NegateNode(arg)
if (e->op == L"+(") // +(node) (a no-op)
return argValPtr;
else // -(node)
return EvaluateNodeOp(e, argValPtr, ConfigValuePtr(), ConfigValuePtr(), scope, exprPath);
else
Fail(L"operator '" + e->op.substr(0, 1) + L"' cannot be applied to this operand (which has type " + Microsoft::MSR::CNTK::ToFixedWStringFromMultiByte(argValPtr.TypeName()) + L")", e->location);
}
else if (e->op == L"!(") // === unary operator !
{
let argValPtr = Evaluate(e->args[0], scope, exprPath, L"_not");
// note on exprPath: since ! has only one argument, we do not include it in the expressionPath --TODO: comment correct?
if (argValPtr.Is<Bool>()) // !(bool)
return MakePrimitiveConfigValuePtr(!ToBoolean(argValPtr, e->args[0]), MakeFailFn(e->location), exprPath);
else // !(node)
return EvaluateNodeOp(e, argValPtr, ConfigValuePtr(), ConfigValuePtr(), scope, exprPath);
}
// --- regular infix operators such as '+' and '=='
else
{
let opIter = infixOps.find(e->op);
if (opIter == infixOps.end())
LogicError("e->op '%ls' not implemented", e->op.c_str());
let &functions = opIter->second;
let &leftArg = e->args[0];
let &rightArg = e->args[1];
#if 1
let leftValPtr = Evaluate(leftArg, scope, exprPath, functions.prettyName + L"Args[0]");
let rightValPtr = Evaluate(rightArg, scope, exprPath, functions.prettyName + L"Args[1]");
#else // This does not actually work. --TODO: find out why
// In the special case of >>, we evaluate the right arg first, as to mimic the same behavior
// as writing the functions as a direct nested evaluation.
let rightValPtr1 = e->op == L">>" ? Evaluate(rightArg, scope, exprPath, functions.prettyName + L"Args[1]") : ConfigValuePtr();
let leftValPtr = Evaluate(leftArg, scope, exprPath, functions.prettyName + L"Args[0]");
let rightValPtr = e->op != L">>" ? Evaluate(rightArg, scope, exprPath, functions.prettyName + L"Args[1]") : rightValPtr1;
#endif
if (leftValPtr.Is<Double>() && rightValPtr.Is<Double>())
return functions.NumbersOp(e, leftValPtr, rightValPtr, scope, exprPath);
else if (leftValPtr.Is<String>() && rightValPtr.Is<String>())
return functions.StringsOp(e, leftValPtr, rightValPtr, scope, exprPath);
else if (leftValPtr.Is<Bool>() && rightValPtr.Is<Bool>())
return functions.BoolOp(e, leftValPtr, rightValPtr, scope, exprPath);
// ComputationNode is "magic" in that we map infix operators like *, +, ==, && to know classes of fixed names.
else if (leftValPtr.Is<ComputationNodeObject>() || rightValPtr.Is<ComputationNodeObject>())
return functions.ComputeNodeOp(e, leftValPtr, rightValPtr, scope, exprPath);
else
return functions.OtherOp(e, leftValPtr, rightValPtr, scope, exprPath);
}
}
catch (ConfigException &err)
{
// in case of an error, we keep track of all parent locations in the parse as well, to make it easier for the user to spot the error
err.AddLocation(e->location);
throw;
}
}