Search code examples
javascriptc++qtinterpreterread-eval-print-loop

What's the point of returning an "Undefined Value" when re-defining "print()" function for QScriptEngine?


[Background]

The default print() function of QScriptEngine prints the result to the terminal of Qt Creator IDE for debugging purpose. As a result, the output must be redirected to our texteditor if we are going to make a ECMA script interpreter ourselves.

This part of the document "Making Applications Scriptable" remains untouched since Qt 4.3.

Section "Redefining print()":

Qt Script provides a built-in print() function that can be useful for simple debugging purposes. The built-in print() function writes to standard output. You can redefine the print() function (or add your own function, e.g. debug() or log()) that redirects the text to somewhere else. The following code shows a custom print() that adds text to a QPlainTextEdit.

So here is the suggested re-definition of print():

QScriptValue QtPrintFunction(QScriptContext *context, QScriptEngine *engine)
 {
     QString result;
     for (int i = 0; i < context->argumentCount(); ++i) {
         if (i > 0)
             result.append(" ");
         result.append(context->argument(i).toString());
     }

     QScriptValue calleeData = context->callee().data();
     QPlainTextEdit *edit = qobject_cast<QPlainTextEdit*>(calleeData.toQObject());
     edit->appendPlainText(result);

     return engine->undefinedValue();
 }

At first, I doubted the need of returning an "Undefined Value" by return engine->undefinedValue();, and it looks like the role of the argument *engine is just to return this void value.

So here is what I've done to change the function:

QScriptValue myPrintFunction(QScriptContext *context, QScriptEngine *engine)
{
    QString result;

    for (int i = 0; i < context->argumentCount(); ++i) {
        if (i > 0)
            result.append(" ");
        result.append(context->argument(i).toString());
    }

    /*
    QScriptValue calleeData = context->callee().data();
    QPlainTextEdit *edit = qobject_cast<QPlainTextEdit*>(calleeData.toQObject());
    edit->appendPlainText(result);

    return engine->undefinedValue();
    */
    return engine->toScriptValue(result); // ---> return the result directly
}

which I think is more reasonable to me: returning an evaluated QScriptValue from script engine, and the value can later be translated to QString for output. This bypass the need of dynamic type cast, which could become messy especially for customized QObjects.

For both kinds of print function, here is the exposition to the script engine:

 QScriptEngine *engine = new QScriptEngine(this); 
 QTextEdit *input = new QTextEdit(this);
 QTextEdit *output = new QTextEdit(this);

 // Use documented print function : 
 QScriptValue fun = engine->newFunction(QtPrintFunction);
 // Use my revised print function : 
 // QScriptValue fun = engine->newFunction(myPrintFunction);
 fun.setData(engine->newQObject(output));
 engine->globalObject().setProperty("print", fun);

Evaluation and output:

QString command = input->toPlainText();
QScriptValue result = engine->evaluate(command);
output->append(result.toString());

[Compilable Code]

(Qt version > 4 is needed)

test.pro

QT += core gui widgets script
TARGET = Test
TEMPLATE = app


SOURCES += main.cpp\
        console.cpp

HEADERS  += console.h

main.cpp

#include <QApplication>
#include "console.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    Console w;
    w.show();

    return app.exec();
}

console.h

#ifndef CONSOLE_H
#define CONSOLE_H

#include <QWidget>
#include <QVBoxLayout>
#include <QTextEdit>
#include <QPushButton>
#include <QScriptEngine>

class Console : public QWidget
{
    Q_OBJECT

public:
    Console();
    ~Console();

public slots:
    void runScript();

private:
    QScriptEngine *engine;
    QVBoxLayout *layout;
    QPushButton *run;
    QTextEdit *input, *output;
};

QScriptValue QtPrintFunction(QScriptContext *context, QScriptEngine *engine);
QScriptValue myPrintFunction(QScriptContext *context, QScriptEngine *engine);

#endif // CONSOLE_H

console.cpp

#include "console.h"

Console::Console()
{
    engine = new QScriptEngine(this);
    layout = new QVBoxLayout(this);
    run = new QPushButton("Run",this);
    input = new QTextEdit(this);
    output = new QTextEdit(this);

    layout->addWidget(input);
    layout->addWidget(run);
    layout->addWidget(output);

    //QScriptValue fun = engine->newFunction(QtPrintFunction);
    QScriptValue fun = engine->newFunction(myPrintFunction);
    fun.setData(engine->newQObject(output));
    engine->globalObject().setProperty("print", fun);

    connect(run, SIGNAL(clicked()), this, SLOT(runScript()));
}

