[][src]Module molt::interp

The Molt Interpreter

The Interp struct is the primary API for embedding Molt into a Rust application. Given an Interp, the application may:

  • Evaluate scripts and expressions
  • Check scripts for completeness
  • Extend the language by defining new Molt commands in Rust
  • Set and get Molt variables
  • Access application data via the context cache

The following describes the features of the Interp in general; follow the links for specifics of the various types and methods. See also The Molt Book for a general introduction to Molt and its API.

Interp is not Sync!

The Interp class (and the rest of Molt) is intended for use in a single thread. It is safe to have Interps in different threads; but use String (or another Sync) when passing data between them. In particular, Value is not Sync.

Creating an Interpreter

There are two ways to create an interpreter. The usual way is to call Interp::new, which creates an interpreter and populates it with all of the standard Molt commands. The application can then add any application-specific commands.

Alternatively, Interp::empty creates an interpreter with no built-in commands, allowing the application to define only those commands it needs. Such an empty interpreter can be configured as the parser for data and configuration files, or as the basis for a simple console command set.

TODO: Define a way to add various subsets of the standard commands to an initially empty interpreter.

use molt::Interp;
let mut interp = Interp::new();

// add commands, evaluate scripts, etc.

Evaluating Scripts

There are a number of ways to evaluate Molt scripts. The simplest is to pass the script as a string to Interp::eval. The interpreter evaluates the string as a Molt script, and returns either a normal Value containing the result, or a Molt error. The script is evaluated in the caller's context: if called at the application level, the script will be evaluated in the interpreter's global scope; if called by a Molt command, it will be evaluated in the scope in which that command is executing.

For example, the following snippet uses the Molt expr command to evaluate an expression.

use molt::Interp;
use molt::molt_ok;
use molt::types::*;

let _ = my_func();

fn my_func() -> MoltResult {
// FIRST, create the interpreter and add the needed command.
let mut interp = Interp::new();

// NEXT, evaluate a script containing an expression,
// propagating errors back to the caller
let val = interp.eval("expr {2 + 2}")?;
assert_eq!(val.as_str(), "4");
assert_eq!(val.as_int()?, 4);

molt_ok!()
}

Interp::eval_value is equivalent to Interp::eval but takes the script as a Value instead of as a &str. When called at the top level, both methods convert the break and continue return codes (and any user-defined return codes) to errors; otherwise they are propagated to the caller for handling. It is preferred to use Interp::eval_value when possible, as Interp::eval will reparse its argument each time if called multiple times on the same input.

All of these methods return MoltResult:

This example is not tested
pub type MoltResult = Result<Value, Exception>;

Value is the type of all Molt values (i.e., values that can be passed as parameters and stored in variables). Exception is a struct that encompasses all of the kinds of exceptional return from Molt code, including errors, return, break, and continue.

Evaluating Expressions

In Molt, as in Standard Tcl, algebraic expressions are evaluated by the expr command. At the Rust level this feature is provided by the Interp::expr method, which takes the expression as a Value and returns the computed Value or an error.

There are three convenience methods, Interp::expr_bool, Interp::expr_int, and Interp::expr_float, which streamline the computation of a particular kind of value, and return an error if the computed result is not of that type.

For example, the following code shows how a command can evaluate a string as a boolean value, as in the if or while commands:

use molt::Interp;
use molt::molt_ok;
use molt::types::*;

// FIRST, create the interpreter
let mut interp = Interp::new();

// NEXT, get an expression as a Value.  In a command body it would
// usually be passed in as a Value.
let expr = Value::from("1 < 2");

// NEXT, evaluate it!
assert!(interp.expr_bool(&expr)?);

These methods will return an error if the string cannot be interpreted as an expression of the relevant type.

Defining New Commands

The usual reason for embedding Molt in an application is to extend it with application-specific commands. There are several ways to do this.

The simplest method, and the one used by most of Molt's built-in commands, is to define a CommandFunc and register it with the interpreter using the Interp::add_command method. A CommandFunc is simply a Rust function that returns a MoltResult given an interpreter and a slice of Molt Value objects representing the command name and its arguments. The function may interpret the array of arguments in any way it likes.

The following example defines a command called square that squares an integer value.

