1
0
Fork
You've already forked partcl
0
ParTcl - a micro Tcl implementation https://zserge.com/posts/tcl-interpreter/
  • C 99.6%
  • Makefile 0.4%
thiadmer 8163c8855d Add helper functions tcl_int2string() and tcl_float2string().
Add utility functions tcl_get_scope() and tcl_set_scope(), to allow creating global variables from C functions that are called from TCL.
Add utility function tcl_clear_error() to make sure there is no pending error state before calling tcl_eval().
Bug fix in tcl_type() related to parsing floating point values.
Bug fix in tcl_list_item_ptr() related to parsing quoted strings in a list.
Renamed "env" and "environment" to "scope" because the main use of the "environment" is to keep local variables.
Signed-off-by: thiadmer <thiadmer.riemersma@gmail.com>
2026年03月28日 16:06:14 +01:00
.github/workflows build and test separately 2021年05月15日 13:08:45 +02:00
LICENSE Initial commit 2016年04月12日 18:20:27 +08:00
Makefile Change error reporting method, add "catch" and "error" commands. 2024年04月09日 21:41:02 +02:00
README.md Add helper functions tcl_int2string() and tcl_float2string(). 2026年03月28日 16:06:14 +01:00
Tcl-Tutorial.pdf Add floating point support in the expression parser, including a small set of standard functions. 2025年09月22日 22:23:16 +02:00
tcl.c Add helper functions tcl_int2string() and tcl_float2string(). 2026年03月28日 16:06:14 +01:00
tcl.h Add helper functions tcl_int2string() and tcl_float2string(). 2026年03月28日 16:06:14 +01:00
tcl_test.c Change error reporting method, add "catch" and "error" commands. 2024年04月09日 21:41:02 +02:00
tcl_test_flow.h Generalize tcl_value() so that the special-case tcl_value_string() is not longer needed. 2024年04月15日 21:33:49 +02:00
tcl_test_lexer.h Add floating point support in the expression parser, including a small set of standard functions. 2025年09月22日 22:23:16 +02:00
tcl_test_math.h Add floating point support in the expression parser, including a small set of standard functions. 2025年09月22日 22:23:16 +02:00
tcl_test_subst.h Small fix in returning error result. 2024年05月04日 18:56:01 +02:00
tclrun.c Add helper functions tcl_int2string() and tcl_float2string(). 2026年03月28日 16:06:14 +01:00

ParTcl - a minimal Tcl interpreter

Note: This is a fork; see https://github.com/zserge/partcl for the original.

In 1988, John Ousterhout developed Tcl with the goal to be a light-weight extension language for applications; a "macro language", so to speak. It has grown significantly, and is now often used to create complete tools or utilities in.

ParTcl reverts back to the roots of Tcl: ParTcl's goal is to be easily embeddable in applications, and function as an extension language for these applications. In pursuit of simplicitly and compactness of the implementation, ParTcl is closer to the original versions of Tcl (pre 7.0), in syntax and in semantics, than to the lastest release (9.0, at the time of writing).

There is documentation in this repository: Tcl-Tutorial.pdf is a tutorial specific for ParTcl. For more details, or background information on Tcl, you can pick up a general book on Tcl. Old books are fine (because ParTcl draws back to the original Tcl). See, for example, "Tcl and the Tk Toolkit" by John Ousterhout, and which is available for free.

Features

  • Easily embeddable, plain C99 code (one C file, one H file).
  • No external dependencies, apart from the standard C library.
  • Good test coverage.
  • Flexible and easy-to-use interface to C/C++ programs, can be extended with custom Tcl commands.
  • A decent number of built-in commands; some less-essential commands can be removed on compilation (by setting a macro).
  • Includes an expression parser (the expr command) which supports integer and floating point arithmetic.

Usage

The API is documented in the tcl.h file.

struct tcl tcl;
const char *script = "set x 4; puts [expr 2 + $x * 10]";
tcl_init(&tcl, NULL);
if (tcl_eval(&tcl, script, strlen(script)) != FERROR) {
 struct tcl_value *retval = tcl_return(&tcl);
 printf("Return: %.*s\n", tcl_length(retval), tcl_data(retval));
} else {
 int line;
 const char *msg = tcl_errorinfo(&tcl, &line);
 printf("Error on or after line %d: %s\n", line, msg);
}
tcl_destroy(&tcl);

