wartcl 0.1.0

A minimal embeddable Tcl-like language
Documentation

This is an aggressively simple Tcl-like language optimized for binary size, code complexity, and performance, in that order. That is, it's mostly intended to be small, but with readable code and ok performance.

wartcl has been used on a Cortex-M0 for basic boundary scan and manufacturing test scripting. In that application it required 10 kiB of flash and 8 kiB of RAM.

Putting this in your application

wartcl is designed to be very easy to embed in a larger application, including exposing custom commands. Here's a toy example:

let mut tcl = wartcl::Env::default();

tcl.register(b"my-custom-command", 1, |_, _| {
    println!("hi!");
    Ok(wartcl::empty())
});

match tcl.eval(b"my-custom-command\n") {
    Ok(_) => {
        // the script worked!
    }
    Err(e) => panic!("script failed: {e:?}"),
}

The wartcl language

The language implemented by wartcl is intended to be very close to Tcl, but smaller. Most (all?) wartcl programs should be valid Tcl programs, but not vice versa.

wartcl supports the following Tcl-like commands by default. Some are controlled by a Cargo feature in case you want to disable them.

  • break
  • continue
  • +, -, *, / (feature arithmetic)
  • >, >=, <, <=, ==, != (feature comparison)
  • if
  • incr (feature incr)
  • proc (feature proc)
  • puts (feature std)
  • return
  • set
  • subst
  • while

Probably the biggest difference is that the expr command, which does math-style expression parsing, is not included. You can still do math, but in prefix notation; instead of expr 2*(3+4), you must write * 2 [+ 3 4]. This isn't ideal, but expression parsing is big and wartcl is small.

Implementation design and theory of operation

The Tcl language is an extended meditation on the idea "everything is a string." All of Tcl's data types are --- notionally, at least --- represented as strings, and they can be converted from one to the other by parsing. Modern Tcl implementations provide this illusion while using more efficient representations under the hood.

wartcl takes it literally. Everything is a string, a heap-allocated sequence of human-readable bytes, encoded in either ASCII (if you leave the top bit of each byte clear) or UTF-8 (if you don't). wartcl doesn't actually care about character encoding.

This keeps the implementation very simple but has significant performance costs. Want to add two numbers? Well, you'll have to parse two numeric strings, add the result, and then re-format the result into another (heap-allocated) numeric string. (This is not the fastest way to use a computer, but wartcl is not really designed for arithmetic-heavy applications.)

Basically every value, from the program's source code on up, is represented as a Box<[u8]>. This is an owned pointer to a slice of bytes. Cloning it implies a full copy of its contents; dropping it deallocates the contents.

The advantages of Box<[u8]> over Vec<u8> are:

  1. Vec may retain extra memory for expansion, which we don't generally need because values are immutable once constructed.
  2. Vec is one word larger, making it correspondingly more expensive to store and pass around.

To clarify intent, in the implementation, [u8] is given the type alias Value.

About the name

wartcl stands for "wartcl Ain't Really Tcl" because the language differs from standard Tcl in a whole bunch of ways.

It's also a pun on the C partcl library's name, after the "warticle" term humorously used to describe phenomena exhibiting wave/partical duality in quantum physics.