use molt::Interp;
use molt::check_args;
use molt::molt_ok;
use molt::types::*;

// FIRST, create the interpreter and add the needed command.
let mut interp = Interp::new();
interp.add_command("square", cmd_square);

// NEXT, try using the new command.
let val = interp.eval("square 5")?;
assert_eq!(val.as_str(), "25");

// The command: square intValue
fn cmd_square(_: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
    // FIRST, check the number of arguments.  Returns an appropriate error
    // for the wrong number of arguments.
    check_args(1, argv, 2, 2, "intValue")?;

    // NEXT, get the intValue argument as an int.  Returns an appropriate error
    // if the argument can't be interpreted as an integer.
    let intValue = argv[1].as_int()?;

    // NEXT, return the product.
    molt_ok!(intValue * intValue)
}

The new command can then be used in a Molt interpreter:

% square 5
25
% set a [square 6]
36
% puts "a=$a"
a=36

Accessing Variables

Molt defines two kinds of variables, scalars and arrays. A scalar variable is a named holder for a Value. An array variable is a named hash table whose elements are named holders for Values. Each element in an array is like a scalar in its own right. In Molt code the two kinds of variables are accessed as follows:

% set myScalar 1
1
% set myArray(myElem) 2
2
% puts "$myScalar $myArray(myElem)"
1 2

In theory, any string can be a valid variable or array index string. In practice, variable names usually follow the normal rules for identifiers: letters, digits and underscores, beginning with a letter, while array index strings usually don't contain parentheses and so forth. But array index strings can be arbitrarily complex, and so a single TCL array can contain a vast variety of data structures.

Molt commands will usually use the Interp::var, Interp::set_var, and Interp::set_var_return methods to set and retrieve variables. Each takes a variable reference as a Value. Interp::var retrieves the variable's value as a Value, return an error if the variable doesn't exist. Interp::set_var and Interp::set_var_return set the variable's value, creating the variable or array element if it doesn't exist.

Interp::set_var_return returns the value assigned to the variable, which is convenient for commands that return the value assigned to the variable. The standard set command, for example, returns the assigned or retrieved value; it is defined like this:

use molt::Interp;
use molt::check_args;
use molt::molt_ok;
use molt::types::*;

pub fn cmd_set(interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
   check_args(1, argv, 2, 3, "varName ?newValue?")?;

   if argv.len() == 3 {
       interp.set_var_return(&argv[1], argv[2].clone())
   } else {
       molt_ok!(interp.var(&argv[1])?)
   }
}

At times it can be convenient to explicitly access a scalar variable or array element by by name. The methods Interp::scalar, Interp::set_scalar, Interp::set_scalar_return, Interp::element, Interp::set_element, and Interp::set_element_return provide this access.

Managing Application or Library-Specific Data

Molt provides a number of data types out of the box: strings, numbers, and lists. However, any data type that can be unambiguously converted to and from a string can be easily integrated into Molt. See the value module for details.

Other data types cannot be represented as strings in this way, e.g., file handles, database handles, or keys into complex application data structures. Such types can be represented as key strings or as object commands. In Standard TCL/TK, for example, open files are represented as strings like file1, file2, etc. The commands for reading and writing to files know how to look these keys up in the relevant data structure. TK widgets, on the other hand, are presented as object commands: a command with subcommands where the command itself knows how to access the relevant data structure.

Application-specific commands often need access to the application's data structure. Often many commands will need access to the same data structure. This is often the case for complex binary extensions as well (families of Molt commands implemented as a reusable crate), where all of the commands in the extension need access to some body of extension-specific data.

All of these patterns (and others) are implemented by means of the interpreter's context cache, which is a means of relating mutable data to a particular command or family of commands. See below.

Commands and the Context Cache

Most Molt commands require access only to the Molt interpreter in order to do their work. Some need mutable or immutable access to command-specific data (which is often application-specific data). This is provided by means of the interpreter's context cache:

  • The interpreter is asked for a new ContextID, an ID that is unique in that interpreter.

  • The client associates the context ID with a new instance of a context data structure, usually a struct. This data structure is added to the context cache.

    • This struct may contain the data required by the command(s), or keys allowing it to access the data elsewhere.
  • The ContextID is provided to the interpreter when adding commands that require that context.

  • A command can mutably access its context data when it is executed.

  • The cached data is dropped when the last command referencing a ContextID is removed from the interpreter.