When TCL is used to script applications, a common scenario is that the user-supplied script contains procedures (and possibly global variables), and that the application calls these procedures. The first step (after initializing the interpreter context with tcl_init) is to call tcl_eval on the user-supplied script. This adds all procs and all global variables to the interpreter context. Until the context is cleared with tcl_destroy, the application can invoke any proc (with parameters) by constructing a command and passing that to tcl_eval.

TCL Synopsis

There are a few key concepts in ParTcl: values, lists and variables.

Values

A "value" is a string, as used internally by ParTcl. It can be used as a C-language string (it is zero-terminated). However, it may contain embedded zero bytes (in case the Tcl script works on binary data), so it is safer to explicitly get the length. Values are allocated dynamically. You create a value with tcl_value() and delete it with tcl_free().

Function tcl_data() returns a pointer to the byte string, tcl_length() its length. You should not modify the contents directly; instead, you create a new value and delete the old one. The raw data of a value may move in memory; you should therefore not cache the pointer returned by tcl_data() either.

An exception to the "don't modify a value" rule, is that you can append a value to another with tcl_append(). The tcl_append() function modifies the primary value (first argument) and deletes the "tail" argument.

/* Raw string values */
struct tcl_value *tcl_value(const char *data, size_t len);
struct tcl_value *tcl_dup(struct tcl_value *value);
bool tcl_append(struct tcl_value *value, struct tcl_value *tail);
struct tcl_value *tcl_free(struct tcl_value *value);
/* Helpers to access raw string or numeric value */
const char *tcl_data(struct tcl_value *value);
int tcl_length(struct tcl_value *value);
bool tcl_type(const struct tcl_value *value);
long tcl_intvalue(struct tcl_value *value);
double tcl_floatvalue(struct tcl_value *value);

Function tcl_type() is a helper function to check whether a value represents a string or a number (and whether that is in integer or floating point format). The functions tcl_intvalue and tcl_floatvalue return the value of a numeric parameter, in long or double formats respectively.

Lists

A list is a string, as is common in Tcl. Thus, in ParTcl, a list is a "value". There are a few special functions on lists, however, that make sure that the list is well-formed. You start a new (empty) list with tcl_list_new() and add items to it with tcl_list_append(). Function tcl_list_append() takes two parameters: a list and a value to append. After the call, the value is owned by the list, and you should therefore not free the item.

When done, the entire list is freed with tcl_free(), just like values.

/* List values */
struct tcl_value *tcl_list_new();
struct tcl_value *tcl_list_append(struct tcl_value *value, struct tcl_value *tail);
struct tcl_value *tcl_list_item(struct tcl_value *value, int index);
int tcl_list_length(struct tcl_value *value);

In the default implementation lists are implemented as raw strings that add some escaping (braces) around each item (lists thus resemble Tcl source code). It's a simple solution that also reduces the code, but in some exotic cases the escaping can go wrong and invalid results will be returned.

Variables

A variable holds a value. Or in the case of an array, a variable holds multiple values. You create a variable with tcl_var(). Currently, you should do this before tcl_eval() (and after tcl_init()). This creates a global variable. When you create C functions that you register to ParTcl as commands, and you would create a variable in that function (when it is called), that variable will be a local variable.

When you create a variable, you pass a value as its content. After the call, the value is owned by the variable. You must not free the value.

You also use tcl_var() to have it return its value. Note that the returned value may not be modified and should not be cached (as a pointer). You can, of course, make a local copy of the value.

There is currently no function to delete a single variable. Calling tcl_destroy() removes all variables (and you should then call tcl_init() again).

Language syntax

Tcl script is made up of commands separated by semicolons or newline symbols. Commands in their turn are made up of fields separated by whitespace. A word is a field, as long as it does not contain a space character. To make whitespace a part of the field one may use double quotes or curly braces.