void Console::runScript()
{
    QString command = input->toPlainText();
    QScriptValue result = engine->evaluate(command);
    output->append(result.toString());
}

QScriptValue QtPrintFunction(QScriptContext *context, QScriptEngine *engine)
{
    QString result;
    for (int i = 0; i < context->argumentCount(); ++i) {
        if (i > 0)
            result.append(" ");
        result.append(context->argument(i).toString());
    }

    QScriptValue calleeData = context->callee().data();
    QTextEdit *edit = qobject_cast<QTextEdit*>(calleeData.toQObject());
    edit->append(result);

    return engine->undefinedValue();
}

QScriptValue myPrintFunction(QScriptContext *context, QScriptEngine *engine)
{
    QString result;
    for (int i = 0; i < context->argumentCount(); ++i) {
        if (i > 0)
            result.append(" ");
        result.append(context->argument(i).toString());
    }

    return engine->toScriptValue(result);
}

Console::~Console()
{

}


[Example]

Input 1:

print(123);

Output (Qt Document QtPrintFunction()):

123
undefined

Output (My version myPrintFunction()):

123

Input 2:

for (i = 0; i < 3; i++)
    print(i);

Output (Qt Document QtPrintFunction() ):

0

1

2

undefined

Output (myPrintFunction()):

2


Input 3:

print("Stack");
print("Overflow");

Output (Qt Document QtPrintFunction() ):

Stack

Overflow

undefined

Output (My version myPrintFunction()):

Overflow


[Question]

Although myPrintFunction seems to work fine at first, it didn't work when there are more than two print called in a script, where only the last print will be executed.

It seems the returning of an "Undefined Value" is NECESSARY for the print function. But why???


Solution

  • It's not that it is NECESSARY to return undefinedValue(), but when you do, it's the same as not returning anything. Or essentially, as if you declared the function as void print(...), so to speak.

    That's what the QtPrintFunction does -- it returns "nothing". But it does have a side effect of appending its argument to the internal data object, whenever you call it. That's why you get all of the values passed to print in the output object.

    Now, when you call engine->evaluate() it returns the value of the last evaluated expression. So, with myPrintFunction you get the last value only.

    So, if you were to enter the following:

    print("Stack");
    print("Overflow");
    "garbage";
    

    you will only get garbage back (pun intended), as this was the last evaluated expression.

    But, if you were to enter this:

    print("Stack") + '\n' +
    print("Overflow");
    

    you will get both values, as you expected.

    Additionally, if you enter:

    result = "";
    for (i = 0; i < 3; i++)
        result += print(i) + '\n';
    

    you will also get what you expected.

    Hopefully this explains why you functions behave the way they are.

    However, I don't think this is what you are trying to achieve. So... moving right along.

    One thing you can do is to define the myPrintFunction as follows:

    QScriptValue myPrintFunction(QScriptContext *context, QScriptEngine *engine)
    {
        static QString result;
        for (int i = 0; i < context->argumentCount(); ++i) {
            if (i > 0)
                result.append(" ");
            result.append(context->argument(i).toString());
        }
        result.append('\n');
    
        return engine->toScriptValue(result);
    }
    

    This will "work" the way you expected it to work. The only problem is that you can't clear the value of result. If that works for you, then that will be that.

    There is a more betterer way to do this, which is probably to define a class, eg:

    class QTrace: public QObject
    {
        ...
        void clear();
        void append(const QString& value);
        const QString& get();
    }
    

    and pass an object of that class to fun.setData(engine->newQObject(trace)) and define your function as:

    QScriptValue myPrintFunction(QScriptContext *context, QScriptEngine *engine)
    {
        QString result;
        for (int i = 0; i < context->argumentCount(); ++i) {
            if (i > 0)
                result.append(" ");
            result.append(context->argument(i).toString());
        }
        result.append('\n');
    
        QScriptValue calleeData = context->callee().data();
        QTrace *trace = qobject_cast<QTrace*>(calleeData.toQObject());
        trace->append(result);
    
        return engine->undefinedValue();
    }
    

    Lastly, you would change your runScript function to something like:

    trace->clear();
    
    QScriptValue result = engine->evaluate(command);
    if(result.isError())
        output->append(result.toString());
    else
        output->append(trace->get());
    

    Or there are probably other ways, but hopefully will help you get the ball rolling in the right direction.