[−][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, non-scriptable 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::*; // FIRST, create the interpreter and add the needed command. let mut interp = Interp::new(); // NEXT, evaluate a script containing an expression let val = interp.eval("expr {2 + 2}")?; assert_eq!(val.as_str(), "4"); assert_eq!(val.as_int()?, 4);
Interp::eval_value
evaluates the string
representation of a Value
as a script.
Interp::eval_body
is used to evaluate the body
of loops and other control structures. Unlike Interp::eval
and Interp::eval_value
, it
passes the return
, break
, and continue
result codes back to the caller for handling.
All of these methods return MoltResult
:
pub type MoltResult = Result<Value, ResultCode>;
Value
is the type of all Molt values (i.e., values that can be passed as parameters and
stored in variables). ResultCode
is an enum 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.
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
viaInterp::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 ensembleCommandFunc
. -
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. |