{"id":13778515,"url":"https://github.com/zserge/partcl","last_synced_at":"2025-04-05T05:08:24.884Z","repository":{"id":47572912,"uuid":"56053003","full_name":"zserge/partcl","owner":"zserge","description":"ParTcl - a micro Tcl implementation","archived":false,"fork":false,"pushed_at":"2024-04-18T08:14:32.000Z","size":18,"stargazers_count":476,"open_issues_count":12,"forks_count":50,"subscribers_count":27,"default_branch":"master","last_synced_at":"2024-10-14T12:23:28.650Z","etag":null,"topics":["c","interpreter","tcl"],"latest_commit_sha":null,"homepage":"https://zserge.com/posts/tcl-interpreter/","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zserge.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2016-04-12T10:20:27.000Z","updated_at":"2024-09-29T15:25:17.000Z","dependencies_parsed_at":"2024-07-05T18:40:33.092Z","dependency_job_id":null,"html_url":"https://github.com/zserge/partcl","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zserge%2Fpartcl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zserge%2Fpartcl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zserge%2Fpartcl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zserge%2Fpartcl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zserge","download_url":"https://codeload.github.com/zserge/partcl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247289428,"owners_count":20914464,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["c","interpreter","tcl"],"created_at":"2024-08-03T18:00:54.375Z","updated_at":"2025-04-05T05:08:24.860Z","avatar_url":"https://github.com/zserge.png","language":"C","funding_links":[],"categories":["C"],"sub_categories":[],"readme":"# Partcl - a minimal Tcl interpreter\n\n[![Build Status](https://img.shields.io/github/workflow/status/zserge/partcl/Build%20Pipeline)](https://github.com/zserge/partcl)\n\n## Features\n\n* ~600 lines of \"pedantic\" C99 code\n* No external dependencies\n* Good test coverage\n* Can be extended with custom Tcl commands\n* Runs well on bare metal embedded MCUs (~10k of flash is required)\n\nBuilt-in commands:\n\n* `subst arg`\n* `set var ?val?`\n* `while cond loop`\n* `if cond branch ?cond? ?branch? ?other?`\n* `proc name args body`\n* `return`\n* `break`\n* `continue`\n* arithmetic operations: `+, -, *, /, \u003c, \u003e, \u003c=, \u003e=, ==, !=`\n\n## Usage\n\n```c\nstruct tcl tcl;\nconst char *s = \"set x 4; puts [+ [* $x 10] 2]\";\n\ntcl_init(\u0026tcl);\nif (tcl_eval(\u0026tcl, s, strlen(s)) != FERROR) {\n    printf(\"%.*s\\n\", tcl_length(tcl.result), tcl_string(tcl.result));\n}\ntcl_destroy(\u0026tcl);\n```\n\n## Language syntax\n\nTcl script is made up of _commands_ separated by semicolons or newline\nsymbols. Commnads in their turn are made up of _words_ separated by whitespace.\nTo make whitespace a part of the word one may use double quotes or braces.\n\nAn important part of the language is _command substitution_, when the result of\na command inside square braces is returned as a part of the outer command, e.g.\n`puts [+ 1 2]`.\n\nThe only data type of the language is a string. Although it may complicate\nmathematical operations, it opens a broad way for building your own DSLs to\nenhance the language.\n\n## Lexer\n\nAny symbol can be part of the word, except for the following special symbols:\n\n* whitespace, tab - used to delimit words\n* `\\r`, `\\n`, semicolon or EOF - used to delimit commands\n* Braces, square brackets, dollar sign - used for substitution and grouping\n\nPartcl has special helper functions for these char classes:\n\n```\nstatic int tcl_is_space(char c);\nstatic int tcl_is_end(char c);\nstatic int tcl_is_special(char c, int q);\n```\n\n`tcl_is_special` behaves differently depending on the quoting mode (`q`\nparameter). Inside a quoted string braces, semicolon and end-of-line symbols\nlose their special meaning and become regular printable characters.\n\nPartcl lexer is implemented in one function:\n\n```\nint tcl_next(const char *s, size_t n, const char **from, const char **to, int *q);\n```\n\n`tcl_next` function finds the next token in the string `s`. `from` and `to` are\nset to point to the token start/end, `q` denotes the quoting mode and is\nchanged if `\"` is met.\n\nA special macro `tcl_each(s, len, skip_error)` can used to iterate over all the\ntokens in the string. If `skip_error` is false - loop ends when string ends,\notherwise loop can end earlier if a syntax error is found. It allows to\n\"validate\" input string without evaluating it and detect when a full command\nhas been read.\n\n## Data types\n\nTcl uses strings as a primary data type. When Tcl script is evaluated, many of\nthe strings are created, disposed or modified. In embedded systems memory\nmanagement can be complex, so all operations with Tcl values are moved into\nisolated functions that can be easily rewritten to optimize certain parts (e.g.\nto use a pool of strings, a custom memory allocator, cache numerical or list\nvalues to increase performance etc).\n\n```\n/* Raw string values */\ntcl_value_t *tcl_alloc(const char *s, size_t len);\ntcl_value_t *tcl_dup(tcl_value_t *v);\ntcl_value_t *tcl_append(tcl_value_t *v, tcl_value_t *tail);\nint tcl_length(tcl_value_t *v);\nvoid tcl_free(tcl_value_t *v);\n\n/* Helpers to access raw string or numeric value */\nint tcl_int(tcl_value_t *v);\nconst char *tcl_string(tcl_value_t *v);\n\n/* List values */\ntcl_value_t *tcl_list_alloc();\ntcl_value_t *tcl_list_append(tcl_value_t *v, tcl_value_t *tail);\ntcl_value_t *tcl_list_at(tcl_value_t *v, int index);\nint tcl_list_length(tcl_value_t *v);\nvoid tcl_list_free(tcl_value_t *v);\n```\n\nKeep in mind, that `..._append()` functions must free the tail argument.\nAlso, the string returned by `tcl_string()` it not meant to be mutated or\ncached.\n\nIn the default implementation lists are implemented as raw strings that add\nsome escaping (braces) around each iterm. It's a simple solution that also\nreduces the code, but in some exotic cases the escaping can become wrong and\ninvalid results will be returned.\n\n## Environments\n\nA special type, `struct tcl_env` is used to keep the evaluation environment (a\nset of functions). The interpreter creates a new environment for each\nuser-defined procedure, also there is one global environment per interpreter.\n\nThere are only 3 functions related to the environment. One creates a new environment, another seeks for a variable (or creates a new one), the last one destroys the environment and all its variables.\n\nThese functions use malloc/free, but can easily be rewritten to use memory pools instead.\n\n```\nstatic struct tcl_env *tcl_env_alloc(struct tcl_env *parent);\nstatic struct tcl_var *tcl_env_var(struct tcl_env *env, tcl_value_t *name);\nstatic struct tcl_env *tcl_env_free(struct tcl_env *env);\n```\n\nVariables are implemented as a single-linked list, each variable is a pair of\nvalues (name + value) and a pointer to the next variable.\n\n## Interpreter\n\nPartcl interpreter is a simple structure `struct tcl` which keeps the current\nenvironment, array of available commands and a last result value.\n\nInterpreter logic is wrapped around two functions - evaluation and\nsubstitution.\n\nSubstitution:\n\n- If argument starts with `$` - create a temporary command `[set name]` and\n  evaluate it. In Tcl `$foo` is just a shortcut to `[set foo]`, which returns\n  the value of \"foo\" variable in the current environment.\n- If argument starts with `[` - evaluate what's inside the square brackets and\n  return the result.\n- If argument is a quoted string (e.g. `{foo bar}`) - return it as is, just\n  without braces.\n- Otherwise return the argument as is.\n\nEvaluation:\n\n- Iterates over each token in a list\n- Appends words into a list\n- If the command end is met (semicolor, or newline, or end-of-file - our lexer\n  has a special token type `TCMD` for them) - then find a suitable command (the\n  first word in the list) and call it.\n\nWhere the commands are taken from? Initially, a Partcl interpeter starts with\nno commands, but one may add the commands by calling `tcl_register()`.\n\nEach command has a name, arity (how many arguments is shall take - interpreter\nchecks it before calling the command, use zero arity for varargs) and a C\nfunction pointer that actually implements the command.\n\n## Builtin commands\n\n\"set\" - `tcl_cmd_set`, assigns value to the variable (if any) and returns the\ncurrent variable value.\n\n\"subst\" - `tcl_cmd_subst`, does command substitution in the argument string.\n\n\"puts\" - `tcl_cmd_puts`, prints argument to the stdout, followed by a newline.\nThis command can be disabled using `#define TCL_DISABLE_PUTS`, which is handy\nfor embedded systems that don't have \"stdout\".\n\n\"proc\" - `tcl_cmd_proc`, creates a new command appending it to the list of\ncurrent interpreter commands. That's how user-defined commands are built.\n\n\"if\" - `tcl_cmd_if`, does a simple `if {cond} {then} {cond2} {then2} {else}`.\n\n\"while\" - `tcl_cmd_while`, runs a while loop `while {cond} {body}`. One may use\n\"break\", \"continue\" or \"return\" inside the loop to contol the flow.\n\nVarious math operations are implemented as `tcl_cmd_math`, but can be disabled,\ntoo if your script doesn't need them (if you want to use Partcl as a command\nshell, not as a programming language).\n\n## Building and testing\n\nAll sources are in one file, `tcl.c`. It can be used as a standalone\ninterpreter, or included as a single-file library (you may want to rename it\ninto tcl.h then).\n\nTests are run with clang and coverage is calculated. Just run \"make test\" and\nyou're done.\n\nCode is formatted using clang-format to keep the clean and readable coding\nstyle. Please run it for pull requests, too.\n\n## License\n\nCode is distributed under MIT license, feel free to use it in your proprietary\nprojects as well.\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzserge%2Fpartcl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzserge%2Fpartcl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzserge%2Fpartcl/lists"}