static ConfigValuePtr Evaluate()

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;
    }
}