Partcl is 600 lines of C99 that implements enough of Tcl to script an embedded device
ParTcl - a micro Tcl implementation
At a glance
- What is it?
- Partcl is a minimal Tcl interpreter written in pedantic C99 with no external dependencies, small enough to run on a bare metal microcontroller with about 10k of flash, with a lexer, substitution, environments and a registry of built-in commands you can extend. It suits an embedded developer who wants a scriptable configuration layer, and it implements a subset rather than the whole of Tcl.
- Who is it for?
- Partcl fits an embedded developer who needs a tiny scriptable layer for configuration or commands on a microcontroller, since the whole interpreter is one C file you can read in an afternoon and the allocation layer is designed to be swapped for pools.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 165 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The size claim and what it buys you
Partcl is described as a minimal Tcl interpreter, and the feature list is stated as five bullets that are really one claim in different forms.
There are around 600 lines of pedantic C99 code, there are no external dependencies, there is good test coverage, it can be extended with custom Tcl commands, and it runs well on bare metal embedded MCUs with about 10k of flash required.
That last number is the headline. Ten kilobytes of flash is small enough that the interpreter fits alongside a real application on a microcontroller, which is the case the author had in mind. It also explains the design decisions that follow, because everything else in the README is a consequence of wanting a Tcl-like language inside that budget.
The pedantic C99 is deliberate rather than incidental. The Makefile compiles with -Os -Wall -Wextra -std=c99 -pedantic, and the pedantic flag means the code does not lean on compiler extensions, which is what you want when your target toolchain is not the one on your laptop.
The project is by Serge Alekseev, the primary language is C, the licence is MIT, and the last push to the repository was on 2026-05-13 with no GitHub releases listed.
The built-in command set, which is much smaller than Tcl
The built-in commands are listed explicitly, and the list is short enough to read in one breath.
There is subst, set, while, if, proc, return and break, along with continue, and arithmetic operations for +, -, *, /, <, >, <=, >=, == and !=.
Compare that with the Tcl language as a whole and the scope becomes clear. There is no string command, no list command, no file I/O, no regexp, no namespaces, no exceptions, no coroutines, no packages. There is no while loop's break-with-value semantics and no uplevel.
So this is not a Tcl implementation you can point an existing script at. It is a Tcl-shaped language with a small vocabulary, and the design goal of the README is explicit about why: the only data type of the language is a string, which although it may complicate mathematical operations opens a broad way for building your own DSLs to enhance the language.
That framing is the key to reading the project. The argument is that you are not adopting Tcl, you are adopting the Tcl evaluation model, and then you extend it with the commands your product actually needs.
The interpreter starts with no commands at all. You add them by calling `tcl_register()`, and each command has a name, an arity that the interpreter checks before calling it, with zero arity for varargs, and a C function pointer that implements it.
The evaluation model, substitution and the lexer
The README explains the language in three parts, and the parts fit together cleanly.
A Tcl script is made up of commands separated by semicolons or newline symbols. Commands in their turn are made up of words separated by whitespace. To make whitespace part of a word you use double quotes or braces.
Command substitution is the part that matters most, and the example given is `puts [+ 1 2]`, where the result of a command inside square brackets is returned as part of the outer command.
Substitution has four cases. If the argument starts with `$`, it creates a temporary command `[set name]` and evaluates it, because in Tcl `$foo` is a shortcut for `[set foo]`. If it starts with `[`, it evaluates what is inside the brackets and returns the result. If it is a quoted string such as `{foo bar}`, it returns it as is, just without the braces. Otherwise it returns the argument unchanged.
Evaluation iterates over each token, appends words into a list, and when it meets the end of a command, which the lexer marks with a special `TCMD` token for semicolon, newline or end of file, it finds a suitable command and calls it. The command is the first word in the list.
The lexer is one function.
int tcl_next(const char *s, size_t n, const char **from, const char **to, int *q);It finds the next token in the string, sets `from` and `to` to the token start and end, and `q` denotes the quoting mode, changed when a double quote is met.
The special characters are whitespace and tab to delimit words, carriage return, newline, semicolon or EOF to delimit commands, and braces, square brackets and the dollar sign for substitution and grouping. The helper functions tcl_is_space, tcl_is_end and tcl_is_special implement those classes, and tcl_is_special behaves differently depending on the quoting mode, since inside a quoted string braces, semicolon and end-of-line symbols lose their special meaning.
The memory model, which is the part you will rewrite first
Tcl uses strings as its primary data type, and when a script is evaluated many strings are created, disposed or modified. In embedded systems that can be complex, so all operations with Tcl values are moved into isolated functions that can be easily rewritten to optimise certain parts.
The listed candidates for that rewrite are a pool of strings, a custom memory allocator, and caching numerical or list values to increase performance.
/* Raw string values */
tcl_value_t *tcl_alloc(const char *s, size_t len);
tcl_value_t *tcl_dup(tcl_value_t *v);
tcl_value_t *tcl_append(tcl_value_t *v, tcl_value_t *tail);
int tcl_length(tcl_value_t *v);
void tcl_free(tcl_value_t *v);
/* Helpers to access raw string or numeric value */
int tcl_int(tcl_value_t *v);
const char *tcl_string(tcl_value_t *v);
/* List values */
tcl_value_t *tcl_list_alloc();
tcl_value_t *tcl_list_append(tcl_value_t *v, tcl_value_t *tail);
tcl_value_t *tcl_list_at(tcl_value_t *v, int index);
int tcl_list_length(tcl_value_t *v);
void tcl_list_free(tcl_value_t *v);Two ownership rules come with it, and they are the kind of detail that causes bugs if you miss them. The `..._append()` functions must free the tail argument, and the string returned by `tcl_string()` is not meant to be mutated or cached.
The environment layer follows the same pattern. `struct tcl_env` holds the evaluation environment, the interpreter creates a new environment for each user-defined procedure, and there is one global environment per interpreter. Only three functions touch it: one allocates a new environment, one seeks a variable or creates it, and one destroys the environment and all its variables. They use malloc and free but can easily be rewritten to use memory pools instead.
Variables are a single-linked list where each variable is a pair of values, name and value, plus a pointer to the next one.
So the design is explicitly hostile to hidden allocation, which is the right call for the target.
Using it, and the list escaping caveat
The usage example is a handful of lines.
struct tcl tcl;
const char *s = "set x 4; puts [+ [* $x 10] 2]";
tcl_init(&tcl);
if (tcl_eval(&tcl, s, strlen(s)) != FERROR) {
printf("%.*s\n", tcl_length(tcl.result), tcl_string(tcl.result));
}
tcl_destroy(&tcl);The lifecycle is init, eval, destroy, and the eval returns an error code rather than throwing, which is what you want in C and in an embedded context with no exceptions.
The script itself is a good compact demonstration of the model. It sets a variable, then prints the result of a nested arithmetic expression with a command substitution and a variable reference inside it, so you can see `$x`, brackets and operator commands combining.
The result is read through `tcl_length` and `tcl_string`, and the printf uses the precision form so the length is honoured rather than relying on a null terminator, which matters for embedded string handling.
One caveat is worth flagging. In the default implementation, lists are implemented as raw strings that add some escaping, braces, around each item. The README calls it a simple solution that also reduces the code, but says that in some exotic cases the escaping can become wrong and invalid results will be returned.
That is an honest statement of a known limitation, and it means list handling is the part of Partcl to test first if your scripts build lists.
The build, the test suite and how to extend it
The Makefile is short and uses clang by default with an overridable CC.
The build compiles with -Os -Wall -Wextra -std=c99 -pedantic, and the test target is separate. The test binary is compiled with a different compiler setting, -O0 -g -std=c11 -pedantic with -fprofile-arcs and -ftest-coverage, so the test build is optimised for instrumentation rather than speed.
all: $(TCLBIN) test
tcl: tcl.o
test: $(TCLTESTBIN)
./tcl_testThe coverage target runs gcov over tcl_test.c after the tests, and the fmt target runs clang-format and cloc, which also prints the line count of tcl.c so you can track the size claim.
The test source is one file, tcl_test.c, with four headers that name the areas under test: tcl_test_lexer.h, tcl_test_subst.h, tcl_test_flow.h and tcl_test_math.h. The header-per-area layout in a single test binary keeps the whole thing small while still separating concerns.
Extension is the other half of the story. You register your own command with `tcl_register()`, giving it a name, an arity and a C function pointer. The `proc` built-in does the same thing in the language, creating a new command and appending it to the list of current interpreter commands, which is how user-defined commands are built.
So there are two extension paths. A C function registered at startup for things that need to touch the host, and a Tcl procedure defined at runtime for everything else.
The README also notes that puts can be disabled with TCL_DISABLE_PUTS, which is handy for embedded systems that do not have stdout.
Where Partcl is the wrong choice
Four cases argue against it.
The first is anything that needs real Tcl. The command set is a dozen commands, there is no string or list or file or regexp command, and the list implementation is a documented rough edge. A script written for Tcl will not run, and a user accustomed to Tcl will find the language nearly empty. This is a language with Tcl's evaluation model, not Tcl.
The second is a scripting environment for a desktop or server application where a real language is available. If you are on Linux or macOS you have awk, sed and perl, and if you are on Windows you have PowerShell. Partcl exists because those are not on a microcontroller.
The third is a project with a memory budget but a willingness to write a domain specific parser instead. If you need to parse a small configuration format, a purpose-built parser is often smaller than an interpreter with a lexer, a substituter and an environment. Partcl pays off when the logic is genuinely dynamic and users need to extend it at runtime, which is the case for a product where the scripting is a feature.
The alternative in the same direction is an existing embedded scripting language, such as Lua, which is small and proven but brings a larger binary than 10k of flash, or a JSON or CBOR parser with a fixed schema, which is smaller still but not extensible. The difference in approach is that Partcl gives you a Tcl-style language, including command substitution and procedures, in a footprint that fits alongside a real application, and the price is that you get the language without the ecosystem, the tooling and the debugging support that come with a mature implementation.
Editorial conclusion
Partcl fits an embedded developer who needs a tiny scriptable layer for configuration or commands on a microcontroller, since the whole interpreter is one C file you can read in an afternoon and the allocation layer is designed to be swapped for pools. It does not fit anyone expecting Tcl compatibility, because the built-in command set is a dozen commands rather than the full language, and the list representation can produce invalid results in exotic cases as the README admits. Build it with `make`, add commands through `tcl_register()`, and check `tcl_test` before you rely on any of it.
Frequently asked questions
What is Partcl?
It is a minimal Tcl interpreter written in about 600 lines of pedantic C99 with no external dependencies. It provides substitution, environments, a lexer and a registry of built-in commands that you can extend, and it runs on bare metal microcontrollers with about 10k of flash required.
Which commands does Partcl implement?
subst, set, while, if, proc, return, break, continue, plus arithmetic operations for +, -, *, /, the comparison operators and equality. A Partcl interpreter starts with no commands, and you add your own by calling tcl_register() with a name, an arity and a C function pointer.
How does Partcl handle memory on an embedded target?
All operations on Tcl values are isolated in a small set of functions so they can be rewritten, for example to use a pool of strings or a custom allocator. The environment functions use malloc and free but can easily be rewritten to use memory pools instead.
What are the caveats in Partcl's list implementation?
In the default implementation, lists are raw strings with some escaping, braces, added around each item. The README calls it a simple solution that reduces the code, but notes that in some exotic cases the escaping can become wrong and invalid results will be returned. Two ownership rules also apply: the append functions must free the tail argument, and the string returned by tcl_string() is not meant to be mutated or cached.
How do I build and test Partcl?
With make. The default target builds the tcl binary and runs the tests, the compiler is clang with -Os -Wall -Wextra -std=c99 -pedantic, and the test binary is built separately with coverage instrumentation. A make coverage target runs gcov over the test file, and a make fmt target runs clang-format and prints the line count of tcl.c.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/zserge-partcl)Was this useful?