Molt-forked
This is a forked version of molt
, a embeddable TCL interpreter for Rust applications. The original repository is no longer actively maintained, and this version aims to continue its development, fix bugs, and add new features.
New in Molt-forked 0.4.1
- The subcommands now is static, we can use
gen_subcommand!
macro to init SubCommand.
New in Molt-forked 0.4.0
-
WASM runtime support, see demo at here, the demo project is in
molt-wasm
and you can find that the size of compiled WASM binary is only ~600k. -
Remove
ContextMap
and related attributes / function parameters in Interpreter. Now the definiton of Interpreter isInterp<Ctx>
(with user-defined genericCtx
), we can access Interpreter's Context directly viainterp.context
. -
The native commands now is static, we need to use
gen_command!
macro to init Command.The benefit is that
molt-fork
can usematch
block to implement token matching, rather thanHashMap
-
New document (The Molt Book, Code Description) is not unimplemented yet.
Benchmark Result
-
Command:
cd molt-app && cargo run --release bench ../benchmarks/basic.tcl
-
Platform: Intel Xeon 6348 CPU
molt-forked 0.4.0 (time unit in Nanos) | molt 0.3.2 | Speedup (×) | Benchmark |
---|---|---|---|
89 | 208 | 2.34 | ok-1.1 ok, no arguments |
90 | 207 | 2.3 | ok-1.2 ok, one argument |
97 | 219 | 2.26 | ok-1.3 ok, two arguments |
119 | 209 | 1.76 | ident-1.1 ident, simple argument |
209 | 402 | 1.92 | incr-1.1 incr a |
158 | 311 | 1.97 | set-1.1 set var value |
201 | 348 | 1.73 | list-1.1 list of six items |
===================== Below is Origin Document =====================
Molt: More Or Less Tcl
Molt 0.3.2 is a minimal implementation of the TCL language for embedding in Rust apps and for scripting Rust libraries. Molt is intended to be:
-
Small in size. Embedding Molt shouldn't greatly increase the size of the application.
-
Small in language. Standard TCL has many features intended for building entire software systems. Molt is intentionally limited to those needed for embedding.
-
Small in dependencies. Including the Molt interpreter in your project shouldn't drag in anything else--unless you ask for it.
-
Easy to build. Building Standard TCL is non-trivial. Embedding Molt should be as simple as using any other crate.
-
Easy to embed. Extending Molt with TCL commands that wrap Rust APIs should be easy and simple.
Hence, perfect compatibility with Standard TCL is explicitly not a goal. Many
features will not be implemented at all (e.g., octal literals); and others may
be implemented somewhat differently where a clearly better alternative exists
(e.g., -nocomplain
will always be the normal behavior). In addition, Molt will
prefer Rust standards where appropriate.
On the other hand, Molt is meant to be TCL (more or less), not simply a "Tcl-like language", so gratuitous differences are to be avoided. One of the goals of this document is to carefully delineate:
- The features that have not yet been implemented.
- The features that likely will never be implemented.
- Any small differences in behavior.
- And especially, any features that have intentionally been implemented in a different way.
What Molt Is For
Using Molt, you can:
- Create a shell interpreter for scripting and interactive testing of your Rust crates.
- Provide your Rust applications with an interactive REPL for debugging and administration.
- Extend your Rust application with scripts provided at compile-time or at run-time.
- Allow your users to script your applications and libraries.
See the molt-sample
repo for a sample Molt client
skeleton.
New in Molt 0.3.2
Nothing, yet! See the Annotated Change Log for the new features by version.
Coming Attractions
At this point Molt is capable and robust enough for real work, though the Rust-level API is
not yet completely stable. Standard Rust 0.y.z
semantic versioning applies: ".y" changes
can break the Rust-level API, ".z" changes will not.
- More TCL Commands
- Testing improvements
- Documentation improvements
- Feature: Regex and Glob pattern matching by Molt commands
Tcl Compatibility
Molt is aiming at limited compatibility with TCL 8.x, the current stable version of Standard TCL, as described above. The development plan is as follows:
- Implement the complete Molt semantics
- Core interpreter
- Essential TCL commands
- Robust and ergonomic Rust-level API for extending TCL in Rust
- Related tools (e.g., TCL-level test harness)
- Thorough and complete test suite (at both Rust and TCL levels)
- Thorough documentation
- Optimize for speed
- Ideally including byte-compilation
- Extend with new features as requested.
Each TCL command provided standard by the Molt interpreter is documented in this book with a complete man page. A command's man page documents the semantics of the command, and any temporary or permanent differences between it and the similarly named command in Standard TCL.
The remainder of this section documents overall differences; see the Molt README for details on current development.
Note that some of the features described as never to be implemented could conceivably be added as extension crates.
Features that already exist
See the command reference for the set of commands that have already been implemented. The current set of features includes:
At the TCL Level:
- Script execution
- Procedure definition
- Standard control structures (except the
switch
command) - Local and global variables, including associative arrays
- Boolean and numeric expressions
- Dictionaries
- Many standard TCL commands
- A modicum of introspection
At the Rust Level:
- A clean and modular embedding API
- The
Interp
struct (e.g., Standard TCL's Interp)- API for defining new commands in Rust, setting and querying variables, and evaluating TCL code.
- The
Value
type (e.g., Tcl_Obj)- TCL values are strings;
Value
shares them efficiently by reference counting, and caches binary data representations for run-time efficiency.
- TCL values are strings;
Related Tools:
- An interactive REPL
- Using the
rustyline
crate for line editing.
- Using the
- A shell, for execution of script files
- A test harness
Features to be added soon
See the overview and the Molt README.
Features to be added eventually
- Globs and Regexes
- Some way to create ensemble commands and simple objects
Features that might someday be added (depending on demand)
- Namespaces
- Slave interpreters
- File I/O
- Event loop
- Byte Compilation
- Communication between
Interps
in different threads - Traces
- Some kind of TCL-level module architecture
Features that will almost certainly never be added
- The TCL autoloader
- Packages/TCL Modules (as represented in Standard TCL)
- Coroutines
- Support for dynamically loading Molt extensions written in Rust
- Support for Molt extensions written in C (or anything but Rust)
- But note that a Molt extension written in Rust can certainly call into C libraries in the usual way.
- Network I/O
- OOP (in the form of TclOO)
Miscellaneous Differences
See the man pages for specific commands for other differences.
- Integer literals beginning with "0" are NOT assumed to be octal, Nor will they ever be.
- The encoding is currently always UTF-8.
- In variable names, e.g.
$name
, the name may include underscores and any character that Rust considers to be alphanumeric. - The notion of what constitutes whitespace is generally left up to Rust.
- When using the TCL shell interactively, TCL will attempt to match
partial names of commands and subcommands as a convenience. Molt does not.
- In principle, some form of tab-completion could be added at some point.
Annotated Change Log
New in Molt 0.3.2
Nothing yet!
New in Molt 0.3.1
- Added the molt_throw! macro.
- Improved the API documentation for
molt_ok!
andmolt_err!
. - Added
Exception::error_code
andException::error_info
, to streamline using exceptions. - Added the
env()
array, which contains the current environment variable settings.- Note: as yet, changes to the
env()
array are not mirrored back to the process environment.
- Note: as yet, changes to the
- Added the string command
string cat
string compare
string first
string last
string length
string map
string range
string tolower
string toupper
string trim
string trimleft
string trimright
New in Molt 0.3.0
The changes in Molt 0.3.0 break the existing API in two ways:
- The syntax for
molt_shell::repl
has changed slightly. - The
MoltResult
type has changed significantly.
Keep reading for the full details.
Molt Shell: User-Definable Prompts
Thanks to Coleman McFarland, molt_shell::repl
now supports programmable prompts via the
tcl_prompt1
variable. See the rustdocs and the molt_shell
discussion in this book for more information.
Error Stack Traces
Molt now provides error stack traces in more-or-less the same form as standard TCL. Stack
traces are accessible to Rust clients, are printed by the Molt shell, and can be
accessed in scripts via the catch
command and the errorInfo
variable
in the usual TCL way.
Error Codes
Molt scripts and Rust code can now throw errors with an explicit error code, as in Standard
TCL; see the throw
and catch
commands.
Return Protocol
Molt now supports the full return
/catch
protocol for
building application-specific control structures in script code. The mechanism as implemented
is slightly simpler than in Standard TCL, but should be sufficient for all practical
purposes. See the referenced commands for specifics.
MoltResult
and the Exception
Struct
In order to support the above changes, the definition of the
MoltResult
type has changed. Instead of
#![allow(unused)] fn main() { pub type MoltResult = Result<Value, ResultCode>; }
it is now
#![allow(unused)] fn main() { pub type MoltResult = Result<Value, Exception>; }
where Exception
is a struct containing the ResultCode
and other necessary data. The
ResultCode
enum still exists, but has been simplified. See the rust doc for details.
New in Molt 0.2
Dictionaries and the dict
command
Molt now supports TCL dictionary values. The dict
command provides the
following subcommands:
- dict create
- dict exists
- dict keys
- dict get
- dict remove
- dict set
- dict size
- dict unset
- dict values
Other dict
subcommands will be added over time.
Associative Arrays
Molt now includes TCL's associative array variables:
% set a(1) "Howdy"
Howdy
% set a(foo.bar) 5
5
% puts [array get a]
1 Howdy foo.bar 5
The Expansion Operator
Molt now supports TCL's {*}
operator, which expands a single
command argument into multiple arguments:
% set a {a b c}
a b c
% list 1 2 $a 3 4
1 2 {a b c} 3 4
% list 1 2 {*}$a 3 4
1 2 a b c 3 4
More info
Subcommands
Molt now supports the following subcommands of the info
command:
info args
info body
info cmdtype
info default
info exists
info globals
(no glob-filtering as yet)info locals
(no glob-filtering as yet)info procs
Rust API Change: Test Harness
The Molt test harness code has moved from molt_shell:test_harness
to molt::test_harness
,
so that it can be used in the molt/tests/tcl_tests.rs
integration test.
Rust API Change: Variable Access
The addition of array variables required changes to the molt::Interp
struct's API for
setting and retrieving variables. In particular, the molt::Interp::var
,
molt::Interp::set_var
, and molt::Interp::set_and_return
methods now take the variable
name as a &Value
rather than a &str
; this simplifies client code, and means that most
commands implemented in Rust that work with variables don't need to care whether the
variable in question is a scalar or an array element.
Rust API Change: Command Definition
Defining Molt commands in Rust has been simplified.
First, the Command
trait has been removed. It was intended to provide a way to
attach context data to a command; but it was not very good for mutable data, and had
no way to share data among related commands (a common pattern).
Second, the interpreter's context cache has been improved. Multiple commands can share a context ID (and hence access to the shared context); and the cached data will be dropped automatically when the last such command is removed from the interpreter.
Third, there is now only one command function signature:
fn my_command(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult {
...
}
Commands that don't use a cached context should be defined as follows:
fn my_command(interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
...
}
Molt Command Line Tool
The molt-app
crate provides a command line tool for use in development and
experimentation. The command line tool, called molt
, has several subcommands:
molt shell executes scripts and provides an interactive REPL.
molt test executes Molt test suites, most notably Molt's own test suite.
molt bench executes Molt benchmarks. This tool is experimental, and is primarily for use in optimizing molt itself.
Note: the molt-shell
crate provides the same features for use with customized Molt interpreters.
molt shell ?script? ?args...?
The molt shell
command invokes the Molt interpreter.
Interactive Use
When called without any arguments, the command invokes the interactive interpreter:
$ molt shell
Molt 0.3.0
%
Molt commands may be entered at the %
prompt. Enter exit
to leave the interpreter.
Script Execution
When called with arguments, the first argument is presumed to be the name of a Molt script; any subsequent arguments are passed to the script.
$ molt shell my_script.tcl arg1 arg2 arg3
...
$
When called in this way, the variable arg0 contains the name of the script, and the variable argv contains a list of the additional arguments (if any).
For example, consider the following script, args.tcl
:
puts "arg0 = $arg0"
puts "argv = $argv"
This script may be run as follows
$ molt shell args.tcl a b c
arg0 = args.tcl
argv = a b c
$
Interactive Prompts
The molt shell
and its underlying method, molt_shell::repl
, support interactive prompts
via the tcl_prompt1
variable. If defined, tcl_prompt1
should be a script; its value
will be output as the prompt.
$ molt shell
% set count 0
% set tcl_prompt1 {return "[incr count]> "}
return "[incr count]> "
1> puts "Howdy!"
Howdy!
2>
This is slightly different than in Standard TCL, where the tcl_prompt1
script is intended
to output the prompt rather than return it.
TCL Liens
The Standard TCL shell, tclsh
, provides a number of features that Molt currently does not.
-
A
.tclshrc
file for initializing interactive shells.- A similar file will be added in the future.
-
An option to execute a script and then start the interactive shell.
- This can be added if there is demand.
-
Environment variables for locating the interpreter's library of TCL code, locally installed TCL packages, etc.
- Molt's library of TCL code is compiled into the interpreter, rather than being loaded from disk at run-time.
- At present, Molt has no support for externally-defined TCL packages
(other than the
source
command).
molt test filename ?args...?
This command executes the test script called filename using the Molt
test harness, which is similar to Standard TCL's tcltest
framework (though
much simpler, at present). Any arguments are passed to the test harness
(which ignores them, at present).
Test Suites
molt test
is often used to execute an entire test suite, spread over
multiple files. To simplify writing such a suite, molt test
assumes
that the folder containing the specified filename is the base folder for
the test suite, and sets the current working directory to that folder.
This allows the named test script to use source to
load other test scripts using paths relative to its own location.
Writing Tests
Tests are written using the test command. See that man page for examples.
Running Tests
For example,
$ molt test good_tests.tcl
molt 0.2.0 -- Test Harness
5 tests, 5 passed, 0 failed, 0 errors
$ molt test bad_tests.tcl
molt 0.2.0 -- Test Harness
*** FAILED mytest-1.1 some proc
Expected -ok <this result>
Received -ok <that result>
2 tests, 1 passed, 1 failed, 0 errors
test name description args ...
Available in molt test scripts only!
The test
command executes its body as a Molt script and compares its result
to an expected value. It may be used to test Molt commands, whether built-in or coded
in Molt. The expected value may be an -ok
result or an -error
message.
The name and description are used to identify the test in the output. The
name can be any string, but the convention is to use the format
"baseName-x.y", e.g., mycommand-1.1
. In the future, molt test
will allow the user to filter the set of tests on this name string.
The test is executed in its own local variable scope; variables used by the test will be cleaned up automatically at the end of the test. The global command may be used to reference global variables; however, changes to these must be cleaned up explicitly. Similarly, any procs defined by the test must be cleaned up explicitly.
The test
command has two forms, a brief form and an extended form with more options.
test name description body -ok|-error expectedValue
In the brief form, the body is the test script itself; and it is expected to return a normal result or an error message. Either way, expectedValue is the expected value.
- The test passes if the body returns the right kind of result with the expected value.
- The test fails if the body returns the right kind of result (e.g.,
-ok
) with some other value. - The test is in error if the body returns the wrong kind of result, (e.g., an error was returned when a normal result was expected).
test name description option value ?option value ...?
In the extended form, the details of the test are specified using options:
-
-setup: indicates a setup script, which will be executed before the body of the test. The test is flagged as an error if the setup script returns anything but a normal result.
-
-body: indicates the test's body, which is interpreted as described above.
-
-cleanup: indicates a cleanup script, which will be executed after the body of the test. The test is flagged as an error if the cleanup script returns anything but a normal result.
-
-ok | -error: indicates the expected value, as described above.
Examples
The following tests are for an imaginary square
command that returns the square
of a number. They use the brief form.
test square-1.1 {square errors} {
square
} -error {wrong # args: should be "square number"}
test square-2.1 {square command} {
square 3
} -ok {9}
The following test shows the extended form:
test newproc-1.1 {new proc} -setup {
# Define a proc for use in the test
proc myproc {} { return "called myproc" }
} -body {
# Call the proc
myproc
} -cleanup {
# Clean up the proc
rename myproc ""
} -error {called myproc}
TCL Notes
This command is a simplified version of the test
command defined by
Standard TCL's tcltest(n)
framework. The intention is to increase the
similarity over time.
This command has an enhancement over TCL's test
command: the test has
its own local variable scope, just as a proc does. The body
must use the global command to access global variables.
molt bench filename ?-csv?
This command executes the benchmark script called filename using the Molt benchmark framework. The framework runs the benchmarks in the script and outputs the results in nanoseconds.
NOTE: The benchmark tool is experimental, subject to change, and primarily intended as aid for Molt optimization.
The output looks like this.
$ molt bench benchmarks/basic.tcl
Molt 0.2.0 -- Benchmark
Nanos Norm -- Benchmark
3344 1.00 -- ok-1.1 ok, no arguments
4110 1.23 -- ok-1.2 ok, one argument
4442 1.33 -- ok-1.3 ok, two arguments
4005 1.20 -- ident-1.1 ident, simple argument
7175 2.15 -- incr-1.1 incr a
6648 1.99 -- set-1.1 set var value
7926 2.37 -- list-1.1 list of six items
...
$
The Norm
column shows the times relative to the first benchmark in the set.
CSV Output
Use the -csv
option to produce output in CSV format:
$ molt bench benchmarks/basic.tcl -csv
"benchmark","description","nanos","norm"
"ok-1.1","ok, no arguments",3313,1
"ok-1.2","ok, one argument",4027,1.2155146392997283
"ok-1.3","ok, two arguments",4439,1.3398732266827649
"ident-1.1","ident, simple argument",4026,1.2152127980682161
"incr-1.1","incr a",7325,2.210987020827045
"set-1.1","set var value",6499,1.9616661635979475
"list-1.1","list of six items",7848,2.3688499849079383
...
Writing Benchmarks
Benchmarks are written using the benchmark or measure commands. See those man pages for examples.
benchmark name description body ?count?
Available in molt bench scripts only!
Defines a benchmark with the given name and description. The body is a Tcl script; it is executed count times via the time command, and records the average runtime in microseconds. The count defaults to 1000 iterations.
The name should be a symbolic name for easy searching; the description should be a brief human-readable description of the benchmark.
Example
The following is a simple benchmark of the incr command.
benchmark incr-1.1 {incr a} {
incr a
}
measure name description micros
Available in molt bench scripts only!
This is a low-level command used by the benchmark command to record measurements. All recorded measurements will be included in the tool's output.
Benchmark scripts won't usually need to call this; however, it can be useful when defining custom benchmarking commands.
Example
measure incr-1.1 "incr a" 1.46
ok ?arg arg...?
Available in molt bench scripts only!
This command takes any number of arguments and returns the empty string. It is useful when benchmarking code that calls other commands, as (with no arguments) it represents the minimum amount of computation the Molt interpreter can do.
Example
For example, Molt's own benchmark suite includes the following as its baseline, as a lower bound on the run-time of evaluating a script:
benchmark ok-1.1 {ok, no arguments} {
ok
}
ident value
Available in molt bench scripts only!
Returns its argument unchanged. Like ok, this is a command used for constructing benchmarks of the Molt interpreter itself.
Molt Command Reference
Molt implements the following commands. See the reference for each command to see any differences from Standard TCL.
Command | Description |
---|---|
append | Appends values to a list |
array | Query and manipulate array variables |
assert_eq | Equality assertion |
break | Break loop execution |
catch | Catch exceptions |
continue | Continue with next iteration |
dict | Dictionary manipulation |
error | Throws an error |
exit | Exit the application |
expr | Evaluate algebraic expressions |
for | "For" loop |
foreach | "For each" loop |
global | Bring global into scope |
if | If/then/else |
incr | Increment integer |
info | Interpreter introspection |
join | Join list elements into a string |
lindex | Index into a list |
list | Create a list |
llength | Length of a list |
proc | Procedure definition |
puts | Print a string |
rename | Rename a command |
return | Return a value |
set | Set a variable's value |
source | Evaluate a script file |
string | String manipulation |
throw | Throws an exception |
time | Time script execution |
unset | Clear a variable |
while | "While" loop |
append -- Appends values to a list
Syntax: append varName ?value ...?
Appends zero or more values to the value of variable varName. If varName didn't previously exist, it is set to the concatenation of the values.
Examples
set x "this"
append x "that"
assert_eq $x "thisthat"
append y a b c
assert_eq $y abc
array -- Query and manipulate array variables
Syntax: array subcommand ?arg ...?
This command queries and manipulates array variables.
Subcommand | Description |
---|---|
array exists | Is the string the name of an array variable? |
array get | A dictionary of the array's elements by name |
array names | A list of the array's indices |
array set | Merges a dictionary of elements into the array |
array size | The number of elements in the array |
array unset | Unsets an array variable |
TCL Liens
- Does not support filtering using glob or regex matches at this time. The plan is to support glob and regex matching as a configuration option at build time.
- Will never support the array iteration commands
array startsearch
,array anymore
,array donesearch
,array nextelement
, because they are unnecessary and rarely used. The normal idiom for iterating over an array is aforeach
overarray names
. - Will never support
array statistics
, as Rust'sstd::collections::HashMap
doesn't provide a way to gather them.
array exists
Syntax: array exists arrayName
Returns 1 if arrayName names an array variable, and 0 otherwise.
array get
Syntax: array get arrayName
Returns a flat list of the keys and values in the named array. The key/value pairs appear in unsorted order. If there is no array variable with the given name, returns the empty list.
TCL Liens: does not support filtering the list using glob and regex matches.
array names
Syntax: array names arrayName
Returns an unsorted list of the indices of the named array variable. If there is no array variable with the given name, returns the empty list.
TCL Liens: does not support filtering the list using glob and regex matches.
array set
Syntax: array set arrayName list
Merges a flat list of keys and values into the array, creating the array variable if necessary. The list must have an even number of elements. It's an error if the variable exists but has a scalar value, or if arrayName names an array element.
array size
Syntax: array size arrayName
Returns the number of elements in the named array. If there is no array variable with the given name, returns "0".
array unset
Syntax: array unset arrayName ?index?
Unsets the array element in arrayName with the given index. If index is not given, unsets the entire array.
Note:
array unset my_array
is equivalent tounset my_array
, but only works on array variables.array unset my_array my_index
is equivalent tounset my_array(my_index)
The real value of array unset
depends on pattern matching on the index argument, which is
not yet available.
TCL Liens: does not support glob matching on the optional argument.
assert_eq -- Equality assertion
Syntax: assert_eq received expected
Asserts that the string received equals the string expected. On success, returns the empty string; on failure, returns an error.
This command is primarily intended for use in examples, to show the expected
result of a computation, rather than for use in test suites. For testing,
see the test
command and the
molt test
tool.
TCL Notes
This command is not part of Standard TCL; it is provided because of its
similarity to the Rust assert_eq!
macro.
break -- Break loop execution
Syntax: break
Breaks execution of the inmost loop containing the break
command,
continuing execution after the loop.
foreach item $list {
...
if {[someCondition]} {
break
}
...
}
# Execution continues here after the break
break
and return
The break
command is semantically equivalent to return -code break -level 0
, as is
the following procedure:
proc my_break {} {
return -code break -level 1
}
See the return reference page for more information.
catch -- Catch exceptions
Syntax: catch script ?resultVarName? ?optionsVarName?
Executes the script, catching the script's result. The catch
command returns an integer
result code, indicating why the script returned. If resultVarName is given, the
named variable in the caller's scope is set to the script's actual return value. If
optionsVarName is given, the named variable is set to the return options
dictionary in the caller's scope.
catch
is most often used to catch errors. For example,
if {[catch {do_something} result]} {
puts "Error message: $result"
} else {
puts "Good result: $result"
}
Return Codes
The return value of catch
is an integer code that indicates why the script returned. There are
five standard return codes:
Return Code | Effect |
---|---|
0 (ok) | Normal. The result variable is set to the script's result. |
1 (error) | A command in the script threw an error. The result variable is set to the error message. |
2 (return) | The script called return. The result variable is set to the returned value. |
3 (break) | The script called break. |
4 (continue) | The script called continue. |
In addition, the return
command allows any integer to be used as a return code; together with
catch
, this can be used to implement new control structures.
The errorCode
and errorInfo
Variables
When catch
catches an error (or when an error message is output in the Molt REPL), the
global variable errorCode
will be set to the specific error code (see throw)
and the global variable errorInfo
will be set to a human-readable stack trace.
The Options Dictionary
The options dictionary saved to the optionsVarName contains complete information about the return options. See return for a complete discussion of what the return options are and how they are used.
Rethrowing an Error
Sometimes it's desirable to catch an error, take some action (e.g., log it), and then rethrow
it. The return
command is used to do this:
set code [catch {
# Some command or script that can throw an error
} result opts]
if {$code == 1} {
# Log the error message
puts "Got an error: $result"
# Rethrow the error by returning with exactly the options and return
# result that we received.
return {*}$opts $result
}
Visualizing the Return Protocol
The semantics of the return
/catch
protocol are tricky. When implementing a new control
structure, or a modified or extended version of return
, break
, continue
, etc., it is
often useful to execute short scripts and examine the options dictionary in the REPL:
% catch { break } result opts
3
% set result
% set opts
-code 3 -level 0
% catch { return "Foo" } result opts
2
% set result
Foo
% set opts
-code 0 -level 1
%
This REPL dialog shows that break
yields result code 3 immediately, to be handled by the
calling command (usually a loop), while return
returns from the calling procedure (-level 1
)
and then yields an ok
(i.e., normal) result to its caller.
TCL Liens
Molt's catch
command differs from Standard TCL's in the following ways:
-
The options dictionary, as returned, lacks the
-errorline
and-errorstack
options. These might be added over time. -
All options passed to
return
, whether understood by Standard TCL or not, are passed through and included in thecatch
options dictionary. Molt does not currently support this.
All of the common patterns of use are supported.
continue -- Continue with next iteration
Syntax: continue
Continues execution with the next iteration of the inmost loop containing
the continue
command.
foreach item $list {
...
if {[someCondition]} {
continue
}
# Skips this code on [someCondition]
...
}
continue
and return
The continue
command is semantically equivalent to return -code continue -level 0
, as is
the following procedure:
proc my_continue {} {
return -code continue -level 1
}
See the return reference page for more information.
dict -- Dictionary manipulation
Syntax: dict subcommand ?arg ...?
This command manipulates TCL dictionaries. A dictionary is a Molt value containing a hash map from keys to values. Keys are maintained in order of initial insertion.
Subcommand | Description |
---|---|
dict create | Creates a dictionary |
dict exists | Is there a value with these keys? |
dict get | Gets a value from the dictionary |
dict keys | Gets the keys from the dictionary |
dict remove | Removes keys from the dictionary |
dict set | Sets a value in a dictionary |
dict unset | Unsets a value in a dictionary |
dict size | The number of elements in the dictionary |
dict values | Gets the values from the dictionary |
TCL Liens
- Not all of the standard TCL
dict
subcommands are implemented at this time. dict keys
anddict values
do not support filtering using glob or regex matches at this time. The plan is to support glob and regex matching as an optional feature.dict info
is not supported; it is intended for tuning the standard TCL hash table implementation. Molt relies onstd::collections::HashMap
.
dict create
Syntax: dict create ?key value ...?
Creates a dictionary given any number of key/value pairs.
% set dict [dict create a 1 b 2]
a 1 b 2
% dict get $dict a
1
dict exists
Syntax: dict exists dictionary key ?key ...?
Returns 1 if the key (or the path of keys through nested dictionaries) is found in the
given dictionary value, and 0 otherwise. It returns 1 exactly when dict get
will
succeed for the same arguments. It does not throw errors on invalid dictionary values, but
simply returns 0.
Looks up the key in the dictionary and returns its value. It's an error if the key is not present in the dictionary. If multiple keys are provided, the command looks up values through nested dictionaries. If no keys are provided, the dictionary itself is returned.
% dict exists {a 1 b 2} b
1
% dict exists {a {x 1 y2} b {p 3 q 4}} b p
1
% dict exists {a 1 b 2} c
0
% dict exists not-a-dict a
0
dict get
Syntax: dict get dictionary ?key ...?
Looks up the key in the dictionary and returns its value. It's an error if the key is not present in the dictionary. If multiple keys are provided, the command looks up values through nested dictionaries. If no keys are provided, the dictionary itself is returned.
% dict get {a 1 b 2} b
2
% dict get {a {x 1 y2} b {p 3 q 4}} b p
3
dict keys
Syntax: dict keys dictionary
Returns a list of the keys in the dictionary, in the order of initial insertion.
% dict keys {a 1 b 2}
a b
dict remove
Syntax: dict remove dictionary ?key ...?
Removes each of the keys from the dictionary, returning the modified dictionary. The keys need not be present in the original dictionary value. If no keys are given, returns the dictionary unchanged.
% dict remove {a 1 b 2 c 3 d 4} b c
a 1 d 4
dict set
Syntax: dict set dictVarName key ?key ...? value
Given the name of a variable containing a dictionary, sets the value of the given key in the dictionary. If multiple keys are given, the command indexes down the path of keys and sets the value in the nested dictionary. The variable is created if it does not exist, and the nested dictionaries are also created as needed. Returns the modified dictionary, which is also saved back into the variable.
For example,
% dict set var a 1
a 1
% dict set var b 2
a 1 b 2
% dict set var c x 3
a 1 b 2 c {x 3}
% dict set var c y z 4
a 1 b 2 c {x 3 y {z 4}}
dict size
Syntax: dict size dictionary
Gets the number of entries in the dictionary.
% set dict [dict create a 1 b 2 c 3]
a 1 b 2 c 3
% dict size $dict
3
dict unset
Syntax: dict unset dictVarName ?key ...?
Given the name of a variable containing a dictionary, removes the value at the end of the path of keys through any number of nested dictionaries. The last key need not exist in the inmost dictionary, but it is an error if any of the other dictionaries in the path are unknown. Returns the modified dictionary, which is also saved back into the variable.
For example,
% set var {a 1 b {x 2 z 3} c 4}
a 1 b {x 2 y 3} c 4
% dict unset c ;# Remove "c" from the outermost dictionary
a 1 b {x 2 y 3}
% dict unset b y ;# Remove "y" from an inner dictionary "b"
a 1 b {x 2}
% dict unset var c ;# "c" is already not there
a 1 b {x 2}
% dict unset var b y ;# "y" is already not in "b"
a 1 b {x 2}
% dict unset var c z ;# Inner dictionary "c" is not present.
key "c" is not known in dictionary
dict values
Syntax: dict values dictionary
Returns a list of the values in the dictionary, in the order of initial insertion of their keys.
% dict values {a 1 b 2}
1 2
error -- Throws an error
Syntax: error message
Returns an error with the given message and an error code of NONE
. The error may
be caught using the catch command.
Example
proc myproc {x} {
if {$x < 0} {
error "input must be non-negative"
}
...
}
TCL Liens
In standard TCL, the error
also has optional errorInfo
and errorCode
arguments. These
are used in older TCL code to rethrow errors without polluting the stack trace. Modern TCL code
uses the throw command to throw an error with an error code and the
return command to rethrow an error (see the reference page for an
example). Consequently, Molt doesn't implement these arguments.
exit -- Exit the application
Syntax: exit ?returnCode?
Terminates the application by calling
std::process:exit()
with the given returnCode, which must be an integer. If not present,
the returnCode defaults to 0.
expr -- Evaluate algebraic expressions
Syntax: expr expr
Evaluates the expression, returning the result.
expr
implements a little language that has a syntax separate from that of Molt. An
expression is composed of values and operators, with parentheses for grouping, just
as in C, Java, and so forth. Values consist of numeric and boolean literals,
function calls, variable and command interpolations, and double-quoted
and braced strings. Every value that looks like a number is treated as a number, and every
value that looks like a boolean is treated as a boolean.
The operators permitted in expressions include most of those permitted in C expressions, with a few additional ones The operators have the same meaning and precedence as in C. Expressions can yield numeric or non-numeric results.
Integer computations are done with Rust's i64
type; floating-point computations are
done with Rust's f64
type.
Examples
expr {1 + 1}
set x 7.5
set y 3.4
expr {$x + $y}
expr {[mycommand] + 2}
expr {2*(1 + abs($x))}
Operators and Precedence
The following table shows the operators in order of precedence.
Operators | Details |
---|---|
- + ~ ! | Unary plus, minus, bit-wise not, and logical not |
* / % | Multiplication, division, integer remainder |
+ - | Addition, subtraction |
<< >> | Left and right shift. |
< > <= >= | Ordering relations (see below) |
== != | Equality, inequality (see below) |
eq ne | String equality, inequality |
in ni | List inclusion, exclusion |
& | Bit-wise AND |
^ | Bit-wise exclusive OR |
| | Bit-wise OR |
&& | Logical AND, short circuiting |
|| | Logical OR, short circuiting |
x ? y : z | Ternary "if-then-else" operator. |
Boolean Values
- True values: any non-zero number,
true
,yes
,on
. - False values: zero,
false
,no
,off
. - Logical operators always return 0 or 1.
- By convention, predicate commands also return 0 or 1.
Math Functions
Functions are written as "name(argument,...)". Each argument is itself a complete expression.
The following functions are available in Molt expressions:
abs(x) — Absolute value of x.
double(x) — Returns integer x as a floating-point value.
int(x) — Truncates floating-point value x and returns it as an integer.
round(x) — Rounds floating-point value x to the nearest integer and returns it as an integer.
TCL Liens
Expr Command Syntax: In standard TCL expr
takes any number of arguments, which it
concatenates into a single expression for evaluation. This means that variable and command
interpolation is done twice, once by the TCL parser and once by expr
, which hurts
performance and can also be a source of subtle and confusing errors. Consequently it is
almost always best to provide the expression as a single braced string, and so Molt's expr
takes a single argument. This is unlikely to change.
Expression Syntax: Molt's expression parsing is meant to be consistent with TCL 7.6, with the
addition of the TCL 8.x eq
, ne
, in
, and ni
operators.
- Molt does not yet support the full range of math functions supported by TCL 7.6.
- Molt does not yet do precise float-to-string-to-float conversions, per TCL 8.6. See
"String Representation of Floating Point Numbers" on the Tcler's Wiki expr page. - Molt's handling of floating point arithmetic errors is still naive.
Integer Division: Integer division in Molt rounds down towards zero, following the example of Rust, Python, C99, and many other languages. Standard TCL rounds toward negative infinity, a decision that dates to a time when the C standard did not define the correct behavior and C compilers varied. It seems reasonable that an extension language should do something as basic as this in the same way as the host language.
Possible Futures: The following TCL 8.6 features are not on the road map at present, but might be added in the future.
- Bignums
- The exponential operator,
**
- The
tcl::mathfunc::
namespace, and the ability to define new functions in TCL code.
for -- "For" loop
Syntax: for start test next command
The for
command provides a C-like "for" loop, where start is a script that initializes the
loop counter, test is a conditional expression, next is a script that updates the loop
counter, and command is the body script.
If the command script calls the break command, the loop terminates immediately; if the command script calls the continue command, loop execution continues with the next iteration.
Example
For example, the following loop counts from 0 to 9:
for {set i 0} {$i < 10} {incr i} {
puts "i=$i"
}
Note, though, that the start and next arguments are arbitrary scripts; for example, start can initialize multiple variables, and next can update multiple variables.
foreach -- "For each" loop
Syntax: foreach varList list body
Loops over the elements in the list, assigning them to the variables in the varList and executing the body for each set of assignments.
The break and continue commands can be used to control loop execution; see their reference pages for details.
Examples
Prints out the values "1", "2", and "3" on successive lines.
foreach a {1 2 3} {
puts $a
}
Prints out pairs of values from the list. In the final iteration there
is only value left, so b
is assigned the empty string.
foreach {a b} {1 2 3 4 5} {
puts "$a,$b"
}
# Outputs:
#
# 1,2
# 3,4
# 5,
TCL Liens
In standard TCL, foreach
can iterate over multiple lists at the
same time, e.g., the following script will output the pairs "a,1",
"b,2", and "c,3". Molt doesn't currently support this extended syntax.
foreach x {a b c} y {1 2 3} {
puts "$x,$y"
}
global -- Bring global into scope
Syntax: global ?varname ...?
Brings global variable(s) varname into scope in a
proc
body. This command has no effect if called in the
global scope.
TCL Differences
At the script level, global
works the same in Molt as in Standard
TCL. However, Molt's internal implementation of variables is currently much
simpler than standard TCL's, e.g., no arrays, no namespaces.
if -- If/then/else
Syntax: if expr1 ?then? body1 elseif expr2 ?then? body2 elseif ... ?else? ?bodyN?
Tests a chain of one or more expressions, and executes the matching body, which must be a script. Returns the result of the last command executed in the selected body.
Both the then
and else
keywords are optional. The standard TCL
convention is to always omit the then
keywords and to always
include the else
keyword when there's an else
clause.
Examples
if {$x > 0} {
puts "positive"
}
if {$x < 0} {
puts "negative"
} else {
puts "non-negative"
}
if {$x > 0} {
puts "positive"
} elseif {$x < 0} {
puts "negative"
} else {
puts "zero"
}
set value [if {$x > 0} {
expr {$x + $y}
} else {
expr {$x - $y}
}]
incr -- Increment integer
Syntax: incr varName ?increment?
Increments integer-valued-variable varName by the given increment, which defaults to 1. If the variable is unset, it is set to the increment. The command returns the incremented value.
Examples
unset a
incr a ;# => 1
incr a ;# => 2
incr a 3 ;# => 5
for {set a 1} {$a < 10} {incr a} {
...
}
info -- Interpreter introspection
Syntax: info subcommand ?arg ...?
Returns information about the state of the Molt interpreter.
Subcommand | Description |
---|---|
info args | Names of procedure's arguments |
info body | Gets procedure body |
info cmdtype | Queries a command's type |
info commands | Names of all defined commands |
info complete | Is this string a syntactically complete command? |
info default | A procedure argument's default value |
info exists | Is this a variable in the current scope? |
info globals | Names of all variables in the global scope |
info locals | Names of all local variables in the current scope |
info procs | Names of all defined procedures |
info vars | Names of all variables in the current scope |
info args
Syntax: info args procname
Retrieves a list of the names of the arguments of the named procedure. Returns an error if the command is undefined or is a binary command.
For example,
% proc myproc {a b c} { ... }
% info args myproc
a b c
%
info body
Syntax: info body procname
Retrieves the body of the named procedure. Returns an error if the command is undefined or is a binary command.
For example,
% proc myproc {name} { puts "Hello, $name" }
% info body myproc
puts "Hello, $name"
%
info cmdtype
Syntax: info cmdtype command
Retrieves the named command's type, either native
or proc
. The command is native
if it's
implemented in Rust and proc
if it's implemented as a TCL procedure.
% proc myproc {} { ... }
% info cmdtype set
native
% info cmdtype myproc
proc
%
TCL Liens: Standard TCL defines a variety of other command types, e.g., slave interpreters, interpreter aliases, objects, and so forth. These will be added naturally if and when they are added to Molt.
info commands
Syntax: info commands
Returns an unsorted list of the names of the commands defined in the interpreter, including both binary commands and procedures.
TCL Liens: does not support filtering the list using a glob
pattern.
info complete
Syntax: info complete command
Returns 1 if the command appears to be a complete Tcl command, i.e., it has no unmatched quotes, braces, or brackets, and 0 otherwise. REPLs can use this to allow the user to build up a multi-line command.
For example,
% info complete { puts "Hello, world!" }
1
% info complete { puts "Hello, world! }
0
%
info default
Syntax: info default procname arg varname
Retrieves the default value of procedure procname's argument called arg. If arg has
a default value, info default
returns 1 and assigns the default value to the variable
called varname. Otherwise, info default
returns 0 and assigns the empty string to the
variable called varname.
The command throws an error if:
- procname doesn't name a procedure
- The procedure procname has no argument called arg
- The value can't be assigned to a variable called varname.
In the following example, myproc
has two arguments, a
and b
. a
has no default value;
b
has the default value Howdy
.
% proc myproc {a {b Howdy}} { ... }
% info default myproc a defvalue
0
% puts "<$defval>"
<>
% info default myproc b defvalue
1
% puts "<$defval>"
<Howdy>
%
info exists
Syntax: info exists varname
Returns 1 if varname is the name of a variable or array element in the current scope, and 0 otherwise.
% set a 1
1
% set b(1) 1
1
% info exists a
1
% info exists b
1
% info exists c
0
% info exists b(1)
1
% info exists b(2)
0
info globals
Syntax: info globals
Returns an unsorted list of the names of all variables defined in the global scope.
TCL Liens: does not support filtering the list using a glob
pattern.
info locals
Syntax: info locals
Returns an unsorted list of the names of all local variables defined in the current scope, e.g.,
proc
arguments and variables defined locally, but no variables brought in from other scopes
via global
or upvar
.
TCL Liens: does not support filtering the list using a glob
pattern.
info procs
Syntax: info procs
Returns an unsorted list of the names of the procedures defined in the interpreter, omitting binary commands.
TCL Liens: does not support filtering the list using a glob
pattern.
info vars
Syntax: info vars
Returns an unsorted list of the names of all variables that are visible in the current scope, whether global or local.
TCL Liens: does not support filtering the list using a glob
pattern.
join -- Join list elements into a string
Syntax: join list ?joinString?
Joins the elements of a list into a string, including the joinString in between each element. If not given, the joinString defaults to a single space character.
lindex -- Index into a list
Syntax: lindex list ?index ...?
Returns an element from the list, indexing into nested lists. The indices
may be represented as individual indices on the command line, or as a list
of indices. Indices are integers from 0 to length - 1. If an index is less
than 0 or greater than or equal to the list length, lindex
will return
the empty string.
Examples
lindex {a {b c d} e} ;# "a {b c d} e"
lindex {a {b c d} e} 1 ;# "b c d"
lindex {a {b c d} e} 1 1 ;# "c"
lindex {a {b c d} e} {} ;# "a {b c d} e"
lindex {a {b c d} e} {1 1} ;# "c"
TCL Liens
Indices in standard TCL may take several additional forms. For example,
end
indexes the last entry in the list; end-1
indexes the next to last
entry, and so forth. Molt doesn't yet support this.
list -- Create a list
Syntax: list ?arg ...?
Returns a list whose elements are the given arguments. The list will be in canonical list form.
llength -- Length of a list
Syntax: llength list
Returns the length of the list.
proc -- Procedure definition
Syntax: proc name args body
Defines a procedure with the given name, argument list args, and script body. The procedure may be called like any built-in command.
The argument list, args, is a list of argument specifiers, each of which may be:
- A name, representing a required argument
- A list of two elements, a name and a default value, representing an optional argument
- The name
args
, representing any additional arguments.
Optional arguments must follow required arguments, and args
must
appear last.
When called, the procedure returns the result of the last command in the
body script, or the result of calling return
, or an
error.
TCL Liens
Molt does not support namespaces or namespace syntax in procedure names.
puts -- Print a string
Syntax: puts string
Outputs the string to standard output.
TCL Liens
- Does not support
-nonewline
- Does not support
?channelId?
rename -- Rename a command
Syntax: rename oldName newName
Renames the command called oldName to be newName instead.
Any command may be renamed in this way; it is a common TCL approach to wrap a command by renaming it and defining a new command with the oldName that calls the old command at its newName.
If the newName is the empty string, the command will be removed from the interpreter.
Examples
proc myproc {} { ... }
# Rename the proc
rename myproc yourproc
# Remove the proc from the interpreter
rename yourproc ""
return -- Return a value
Syntax: return ?options? ?value?
Returns from a TCL procedure or script, optionally including a value. By default, the command simply returns the given value, or the empty string if value is omitted.
proc just_return {} {
...
if {$a eq "all done"} {
# Just return. The return value will be the empty string, ""
return
}
...
}
proc identity {x} {
# Return the argument
return $x
}
The options allow the caller to return any TCL return code and to return through multiple procedures at once. The options are as follows:
Option | Description |
---|---|
-code code | The TCL result code; defaults to ok . |
-level level | Number of stack levels to return through; defaults to 1. |
-errorcode errorCode | The error code, when -code is error . Defaults to NONE . |
-errorinfo errorInfo | The initial error stack trace. Defaults to the empty string. |
The -code
and -level
Options
The -code
and -level
options work together. The semantics are tricky to understand; a good
aid is to try things and use catch to review the result value and options.
If -code
is given, the code must be one of ok
(the default), error
, return
, break
,
continue
, or an integer. Integer codes 0, 1, 2, 3, and 4 correspond to the symbolic constants
just given. Other integers can be used to implement application-specific control structures.
If -level
is given, the level must be an integer greater than or equal to zero; it represents
the number of stack levels to return through, and defaults to 1
.
Because of the defaults, a bare return
is equivalent to return -code ok -level 1
:
# These are the same:
proc simple {} { return "Hello world" }
proc complex {} { return -code ok -level 1 "Hello world" }
Both tell the interpreter to return "Hello world" to caller the caller of the current procedure
as a normal (ok
) return value.
By selecting a different -code
, one can return some other error code. For example,
break
and return -code break -level 0
are equivalent. This can be useful in several ways. For
example, suppose you want to extend the language to support break
and continue
with labels,
to be used with some new control structure. You could do the following; note the -level 1
. The
return
command returns from your labeled_break
procedure to its caller, where it is understood
as a break
result.
proc labeled_break {{label ""}} {
return -code break -level 1 $label
}
Your new control structure would [catch] the result, see that it's a break
, and jump to
the indicated label.
Similarly, suppose you want to write a command that works like return
but does some additional
processing. You could do the following; note the -level 2
. The 2
is because the command needs
to return from your list_return
method, and then from the calling procedure: two stack levels.
# Return arguments as a list
proc list_return {a b c} {
return -level 2 -code ok [list a b c]
}
Returning Errors Cleanly
The normal way to throw an error in TCL is to use either the error or throw command; the latter is used in more modern code when there's an explicit error code. However, both of these commands will appear in the error stack trace.
Some TCL programmers consider it good style in library code to throw errors using return
, as
follows (with or without the -errorcode
):
proc my_library_proc {} {
...
return -code error -level 1 -errorcode {MYLIB MYERROR} "My Error Message"
}
The advantage of this approach is that the stack trace will show my_library_proc
as the source
of the error, rather than error
or catch
.
The -errorinfo
Option and Re-throwing Errors
Sometimes it's desirable to catch an error, take some action (e.g., log it), and then rethrow
it. The return
command is used to do this:
set code [catch {
# Some command or script that can throw an error
} result opts]
if {$code == 1} {
# Log the error message
puts "Got an error: $result"
# Rethrow the error by returning with exactly the options and return
# result that we received.
return {*}$opts $result
}
TCL Liens
The standard TCL return
command is more complicated than shown here; however, the Molt
implementation provides all of the useful patterns the author has ever seen in use. Some of the
specific differences are as follows:
-
Molt rejects any options other than the ones listed above, and ignores
-errorcode
and-errorinfo
if the-code
is anything other thanerror
. Standard TCL'sreturn
retains all option/value pairs it is given, to be included in thecatch
options. -
Standard TCL's
return
takes an-options
option; in Standard TCL,return -options $opts
is equivalent toreturn {*}$ops
. Molt doesn't support-options
, as it doesn't add any value and is confusing. -
Standard TCL provides two versions of the stack trace: the "error info", meant to be human readable, and the "error stack", for programmatic use. The
-errorstack
is used to initialize the error stack when rethrowing errors, as-errorinfo
is used to initialize the error info string. Molt does not support the error stack at this time.
Some of these liens may be reconsidered over time.
set -- Set a variable's value
Syntax: set varName ?newValue?
Sets variable varName to the newValue, returning the newValue. If newValue is omitted, simply returns the variable's existing value, or returns an error if there is no existing value.
The set
command operates in the current scope, e.g., in
proc
bodies it operates on the set of local variables.
See also: global
TCL Liens
- Molt does not support namespaces or namespace notation.
source -- Evaluate a script file
Syntax: source filename
Executes the named file as a Molt script, returning the result of the final command executed in the script.
TCL Differences
-
Standard TCL provides a
-encoding
option, for choosing a specific Unicode encoding. Molt assumes that the text read from the file is in the UTF-8 encoding, and does nothing special about it. -
Standard TCL reads from the
source
'd file only up to the first ^Z.
This allows for the creation of scripted documents: binary files beginning with a TCL script. The script can then open the file and read the rest of the data. Molt does not implement this behavior.
string -- String manipulation
Syntax: string subcommand ?args...?
Subcommand | Description |
---|---|
string cat | Concatenates zero or more strings |
string compare | Compares two strings lexicographically |
string equal | Compares two strings for equality |
string first | Finds first occurrence of a string |
string last | Finds last occurrence of a string |
string length | String length in characters |
string map | Maps keys to values in a string |
string range | Extracts a substring |
string tolower | Converts a string to lower case |
string toupper | Converts a string to upper case |
string trim | Trims leading and trailing whitespace |
string trimleft | Trims leading whitespace |
string trimright | Trims trailing whitespace |
TCL Liens
- Supports a subset of the subcommands provided by the standard TCL
string
command. The subset will increase over time. - Does not currently support index syntax, e.g.,
end-1
, for thestring first
,string last
, andstring range
commands. These commands accept simple numeric indices only.
Molt Strings and Unicode
Molt strings are exactly and identically Rust String
values, and are treated at the TCL
level as vectors of Rust char
values. A Rust char
is a "Unicode scalar value", and is
also (in most cases) a Unicode code point. It is not a not a grapheme; graphemes that
consist of multiple code points will be treated as multiple characters. This is more or
less the same as Standard TCL, but Unicode being what it is there may be edge cases where
behavior will differ slightly.
string cat
Syntax: string cat ?args ...?
Returns the concatenation of zero or more strings.
string compare
Syntax: string compare ?options? string1 string2
Compares the two strings lexicographically, returning -1
if string1 is less than string2,
0
if they are equal, and 1
if string1 is greater than string2.
The options are as follows:
Option | Description |
---|---|
-nocase | The comparison is case-insensitive. |
-length length | Only the first length characters will be compared. |
Notes:
- When
-nocase
is given, the strings are compared by converting them to lowercase using a naive method that may fail for more complex Unicode graphemes.
string equal
Syntax: string equal ?options? string1 string2
Compares the two strings, returning 1
if they are equal, and 0
otherwise.
The options are as follows:
Option | Description |
---|---|
-nocase | The comparison is case-insensitive. |
-length length | Only the first length characters will be compared. |
Notes:
- When
-nocase
is given, the strings are compared by converting them to lowercase using a naive method that may fail for more complex Unicode graphemes.
string first
Syntax: string first needleString haystackString ?startIndex?
Returns the index of the first occurrence of the needleString in the haystackString, or -1 if the needleString is not found. If the startIndex is given, the search will begin at the startIndex.
string last
Syntax: string last needleString haystackString ?startIndex?
Returns the index of the last occurrence of the needleString in the haystackString, or -1 if the needleString is not found. If the startIndex is given, the search will begin at the startIndex.
string length
Syntax: string length string
Returns the length of the string in Rust characters.
string map
Syntax: string map ?-nocase? mapping string
Replaces old substrings in string with new ones based on the key/value pairs in mapping,
which is a dictionary or flat key/value list. If -nocase
is given, substring matches will
be case-insensitive. The command iterates through the string in a single pass, checking for
each key in order, so that earlier key replacements have no effect on later key replacements.
string range
Syntax: string range string first last
Returns the substring of string starting with the character whose index is first and ending with the character whose index is last. Values of first that are less than 0 are treated as 0, and values of last that are greater than the index of the last character in the string are treated as that index.
string tolower
Syntax: string tolower string
Converts the string to all lower case, using the standard Rust String::to_lowercase
method.
TCL Liens: Tcl 8.6 provides for optional first and last indices; only the text in that range is affected.
string toupper
Syntax: string toupper string
Converts the string to all upper case, using the standard Rust String::to_uppercase
method.
TCL Liens: Tcl 8.6 provides for optional first and last indices; only the text in that range is affected.
string trim
Syntax: string trim string
Returns string trimmed of leading and trailing whitespace by the standard Rust String::trim
method.
string trimleft
Syntax: string trimleft string
Returns string trimmed of leading whitespace by the standard Rust String::trim_start
method.
string trimright
Syntax: string trimright string
Returns string trimmed of trailing whitespace by the standard Rust String::trim_end
method.
throw -- Throws an exception
Syntax: throw type message
Throws an error with error code type and the given error message. The error may be caught using the catch command.
The error code is usually defined as a TCL list of symbols, e.g., ARITH DIVZERO
. Most standard
TCL error codes begin with ARITH
(for arithmetic errors) or TCL
.
Example
proc myproc {x} {
if {$x < 0} {
throw NEGNUM "input must be non-negative"
}
...
}
Note that the error command is equivalent to throw NONE
; also, the return
command can also throw an error with an error code. The three following
commands are semantically identical:
error "My error message"
throw NONE "My error message"
return -code error -level 0 -errorcode NONE "My error message"
time -- Time script execution
Syntax: time command ?count?
Evaluates the given command the given number of times, or once if no count is specified, timing each execution. The average run time in microseconds is returned as a string, "average microseconds per iteration".
Example
% time { mycommand } 1000
15 microseconds per iteration
%
unset -- Clear a variable
Syntax: unset ?-nocomplain? ?--? ?name name name...?
Unsets one or more variables whose names are passed to the command. It does not matter whether the variables actually exist or not.
The -nocomplain
option is ignored. The argument --
indicates the
end of options; all arguments following --
will be treated as variable
names whether they begin with a hyphen or not.
TCL Differences
In standard TCL, it's an error to unset a variable that doesn't exist; the
command provides the -nocomplain
option to cover this case. In Molt,
unset
never complains; the -nocomplain
option is provided only for
compatible with legacy TCL code. (Per the TCL Core Team, the -nocomplain
option indicates, wherever it is found, that the original definition of the
command got the default behaviour wrong.)
while -- "While" loop
Syntax: while test command
The while
command is a standard "while" loop, executing the command script just so
long as the test expression evaluates to true.
Example
The following code will output the numbers from 1 to 10.
set i 0
while {$i < 10} {
puts "i=[incr i]"
}
Embedding Molt
This chapter explains how to embed Molt in a Rust application. There are several parts to this:
- Creating a Molt interpreter
- Defining application-specific Molt commands
- Invoking the interpreter to evaluate Molt commands and scripts
An application may execute scripts for its own purposes and arbitrary scripts defined by the user. One common pattern is to define a shell application the user may use to execute their own scripts using the application-specific command set.
It is also possible to define Molt library crate that defines commands for installation into an interpreter.
The initial step, creating a Molt interpreter, is trivially easy:
#![allow(unused)] fn main() { use molt::Interp; let mut interp = Interp::new(); // Add application-specific commands }
This creates an interpreter containing the standard set of Molt commands. Alternatively, you can create a completely empty interpreter and add just the commands you want:
#![allow(unused)] fn main() { use molt::Interp; let mut interp = Interp::empty(); // Add application-specific commands }
This is useful if you wish to use the Molt interpreter as a safe file parser.
Eventually there will be an API for adding specific standard Molt commands back into an empty interpreter so that the application can create a custom command set (e.g., including variable access and control structures but excluding file I/O), but that hasn't yet been implemented.
We'll cover the remaining topics in the following sections.
The Molt Value
Type
The Value
type is the standard representation in Rust of Molt values. In the Tcl
language, "everything is a string"; which is to say, every value can be represented
as a string. Many values—e.g., numbers and lists—also have a binary data representation,
but a single value can move from one binary data representation to another depending
on how it is used by the user. Consider the following:
set x [expr {2 + 3}] ;# It's the integer 5.
puts "x=$x" ;# It's converted to a string.
set y [lindex $x 0] ;# It's converted to a one-element list.
Initially, the variable x
contains a Value
with only a data representation, the
integer 5. Then puts
needs it as a string, and so the Value
acquires a string
representation as well, but retains its integer representation. Then lindex
needs
to look at it as a list, so the string is parsed into a Molt list and the 0th element
is returned. The integer representation is lost and replaced by the list
representation. The Value
type manages all of these transformations internally, with the effect that string-to-binary and binary-to-string conversions happen only when
absolutely necessary.
Note: A Value
's string representation is never lost, once acquired: semantically,
Values
are immutable. The data transformations that go on under the hood are an
aid to performance, but in principle the value is unchanged.
Creating Values
Values
can be created easily from a variety of kinds of input:
let a = Value::from("abc"); // &str
let b = Value::from("def".to_string()); // String
let c = Value::from(123); // MoltInt (i64)
let d = Value::from(45.67); // MoltFloat (f64)
let e = Value::from(true); // bool
let f = Value::from(&[Value::from(1), Value::from(2)]); // &[Value]
And in fact, a Value
can contain any Rust type that supports the Display
,
Debug
, and FromStr
types via the Value::from_other
method. Such types are
called "external types" in the Molt documentation set.
Cloning Values
Because Values
are immutable, they have been designed to be cheaply and easy cloned
with reference counting via the standard Rc
type.
Retrieving Data from Values
It is always possible to retrieve a Value
's data as a string:
let value = Value::from(5);
let text: String = value.to_string();
assert_eq!(&text, "5");
The to_string
method creates a brand new String
in the usual way; it is usually better to
use as_str
, which returns an &str
:
let value = Value::from(5);
let text = value.as_str();
assert_eq!(text, "5");
It is also possible to retrieve data representations; but since this isn't guaranteed to
work the relevant methods all return Result<_,ResultCode>
. (See
The MoltResult
type for a discussion of ResultCodes
.) For
example,
let value = Value::from("123");
let x = value.as_int()?;
assert_eq!(x, 123);
Retrieving Values of External Types
Values of external types can be retrieved as well using the Value::as_copy
or
Value::as_other
method, depending on whether the type implements the Copy
trait. These are different than their peers, in that they return Option<T>
and Option<Rc<T>>
rather than Result<T,ResultCode>
or Result<Rc<T>,ResultCode>
.
The reason is that Molt doesn't know what the appropriate
error message should be when it finds a value it can't convert into the external
type T
and so returns None
, leaving the error handling up to the client.
For this reason, when using an external type MyType
with Molt it is usual to define a
function that converts a Value
to a Result<MyType,ResultCode>
. If MyType
is an
enum, for example, you might write this:
#![allow(unused)] fn main() { impl MyType { /// A convenience: retrieves the enumerated value, converting it from /// `Option<MyType>` into `Result<MyType,ResultCode>`. pub fn from_molt(value: &Value) -> Result<Self, ResultCode> { if let Some(x) = value.as_copy::<MyType>() { Ok(x) } else { Err(ResultCode::Error(Value::from("Not a MyType string"))) } } } }
The MoltResult Type
MoltResult
is Molt's standard Result<T,E>
type; it is defined as
#![allow(unused)] fn main() { pub type MoltResult = Result<Value, Exception>; }
The Value
type is described in the previous section; by default, many
Molt methods and functions return Value
on success.
The Exception
struct is used for all exceptional returns, including not only errors but also
procedure returns, loop breaks and continues, and application-specific result codes defined
as part of application-specific control structures.
The heart of the Exception
struct is the ResultCode
, which indicates the kind of
exception return. It is defined as follows:
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ResultCode { Okay, Error, Return, Break, Continue, Other(MoltInt), } }
-
ResultCode::Okay
is used internally. -
ResultCode::Error
indicates that an error has been thrown; the exception'svalue()
is the error message. Use the exception'serror_code()
anderror_info()
methods to access the error code and stack trace. -
ResultCode::Return
, which indicates that the Molt code has called thereturn
command; thevalue
is the returned value. Molt procedures, defined using theproc
command, will catch this and returnvalue
as the value of the procedure call. See the documentation for the return and catch commands for information on a variety of advanced things that can be done using this result code. -
ResultCode::Break
andResultCode::Continue
are returned by thebreak
andcontinue
commands and control loop execution in the usual way. -
ResultCode::Other
can be returned by the return command, and is used when defining application-specific control structures in script code.
Of these, client Rust code will usually only deal with ResultCode::Error
and
ResultCode::Return
. For example,
#![allow(unused)] fn main() { use molt::types::*; use molt::Interp; let mut interp = Interp::new(); let input = "set a 1"; match interp.eval(input) { Ok(val) => { // Computed a Value println!("Value: {}", val); } Err(exception) => { if exception.is_error() { // Got an error; print it out. println!("Error: {}", exception.value()); } else { // It's a Return. println!("Value: {}", exception.value()); } } } }
Result Macros
Application-specific Rust code will usually only use Ok(value)
and
ResultCode::Error
. Since these two cases pop up so often,
Molt provides several macros to make them easier: molt_ok!
, molt_err!
,
and molt_throw!
.
molt_ok!
takes one or more arguments and converts them into an Ok(Value)
.
#![allow(unused)] fn main() { // Returns the empty result. return molt_ok!(); // Returns its argument as a Value (if Molt knows how to convert it) return molt_ok!(5); // A plain Value is OK to. return molt_ok!(Value::from(5)); // Returns a formatted string as a Value using a Rust `format!` string. return molt_ok!("The answer is {}.", x); }
molt_err!
works just the same way, but returns Err(Exception)
with ResultCode::Error
.
// Return a simple error message
return molt_err!("error message");
// Return a formatted error message
if x > 5 {
return molt_err!("value is out of range: {}", x);
}
molt_throw!
is like molt_err!
, but allows the caller to set an explicit error code. (By
default, Molt errors have an error code of NONE
.) Error codes can be retrieved from the
Exception
object in Rust code and via the catch command in scripts.
// Throw a simple error
return molt_throw!("MYCODE", "error message");
// Throw a formatted error message
if x > 5 {
return molt_throw!("MYCODE", "value is out of range: {}", x);
}
Defining Commands
At base, a Molt command is a Rust function that performs some kind of work and optionally returns a value in the context of a specific Rust interpreter. There are two ways an application (or library crate) can define application-specific Rust commands:
- As a Rust
CommandFunc
function - As a Molt procedure, or
proc
.
CommandFunc
Commands
A CommandFunc
command is any Rust function that implements CommandFunc
:
#![allow(unused)] fn main() { pub type CommandFunc = fn(&mut Interp, ContextID, &[Value]) -> MoltResult; }
For example, here's a simple command that takes one argument and returns it unchanged.
#![allow(unused)] fn main() { fn cmd_ident(_interp: &mut Interp, _context_id: ContextID, argv: &[Value]) -> MoltResult { check_args(1, argv, 2, 2, "value")?; molt_ok!(argv[1].clone()) } }
The argv
vector contains the arguments to the command, beginning with the
command's name. The check_args
method verifies that the command has the right
number of arguments, and returns the standard Tcl error message if not. Finally,
it uses molt_ok!
to return its first argument.
Install this command into the interpreter using the Interp::add_command
method:
#![allow(unused)] fn main() { interp.add_command("ident", cmd_ident); }
CommandFunc
Commands with Context
A normal CommandFunc
is useful when extending the Molt language itself; but
application-specific commands need to manipulate the application and its data. In this case,
add the required data to the interpreter's context cache. The cached data can be retrieved,
used, and mutated by commands tagged with the relevant context ID.
The context cache is a hash map that allows the interpreter to keep arbitrary data and make it available to commands. The usual pattern is like this:
-
The application defines a type containing the data the command (or commands) requires. We'll call it
AppContext
for the purpose of this example. -
The application saves an instance of
AppContext
into the context cache, retrieving aContextID
. -
The application includes the
ContextID
when adding the command to the interpreter. -
The command retrieves the
AppContext
as a mutable borrow.
// The AppContext struct AppContext { text: String } // The Command fn cmd_whatsit(interp: &mut Interp, context_id: ContextID, argv: &[Value]) -> MoltResult { check_args(1, argv, 2, 2, "value")?; let ctx = interp.context::<AppContext>(context_id); // Append the first argument's string rep to the // AppContext struct's text field. ctx.text.push_str(argv[1].as_str()); molt_ok!() } // Registering the command fn main() { let interp = Interp::new(); let id = interp.save_context(AppContext::new()); interp.add_context_command("whatsit", cmd_whatsit, id); ... }
The saved AppContext
will be dropped automatically if the whatsit
command is
removed from the interpreter.
Commands with Shared Context
Any number of Molt commands can share a single cached context struct:
let interp = Interp::new();
let id = interp.save_context(AppContext::new());
interp.add_context_command("first", cmd_first, id);
interp.add_context_command("second", cmd_second, id);
interp.add_context_command("third", cmd_third, id);
...
The context struct will persist in the cache until the final command is removed (or, of course, until the interpreter is dropped).
Molt Objects
The standard way to represent an object in TCL is to define a command with attached context data. The command's methods are implemented as subcommands.
The context cache supports this pattern trivially. Define the object's instance variables as a context struct, and define a command to create instances.
// Instance Data
struct InstanceContext { text: String }
// Command to make an instance
fn cmd_make(interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
check_args(1, argv, 2, 2, "name")?;
let id = interp.save_context(InstanceContext::new());
interp.add_context_command(argv[1].as_str(), cmd_instance, id);
molt_ok!()
}
// Instance Command
fn cmd_instance(interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
check_args(1, argv, 2, 0, "subcommand ?args...?")?;
// Get the context
let ctx = interp.context::<AppContext>(context_id);
// Do stuff based on argv[1], the subcommand.
...
}
// Registering the command
fn main() {
let interp = Interp::new();
interp.add_command("make", cmd_make);
...
}
Then, in Molt code you can create an object called fred
, use its methods, and then
destroy it by renaming it to the empty string.
% make fred
% fred do_something 1 2 3
...
% rename fred ""
Molt Procedures
A Molt procedure is a routine coded in Tcl and defined using the proc
command. A
crate can compile Tcl procedures into itself using the include_str!
macro. Start
by defining a script that defines the required procedure, say, procs.tcl
, and put it
in the crate's src/
folder adjacent to the Rust file that will load it. The Rust
file can then do this:
#![allow(unused)] fn main() { let mut interp = Interp::new(); match interp.eval(include_str!("commands.tcl")) { Err(exception) => { if exception.is_error() { panic!("Couldn't load procs.tcl: {}", msg.value()); } } _ => () } }
Evaluating Molt Code
An application can evaluate Molt code in several ways:
-
Use one of the
molt::Interp::eval
ormolt::Interp::eval_value
to evaluate an individual Molt command or script. -
Use the
molt::expr
function to evaluate a Molt expression, returning a MoltValue
, ormolt::expr_bool
,molt::expr_int
, andmolt::expr_float
for results of specific types. -
Use the
molt_shell::repl
function to provide an interactive REPL to the user. -
Use the
molt_shell::script
function to evaluate a script file (or just load the script's content and pass it tomolt::Interp::eval
).
Evaluating Scripts with eval
The molt::Interp::eval
method evaluates a string as a Molt script and returns the
result. When executed at the top level, ResultCode::Break
, ResultCode::Continue
,
and ResultCode::Other
are converted to errors, just as they are in proc
bodies. See
The MoltResult
Type for details.)
Thus, the following code will execute a script, returning its value and propagating any exceptions to the caller.
#![allow(unused)] fn main() { use molt::Interp; use molt::types::*; let mut interp = Interp::new(); ... let value: Value = interp.eval("...some Molt code...")?; }
The molt::Interp::eval_value
method has identical semantics, but evaluates the string
representation of a molt Value
. In this case, the Value
will cache the parsed internal
form of the script to speed up subsequent evaluations.
Evaluating Control Structure Bodies
The molt::Interp::eval_value
method is used when implementing control structures. For
example, this is an annotated version of of Molt's while command.
#![allow(unused)] fn main() { pub fn cmd_while(interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult { check_args(1, argv, 3, 3, "test command")?; // Here we evaluate the test expression as a boolean. Any errors are propagated. while interp.expr_bool(&argv[1])? { // Here we evaluate the loop's body. let result = interp.eval_value(&argv[2]); if let Err(exception) = result { match exception.code() { // They want to break; so break out of the rust loop. ResultCode::Break => break, // They want to continue; so continue with the next iteration. ResultCode::Continue => (), // It's some other exception; just propagate it. _ => return Err(exception), } } } // All is good, so return Ok! molt_ok!() } }
See The MoltResult
Type for more information.
Evaluating Expressions with expr
and expr_bool
.
Evaluating Molt expressions is similar. To get any expression result (usually a
numeric or boolean Value
), use the Interp::expr
method.
#![allow(unused)] fn main() { use molt::Interp; use molt::types::*; use molt::expr; let mut interp = Interp::new(); ... let value: Value = interp.expr("1 + 1")?; }
Use Interp::expr_bool
when a specifically boolean result is wanted:
#![allow(unused)] fn main() { let flag: bool = interp.expr_bool("1 == 1")?; }
(See the expr
command reference for more about Molt expressions.)
Providing an interactive REPL
An interactive user shell or "REPL" (Read-Eval-Print-Loop) can be a great convenience
when developing and debugging application scripts; it can also be useful tool for
administering server processes. To provide an interactive shell, use
the molt_shell::repl
function.
use molt::Interp;
// FIRST, create and initialize the interpreter.
let mut interp = Interp::new();
// NOTE: commands can be added to the interpreter here.
// NEXT, invoke the REPL.
molt_shell::repl(&mut interp);
The REPL's prompt can be set using the tcl_prompt1
variable to a script; see the
molt shell documentation for an example.
Evaluating Script Files
To execute a user script file, one can load the file contents and use Interp::eval
in
the normal way, or use the molt_shell::script
function. A shell application might
execute a user script as follows. Any errors are output to the console.
use molt::Interp;
use std::env;
// FIRST, get the command line arguments.
let args: Vec<String> = env::args().collect();
// NEXT, create and initialize the interpreter.
let mut interp = Interp::new();
// NOTE: commands can be added to the interpreter here.
// NEXT, evaluate the file, if any.
if args.len() > 1 {
molt_shell::script(&mut interp, &args[1..]);
} else {
eprintln!("Usage: myshell filename.tcl");
}
Custom Shells
A custom Molt shell is simply an application that:
- Creates a Molt
interp
- Adds any desired commands by the methods described in the previous section
- Passes the
interp
tomolt_shell::repl
(for an interactive shell) - Passes the
interp
and a file tomolt_shell::script
The sample Molt application provides a full example; here's a sketch:
fn main() {
use std::env;
// FIRST, get the command line arguments.
let args: Vec<String> = env::args().collect();
// NEXT, create and initialize the interpreter.
let mut interp = Interp::new();
// NOTE: commands can be added to the interpreter here, e.g.,
// Add a single module
interp.add_command("hello", cmd_hello);
// Install a Molt extension crate
molt_sample::install(&mut interp).expect("Could not install.");
// NEXT, evaluate the file, if any.
if args.len() > 1 {
molt_shell::script(&mut interp, &args[1..]);
} else {
molt_shell::repl(&mut interp);
}
}
pub fn cmd_hello(_interp: &mut Interp, _: ContextID, argv: &[Value]) -> MoltResult {
// Correct number of arguments?
check_args(1, argv, 2, 2, "name")?;
println!("Hello, {}", argv[1].as_str());
molt_ok!()
}
Molt Library Crates
A Molt library crate is simply a Rust crate that can install commands into a
Molt interpreter using any of the methods described in this chapter. For example,
a crate might provide an install
function:
#![allow(unused)] fn main() { use molt::Interp pub fn install(interp: &mut Interp) { interp.add_command("mycommand", mycommand); ... } }