-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathprojectk.js
512 lines (472 loc) · 18.7 KB
/
projectk.js
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
//
// project-k Forth kernel
// Use the same kernel code for all applications.
// FigTaiwan H.C. Chen [email protected]
//
"uses strict";
// https://stackoverflow.com/questions/20094139/is-there-a-way-to-stop-scripts-from-loading-twice-javascript
if(!jeForth){ function jeForth() {
var vm = this;
vm.major_version = 3; // major version, projectk.js kernel version.
var ip=0;
var stack = [] ;
var rstack = [];
var vocs = [];
var words = [];
var current = "forth";
var context = "forth";
var order = [context];
var wordhash = {};
var dictionary=[]; dictionary[0]=0;
var here=1;
var tib="";
var ntib=0;
var RET=null; // The 'ret' instruction code. It marks the end of a colon word.
var EXIT=""; // The 'exit' instruction code.
var compiling=false;
var stop = false; // Stop the outer loop
var newname = ""; // new word's name
var newxt = function(){}; // new word's function()
var newhelp = "";
// Call out vm.type()
// Kernel has no idea of typing, it does not need to 'type'. This is for
// the convenience of programs in code ... end-code that always need to
// print something.
function type(s) {
// defined in project-k kernel projectk.js
if(vm.type) vm.type(s);
}
// Reset the forth VM
function reset(){
// defined in project-k kernel projectk.js
rstack = [];
compiling=false;
ip=0; // forth VM instruction pointer
stop = true;
ntib = tib.length; // don't clear tib, a clue for debug.
// stack = []; I guess it's a clue for debug
}
// panic() calls out to vm.panic()
// The panic() function gets only message and severity level.
// Kernel has no idea how to handle these information so it checks if vm.panic() exists
// and pass the {msg,serious}, or even more info, over that's all. That's why vm.panic() has to
// receive a hash structure, because it must be.
function panic(msg,serious) {
// defined in project-k kernel projectk.js
var state = {
msg:msg, serious:serious
// , compiling:compiling,
// stack:stack.slice(0), rstack:rstack.slice(0),
// ip:ip, tib:tib, ntib:ntib, stop:stop
};
if(vm.panic) vm.panic(state);
}
// Forth words are instances of Word() constructor.
function Word(a) {
this.name = a.shift(); // name and xt are mandatory
this.xt = a.shift();
var statement;
while(statement=a.shift()) { // extra arguments are statement strings
eval(statement);
}
}
Word.prototype.toString = function(){return this.name + " " + this.help}; // every word introduces itself
// Support Vocabulary
function last(){ // returns the last defined word.
return words[current][words[current].length-1];
}
function current_word_list(){ // returns the word-list where new defined words are going to
return words[current];
}
function context_word_list(){ // returns the word-list that is searched first.
return words[context];
}
// Get string from recent ntib down to, but not including, the next delimiter.
// Return {str:"string", flag:boolean}
// If delimiter is not found then return the entire remaining TIB, multi-lines, through result.str。
// result.flag indicates delimiter found or not found.
// o If you want to read the entire line in TIB, use nexttoken('\n|\r').
// nexttoken() skip the next character which is usually white space in Forth source code,
// e.g. s", this is reasonable because it's Forth. While the leading white space(s)
// will be included if useing the lower level nextstring('\\s') instead of nexttoken().
// o If you need to know whether the delimiter is found, use nextstring()。
// o result.str is "" if TIB has nothing left.
// o The ending delimiter is remained.
// o The delimiter is a regular expression.
function nextstring(deli){
var result={}, index;
index = (tib.substr(ntib)).search(deli); // search for delimiter in tib from ntib
if (index!=-1) { // delimiter found
result.str = tib.substr(ntib,index); // found, index is the length
result.flag = true;
ntib += index; // Now ntib points at the delimiter.
} else { // delimiter not found.
result.str = tib.substr(ntib); // get the tib from ntib to EOL
result.flag = false;
ntib = tib.length; // skip to EOL
}
return result;
}
// Get next token which is found after the recent ntib of TIB.
// If delimiter is RegEx white-space ('\\s') or absent then skip all leading white spaces first.
// Usual case, skip the next character which should be a white space for Forth.
// But if delimiter is CRLF, which is to read the entire line, for blank lines the ending CRLF won't be skipped.
// o Return "" if TIB has nothing left.
// o Return the remaining TIB if delimiter is not found.
// o The ending delimiter is remained.
// o The delimiter is a regular expression.
function nexttoken(deli){
if (arguments.length==0) deli='\\s'; // white space
switch(deli){
case '\\s': skipWhiteSpaces(); break; // skip all leading white spaces
case '\\n': case '\n': if (tib[ntib]!='\n') ntib += 1; break;
case '\\r': case '\r': if (tib[ntib]!='\r') ntib += 1; break;
case '\\n|\\r': case '\n|\r': case '\\r|\\n': case '\r|\n':
if (tib[ntib]!='\n' && tib[ntib]!='\r') ntib += 1; break;
default: ntib += 1; // skip next character
}
var token = nextstring(deli).str;
return token;
function skipWhiteSpaces(){ // skip all white spaces at tib[ntib]
var index = (tib.substr(ntib)).search('\\S'); // Skip leading whitespaces. index points to next none-whitespace.
if (index == -1) { // \S not found, entire line are all white spaces or totally empty
ntib = tib.length;
}else{
ntib += index ; // skip leading whitespaces
}
}
}
// tick() is same thing as forth word '。
// Let words[voc][0]=0 also means tick() return 0 indicates "not found".
// Return the word obj of the given name or 0 if the word is not found.
// May be redefined for selftest to detect private words referenced by name.
// vm.tick keeps the original version.
function tick(name) {
// defined in project-k projectk.js
return wordhash[name] || 0; // 0 means 'not found'
}
// Return a boolean.
// Is the new word reDef depends on only the words[current] word-list, not all
// word-lists, nor the word-hash table. Can't use tick() because tick() searches
// the word-hash that includes not only the words[current] word-list.
function isReDef(name){
var result = false;
var wordlist = current_word_list();
for (var i in wordlist)
if (wordlist[i].name == name) {
result = true;
break;
}
return result;
}
// comma(x) compiles anything into dictionary[here]. x can be number, string,
// function, object, array .. etc。
// To compile a word, comma(tick('word-name'))
function comma(x) {
dictionary[here++] = x;
dictionary[here] = RET; // dummy
// [here] will be overwritten, we do this dummy because
// RET is the ending mark for 'see' to know where to stop.
}
// Discussions:
// 'address' or 'ip' are index of dictionary[] array. dictionary[] is the memory of the
// Forth virtual machine.
// execute() executes a function, a word "name", and a word Object.
// inner(entry) jumps into the entry address. The TOS of return stack can be 0, in that
// case the control will return back to JavaScript host, or the return address.
// inner() used in outer(), and colon word's xt() while execute() is used everywhere.
// We have 3 ways to call forth words from JavaScript: 1. execute('word'),
// 2. dictate('word word word'), and 3. inner(cfa).
// dictate() cycles are stand alone tasks. We can suspend an in-completed dictate() and we
// can also run another dictate() within a dictate().
// The ultimate inner loop is like this: while(w){ip++; w.xt(); w=dictionary[ip]};
// Boolean(w) == false is the break condition. So I choose null to be the RET instruction
// and the empty string "" to be the EXIT instruction. Choices are null, "", false, NaN,
// undefined, and 0. Total 6 of them. 0 has another meaning explained below.
// To suspend the Forth virtual machine means to stop inner loop but not pop the
// return stack, resume is possible because return stack remained. We need an instruction
// to do this and it's 0. dictionary[0] and words[<vid>][0] are always 0 thus ip=w=0
// indicates that case. Calling inner loop from outer loop needs to push(0) first so
// as to balance the return stack also letting the 0 instruction to stop popping the
// return stack because there's no more return address, it's outer interpreter remember?
// -------------------- ###### The inner loop ###### -------------------------------------
// Translate all possible entry or input to the suitable word type.
function phaseA (entry) {
var w = 0;
switch(typeof(entry)){
case "string": // "string" is word name
w = vm.tick(entry.replace(/(^( |\t)*)|(( |\t)*$)/mg,'')); // remove leading and tailing white spaces
break;
case "function": case "object": // object is a word
w = entry;
break;
case "number":
// number could be dictionary entry or 0.
// could be does> branch entry or popped from return stack by RET or EXIT instruction.
ip = entry;
w = dictionary[ip];
break;
default :
panic("Error! execute() doesn't know how to handle this thing : "+entry+" ("+mytypeof(entry)+")\n","err");
}
return w;
}
// Execute the given w by the correct method
function phaseB (w) {
switch(typeof(w)){
case "number":
// Usually a number is the entry of does>. Can't use inner() to call it
// The below push-jump mimics the call instruction of a CPU.
rstack.push(ip); // Forth ip is the "next" instruction to be executed.
ip = w; // jump ,
break;
case "function":
w(); // if w(w) then w will be the arguments.callee in the function so is not necessary
break;
case "object": // Word object
try { // take care of JavaScript errors to avoid being kicked out very easily
w.xt(w); // w become _me in the function, the word itself.
} catch(err) {
panic('JavaScript error on word "'+w.name+'" : '+err.message+'\n',"error");
}
break;
default :
panic("Error! don't know how to execute : "+w+" ("+mytypeof(w)+")\n","error");
}
}
// execute("unknown") == do nothing, this is beneficial when executing a future word
// May be redefined for selftest to detect private words called by name.
// vm.execute keeps the original version.
function execute(entry) {
// defined in proejct-k projectk.js
var w;
if (w = phaseA(entry)){
if(typeof(w)=="number")
panic("Error! please use inner("+w+") instead of execute("+w+").\n","severe");
else phaseB(w);
}
return vm; // function cascading
}
function inner (entry, resuming) {
// defined in project-k kernel projectk.js
var w = phaseA(entry);
do{
while(w) {
ip++; // Forth general rule. IP points to the *next* word.
phaseB(w);
w = dictionary[ip];
}
if(w===0) break; // w==0 is suspend, break inner loop but reserve rstack.
else ip = rstack.pop(); // w is either ret(NULL) or exit(""), return to caller, or 0 when resuming through outer(entry)
if(resuming) w = dictionary[ip]; // Higher level of inner()'s have been terminated by suspend, do their job.
} while(ip && resuming); // Resuming inner loop. ip==0 means resuming has done。
}
// ### End of the inner loop ###
// -------------------------- the outer loop ----------------------------------------------------
// forth outer loop,
// If entry is given then resume from the entry point by executing
// the remaining colon thread down until ip reaches 0. That's resume.
// Then proceed with the tib/ntib string.
//
function outer(entry) {
if (entry) inner(entry, true); // resume from the breakpoint
while(!stop) {
var token=nexttoken();
if (token==="") break; // TIB done, loop exit.
outerExecute(token);
}
// Handle one token.
function outerExecute(token){
var w = vm.tick(token); // not found is 0. w is an Word object.
if (w) {
if(!compiling){ // interpret state or immediate words
if (w.compileonly) {
panic(
"Error! "+token+" is compile-only.\n",
tib.length-ntib>100 // error or warning? depends
);
return;
}
execute(w);
} else { // compile state
if (w.immediate) {
execute(w); // inner(w);
} else {
if (w.interpretonly) {
panic(
"Error! "+token+" is interpret-only.\n",
tib.length-ntib>100 // error or warning? depends
);
return;
}
comma(w); // compile w into dictionary. w is a Word() object
}
}
} else if (isNaN(token)) {
// parseInt('123abc') is 123, very wrong! Need to check in prior by isNaN().
panic(
"Error! "+token+" unknown.\n",
tib.length-ntib>100 // error or warning? depends
);
return;
} else {
if(token.substr(0,2).toLowerCase()=="0x") var n = parseInt(token);
else var n = parseFloat(token);
push(n);
if (compiling) execute("literal");
}
}
}
// ### End of the outer loop ###
// code ( -- ) Start to compose a code word. docode() is its run-time.
// "( ... )" and " \ ..." on first line will be brought into this.help.
// projectk.js kernel has only two words, 'code' and 'end-code', jeforth.f
// will be read from a file that will be a big TIB actually. So we don't
// need to consider about how to get user input from keyboard! Getting
// keyboard input is difficult to me on an event-driven or a non-blocking
// environment like Node-webkit.
function docode() {
// All future code words can see local variables in here, so don't use
// any local variable here. They can *see* variables & functions out side
// this function too, that's normal.
compiling = "code"; // it's true and a clue of compiling a code word.
newname = nexttoken();
if(isReDef(newname)) panic("reDef "+newname+"\n"); // don't use tick(newname), it's wrong.
push(nextstring("end-code"));
if(tos().flag){
// _me is the code word object itself.
eval(
'newxt=function(_me){ /* ' + newname + ' */\n' +
pop().str + '\n}' // the ending "\n}" allows // comment at the end
);
} else {
panic("Error! expecting 'end-code'.\n");
reset();
}
}
words[current] = [
0, // Letting current_word_list()[0] == 0 has many advantages. When tick('name')
// returns a 0, current_word_list()[0] is 0 too, indicates a not-found.
new Word([
"code",
docode,
"this.vid='forth'",
"this.wid=1",
"this.type='code'",
"this.help='( <name> -- ) Start composing a code word.'"
]),
new Word([
"end-code",
function(){
if(compiling!="code"){ panic("Error! 'end-code' to a none code word.\n"); return};
current_word_list().push(new Word([newname,newxt]));
last().vid = current;
last().wid = current_word_list().length-1;
last().type = 'code';
last().help = newhelp;
wordhash[last().name]=last();
compiling = false;
},
"this.vid='forth'",
"this.wid=2",
"this.type='code'",
"this.immediate=true",
"this.compileonly=true",
"this.help='( -- ) Wrap up the new code word.'"
])
];
// Use the best of JavaScript to find a word.
wordhash = {"code":current_word_list()[1], "end-code":current_word_list()[2]};
// -------------------- main() ----------------------------------------
// Recursively evaluate the input. The input can be multiple lines or
// an entire ~.f file yet it usually is the TIB.
function dictate(input) {
var tibwas=tib, ntibwas=ntib, ipwas=ip;
tib = input;
ntib = 0;
stop = false; // stop outer loop
outer();
tib = tibwas;
ntib = ntibwas;
ip = ipwas;
return vm; // function cascading
}
// -------------------- end of main() -----------------------------------------
// Top of Stack access easier. ( tos(2) tos(1) tos(void|0) -- ditto )
// tos(i,new) returns tos(i) and by the way change tos(i) to new value this is good
// for counting up or down in a loop.
function tos(index,value) {
switch (arguments.length) {
case 0 : return stack[stack.length-1];
case 1 : return stack[stack.length-1-index];
default :
var data = stack[stack.length-1-index]
stack[stack.length-1-index] = value;
return(data);
}
}
// Top of return Stack access easier. ( rtos(2) rtos(1) rtos(void|0) -- ditto )
// rtos(i,new) returns rtos(i) and by the way change rtos(i) to new value this is good
// for counting up or down in a loop.
function rtos(index,value) {
switch (arguments.length) {
case 0 : return rstack[rstack.length-1];
case 1 : return rstack[rstack.length-1-index];
default :
var data = rstack[rstack.length-1-index]
rstack[rstack.length-1-index] = value;
return(data);
}
}
// Stack access easier. e.g. pop(1) gets tos(1) and leaves ( tos(2) tos(1) tos(void|0) -- tos(2) tos(void|0) )
// push(formula(pop(i)),i-1) manipulate the tos(i) directly, usually when i is the index of a loop.
function pop(index) {
switch (arguments.length) {
case 0 : return stack.pop();
default : return stack.splice(stack.length-1-index, 1)[0];
}
}
// Stack access easier. e.g. push(data,1) inserts data to tos(1), ( tos2 tos1 tos -- tos2 tos1 data tos )
// push(formula(pop(i)),i-1) manipulate the tos(i) directly, usually when i is the index of a loop.
function push(data, index) {
switch (arguments.length) {
case 0 : panic(" push() what?\n");
case 1 : stack.push(data);
break;
default : if (index >= stack.length) {
stack.unshift(data);
} else {
stack.splice(stack.length-1-index,0,data);
}
}
return vm; // function cascading
}
// typeof(array) and typeof(null) are "object"! So a tweak is needed.
function mytypeof(x){
var ty = typeof x;
switch (ty) {
case 'object':
if (!x) ty = 'null';
if (Object.prototype.toString.apply(x) === '[object Array]') ty = "array";
}
return ty;
}
// js> mytypeof([]) \ ==> array (string)
// js> mytypeof(1) \ ==> number (string)
// js> mytypeof('a') \ ==> string (string)
// js> mytypeof(function(){}) \ ==> function (string)
// js> mytypeof({}) \ ==> object (string)
// js> mytypeof(null) \ ==> null (string)
vm.dictate = dictate; // This is where commands are from. A clause or more.
vm.execute = execute; // Original version. Execute a single command.
vm.stack = function(){return(stack)}; // debug easier. stack got manipulated often, need a fresh grab.
vm.rstack = function(){return(rstack)}; // debug easier especially debugging TSR
vm.words = words; // debug easier. works.forth is the root vocabulary or word-list
vm.dictionary = dictionary; // debug easier
vm.push = push; // interface for passing data into the VM.
vm.pop = pop; // interface for getting data out of the VM.
vm.tos = tos; // interface for getting data out of the VM.
vm.reset = reset; // Recovery from a crash
vm.tick = tick; // Original version. Get a word object.
}}
if (typeof exports!='undefined') exports.jeForth = jeForth; // export for node.js APP