An important part of the language is command substitution, when the result of a command inside square braces is returned as a part of the outer command, e.g. puts [expr 1 + 2]. Apart from command substitution, Tcl also knows variable substitution. However, command and variable substitution does not happen inside fields that are enveloped in curly braces.

The only data type of the language is a string. When a variable (or field) contains only digits, it may be implicitly interpreted as a number (specifically in the expr builtin command), but that same variable can still be used in string operations.

Builtin commands

The table below is a brief summary of the built-in commands in ParTcl. See the "Tcl Primer" (or other documentation on Tcl) for details on these commands.

name summary
append Append contents to a variable (concatenate strings).
array Functions on array variables: length (same as size) and slice.
binary Binary-to-integer conversion (and vice versa), width subcommands format and scan. This command can be disabled using #define TCL_DISABLE_BINARY, for implementations that do not need to handle binary data.
break Abort a loop, jumps to the first instruction following the loop.
catch Evaluate a command and catch (and return) any error.
clock Time query and formatting functions, with subcommands seconds and format. This command can be disabled using #define TCL_DISABLE_CLOCK, for any context where it does not make sense.
close Close a file. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
concat Join multiple lists into a single list.
continue Skip the remainder of the loop body, jumps back to the condition of the loop.
eof Check whether End-Of-File has been reached. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
error throw an exception (which can be caught with catch).
exec Run a program and capture output. This command can be disabled using #define TCL_DISABLE_EXEC.
exit End the script with an optional return code. Note that this command aborts the script, but not the program that ParTcl is embedded in.
expr Interpret the infix expression that follows. It supports most of the Tcl operator set, with the same precedence levels as standard Tcl. Missing are the list operators in and ni, and some math functions.
file An assortment of file information functions: dirname, exists, extension, isdirectory, isfile, rootname, size and tail. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
flush Flush buffered data to file. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
for Run a loop for {setup} {condition} {post} {body}. One may use break, continue (or return) inside the loop to contol the flow.
foreach Run a loop over all elements in a list.
format Format a string with placeholders, similar to sprintf in C. Currently only %c, %d, %i, %x and %s are supported, plus optional "padding" and "alignment" modifiers (e.g. %04x or %-20s).
gets Read a line for a file. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
global Mark any variable following it as a global variable. There may be a list of names, separated by spaces. Each name may not exists locally, and must already exists as a global variable.
if Conditional execution, if {cond} {then} {cond2} {then2} {else}.
incr Increment or decrements a variable.
info Return some information on the Tcl interpreter, with subcommands exists and tclversion. Notably, info exists var returns 1 if the variable exists, and 0 otherwise.
join Create a string from a list, by concatenating elements, with a separator chosen by the user.
lappend Append values to a variable (where the variable is presumed to contain a list).
lindex Return a specified element from the list.
linsert Insert elements in a list in front of the specified element.
list Create a list from the values that follow it.
llength Return the number of elements in a list.
lrange Return a subset of a source list as a new list.
lreplace Delete a range of elements in a list and inserts a new set of elements at that position.
lsearch Search a list for the first element that matches a pattern.
lsort Sort the elements in a list.
open Open a file. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g for embedded systems without file system.
proc Create a new command appending it to the list of current interpreter commands. That's how user-defined commands are built.
puts Print argument to the stdout, followed by a newline. This command can be disabled using both #define TCL_DISABLE_PUTS (for "stdout") and #define TCL_DISABLE_FILEIO (for output to file).
read Read a file competely in memory. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
return Jump out of the current command (proc), with an optional explicit return value.
scan Parse a string and stores extracted values into variables. This command currently only supports %c, %d, %i and %x placeholders, plus optional "width" modifiers (e.g. %2x).
seek Set file read/write position. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
set Assign value to the variable and/or returns the current variable value.
source Read a file and evaluate it as a Tcl script. This command can be disabled using #define TCL_DISABLE_SOURCE, e.g for embedded systems without file system.
split Create a list from a string, by splitting the string on a separator chosen by the user.
string An assortment of string functions: compare, equal, first, index, last, length, match, range, tolower, toupper, trim, trimleft, trimright.
subst Perform command and variable substitution in the argument string.
switch Control flow structure, executing a block selected from matching one out of several patterns.
tell Get the current file read/write position. This command can be disabled using #define TCL_DISABLE_FILEIO, e.g. for embedded systems without file system.
unset Clear a variable (remove it completely).
upvar Create an alias for a variable at a different scope, e.g. to implement pass-by-reference arguments.
while Run a loop as long as the condition is true; while {cond} {body}. If the condition is already false on start, the body is never evaluated. One may use break, continue (or return) inside the loop to contol the flow.

