Anatomy of a Molt Command

July 2, 2019

Tcl is a sort of a cross between shell languages and LISP, with pretensions to C syntax. As such, the language consists of a collection of commands. A statement in the language is also called a command, and consists of one or more white-space delimited words. For example, the following command appends the string "Some more text" to the variable buffer:

append buffer "Some more text"

The append command can append any number of values to the variable. The following command is equivalent to the first.

append buffer "Some " more " text"

Also, notice that strings in Tcl don’t actually need to be quoted unless they contain whitespace. That’s because in Tcl, Everything is a string. It’s a dynamic language, and so any value can be used in any way that’s consistent with its string representation. The number “5” can be an integer, a string, or a list of one element.

In Tcl 8.6, the append command is defined as follows. (I include the entire function for reference; I’ll be calling attention to specific features further down.)

int
Tcl_AppendObjCmd(
    ClientData dummy,           /* Not used. */
    Tcl_Interp *interp,         /* Current interpreter. */
    int objc,                   /* Number of arguments. */
    Tcl_Obj *const objv[])      /* Argument objects. */
{
    Var *varPtr, *arrayPtr;
    register Tcl_Obj *varValuePtr = NULL;
                                /* Initialized to avoid compiler warning. */
    int i;

    if (objc < 2) {
        Tcl_WrongNumArgs(interp, 1, objv, "varName ?value ...?");
        return TCL_ERROR;
    }

    if (objc == 2) {
        varValuePtr = Tcl_ObjGetVar2(interp, objv[1], NULL,TCL_LEAVE_ERR_MSG);
        if (varValuePtr == NULL) {
            return TCL_ERROR;
        }
    } else {
        varPtr = TclObjLookupVarEx(interp, objv[1], NULL, TCL_LEAVE_ERR_MSG,
                "set", /*createPart1*/ 1, /*createPart2*/ 1, &arrayPtr);
        if (varPtr == NULL) {
            return TCL_ERROR;
        }
        for (i=2 ; i<objc ; i++) {
            /*
             * Note that we do not need to increase the refCount of the Var
             * pointers: should a trace delete the variable, the return value
             * of TclPtrSetVarIdx will be NULL or emptyObjPtr, and we will not
             * access the variable again.
             */

            varValuePtr = TclPtrSetVarIdx(interp, varPtr, arrayPtr, objv[1],
                    NULL, objv[i], TCL_APPEND_VALUE|TCL_LEAVE_ERR_MSG, -1);
            if ((varValuePtr == NULL) ||
                    (varValuePtr == ((Interp *) interp)->emptyObjPtr)) {
                return TCL_ERROR;
            }
        }
    }
    Tcl_SetObjResult(interp, varValuePtr);
    return TCL_OK;
}

The Molt equivalent looks like this:

pub fn cmd_append(interp: &mut Interp, argv: &[Value]) -> MoltResult {
    check_args(1, argv, 2, 0, "varName ?value value ...?")?;

    // FIRST, get the value of the variable.  If the variable is undefined,
    // start with the empty string.

    let var_name = &*argv[1].as_string();
    let var_result = interp.var(var_name);

    let mut new_string = if var_result.is_ok() {
        // Use to_string() because we need a mutable owned string.
        var_result.unwrap().to_string()
    } else {
        String::new()
    };

    // NEXT, append the remaining values to the string.
    for item in &argv[2..] {
        new_string.push_str(&*item.as_string());
    }

    // NEXT, save and return the new value.
    molt_ok!(interp.set_var2(var_name, new_string.into()))
}

First, observe the function signatures:

int
Tcl_AppendObjCmd(ClientData dummy, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[]) {
    ...
}
pub fn cmd_append(interp: &mut Interp, argv: &[Value]) -> MoltResult {
    ...
}

In both cases the function takes the interpreter and an array containing the command and its arguments. In Tcl 7.6 and earlier, the array would have been any array of strings; in Tcl 8.6 and Molt, it’s a special type (Tcl_Obj, Value) an immutable string that may also take on specific binary data representations. (I’ll talk about the anatomy of Value in a later post.)

The Tcl_AppendObjCommand also takes a ClientData argument; this is used to associate context data with the command. It isn’t needed for append.

Finally, notice the return value: int vs. MoltResult, which is a type alias:

pub type MoltResult = Result<Value, ResultCode>;

In Rust, the append command returns a Value on success and a ResultCode (which is very much more than a simple error) on failure. We’ll come back to that.

The next step is to validate the argument list. The append command must take at least two words, the command name and a variable name. If it contains only one word, that’s an error. The C code handles this by calling Tcl_WrongNumArgs to generate an error message, and then returns the integer code TCL_ERROR. The error message gets stashed in the interpreter’s “result” field.

if (objc < 2) {
     Tcl_WrongNumArgs(interp, 1, objv, "varName ?value ...?");
     return TCL_ERROR;
}

The error message looks like this (question marks in Tcl command signatures indicate optional arguments):

wrong # args: should be "append varName ?value value ...?"

The Molt code does exactly the same thing, but much more concisely. The check_args method checks the minimum and maximum number of arguments against the actual number; if there are two few it produces the same error message and returns it as part of the MoltResult.

check_args(1, argv, 2, 0, "varName ?value value ...?")?;

Interestingly, the pattern is exactly the same; the only significant difference is that Rust provides first class support for it in the language itself via the Result<T,E> type and the ? operator.

Once the new string has been created, it’s assigned back to the variable; and then new string is returned to the caller. In C the value is returned by explicitly setting the interpreter’s result field, and the integer constant TCL_OK is returned to indicate that the command executed successfully.

Tcl_SetObjResult(interp, varValuePtr);
return TCL_OK;

The constant TCL_OK indicates that the command executed successfully.

In Rust, we use the following code, which assigns the new string to the variable, gets it back as a Value, and wraps it up in Ok():

molt_ok!(interp.set_var2(var_name, new_string.into()))

The molt_ok! macro takes an argument or arguments in a number of forms, converts its arguments to a Value if necessary, and wraps the value in Ok(). We could also write it like this:

let value = interp.set_var2(var_name, new_string.into());
Ok(value)

The Rust version is shorter, and undeniably easier to read. (And I should add that the Tcl code base is considered quite readable—for C code.)

Coming Attractions

  • The Molt ResultCode
  • The Molt Value Type
  • Commands with Context