-
Notifications
You must be signed in to change notification settings - Fork 4
/
tnim.nim
619 lines (560 loc) · 18.8 KB
/
tnim.nim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
## **TNim** (TinyNim) is a quasi-interactive REPL
##
## It is a stop-gap replacement for the
## functionality that was in old versions of Nim, and
## is now
## ``nim secret``
## (note: nim secret uses the VM in Nim, so it only allows the import of a subset of modules)
##
## It compiles and runs code similar to the
## way you would do this, and does this using a shell
## command in the background (
## ``nim c -r --verbosity:0 --hints:off <file>`` )
##
## *Warning: this is SLOW!!. It is a quick and dirty
## interactive tool, not a sleek and shiny speed demon.*
##
## If you need to work with blocks of code as part of some project, then TNim can be primed with this code
## prior to being run (rather than having to paste chunks of code into TNim). Add this code to
## the `SavedFileName<#SavedFileName>`_ (tnim_dat.dat), then run TNim.
##
## If the buffers are not cleared (``\qc`` or ``\c``), then the code will remain in
## the `SavedFileName<#SavedFileName>`_, and
## will be available next time TNim is run.
##
## Commands:
## ---------
## .. code-block:: Nim
##
## \?, \h, \help this information.
## \l, \list list the previous code history (w/ line nr's)
## \ln, \listnn list with No (line) Numbers (raw code listing).
## \c, \clear clear the current history buffer.
## \d, \delete [f [t]] delete lines(s) [f [optionally to t]].
## delete last line if none specified.
## \e, \eval force the eval (compile/run) of the code buffer.
## \ec, \edconfig <editor> define the path/name to an external editor
## (if not defined, uses notepad (win) or vi)
## \ed, \edit edit code in the code buffer.
## (then reloads the code buffer, lists the code,
## and evals (compile/run) the code)
## \r, \read <filename> read code from <filename> and run.
## Saved history is read on startup.
## data from file is auto evaluated after reading.
## \s, \set [<option=value>] set {maxBlocks,indent}
## \v, \version display the name and version.
## \w, \write [<filename>] write code history [to <filename>].
## \w by itself overwrites saved history (tnim_dat.dat).
## \c followed by \w clears saved history.
## \q, \quit quit, saving code history to tnim_dat.dat file.
## \qc, \quitclear quit, clearing code history in tnim_dat.dat file.
##
## Vars and Consts
## -----
## The Vars and Consts Sections is included to provide clues about the TNim internal settings.
##
import strutils, tables, os, osproc, rdstdin
const
TnimName* = "TNim"
TnimVersion* = 2.06
TnimStart* = "nim> " ## the TNim prompt
TnimContinue* = ".... " # add "..".repeat(n) before this
SavedFileName* = "tnim_dat.dat" ## this file will hold the code you have typed (until cleared), or you can add code
## to this before file prior to running TNim
# run-time Configurable but effectively const Config variables
var
maxBlocks* = 100_000 ## code blocks, or lines of code not in a code block
indentSize* = 2
editorPath* = "" ## set this via \ec command
type
CodeBlock = object ## blocks of code (a block is the outer scope level)
lines: seq[string]
compiles: bool
firstOutput: string
lastOutput: string
CodeBlocks = seq[CodeBlock]
#PCodeBlocks = ptr CodeBlocks
EvalOutput = object
msg: seq[string]
output: seq[string]
EvalOutputs = seq[EvalOutput]
#PEvalOutputs = ptr EvalOutputs
var
code: CodeBlocks = @[] # a code block is multi lines for if, proc, block, etc
evalResults: EvalOutputs = @[]
blockNr: int = 0 # index of next block to add, not current blockNr
currIndent = 0 # starts at 0 for no indent at top scope level
inputCmds = initTable[string, proc(w: seq[string])]()
getOut = false # flag to trigger exit of REPL
#currDir = getAppDir()
doEval = false # force re-evaluation of the code buffer
# ---------------- forward declarations -----------
proc tnimClear(w: seq[string])
proc tnimDelete(w: seq[string])
proc tnimEval(w: seq[string])
proc tnimEdit(w: seq[string])
proc tnimEdConfig(w: seq[string])
proc tnimHelp(w: seq[string])
proc tnimList(w: seq[string])
proc tnimNrList(w: seq[string])
proc tnimQuit(w: seq[string])
proc tnimQuitClear(w: seq[string])
proc tnimRead(w: seq[string])
proc tnimSet(w: seq[string])
proc tnimWrite(w: seq[string])
proc tnimVersion(w: seq[string])
# ---------------- general stuff ------------------
proc doInit() =
inputCmds[r"\?" ] = tnimHelp
inputCmds[r"\h" ] = tnimHelp
inputCmds[r"\help" ] = tnimHelp
inputCmds[r"\l" ] = tnimNrList
inputCmds[r"\list" ] = tnimNrList
inputCmds[r"\ln" ] = tnimList
inputCmds[r"\listnn" ] = tnimList
inputCmds[r"\c" ] = tnimClear
inputCmds[r"\clear" ] = tnimClear
inputCmds[r"\d" ] = tnimDelete
inputCmds[r"\delete" ] = tnimDelete
inputCmds[r"\e" ] = tnimEval
inputCmds[r"\eval" ] = tnimEval
inputCmds[r"\ed" ] = tnimEdit
inputCmds[r"\edit" ] = tnimEdit
inputCmds[r"\ec" ] = tnimEdConfig
inputCmds[r"\edconfig" ] = tnimEdConfig
inputCmds[r"\v" ] = tnimVersion
inputCmds[r"\version" ] = tnimVersion
inputCmds[r"\w" ] = tnimWrite
inputCmds[r"\write" ] = tnimWrite
inputCmds[r"\r" ] = tnimRead
inputCmds[r"\read" ] = tnimRead
inputCmds[r"\s" ] = tnimSet
inputCmds[r"\set" ] = tnimSet
inputCmds[r"\q" ] = tnimQuit
inputCmds[r"\quit" ] = tnimQuit
inputCmds[r"\qc" ] = tnimQuitClear
inputCmds[r"\quitclear"] = tnimQuitClear
proc errMsg(s:string) =
writeLine(stderr, TnimStart & "Error: " & s)
#proc add(cb: var CodeBlocks, lines: seq[string]) =
# var newCB: CodeBlock
# newCB.lines = lines
# newCB.compiles = false
# cb.add(newCB)
proc add(cb: var CodeBlocks, line: string) =
var newCB: CodeBlock
newCB.lines = @[line]
newCB.compiles = false
cb.add(newCB)
inc(blockNr)
proc getCmdLineOpts() =
## get any configuration and running options
discard
proc words(s: string): seq[string] {.inline.} =
## strip leading/trailing space and split into words
## returning a seq of words (string)
result = s.strip(leading=true, trailing=true).split()
proc isDigits(s: string): bool =
## return true if all chars are Digits
result = (s.len > 0)
for c in s:
result = result and (c in Digits)
proc getIndent(s: string): int =
## number of indent spaces at start of line
## base on the setting of indentSize (default=2)
## so getIndent is 1 for two spaces when indentSize=2
var spaceCnt = 0
result = 0
for i,c in pairs(s):
if c == ' ': inc(spaceCnt)
else: break
let x = spaceCnt.`div`(indentSize)
# allow for end of indent shown by change in indent
if x < currIndent: currIndent = x
# check event nr of spaces
#if spaceCnt != (x * indentSize):
# if spaceCnt < (x * indentSize):
# dec(indentSize)
# result = x
# else:
# errMsg("indentation is incorrect")
# result = -1
#else:
result = x
proc getInt(s: string): int =
var
i = 0
result = 0
while i < s.len and s[i] notIn Digits:
inc(i)
while i < s.len and s[i] in Digits:
result = 10 * result + ord(s[i]) - ord('0')
inc(i)
proc getCodeLine(ln: int): string =
# linenumber ln is a 1.. based index, not a seq[] index
var
actLineNr = 0
cIndx = 0
result = ""
while cIndx < code.len:
for j in 0..<code[cIndx].lines.len:
if actLineNr == ln-1:
result = code[cIndx].lines[j]
return
inc(actLineNr)
code[cIndx].compiles = true
inc(cIndx)
proc filterCompileLines[T](s: T): string =
# return the error message of the fail
result = ""
var
tStr = ""
tStr2 = ""
codeLine = ""
lineNr = 0
posNr = 0
for line in s.splitLines():
if line.contains(") Error"):
tStr = line[SavedFileName.len..<line.len]
tStr2 = tStr[find(tStr,"Error")..<tStr.len]
lineNr = tStr.getInt()
posNr = tStr[($lineNr).len+2..<tStr.len].getInt()
codeLine = getCodeLine(lineNr) & "\n" & " ".repeat(posNr-1) & "^\n"
result = codeLine & tStr2 & "\n"
elif tStr != "":
result &= line
proc filterRunLines[T](s: T): string =
# return the results of running a successful compile
result = ""
var
linkStr = ""
foundStart = false
foundLink = false
for line in s.splitLines():
if foundLink:
linkStr &= linkStr
if foundStart:
result &= line
if line.contains("[SuccessX]"):
foundStart = true
if line.contains("[Link]"):
foundLink = true
if result == "":
if linkStr != "": result = linkStr
else: result = s.strip()
proc deleteLastLine() # fwd decl
proc runEval(): tuple[errCode: int, resStr: string] =
# (silently) compile the code using Nim, and return any errors
# for displaying to stdout
# Remove the offending last line of source code.
# NB: If pasting in a multi lines of code with an error in it,
# the last line will be deleted but may not be the offending
# line of code :-)
var
resStr = ""
tnimWrite(@[SavedFileName])
let (outp, exitCode) = execCmdEx("nim c -r --verbosity:0 --hints:off " & SavedFileName)
# OOPS - Compile or Run failed!!
if exitCode != 0:
resStr = filterCompileLines(outp)
# remove offending code line (stdin history exists to see what last line was)
deleteLastLine()
if resStr == "":
resStr = filterRunLines(outp)
return (exitCode, resStr)
# ---------------- tnimXXXX command jump table procs --------------------
proc tnimHelp(w: seq[string]) =
echo """Commands (only one command per line):
\?, \h, \help this information.
\l, \list list the previous code history (w/ line nr's)
\ln, \listnn list with No (line) Numbers (raw code listing).
\c, \clear clear the current history buffer.
\d, \delete [f [t]] delete lines(s) [f [optionally to t]].
delete last line if none specified.
\e, \eval force the eval (compile/run) of the code buffer.
\ec, \edconfig <editor> define the path/name to an external editor
(if not defined, uses notepad (win) or vi)
\ed, \edit edit code in the code buffer.
(then reloads the code buffer, lists the code,
and evals (compile/run) the code)
\r, \read <filename> read code from <filename> and run.
Saved history is read on startup.
data from file is auto evaluated after reading.
\s, \set [<option=value>] set {maxBlocks,indent}
\v, \version display the name and version.
\w, \write [<filename>] write code history [to <filename>].
\w by itself overwrites saved history (tnim_dat.dat).
\c followed by \w clears saved history.
\q, \quit quit, saving code history to tnim_dat.dat file.
\qc, \quitclear quit, clearing code history in tnim_dat.dat file.
"""
proc deleteCodeLine(lineNrFrom: int, lineNrTo: int) =
var
i = 0
lnTo = if lineNrTo == -1: lineNrFrom else: lineNrTo
lnFrom = lineNrFrom
for cBlock in mitems(code):
for j in 0..<cBlock.lines.len:
if i >= lnFrom and i <= lnTo:
cBlock.lines[j] = ""
inc(i)
dec(i)
if lineNrFrom == -1: lnFrom = i # delete last line
for cb in countDown(code.len-1, 0):
for j in countDown(code[cb].lines.len-1, 0):
if i >= lineNrFrom and i <= lnTo and code[cb].lines[j] == "":
code[cb].lines.delete(j)
dec(i)
proc lastLineNr(): int =
var i = 0
if code.len == 0: return -1
for cBlock in items(code):
for s in cBlock.lines:
inc(i)
return i - 1
proc deleteLastLine() =
# if an error compiling, then this is called
# to delete last line of code
let lln = lastLineNr()
deleteCodeLine(lln, lln)
proc listCode(f: File, withln = false) =
var i = 0
if code.len == 0: return
for cBlock in items(code):
for s in cBlock.lines:
if i > 0: f.writeLine("")
if withln:
f.write(align($i,5) & ": " & s)
else:
f.write(s)
inc(i)
# --------- tnimXXXXX() procedures -----------------
proc tnimList(w: seq[string]) =
listCode(stdout)
doEval = false
echo ""
proc tnimNrList(w: seq[string]) =
listCode(stdout, true)
doEval = false
echo ""
proc tnimClear(w: seq[string]) =
blockNr = 0
code.setLen(0)
evalResults.setLen(0)
doEval = false
proc tnimDelete(w: seq[string]) =
var
f, t = -1
if w.len >= 3: t = w[2].parseInt
if w.len >= 2: f = w[1].parseInt
if t == -1 and f != -1: t = f
deleteCodeLine(f, t)
tnimNrList(w)
doEval = false
proc tnimEval(w: seq[string]) =
doEval = true
proc tnimWrite(w: seq[string]) =
var
fn = if w.len > 1: w[1] else: SavedFileName
f: File = open(fn, mode=fmWrite)
f.listCode()
f.close()
doEval = false
proc tnimRead(w: seq[string]) =
var
fn = if w.len > 1: w[1] else: SavedFileName
f: File
hasCode = false
if fileExists(fn):
f = open(fn)
var lines = f.readAll().splitLines()
f.close()
for line in lines:
if not hasCode:
if line.len == 0:
continue
else: hasCode = true
code.add(line)
tnimEval(w)
elif fn != SavedFileName:
errMsg("Unable to find file: " & fn)
proc tnimSet(w: seq[string]) =
proc showItems() =
echo "maxBlocks: ",maxBlocks
echo "indentSize: ",indentSize
if w.len <= 1: showItems()
else:
var wrds = w[1..<w.len].join(" ").split('=')
for i in 0..<wrds.len:
wrds[i] = wrds[i].strip()
if wrds.len == 1: showItems()
elif wrds[1].isDigits():
case wrds[0]
of "maxBlocks": maxBlocks = wrds[1].parseInt
of "indentSize": indentSize = wrds[1].parseInt
else:
errMsg("variable " & wrds[0] & " is unknown.")
doEval = false
proc tnimQuit(w: seq[string]) =
getOut = true
doEval = false
proc tnimQuitClear(w: seq[string]) =
tnimClear(w)
tnimQuit(w)
proc tnimVersion(w: seq[string]) =
writeLine(stdout, TnimName & " V" & $TnimVersion)
# -------------- EDIT ---------------------------
proc setEditorAsDefault(): bool =
var
cmd = ""
startStr = ""
# Before looking up the path, try the EDITOR environment variable.
if existsEnv("EDITOR"):
editorPath = getEnv("EDITOR")
return true
if defined(windows):
cmd = "where notepad.exe"
startStr = "c:\\"
else:
cmd = "which vi"
startStr = "/"
result = false
let outp = execProcess(cmd)
if outp.toLowerAscii.startsWith(startStr):
editorPath = outp.splitLines()[0]
result = editorPath.len > 0
proc checkOrSetEditor(): bool =
## Check for the existence of an editorPath, and if it doesn't exist, attempt
## to set from the default.
if editorPath == "":
result = setEditorAsDefault()
if not result:
echo "Please define an editor (\ec)"
else:
result = true
proc tnimEdConfig(w: seq[string]) =
if w.len > 1:
editorPath = w[1]
else:
discard checkOrSetEditor()
echo "Editor: ",editorPath
proc tnimEdit(w: seq[string]) =
## If an editorPath defined, use that editor
## Else
## clear the screen, display the code buffer
## and change the code
var
res = 0
if not checkOrSetEditor(): return
if not editorPath.fileExists:
echo "Error: " & editorPath & " not found"
editorPath = ""
else:
res = execCmd(editorPath & " tnim_dat.dat")
if res == 0:
tnimClear(@[])
tnimRead(@[SavedFileName])
tnimNrList(@[])
tnimEval(@[])
else:
echo "Editing failed: returned ",res
# -------------- EVAL ---------------------------
proc nimEval(inp: string): tuple[res: bool, resStr: string] =
# true if something to print
var
res = false
resStr = ""
iput = inp.words()
if iput.len == 0:
if currIndent > 0: currIndent = 0 # continue with eval
else: return (res, resStr)
else:
if inputCmds.hasKey(iput[0]):
# eval user commands
var
cmdToRun = inputCmds[iput[0]] # get command to run
cmdToRun(iput) # tnimEval can set the doEval flag
if not doEval:
return (res, resStr)
else:
doEval = false
let (_, rs) = runEval()
return (true, rs)
if blockNr == maxBlocks:
errMsg("History buffer is full. Write(\\w) and/or Clear (\\c) the history")
return (res, resStr)
# handle a line of code, checking if indent is required
let ident = getIndent(inp)
#if ident == currIndent: # ident should match currIndent
if ident >= 0 and code.len-1 == blockNr:
code[blockNr].lines.add(inp)
else:
code.add(inp)
let lastWrd = iput[iput.len-1]
# if block identified by ':' then inc indent
if lastWrd[lastWrd.len-1] == ':':
inc(currIndent)
# multi-line statement or proc() definition
elif lastWrd[lastWrd.len-1] == ',': # don't eval, more to come
return (res, resStr)
# proc() definition on one line (comment on end of line not handled!)
elif iput[0] in ["proc", "iterator", "method", "converter"] and
lastWrd[lastWrd.len-1] == '=':
inc(currIndent)
# proc() definition on multi lines
elif iput[0] in ["template", "macro"]:
inc(currIndent)
# proc() definition on multi lines
elif ident > 0 and lastWrd[lastWrd.len-1] == '=':
inc(currIndent)
elif iput[0][0..4] == "block":
inc(currIndent)
#elif ident != -1:
# errMsg("indentation is incorrect")
doEval = false # reset the flag
if currIndent > 0: return (res, resStr)
#
# eval code
#
let (_, rStr) = runEval()
return (true, rStr)
# -------------- PRINT ---------------------------
proc print(s: string) {.inline.} =
writeLine(stdout, s)
proc startMsg(): string {.inline.} =
## when indented, print the "..." else the "nim> " text
result = (if currIndent == 0: TnimStart else: TnimContinue)
# -------------- READ ---------------------------
proc readFromStdin(msg: string): string =
result = r"\qc"
try:
result = readLineFromStdin(msg)
except:
tnimQuitClear(@[r"\qc"])
# -------------- REPL ---------------------------
proc REPL() =
## main processing loop
var
inp = ""
while not getOut:
inp = readFromStdin(startMsg()) # R
let (res, resStr) = nimEval(inp) # E
if res: print(resStr) # P
proc main() =
doInit()
# get previous code
tnimRead(@[SavedFileName])
# get command line options
getCmdLineOpts()
# display version
tnimVersion(@[])
# display help commands
tnimHelp(@[])
print(startMsg())
# show current code buffer
tnimNrList(@[])
# and away it runs
REPL()
tnimWrite(@[SavedFileName])
when isMainModule:
main()