Operator table

These are the operators that can be used in the parameter of the expr command. Note that some operators only work on integers.

operator summary
- + ! ~ () unary operators: negate, unary plus (a no-operation operator), logic not, binary invert, and sub-expressions between parentheses
** exponentiation
* / % multiply, divide, remainder after division
+ - addition, subtraction
<< >> binary shift left & right (integer only)
< <= > >= smaller than, smaller than or equal, greater than, greater than or equal
== != equal, not equal
& binary and (integer only)
^ binary exclusive or (integer only)
| binary or (integer only)
&& logic and
|| logic or
? : conditional selection (ternary operator)

Integer arithmetic is used when all operands of an operator have integer type. So notably, "7 / 2" result in 3 (integer division); for floating point division use "7.0 / 2" (or "7 / 2.0" or "7.0 / 2.0"). Also, integer division in Tcl (and thus ParTcl) is floored integer division, in the sense that the remainder after division is always a positive value, and the following relation holds:

 v2 * (v1 / v2) + (v1 % v2) == v1

For positive numerators & denominators, floored division gives the same results as the (more common) truncated division: the division 14 / 3 is 4 (and with remainder 2). The difference is with negative results: with truncated division -14 / 3 equals -4 with remainder -2, but with floored division, -14 / 3 equals -5 with remainder 1.

Functions

The expr command supports the following functions:

name description
abs The absolute value of the parameter (floating-point or integer)
ceil Round up to the nearest whole number, returning a floating-point result)
cos The cosine of the parameter in radians
double The same value as the parameter, but converted to floating-point if needed
exp The mathematical constant 'e' raised to the power of the parameter
floor Round down to the nearest whole number, returning a floating-point result)
int If the parameter is an integer, the same value as the parameter; if the parameter is floating point, the parameter truncated to an integer
log The natural logarithm of the parameter
log10 The 10-based logarithm of the parameter
round Round the nearest whole number and return an integer result
sin The sine of the parameter in radians
sqrt The square root of the parameter
tan The tangent of the parameter in radians

Predefined variables

Mostly for the use of the trigonometric functions, the following variables are predefined:

name description
degtorad The multiplication factor to convert degrees into radians
e The mathematical constant 'e' (Euler's number)
pi The mathematical constant 'π'
sqrt2 The value of the square root of 2
radtodeg The multiplication factor to convert radians into degrees

Internals

Lexer

Any symbol can be part of the word, except for the following special symbols:

  • whitespace, tab - used to delimit words
  • \r, \n, semicolon or EOS (zero-terminator) - used to delimit commands
  • Braces, square brackets, dollar sign - used for substitution and grouping

ParTcl has special helper functions for these char classes:

static bool tcl_is_space(char c);
static bool tcl_is_end(char c);
static bool tcl_is_special(char c, bool quote);

tcl_is_special behaves differently depending on the quoting mode (quote parameter). Inside a quoted string braces, semicolon and end-of-line symbols lose their special meaning and become regular printable characters.

ParTcl lexer is implemented in one function:

int tcl_next(const char *list, size_t length, const char **from, const char **to, unsigned *flags);

tcl_next function finds the next token in the string list. Paramters from and to are set to point to the token start/end. Parameter flags holds flags for the quoting mode (toggled if " is met), and marks whether a comment is allowed (and must be gobbled), plus other(s). When calling the lexer from your own code, flags should be initialized to 0.

A special macro tcl_each(s, len, skip_error) can used to iterate over all the tokens in the string. If skip_error is false - loop ends when string ends, otherwise loop can end earlier if a syntax error is found. It allows to "validate" input string without evaluating it and detect when a full command has been read.

Memory management

Tcl uses strings as a primary data type. When a Tcl script is evaluated, many of the strings are created, disposed or modified. In embedded systems, memory management can be complex, so all operations with Tcl values are moved into isolated functions that can be rewritten to optimize certain parts (e.g. to use a pool of strings, a custom memory allocator, cache numerical or list values to increase performance etc).

/* Functions calling malloc() or free() */
struct tcl_value *tcl_value(const char *data, size_t len);
bool tcl_append(struct tcl_value *value, struct tcl_value *tail);
bool tcl_list_append(struct tcl_value *list, struct tcl_value *tail);
struct tcl_value *tcl_free(struct tcl_value *value);
struct tcl_value *tcl_var(struct tcl *tcl, const char *name, struct tcl_value *value);
struct tcl_cmd *tcl_register(struct tcl *tcl, const char *name, tcl_cmd_fn_t fn,
 unsigned short minargs, unsigned short maxargs,
 struct tcl_value *user);
void tcl_destroy(struct tcl *tcl);
/* Internal functions also doing memory allocation */
static struct tcl_scope *tcl_scope_alloc(struct tcl_scope *parent);
static struct tcl_var *tcl_scope_var(struct tcl_scope *scope, const char *name);
static struct tcl_scope *tcl_scope_free(struct tcl_scope *scope);
static void tcl_var_free_values(struct tcl_var *var);
static void tcl_var_free(struct tcl_scope *scope, struct tcl_var *var);
static int tcl_error_result(struct tcl *tcl, int code, const char *info);
/* Built-in commands */
static int tcl_cmd_array(struct tcl *tcl, struct tcl_value *args, void *arg);
static int tcl_cmd_binary(struct tcl *tcl, struct tcl_value *args, void *arg);
static int tcl_cmd_format(struct tcl *tcl, struct tcl_value *args, void *arg);
static int tcl_cmd_read(struct tcl *tcl, struct tcl_value *args, void *arg);

Scopes

A special type, struct tcl_scope is used to keep the evaluation scope, which holds to local variables at each call nesting level. The interpreter creates a new scope each time a user-defined procedure is called; Also there is one global scope per interpreter.

There are only 3 functions related to the scopes. One creates a new scope, another seeks for a variable (or creates a new one), the last one destroys the scope and all the variables in it.

These functions use malloc()/free(), but can easily be rewritten to use memory pools instead.

static struct tcl_scope *tcl_scope_alloc(struct tcl_scope *parent);
static struct tcl_var *tcl_scope_var(struct tcl_scope *scope, tcl_value_t *name);
static struct tcl_scope *tcl_scope_free(struct tcl_scope *scope);

Variables are implemented as a single-linked list, each variable is a pair of values (name + value) and a pointer to the next variable.

Interpreter

ParTcl interpreter is a simple structure struct tcl which keeps the current scope (see above), an array of available commands, and a "last result" value.

Interpreter logic is wrapped around two functions - evaluation and substitution.

Substitution:

  • If argument starts with $ - evaluate the name that follows and return the variable's value. If the variable does not exist, an empty variable is created.
  • If argument starts with [ - evaluate what's inside the square brackets and return the result.
  • If argument is a quoted string (e.g. {foo bar}) - return it as is, just without braces.
  • Otherwise return the argument as is.

Evaluation:

  • Iterates over each token in a list
  • Appends words into a list
  • If the command end is met (semicolon, or newline, or end-of-file - our lexer has a special token type TCMD for them) - then find a suitable command (the first word in the list) and call it.

Where the commands are taken from? Initially, a ParTcl interpeter starts with no commands, but one may add the commands by calling tcl_register().

Each command has a name, arity (how many arguments it shall take - interpreter checks it before calling the command, use zero arity for varargs) and a C function pointer that actually implements the command.

Building and testing

All sources are in one file, tcl.c. It can be used as a standalone interpreter, or made part of a bigger application. The structure declarations and function prototypes are in tcl.h.

Tests are run with clang and coverage is calculated. Just run "make test" and you're done.

Code is formatted using clang-format to keep the clean and readable coding style. Please run it for pull requests, too.

License

Code is distributed under MIT license, feel free to use it in your proprietary projects as well.