├── nimiTCL.nimble ├── readme.md └── src ├── commands.nim ├── eval.nim ├── main.nim ├── parse.nim └── streamutils.nim /nimiTCL.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | bin = @[ "src/main" ] 4 | version = "0.1.0" 5 | author = "Piotr Klibert" 6 | description = "A minimal TCL intepreter" 7 | license = "MIT" 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 0.15.2" 12 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # nimiTCL - A minimal TCL interpreter in [Nim](nim-lang.org/) 2 | 3 | ## What? 4 | 5 | A toy interpreter in less than *300* loc (current count (incl. empty lines): 6 | **299**). 7 | 8 | The original goalpost was *500* loc, but it was in C. Nim aims to be close to C 9 | in terms of speed, but it competes with languages like Python in terms of 10 | expressivity, so I decided to try to make it in under 300. Initial version was 11 | closer to 350 loc, but fortunately I managed to reduce the count without 12 | compromising readability too much. 13 | 14 | It's a toy: it doesn't implement the full TCL language and it was never meant 15 | to. That being said, it's easy to extend and if you're willing to go above the 16 | 300 loc mark you can quickly make it useful. For this you would need at least: 17 | 18 | * ~~better proc implementation (proper call frames)~~ - **DONE** 19 | * ~~`return` handling~~ - **DONE** 20 | * ~~`while` loop~~ - **DONE** 21 | * for loop 22 | * break and continue 23 | * expr command 24 | * uplevel, upvar 25 | 26 | See `src/main.nim` for supported language constructs. 27 | 28 | You can clone, build and run the interpreter with the following commands, 29 | provided that you have Nim and [Nimble](https://github.com/nim-lang/nimble) 30 | installed: 31 | 32 | git clone https://github.com/piotrklibert/nimiTCL && cd nimiTCL 33 | nimble build && src/main 34 | 35 | When you run the compiled program it'll first run some hardcoded TCL code and 36 | then start a very simple (one-line only, as it's not integrated with the 37 | parser - use `;` instead of `\n` to separate commands in the bodies of `proc`s 38 | or `if`s) REPL. 39 | 40 | ## How? 41 | 42 | Nim is a statically typed, natively compiled (via C) language with rich type 43 | system, a lot of goodies in its standard library, and advanced meta-programming 44 | capabilities. The features I used to make the implementation shorter (compared 45 | to C) include: 46 | 47 | * [built-in string](https://nim-lang.org/docs/manual.html#types-string-type) 48 | type, which is similar to std::string in C++ (all over the place) 49 | * [built-in seq](https://nim-lang.org/docs/manual.html#types-array-and-sequence-types) 50 | type, which is similar to std::vector in C++ (all over the place) 51 | * [hash tables](https://nim-lang.org/docs/tables.html) provided in stdlib, which 52 | are like std::map in C++ (for variables and commands lookup) 53 | * [built-in closures](https://nim-lang.org/docs/manual.html#procedures-closures) 54 | support (for user-defined commands) 55 | * [built-in templates](https://nim-lang.org/docs/manual.html#templates), which 56 | are like [syntax-rule macros](http://www.willdonnelly.net/blog/scheme-syntax-rules/) 57 | in Scheme (to simplify defining commands) 58 | * [built-in support](https://nim-lang.org/docs/manual.html#modules) for modules 59 | to split the code over multiple files 60 | * [enum and object variants](https://nim-lang.org/docs/manual.html#types-object-variants) 61 | to represent tokens in the parser 62 | * [built-in deepCopy support](https://nim-lang.org/docs/manual.html#type-bound-operations-deepcopy) 63 | to get the call frames (it's wastes some memory, but thanks to this feature 64 | it took literaly 3 lines of code to implement call frames) 65 | 66 | ## Why? 67 | 68 | For fun, mostly. But also to showcase Nim's features and to provide a material 69 | for comparison with C versions. 70 | 71 | I started writing this after I read about two other TCL implementations, done in 72 | C, which are around 500 loc: 73 | 74 | * [zserge implementation - ParTcl](http://zserge.com/blog/tcl-interpreter.html) 75 | * [antirez implementation - Picol](http://oldblog.antirez.com/post/picol.html) 76 | 77 | At first I intended to simply translate the former, like I did with my 78 | [slock implementation](https://github.com/piotrklibert/nimlock/), but it seemed 79 | impractical given the amount of code which could be eliminated by using Nim's 80 | features, so I decided to write most of the code from scratch. The code ended up 81 | being similar to the two implementations anyway, but it's noticeably shorter. 82 | 83 | I never used TCL, what little I know about it comes from 84 | [this article](http://antirez.com/articoli/tclmisunderstood.html) by antirez. I 85 | read it years ago and I rememer I thought that TCL was a very elegant language - 86 | one that combines a few simple concepts into a strong whole. I later learned 87 | about [fexprs](https://en.wikipedia.org/wiki/Fexpr), the 88 | [vau](http://lambda-the-ultimate.org/node/4093) and 89 | [Kernel language](http://lambda-the-ultimate.org/node/1680), as well as 90 | [PicoLisp](http://picolisp.com/wiki/?home) and [Io](iolanguage.org/) which all 91 | work in a similar way. 92 | 93 | Still, when learning these, I thought: "Wow, just like TCL!" - which helped me 94 | in their understanding. For that, I'm grateful to antirez and TCL. 95 | -------------------------------------------------------------------------------- /src/commands.nim: -------------------------------------------------------------------------------- 1 | var registeredCommands: TclTable[TclCmd] = newTable[string, TclCmd]() 2 | 3 | template defcmd(cmdName: string, procName, body: untyped): untyped = 4 | proc procName(c: TclContext, a: seq[TclValue]): TclValue = 5 | var ctx {. inject .} = c # for some reason the inject pragma doesn't work 6 | var args {. inject .} = a # in proc formal params list 7 | body 8 | registeredCommands[cmdName] = procName 9 | 10 | defcmd("not", cmdNot): (if args[0] == Null: True else: Null) 11 | 12 | defcmd("while", cmdWhile): 13 | if len(args) != 2: return Null 14 | while ctx.eval(args[0]) != Null: 15 | result = ctx.eval(args[1]) 16 | 17 | defcmd("eval", cmdEval): 18 | if len(args) < 1: return Null 19 | return ctx.eval(args[0]) 20 | 21 | defcmd("proc", cmdProc): 22 | if len(args) != 3: return Null 23 | let 24 | procName = args[0].data 25 | procClosure = 26 | proc (innerCtx: TclContext, innerArgs: seq[TclValue]): TclValue = 27 | let argNames = args[1].data.split(" ") 28 | let ctx = innerCtx.copy(true) 29 | ctx.setVars(zip(argNames, innerArgs)) 30 | try: 31 | result = ctx.eval(args[2]) 32 | except TclReturn: 33 | result = ((ref TclReturn)(getCurrentException())).val 34 | 35 | ctx.cmds.add(procName, procClosure) 36 | Null 37 | 38 | defcmd("set", cmdSetVar): 39 | if len(args) == 1: return ctx.vars[args[0].data] 40 | elif len(args) != 2: return Null 41 | let name = args[0].data 42 | ctx.vars[name] = args[1] 43 | return args[1] 44 | 45 | defcmd("concat", cmdConcat): 46 | result = newValue() 47 | for v in args: 48 | result.data &= v.data 49 | 50 | defcmd("return", cmdReturn): 51 | if ctx.insideProc: 52 | var e = newException(TclReturn, "") 53 | e.val = (if len(args) == 0: Null else: args[0]) 54 | raise e 55 | else: 56 | return args[0] 57 | 58 | defcmd("echo", cmdEcho): 59 | for arg in args: stdout.write(arg.data) 60 | stdout.write("\n") 61 | Null 62 | 63 | defcmd("cmp", cmdCmp): 64 | if len(args) != 2: return Null 65 | return (if args[0] != args[1]: Null else: True) 66 | 67 | defcmd("if", cmdIf): 68 | if len(args) notin {2, 3}: return Null 69 | if args[0] == True: return ctx.eval(args[1]) 70 | else: return (if len(args) == 3: ctx.eval(args[2]) else: Null) 71 | 72 | defcmd("help", cmdHelp): 73 | for k in registeredCommands.keys(): echo $k 74 | Null 75 | -------------------------------------------------------------------------------- /src/eval.nim: -------------------------------------------------------------------------------- 1 | import tables, sequtils, strutils 2 | import parse 3 | 4 | type 5 | TclReturn* = object of Exception 6 | val*: TclValue 7 | TclTable[T] = TableRef[string, T] 8 | TclCmd = proc (ctx: TclContext, args: seq[TclValue]): TclValue 9 | TclValue = ref object of RootObj 10 | data: string 11 | TclContext = ref object 12 | vars: TclTable[TclValue] 13 | cmds: TclTable[TclCmd] 14 | insideProc: bool 15 | 16 | proc newValue(s: string = ""): TclValue = 17 | result = new(TclValue) 18 | result.data = s 19 | 20 | proc `$`*(v: TclValue) : string = $(v.data) 21 | proc `==`*(a,b: TclValue): bool = a.data == b.data 22 | 23 | proc newContext*(): TclContext = 24 | new(result) 25 | result.insideProc = false 26 | result.vars = newTable[string, TclValue]() 27 | result.cmds = newTable[string, TclCmd]() 28 | 29 | proc copy*(ctx: TclContext, inside: bool = false): TclContext = result.deepCopy(ctx); result.insideProc = inside 30 | 31 | proc eval*(ctx: TclContext, e: Expr): TclValue # the most primitive kind of eval 32 | proc eval*(ctx: TclContext, s: string): TclValue = 33 | for expression in tclParse(s): 34 | result = ctx.eval(expression) 35 | 36 | proc eval(ctx: TclContext, v: TclValue): TclValue = ctx.eval(v.data) 37 | proc eval(ctx: TclContext, e: Expr): TclValue = 38 | result = newValue() 39 | case e.eType 40 | of Command: 41 | let cmdName = ctx.eval(e.cBody[0]).data 42 | try: 43 | let cmdProc = ctx.cmds[cmdName] 44 | result.data = cmdProc(ctx, e.cBody[1 .. ^1].map(proc (x: Expr): TclValue = ctx.eval(x))).data 45 | except KeyError: 46 | result.data = "unknown command: " & cmdName 47 | of Variable: 48 | if ctx.vars.hasKey(e.varName): 49 | result.data = ctx.vars[e.varName].data 50 | of Substitution: 51 | result.data = ctx.eval(e.subBody).data 52 | of Word: 53 | for ex in e.wBody: 54 | result.data &= ctx.eval(ex).data 55 | of Quotation: result.data = e.qBody 56 | of StringChunk: result.data = e.str 57 | else: discard 58 | 59 | proc setVars(ctx: TclContext, vars: seq[(string, TclValue)]) = 60 | for v in vars: ctx.vars[v[0]] = v[1] 61 | 62 | let 63 | Null* = newValue() 64 | True* = newValue("true") 65 | let baseContext = newContext() 66 | include "commands" 67 | baseContext.cmds = registeredCommands 68 | proc getContext*(): TclContext = baseContext.copy() 69 | proc eval*(s: string): TclValue = getContext().eval(s) 70 | -------------------------------------------------------------------------------- /src/main.nim: -------------------------------------------------------------------------------- 1 | import parse, eval 2 | 3 | const testString* = """ 4 | set a "str1" 5 | set b [concat -- $a"-str2" longer_str_3] 6 | $a$b 13 # a comment, will be ignored 7 | proc f {a} { 8 | echo $a 9 | } 10 | set x { 11 | echo "Evaling x!" 12 | concat [concat a b] {c} "d" 13 | } 14 | if [cmp [eval $x] abcd] { 15 | echo "Good." 16 | } { 17 | echo "Bad." 18 | } 19 | if [cmp 99 99] {echo "99 does equal 99"} 20 | if true { 21 | echo "It was true!" 22 | } 23 | if "" {} {echo "It was false!"} 24 | f "Calling a command" 25 | if [cmp $a "str1"] {echo "a value: $a"} 26 | set b "a" 27 | while {not [cmp $b "aaaaaaaaaaa"]} { 28 | echo Iterating: $b 29 | set b [concat $b "a"] 30 | } 31 | proc ff {} { 32 | echo "Inside `ff`" 33 | return "asd" 34 | echo "Won't get here!" 35 | } 36 | ff 37 | """ 38 | 39 | let ctx = getContext() 40 | for cmd in tclParse(testString): 41 | let val = ctx.eval(cmd) 42 | if val != Null: echo $(val) 43 | 44 | while true: 45 | stdout.write("> ") 46 | try: 47 | let res = $(ctx.eval(stdin.readLine())) 48 | if res != "": echo res 49 | except IOError: 50 | echo "Exiting." 51 | break 52 | -------------------------------------------------------------------------------- /src/parse.nim: -------------------------------------------------------------------------------- 1 | import streams, strutils, sequtils 2 | import streamutils 3 | 4 | const 5 | commandSeparators = {';', '\l', '\r'} 6 | specialChars = {'{' , '}' , ';' , '\l' , '\r', '[', ']', '"', ' ', '$'} 7 | quotedSpecialChars = {'$', '[', '"'} 8 | 9 | type 10 | ExprType* = enum 11 | Command, Substitution, Word, StringChunk, Quotation, Variable, Comment, Empty 12 | 13 | Expr* = ref ExprObj not nil 14 | ExprObj {. acyclic .} = object 15 | case eType*: ExprType 16 | of Empty: discard 17 | of Word: wBody*: seq[Expr] 18 | of Command: cBody*: seq[Expr] 19 | of StringChunk: str*: string 20 | of Quotation: qBody*: string 21 | of Substitution: subBody*: string 22 | of Comment: content*: string 23 | of Variable: varName*: string 24 | 25 | proc `$`*(e: Expr) : string = 26 | case e.eType 27 | of Empty: return "EmptyToken" 28 | of StringChunk: return e.str 29 | of Quotation: return "{Q:" & e.qBody & "}" 30 | of Substitution: return "[S:" & e.subBody & "]" 31 | of Word: return "Str: '" & e.wBody.map(`$`).join("") & "'" 32 | of Comment: return "" 33 | of Variable: return "Var: " & e.varName 34 | of Command: return "" 35 | 36 | type Parser* = object 37 | insideQuotes: bool 38 | stream: StringStream 39 | 40 | proc init*(p: var Parser, source: string) = p.stream = newStringStream(source) 41 | proc initParser*(source: string): Parser = result.stream = newStringStream(source) 42 | 43 | # Utilities and shortcuts for convenience and readability 44 | proc peekChar(p: var Parser): char = p.stream.peekChar() 45 | proc readChar(p: var Parser): char = p.stream.readChar() 46 | proc discardChar(p: var Parser) = discard p.stream.readChar() 47 | proc readUntil(p: var Parser, m: set[char], l: bool = false): string = p.stream.readUntil(m, l) 48 | proc readWhile(p: var Parser, m: set[char]): string = p.stream.readWhile(m) 49 | proc readBalanced(p: var Parser, s,e: char): string = p.stream.readBalanced(s,e) 50 | 51 | proc skipComment(p: var Parser) : var Parser {. discardable .} = 52 | if p.peekChar() == '#': discard p.readUntil({'\l', '\r'}, true) 53 | return p 54 | 55 | proc skipWhitespace(p: var Parser) : var Parser {. discardable .} = 56 | while p.peekChar() in {' '}: p.discardChar() 57 | return p 58 | 59 | proc skipAndPeek(p: var Parser): char = 60 | p.skipWhitespace().skipComment().peekChar() 61 | 62 | # Parsing logic 63 | proc parseAtom(p: var Parser): Expr; 64 | 65 | proc parseStringChunk(p: var Parser): Expr = 66 | result = Expr(eType: StringChunk) 67 | case p.insideQuotes 68 | of true: result.str = p.readUntil(quotedSpecialChars, true) 69 | of false: result.str = p.readUntil(specialChars, true) 70 | 71 | proc parseQuotedString(p: var Parser): Expr = 72 | p.discardChar() 73 | p.insideQuotes = true 74 | defer: p.insideQuotes = false 75 | var res: seq[Expr] = @[] 76 | while p.peekChar() != '"': 77 | if p.peekChar() == StopChar: 78 | raise newException(ValueError, "Unterminated string") 79 | res.add(p.parseAtom()) 80 | p.discardChar() 81 | return Expr(eType: Word, wBody: res) 82 | 83 | proc parseQuotation(p: var Parser): Expr = 84 | Expr(eType: Quotation, qBody: p.readBalanced('{', '}')) 85 | 86 | proc parseSubstitution(p: var Parser): Expr = 87 | Expr(eType: Substitution, subBody: p.readBalanced('[', ']')) 88 | 89 | proc parseVariable(p: var Parser) : Expr = 90 | p.discardChar() # drop '$' 91 | let name = p.readUntil(specialChars, true) 92 | Expr(eType: Variable, varName: name) 93 | 94 | proc parseAtom(p: var Parser): Expr = 95 | case p.peekChar() 96 | of '"': p.parseQuotedString() 97 | of '[': p.parseSubstitution() 98 | of '{': p.parseQuotation() 99 | of '$': p.parseVariable() 100 | else: p.parseStringChunk() 101 | 102 | proc parseWord(p: var Parser): Expr = 103 | p.skipWhitespace() 104 | var chunks: seq[Expr] = @[] 105 | while p.peekChar() notin {' ', '\l', '\r', StopChar}: 106 | chunks.add(p.parseAtom()) 107 | return Expr(eType: Word, wBody: chunks) 108 | 109 | proc parseCommand(p: var Parser) : Expr = 110 | var args = @[p.parseWord()] 111 | if len(args[0].wBody) == 0: return Expr(eType: Empty) 112 | while p.skipAndPeek() notin ({StopChar} + commandSeparators): 113 | args.add(p.parseWord()) 114 | Expr(eType: Command, cBody: args) 115 | 116 | proc parse(p: var Parser): seq[Expr] = 117 | result = @[] 118 | while p.skipWhitespace().skipComment().peekChar() != StopChar: 119 | result.add(p.parseCommand()) 120 | discard p.stream.readChar() # either commandSeparator or end of input 121 | 122 | proc tclParse*(input: string): seq[Expr] = 123 | var p = initParser(input) 124 | p.parse() 125 | -------------------------------------------------------------------------------- /src/streamutils.nim: -------------------------------------------------------------------------------- 1 | import streams 2 | import strutils 3 | 4 | const StopChar* = '\0' 5 | 6 | proc readWhile*(stream: var StringStream, match: set[char]) : string = 7 | result = "" 8 | while stream.peekChar() != StopChar: 9 | let c = stream.readChar() 10 | if c notin match: break 11 | result.add(c) 12 | 13 | proc readUntil*(stream: var StringStream, match: set[char], 14 | leaveLast: bool = false): string = 15 | result = "" 16 | while stream.peekChar() != StopChar and stream.peekChar() notin match: 17 | result.add(stream.readChar()) 18 | if not leaveLast: 19 | discard stream.readChar() 20 | 21 | proc readBalanced*(stream: var StringStream, startChar, endChar: char) : string = 22 | assert stream.peekChar() == startChar 23 | discard stream.readChar() 24 | var lvl = 1 25 | result = newStringOfCap(200) # resizing should be less frequent thanks to this 26 | while lvl > 0: 27 | let c = stream.readChar() 28 | if c == StopChar: 29 | let msg = "Unbalanced %s, %s" % [$startChar, $endChar] 30 | raise newException(ValueError, msg) 31 | lvl += (if c == startChar: 1 elif c == endChar: -1 else: 0) 32 | if lvl != 0: result.add(c) 33 | --------------------------------------------------------------------------------