This mechanism supports all of the patterns described above. For example, Molt's test harness provides a test command that defines a single test. When it executes, it must increment a number of statistics: the total number of tests, the number of successes, the number of failures, etc. This can be implemented as follows:

use molt::Interp;
use molt::check_args;
use molt::molt_ok;
use molt::types::*;

// The context structure to hold the stats
struct Stats {
    num_tests: usize,
    num_passed: usize,
    num_failed: usize,
}

// Whatever methods the app needs
impl Stats {
    fn new() -> Self {
        Self { num_tests: 0, num_passed: 0, num_failed: 0}
    }
}

// Create the interpreter.
let mut interp = Interp::new();

// Create the context struct, assigning a context ID
let context_id = interp.save_context(Stats::new());

// Add the `test` command with the given context.
interp.add_context_command("test", cmd_test, context_id);

// Try using the new command.  It should increment the `num_passed` statistic.
let val = interp.eval("test ...")?;
assert_eq!(interp.context::<Stats>(context_id).num_passed, 1);

// A stub test command.  It ignores its arguments, and
// increments the `num_passed` statistic in its context.
fn cmd_test(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
    // Pretend it passed
    interp.context::<Stats>(context_id).num_passed += 1;

    molt_ok!()
}

Ensemble Commands

An ensemble command is simply a command with subcommands, like the standard Molt info and array commands. At the Rust level, it is simply a command that looks up its subcommand (e.g., argv[1]) in an array of Subcommand structs and executes it as a command.

The Interp::call_subcommand method is used to look up and call the relevant command function, handling all relevant errors in the TCL-standard way.

For example, the array command is defined as follows.

This example is not tested
const ARRAY_SUBCOMMANDS: [Subcommand; 6] = [
    Subcommand("exists", cmd_array_exists),
    Subcommand("get", cmd_array_get),
    // ...
];

pub fn cmd_array(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
    interp.call_subcommand(context_id, argv, 1, &ARRAY_SUBCOMMANDS)
}

pub fn cmd_array_exists(interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
    check_args(2, argv, 3, 3, "arrayName")?;
    molt_ok!(Value::from(interp.array_exists(argv[2].as_str())))
}

// ...

The cmd_array and cmd_array_exists functions are just normal Molt CommandFunc functions. The array command is added to the interpreter using Interp::add_command in the usual way. Note that the context_id is passed to the subcommand functions, though in this case it isn't needed.

Also, notice that the call to check_args in cmd_array_exists has 2 as its first argument, rather than 1. That indicates that the first two arguments represent the command being called, e.g., array exists.

Object Commands

An object command is an ensemble command that represents an object; the classic TCL examples are the TK widgets. The pattern for defining object commands is as follows:

  • A constructor command that creates instances of the given object type. (We use the word type rather than class because inheritance is usually neither involved or available.)

  • An instance is an ensemble command:

    • Whose name is provided to the constructor
    • That has an associated context structure, initialized by the constructor, that belongs to it alone.
  • Each of the object's subcommand functions is passed the object's context ID, so that all can access the object's data.

Thus, the constructor command will do the following:

  • Create and initialize a context structure, assigning it a ContextID via Interp::save_context.

    • The context structure may be initialized with default values, or configured further based on the constructor command's arguments.
  • Determine a name for the new instance.

    • The name is usually passed in as an argument, but can be computed.
  • Create the instance using Interp::add_context_command and the instance's ensemble CommandFunc.

  • Usually, return the name of the newly created command.

Note that there's no real difference between defining a simple ensemble like array, as shown above, and defining an object command as described here, except that:

  • The instance is usually created "on the fly" rather than at interpreter initialization.
  • The instance will always have data in the context cache.

Checking Scripts for Completeness

The Interp::complete method checks whether a Molt script is complete: e.g., that it contains no unterminated quoted or braced strings, that would prevent it from being evaluated as Molt code. This is useful when implementing a Read-Eval-Print-Loop, as it allows the REPL to easily determine whether it should evaluate the input immediately or ask for an additional line of input.

Structs

Interp

The Molt Interpreter.