├── Coderful_2024_Paolo.pdf ├── JS_Roma_2024.pdf ├── LICENSE ├── README.md ├── S.js ├── S_test.js ├── jsday2025.pdf ├── sjs.html └── test ├── 01.js ├── 02.js ├── 03.js ├── 04.js ├── 05.js ├── 06.js ├── 07.js └── 10.js /Coderful_2024_Paolo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcaressa/sjs/4f760182cde2e7ea29f1330569aa1eb6f3c53f5e/Coderful_2024_Paolo.pdf -------------------------------------------------------------------------------- /JS_Roma_2024.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcaressa/sjs/4f760182cde2e7ea29f1330569aa1eb6f3c53f5e/JS_Roma_2024.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Paolo Caressa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # S.js - Self JavaScript 2 | 3 | ## (c) 2024 by Paolo Caressa 4 | 5 | ## Introduction 6 | 7 | S.js (Self JavaScript) is a didactic compiler/interpreter for a classic subset of JavaScript, written in JavaScript itself, namely in the subset it implements, so that S.js can compile itself. 8 | 9 | The project was the subject of my talk at the first *Coderful* conference held in Catania (28th june 2024), slide [Coderful_2024_Paolo.pdf](Coderful_2024_Paolo.pdf). More recent slides of the same talk, held in september 2024 @ JR Roma meetup are: [JS_Roma_2024.pdf](JS_Roma_2024.pdf). 10 | 11 | Although being a quite useless projects, as typical of mine, this program features some classic techniques that may be worth to know and to use elsewhere. 12 | 13 | Anyway, enjoy, 14 | Paolo 15 | 16 | ### Interpreter, compiler or what? 17 | 18 | S.js can run in a browser (or it can be launched by node.js for what that matters, see below) and it can do the following: 19 | 20 | - to get a source text (string) and to return a list of all tokens parsed from it; 21 | - to get a token list and to compile it as a JavaScript program into a runtime object; 22 | - to get the runtime object and execute it. 23 | 24 | A runtime object `rt` contains an array `rt.code` where the object code, produced by the compiler, is stored: such a code is just a sequence of JavaScript functions that are executed by the runtime engine, the last step in the previous list. 25 | 26 | The front-end and back-end of the compiler are collapsed in a single step: one could disentangle them so to make it possible to compile to real virtual machine (say to produce Web Assembly code) but the purpose here is to to everything within a single JavaScript version. 27 | 28 | So, S.js is a compiler since it takes a source code and produces a corresponding object code for a runtime engine; it is an interpreter since the object code is not for a native processor but for a virtual one. 29 | 30 | ### How to use it? 31 | 32 | The compiler code is contained in the [S.js](S.js) file that defines a bunch of functions: if you use node.js you need to modify the [S.js](S.js) file to make it a module (by exporting variables `sjs_scan`, `sjs_compile_block` and `sjs_run`) before including it into a node.js module to use it. 33 | 34 | Next, to compile and execute a source text, use 35 | 36 | ```javascript 37 | sjs_run(sjs_compile_block(sjs_scan(source_text)), debug); 38 | ``` 39 | 40 | (the `debug` flag is used by `sjs_run` to decide if log information on the browser console when executing any machine code instruction). 41 | 42 | On a browser, just launch the [sjs.html](sjs.html) file in the distribution, that displays an old-fashioned VaX-like terminal in the browser with some easy to understand buttons on its top to load, run, debug (in the console), parse and compile a program with the compiler. 43 | 44 | Use this [sjs.html](sjs.html) playground, with the browser console opened, to follow these notes and experiment with the compiler: the `Compile` button will compile the text in the playground text area and show in a separate window the machine language produced by the compiler. The `Debug` button runs the program by logging on the console each machine language instruction that is executed, along with the value of the stack and of this. Not an actual debugger but it helps. 45 | 46 | Enjoy finding and fixing its bugs ;-) 47 | 48 | P 49 | 50 | ## The implemented language 51 | 52 | This project selects a subset of JavaScript, grosso modo corresponding to the earlier versions of the language, that provides all the basic control and data structures needed to write non trivial programs. Indeed, the compiler itself is written in this subset, thus it is able to compile itself. Actually, the subset of Javascript has been chosen by including only features needed to code the compiler. 53 | 54 | The main implemented features are: 55 | 56 | - implicit "use strict"; 57 | - block scoping and basic control structures: `do-while`, `for`, `if-else`, `while` whose corresponding instruction need to be a block (thus no `while (x > 0) -- x;`); 58 | - the only allowed jump instruction is `return`; 59 | - expressions implementation is fairly complete; 60 | - function definitions, inside expressions, are actually closures, as expected; 61 | - All built-in objects are available; 62 | 63 | Functions can only be defined as the result of an expression, e.g. 64 | 65 | ```javascript 66 | let f = function(x1,...,xn) {...}; 67 | ``` 68 | 69 | There's no `function f(x1,...,xn) {...}` definition nor arrow function definition `(x1,...,xn)=>{...}` inside an expression. 70 | 71 | ### Accepted syntax 72 | 73 | In this section I will formally describe the syntax the interpreter accepts. As meta-language I use the W3C variant of the classic EBNF metalanguage by Niklaus Wirth, see [Extensible Markup Language (XML) 1.0 (Fifth Edition), section 6](https://www.w3.org/TR/xml/#sec-notation). 74 | 75 | #### Tokens of the language 76 | 77 | ``` 78 | /* S means a sequence of spacelike characters. */ 79 | S ::= (#x20 | #x9 | #xD | #xA)+ 80 | Digit ::= [0-9] 81 | Dot ::= "." 82 | Quote ::= #x27 /* ' */ 83 | DQuote ::= #x22 /* " */ 84 | Number ::= Digit+ | Digit+ Dot Digit* 85 | String ::= Quote [^']* Quote | DQuote [^"]* DQuote 86 | Alpha ::= [A-Z] | [a-z] | "$" | "_" 87 | Alnum ::= Alpha | Digit 88 | Name ::= Alpha Alnum* 89 | ``` 90 | 91 | Notice that S.js doesn't allow back-quote string and that numbers are only in decimal fixed point notation, not exponential one. 92 | 93 | #### Expressions 94 | 95 | JavaScript stems from C the use of expressions not only to produce values but also to alter the computational state somewhat. The S.js implementation of expression is quite complete even if it doesn't fullfil the complete JavaScript standard. 96 | 97 | The syntax alone does not give information about execution priorities, see the table below. Recall that `S*` means an optional sequence of spacelike characters. 98 | 99 | ``` 100 | Expression ::= S* (Prefix S)* Operand (S* Postfix)* (S* Operator Expression)? S* 101 | Prefix ::= "-" | "!" | "--" | "++" | "new" | "typeof" 102 | Postfix ::= Dot S* Name | "[" Expression "]" | "(" (Expression ("," Expression)*)+ ")" 103 | Operand ::= "false" | "true" | "null" | "undefined" 104 | | Number | String | Object | Array 105 | | "(" Expression ")" 106 | | "function" S* "(" S* (Name (S* "," S* name)*)+ S* ")" S* block 107 | Operator ::= "=" | "+=" | "-=" | "&&" | "||" | "==" | "!=" | "<" | ">=" | ">" | "<=" | "in" | "+" | "-" | "*" | "/" | "**" 108 | ``` 109 | 110 | There are no postfix operators (they would be "++" and "--") even if it would be easy to add them. 111 | 112 | Operators with higher priority number are executed before one with lesser priority: as it is well nkown, priorities are, from the lowest to the highest, as follows 113 | 114 | - "=", "+=", "-=" have lower priority than 115 | - "||" that has lower priority than 116 | - "&&" that has lower priority than 117 | - "==", "!=", "<", ">", "<=", ">=", "in" that have lower priority than 118 | - "+", "-" that have lower priority than 119 | - "*", "/" that have lower priority than 120 | - "**" that has lower priority than 121 | - "new" that has lower priority than 122 | - "-" (negation), "!", "++", "--", "typeof" that have lower priority than 123 | - "." (membership), "[ ]" (object member), "( )" (function application) 124 | 125 | #### Instructions 126 | 127 | A S.js program is a text file that is interpreted as a block: that means that the compiler adds an initial "{" and final "}" token to the text. Then the following syntax is applied 128 | 129 | ``` 130 | Block ::= S* "{" *S (Instruction *S)* "}" S* 131 | Instruction ::= "do" Block "while" S* "(" Expression ")" S* ";" 132 | | "if" S* "(" Expression ")" Block ("else" Block)+ 133 | | "for" S* "(" (S* "let" S* Name S* "=")+ S* Expression+ S* ";" Expression+ S* ";" Expression+ S* ")" Block 134 | | "for" S* "(" S* "let" S* Name S* "in" Expression ")" Block 135 | | "let" *S Name (*S "=" Expression)+ (*S "," *S Name (*S "=" Expression)+)* *S ";" 136 | | "return" Expression+ *S ";" 137 | | "throw" Expression ";" 138 | | "while" S* "(" Expression ")" Block 139 | | Expression ";" 140 | | S* ";" 141 | ``` 142 | 143 | Sorry but there are no `break` nor `continue`, use variables or objects attributes instead along with conditions in `while` or `for`. 144 | 145 | ### Builtin objects 146 | 147 | S.js assumes to run in a browser and, as a such, it considers the `window` object as defined and uses it as initial outer environment (see below). Thus you have access inside S.js to all standard objects, such as `alert`, `Array`, `Math` etc. 148 | 149 | Moreover, the S.js compiler recognizes a number, string, array or object as belonging to those classes. Thus, the snippet 150 | 151 | ```javascript 152 | console.log("abc".includes("c")); 153 | console.log([1,2,3].at(1)); 154 | console.log(123.456789.toPrecision(4)); 155 | ``` 156 | 157 | will work as expected. 158 | 159 | You can also use the `false`, `true`, `null`, `undefined` and `this` objects with the expected behavior (up to bugs;-). 160 | 161 | ## The compiler 162 | 163 | **WARNING** from now on this document is still work in progress. 164 | 165 | The S.js compiler is a single file containing a bunch of functions arranged in three groups: 166 | 167 | - lexical analysis functions; 168 | - syntactical analysis and code generating functions; 169 | - virtual machine engine; 170 | 171 | ### The lexical analyzer 172 | 173 | The lexical analyzer consists in a single function `sjs_scan` to be used as 174 | 175 | ```javascript 176 | let token_list = sjs_scan(text); 177 | ``` 178 | 179 | Text is any string containing the text of a JavaScript program: `sjs_scan` always encloses this text between braces to make a block out of it and then tokenizes it into a token list returned as value. The token list is an array of objects of the form 180 | 181 | ```javascript 182 | { 183 | s: // the token: a string unless the token is a number 184 | t: // token's type: "number", "string", "name" or s in other cases 185 | l: // number of the line in text where the token occurs 186 | c: // number of the column in text where the token occurs 187 | } 188 | ``` 189 | 190 | For example `let l = "let";` gives rise to the token list: 191 | 192 | ```javascript 193 | [ 194 | {s: "{", t: "{", l: 0, c: 0}, 195 | {s: "let", t: "let", l: 1, c: 0}, 196 | {s: "l", t: "name", l: 1, c: 4}, 197 | {s: "=", t: "=", l: 1, c: 6}, 198 | {s: "let", t: "string", l: 1, c: 9}, 199 | {s: ";", t: ";", l: 1, c: 13}, 200 | {s: "}", t: "}", l: 0, c: 0} 201 | ] 202 | ``` 203 | 204 | The first `let` token is a keyword (its type is not `name` nor `string` but the keyword itself) while the fourth one is a string. Keywords are intercepted by the`sjs_scan` function so that they cannot be used as names. Therefore, if you crazily type `let let = "let";` and run, S.js will stop the execution with a *Error: 'name' expected at 1:4* error message. 205 | 206 | ### The runtime object 207 | 208 | Once the source text has been tokenized into a token list, the latter can be parsed and compiled by the `sjs_compile_block` function, that works as: 209 | 210 | ```javascript 211 | let rt = sjs_compile_block(token_list); 212 | ``` 213 | 214 | If the compilation succeeds, a "runtime object" is returned that can be run by the virtual machine engine (otherwise an exception with an error message is thrown). It is important to understand this object before describing both the compilation process and the execution engine. 215 | 216 | #### The code 217 | 218 | A runtime object `rt` returned by `sjs_compile_block` contains at least the following fields, needed during compilation (actually, `rt` contains more stuff, e.g. all opcode definitions, thus the functions that implement machine code instructions, such as `PUSH` etc. A runtime object is created via the `sjs_runtime`): 219 | 220 | ```javascript 221 | rt = { 222 | code: // an array that contains the compiled code 223 | ic: // (instruction counter) a number used to scan array code 224 | ... 225 | } 226 | ``` 227 | 228 | Each instruction, thus an element of `rt.code`, is an object of the form 229 | 230 | ```javascript 231 | rt.code[ic] = { 232 | i: // function executed when the instruction is executed 233 | l: // line of the source file corresponding to the instruction 234 | c: // column of the source file corresponding to the instruction 235 | } 236 | ``` 237 | 238 | Therefore, `l` and `c` keys are used for debug purposes while the actual "routine" called when an instruction is executed is `rt.code[ic].i`. Notice that most virtual machine uses *opcodes* thus numerical codes that correspond to instructions: the virtual machine engine performs this translation (or a just in time compilation). S.js instead has no opcodes at all but its opcodes are genuine JavaScript functions executed by the virtual machine engine. 239 | 240 | At runtime, the execution of `rt` is a trivial matter: the array `rt.code` is scanned and its elements executed. 241 | 242 | ```javascript 243 | rt.ic = 0; 244 | while (rt.ic < rt.code.length) { 245 | rt.code[rt.ic++](rt).i; 246 | } 247 | ``` 248 | 249 | (the actual code is more complicated also because `rt.code` elements contain debug information but in these explanations I'll ignore that). 250 | 251 | Notice that the `rt.ic` index is advanced before executing the function supposed to be the value of `rt.code[rt.ic].i`. Each `rt.code[i].i` is a JavaScript function implementing a single instruction of the S.js virtual machine. This function takes as parameter the current (at runtime) runtime object `rt` and it uses a stack to hold temporary values and to return values to subsequent functions. Sometimes, such a function also reads the next `rt.code` element, that's why we need to store in the variable `code.rt` the index of the next instruction to execute. 252 | 253 | For example, the `PUSH` instruction is implemented by a `PUSH(rt)` function that reads the object following it in `rt.code`, pointed by `rt.ic` which is next increased, and pushes it on the stack advancing `rt.ic` by 1, so to prevent the execution loop to try to execute a number as if it is a function; also, the `JP()` function implementing instruction `JP` performs an unconditioned jump to another instruction I, which is identified by the offset between the index in `rt.code` of I and of the current one and increases `ic`, too. It is easier to show the code indeed: 254 | 255 | ```javascript 256 | function JP(rt) { 257 | rt.ic += rt.code[rt.ic].i; 258 | }; 259 | ``` 260 | 261 | So, suppose `rt.code` to be: 262 | 263 | ``` 264 | rt.code = [PUSH, "!", JP, -3] 265 | ``` 266 | 267 | This is a crazy infinite loop that pushes `"!"` on the stack and repeats from the first index the execution of code: indeed the `JP` instruction reads the number located after it in `rt.code` and adds it to `rt.ic`: in this case, when `JP` is executed, `rt.ic == 3` (since `rt.ic` points to the element following the instruction under execution) so that the effect of `JP` is to set `rt.ic = 0`, making the execution loop to repeat from the first element of `rt.code`. 268 | 269 | For a detailed discussion of the instruction set of the S.js virtual machine, see the last chapter: here, let me describe the data it uses during its execution at runtime. 270 | 271 | #### Runtime objects inside `rt` 272 | 273 | To execute a runtime object `rt` use the S.js `sjs_run(rt, debug)` function, that gets two parameters, a runtime object to run and a debug flag: if the latter is true, then debug information will pollute the console of the browser. Before executing the `rt.code` object code, `sjs_run` adds the following attributes to `rt`: 274 | 275 | ```javascript 276 | rt.env = {$_outer_$: window}; // scope environment 277 | rt.$_this_$ = undefined; // this object 278 | rt.stack = []; // A stack used to store temporary values 279 | rt.dump = []; // Another stack used to store temporary values 280 | rt.debug = debug; // if true debug information will be logged 281 | ``` 282 | 283 | The `rt.env` object is the *environment*, thus it contains all variables defined at the current scope. Therefore, an instruction 284 | 285 | ```javascript 286 | let x = e 287 | ``` 288 | 289 | is equivalent to `rt.env.x = e`. A special value `rt.env.$_outer_$` points to the environment of the outer scope. 290 | 291 | Indeed, each time a new scope is introduced (for example when evaluating a function or when executing a block of instructions) the `PUSHENV` machine code instruction is executed at runtime, that performs the operation `rt.env = {$_outer_$: rt.env};` defining a new environment as the current one and the current one as outer, so that new variable definitions will be stored in the new environment. When the current environment is no more needed, the `POPENV` instruction is executed, that restores `rt.env = rt.env.$_outer_$`. All variables introduced in the discarded environment are discarded, too lefting the hard job to the JS engine executing S.js. 292 | 293 | When the interpreter looks for a variable `x`, which is done at runtime by the `REF x` instruction, the key `x` is looked for in `rt.env`: if not found, the search continues in `rt.env.$_outer_$` and so on until the outermost scope is reached (whose `$_outer_$` is `null`). 294 | 295 | The `rt.$_this_$` object is the `this` of the runtime interpreter: the name is cumbersome not to be confused with the `this` of the JavaScript engine executing S.js. 296 | 297 | The `rt.dump` array is used to store old environments that need to be restored after a while, for example when invoking a function. 298 | 299 | The `rt.stack` array is used to contain parameters of machine language instructions such as `ADD` that pops the two topmost elements on the stack, sums them and pushes the result on the stack. 300 | 301 | #### Stack, environment and references 302 | 303 | During runtime execution, the compiled code uses the stack to perform operations among data: such operations are implemented as machine language instructions that retrieve their parameters from the stack and leave on it the result of their computation. 304 | 305 | For example consider the expression 306 | 307 | ```javascript 308 | 1 + 2 * 3; 309 | ``` 310 | 311 | That should be evaluated by firstly performing `2 * 3`, resulting in `6` that next should be added to `1` to get the final result `7`. This is compiled as 312 | 313 | ``` 314 | PUSH 1 // Push 1 on the stack 315 | PUSH 2 // Push 2 on the stack 316 | PUSH 3 // Push 3 on the stack 317 | MUL // Pop two numbers, get 3 and 2, and push their product 6 318 | ADD // Pop two numbers, get 6 and 1, and push their sum 7 319 | ``` 320 | 321 | Usually variables are involved in such computations: consider for example 322 | 323 | ```javascript 324 | 1 + 2 * x; 325 | ``` 326 | 327 | When computing this expression (thus when running the machine code that compiles it) we need to use the value of `x` at the moment of evaluation. The not so efficient choice of S.js is to retrieve the value from the environment when needed. Consider the compiled code corresponding to the above expression: 328 | 329 | ``` 330 | PUSH 1 // Push 1 on the stack 331 | PUSH 2 // Push 2 on the stack 332 | REF x // Retrieve the value of x and push it on the stack 333 | MUL // Pop two numbers and push their product 334 | ADD // Pop two numbers and push their sum 335 | ``` 336 | 337 | The `REF` instruction gets its parameter not from the stack but from the next `rt.code` element, just as `PUSH` and `JP`. This element should be a string and this string is checked to be a key in the current environment object `rt.env`. It that is the case, then the value `rt.env.x` is pushed on the stack. Else the search is performed in the `env.$_outer_$` environment and so on until the key is found in some outer environment or not found in any of them (in which case an error is raised). 338 | 339 | But now consider 340 | 341 | ```javascript 342 | x = 1 + 2 * x; 343 | ``` 344 | 345 | In this case the expression has a side effect: it changes the value of `x` to a new value which is the result of the expression `1 + 2 * x` (using the old value of `x`). Therefore, the `x` on the RHS refers to the value of the variable `x`, while the `x` on the LHS refers to the "address" of the variable `x`. Since one cannot take the address of a variable in JavaScript, S.js uses a device to deal with references to variables. 346 | Notice that we could, with some pain, deduce that the `x` on the right is different from the `x` on the left and compile them differently. 347 | 348 | The solution adopted by S.js is simpler, instead: the `REF x` instruction, once it finds a key `x` in the current environment (or in some outer environment) pushes on the stack an object 349 | 350 | ```javascript 351 | { 352 | $_ref_$: env, 353 | $_at_$: "x" 354 | } 355 | ``` 356 | 357 | where `env` is the environment in which the variable has been found and `x` the name of the variable. 358 | 359 | Suppose such a reference `r` is on top of the stack: if an instruction needs just the value of the variable, then it pops the reference and retrieves the valu as `r.$_ref_$[r.$_at_$]`. This is the most common case. 360 | 361 | Consider instead the JavaScript assignment 362 | 363 | ```javascript 364 | let x = 0; 365 | ``` 366 | 367 | This is translated as 368 | 369 | ``` 370 | REF x 371 | PUSH 0 372 | SET 373 | ``` 374 | 375 | The `SET` instruction pops a value `0` and a reference, `r = {$_ref_$: rt.env, $_at_$: "x"}` in this case, next performs the assignment `r.$_ref_$[r.$_at_$] = 0`, thus `rt.env["x"] = 0` in this case. 376 | 377 | When a value is popped from the stack, it is considered a reference if it is an object and it has the `$_ref_$` key, else it is considered as a value, such as the ones pushed by `PUSH` on the stack. 378 | 379 | Notice that an object in the LHS of an assignment can be dereferentiated, thus one can refers to a key of it. Consider, for example, the following snippet: 380 | 381 | ```javascript 382 | let obj = {a:[1,2,3], b:{a:1, b:2, c:3}}; 383 | obj.a[0] = obj.b.a; 384 | console.log(obj.a[0]); 385 | ``` 386 | 387 | Its execution will print `1` in the console, indeed try to execute it with S.js and you'll get this result. Now look at the compiled code corresponding to the second instruction, the assignment (we use symbolic labels for the addresses of each instructions): 388 | 389 | ``` 390 | i0: REF obj 391 | i1: PUSH a 392 | i2: DEREF 393 | i3: PUSHTHIS 394 | i4: PUSH 0 395 | i5: POPTHIS 396 | i6: DEREF 397 | i7: REF obj 398 | i8: PUSH b 399 | i9: DEREF 400 | iA: PUSH a 401 | iB: DEREF 402 | iC: SET 403 | ``` 404 | 405 | The first two instructions push `{$_ref_$: env, $_at_$: "obj"}` and `"a"` on the stack. Next the `DEREF` instruction is executed, that does the following: 406 | 407 | 1. pop a value `v`, the string `"a"` in this case; 408 | 2. pop an object `r` from the stack; 409 | 3. if `r` is a reference `{$_ref_$: e, $_at_$: "i"}` then a new reference `{$_ref_$: r.$_ref_$[r.$_at_$], $_at_$: v}` is pushed on the stack; 410 | 4. else, if `r` is an object then a new reference `{$_ref_$: r, $_at_$: v}` is pushed on the stack; 411 | 5. else, `undefined` is pushed on the stack. 412 | 413 | In each case, `rt.$_this_$` thus the `this` object is settled accordingly (to `r.$_ref_$` in cases 3 or 4, to `undefined` in case 5). Thus dereferencing creates an object that refers to a part of another object. 414 | 415 | Coming back to the previous example, when the `DEREF` at `i2` is executed, it leaves `{$_ref_$: rt.env.obj, $_at_$: "a"}` on the stack. Next `$_this_$` is saved (because inside brackets new objects may appear) and `0` is pushed on the stack, so that the `DEREF` at `i6` will pop the `0` and `{$_ref_$: rt.env.obj, $_at_$: "a"}`, pushing a new reference `{$_ref_$: rt.env.obj.a, $_at_$: 0}` on the stack. 416 | 417 | Analogously, instructions from `i7` to `iB` leave on the stack the reference `{$_ref_$: rt.env.obj.b, $_at_$: "a"}`. 418 | 419 | Finally, the `SET` instruction pops a value, thus `rt.env.obj.b.a` and the reference `{$_ref_$: rt.env.obj.a, $_at_$: 0}`, assigning to the latter the former value, which amounts to execute `rt.env.obj.a[0] = rt.env.b.a`. 420 | 421 | This may appear to be cumbersome (it is!) but it is a fairly straightforward way to handle assignments. 422 | 423 | ### Code generation 424 | 425 | Let us come back to the `sjs_compile_block` function that gets a token list and returns a runtime environment, and see some of its internals. 426 | 427 | #### Compiling instructions 428 | 429 | The `sjs_compile_block` function uses a top-down approach to parse instructions, thus a simple algorithm in which each grammar class is dealt with by a function, such as in the following pseudo-code: 430 | 431 | ``` 432 | let token = first token of the instruction; 433 | case token: 434 | when "do": sjs_compile_do 435 | when "for": sjs_compile_for 436 | when "if": sjs_compile_if 437 | when "let": sjs_compile_let 438 | when "return": sjs_compile_return 439 | when "throw": sjs_compile_throw 440 | when "while": sjs_compile_while 441 | else: sjs_compile_expression. 442 | ``` 443 | 444 | Each of the subfunctions deals with the specific syntax of the instruction it ought to compile. This is repeated for all instructions parsed from the token list. The actual JavaScript code is a bit more complicated but it is essentially as the pseudocode above. 445 | 446 | To compile JavaScript instructions into machine code instructions, the latter need to be available by the compiler: that is done by inserting them as keys into the runtime object. Indeed, the code compiled in `rt.code` it is not an opcode but directly a JavaScript object, typically the function implementing the machine code instruction. 447 | 448 | The compilation of the single instructions is straightforward and uses machine language instructions. At runtime, the runtime object `rt` contains also two stacks `rt.stack` and `rt.dump` (a hommage to the classical Peter Landin SECD machine) and an object `rt.env` that contains all variable associations at current scope. More on that later 449 | 450 | Let me describe briefly as each JavaScript instruction is compiled. 451 | 452 | ##### Compiling `do-while` 453 | 454 | The instruction 455 | 456 | ```javascript 457 | do {I1; ...; In} while (E); 458 | ``` 459 | 460 | is compiled as follows (I show machine language code in a symbolic way, an instruction per line: instructions preceded by a `label:` are marked by that label, so that a jump instruction can resume execution from that point) 461 | 462 | ``` 463 | i0: compiled code for I1 464 | ... 465 | compiled code for In 466 | compiled code for E 467 | JPNZ i0 468 | ``` 469 | 470 | Thus the sequence of instructions is compiled, then the expression is compiled: the latter leaves in the stack its value, and the `JPNZ` function pops it and, if it is non zero (or non `null`, `undefined`, `false`) perform a jump to location `i0`, else execution continues (actually, in the `code` element following `JPNZ` there's not `i0` but the offset between that element and `i0`). 471 | 472 | ##### Compiling `for (E1; E2; E3) {...}` 473 | 474 | The instruction 475 | 476 | ```javascript 477 | for (E1; E2; E3) {I1; ... In;} 478 | ``` 479 | 480 | is compiled as follows: 481 | 482 | ``` 483 | PUSHENV 484 | compiled code for E1 485 | DROP 486 | i0: compiled code for E2 487 | JPZ i1 488 | compiled code for I1 489 | ... 490 | compiled code for In 491 | compiled code for E3 492 | DROP 493 | JP i0 494 | i1: POPENV 495 | ``` 496 | 497 | First of all a new environment is defined, to host variables defined inside the loop. Next, expression E1 is compiled, followed by the DROP instruction: the latter, at runtime, just pop the top of the stack discarding its value. Indeed, since an expression leaves always a result on the stack (perhaps `undefined`) and since we don't need the value of E1 which is used only for its side effect, we drop that value. 498 | 499 | Next, the address of instruction E2 is marked as i0 and E2 is compiled, followed by a `JPZ i1` instruction, that pops the top of the stack (the result of the evaluation of E2) and, if zero, perform a jump, else continues the execution. So, if the E2 condition is false, the block of instructions is skipped and the execution continues after the for-loop. 500 | 501 | Else the I1, ..., In instructions are executed and also the E3 expression, whose value is discarded since, as for E1, E3 is used only for its side effect. The`JP i0` repeats the loop from the evaluation of condition E2. 502 | 503 | Finally, at `i1`, the loop environment is discarded. 504 | 505 | ##### Compiling `for (let V = E1; E2; E3) {...}` 506 | 507 | The following variant of the for-loop 508 | 509 | ```javascript 510 | for (let V = E1; E2; E3) {I1; ... In;} 511 | ``` 512 | 513 | is essentially the same, but for the fact that a new variable is inserted into the loop environment by means of the `let` statement: 514 | 515 | ``` 516 | PUSHENV 517 | REF V 518 | compiled code for E1 519 | LET 520 | i0: compiled code for E2 521 | JPZ i1 522 | compiled code for I1 523 | ... 524 | compiled code for In 525 | compiled code for E3 526 | DROP 527 | JP i0 528 | i1: POPENV 529 | ``` 530 | 531 | Notice that, since the `LET` instruction does not leave any value on the stack, no `DROP` is needed after E1. Indeed, `LET` pops a value `v` and a string `s` and sets `rt.env[s] = v`. 532 | 533 | ##### Compiling `for (let V in E) {...}` 534 | 535 | 536 | 537 | ##### Compiling `if-else` 538 | 539 | This is also straightforward: the instruction 540 | 541 | ```javascript 542 | if (E) { 543 | I1; ... ; In 544 | } else { 545 | J1; ...; Jm 546 | } 547 | ``` 548 | 549 | is compiled as 550 | 551 | ``` 552 | PUSHENV 553 | compiled code for E 554 | JPZ i1 555 | compiled code for I1 556 | ... 557 | compiled code for In 558 | JP i2 559 | i1: compiled code for J1 560 | ... 561 | compiled code for Jn 562 | i2: POPENV 563 | 564 | ``` 565 | 566 | Of course, if the `else` part is omitted, then the compiled code simplifies to 567 | 568 | ``` 569 | PUSHENV 570 | compiled code for E 571 | JPZ i2 572 | compiled code for I1 573 | ... 574 | compiled code for In 575 | i2: POPENV 576 | ``` 577 | 578 | ##### `let` 579 | 580 | I have already discussed how 581 | 582 | ```javascript 583 | let V1 = E1, ..., Vn = En; 584 | ``` 585 | 586 | is implemented, since the `LET` machine code instruction does the all job, by popping a value, a reference and assigning the value to the reference: 587 | 588 | ``` 589 | REF V1 590 | compiled code for E1 591 | LET 592 | ... 593 | REF Vn 594 | compiled code for En 595 | LET 596 | ``` 597 | 598 | ##### `return` 599 | 600 | To discuss `return` I have to discuss how functions are called in the first place. A function can only be defined via the `function(x1,...,xn) {...}` syntax inside an expression. When such a syntax is parsed, the code needed to allocate at runtime the closure corresponding to the function is compiled. 601 | 602 | 603 | 604 | ##### `throw` 605 | 606 | ##### `while` 607 | 608 | #### Compiling expressions 609 | 610 | 611 | ## The virtual machine 612 | 613 | 614 | ### Variables 615 | 616 | The runtime environment object contains an attribute `env` which is used to store symbols defined in the current scope. Each variable definition `let v = x` corresponds to `env.v = x`. 617 | 618 | When a variable's name v is parsed, the compiler dumps the code 619 | 620 | REF v 621 | 622 | At runtime, the `REF` instruction looks for the name `v` in the environment: if it finds it, the object `r = {env: e, at: [v]}` is pushed on the stack, where `e` is the environment containing the variable and `v` the variable's name. 623 | 624 | Although when a variable is quoted in the text the compiled code creates a reference for it at runtime, often only the value of the reference is needed, for example on the RHS of an assignment, so that the virtual machine uses the `VAL` instruction to convert the reference `r` on the stack into its value `r.env[at[0]]`. 625 | 626 | An example of usage of references is the following: consider 627 | 628 | x[1] = x[0]; 629 | 630 | This is compiled as 631 | 632 | REF "x" 633 | PUSH 1 634 | DEREF 635 | REF "x" 636 | PUSH 0 637 | DEREF 638 | SET 639 | < 640 | The `REF` instruction looks for the attribute `x` inside `rt.env` and push the reference `{env: {..., x:v, ...}, at: [x]}` on the stack. Next `PUSH` pushes `1` and the `DEREF` instruction modifies the reference to `{env: {..., x:v, ...}, at: [x,1]}`. The second sequence `REF-PUSH-DEREF` does the same leaving `{env: {..., x:v, ...}, at: [x,0]}` on the stack. 641 | 642 | Finally the `SET` operator pops two references and assigns the value of the topmost to the one below, in this case the final result will be that the attribute `1` of the object `v` will be set to the value of `v[0]`. 643 | 644 | This device is not efficient, since operators that just need the value of a variable will perform taking the reference and then the value, but it is simple. One could get rid of it by defining a new instruction `REFVAL` that retrieve the value of a variable and pushes it on the stack, introducing an optinization in the code by converting sequences `REF s VAL` into `REFVAL s`. 645 | 646 | 647 | 648 | ## Opcodes 649 | 650 | The object code is just a list whose items can be: 651 | 652 | - Strings. 653 | - Numbers. 654 | - True, false, null or undefined. 655 | - Functions. 656 | 657 | Values such as strings or null are pushed on the stack when they are parsed from the object code, while functions are executed. Most functions implements primitive operations, and they are follows: 658 | 659 | In the following description, stack operations are indended on the data stack, while parse operations are intended on the code. 660 | 661 | - LET pop s, pop v, creates a new variable with name s and value v. 662 | - JP parse n, set ic = n. 663 | - JPZ pop v: if v in [0, false, null, undefined] then perform JP, else parse n 664 | - JPNZ pop v: if v not in [0, false, null, undefined] then perform JP, else parse n 665 | 666 | - ADD pop n1, pop n2, push n2 + n1 667 | - SUB pop n1, pop n2, push n2 - n1 668 | - MUL pop n1, pop n2, push n2 * n1 669 | - DIV pop n1, pop n2, push n2 * n1 670 | - EQ pop v1, pop v2, push v2 == v1 671 | - NE pop v1, pop v2, push v2 != v1 672 | - LT pop v1, pop v2, push v2 < v1 673 | - GT pop v1, pop v2, push v2 > v1 674 | - LE pop v1, pop v2, push v2 <= v1 675 | - GE pop v1, pop v2, push v2 >= v1 676 | -------------------------------------------------------------------------------- /S.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* _________________________________________________________________________ 4 | 5 | HOUSEHOLD FUNCTIONS 6 | _________________________________________________________________________ */ 7 | 8 | /** Raises an error with message msg if cond is true, else it does nothing. 9 | If token is not omitted, then it is used to refer to line and column 10 | where the error occurred (see the sjs_scan() function. */ 11 | let sjs_error = function(cond, msg, token) 12 | { 13 | if (cond) { 14 | msg = "Error: " + msg; 15 | if (token) { msg += " at " + token.l + ":" + token.c; } 16 | alert(msg); 17 | throw msg; 18 | } 19 | }; 20 | 21 | /// Raise an error on token.t != expected_token. 22 | let sjs_expected = function(token, expected_token) 23 | { 24 | sjs_error(token.t != expected_token, "'" + expected_token + "' expected", token); 25 | }; 26 | 27 | /** Return a string with a shallow representation of an object o. 28 | Since an env can have cyclic references, we omit it when 29 | parsing it in an object. */ 30 | let sjs_object2string = function(o) { 31 | let s = o; 32 | if (o == null) { s = "null"; } 33 | else if (o == undefined) { s = "undefined"; } 34 | else { 35 | if (typeof(o) == "object" || typeof(o) == "function") { 36 | if (o && o["$name"]) { s = o.$name; } 37 | else { 38 | if (o.length) { 39 | s = "[ "; 40 | for (let x in o) { 41 | // Prevent some possible infinite loop 42 | if (x != "$_ref_$" && x != "env" && x != "$_outer_$") { 43 | s += sjs_object2string(o[x]) + " "; 44 | } else { 45 | s += "{...} "; 46 | }} 47 | s += "]"; 48 | } else { 49 | s = "{ "; 50 | for (let x in o) { 51 | // Prevent some possible infinite loop 52 | //~ if (x != "$_ref_$" && x != "env" && x != "$_outer_$") { 53 | //~ s += x + ":" + sjs_object2string(o[x]) + " "; 54 | //~ } else { 55 | if (typeof(o[x]) == "object") { 56 | s += x + ":{...} "; 57 | } else { 58 | s += x + ":" + sjs_object2string(o[x]) + " "; 59 | }} 60 | s += "}"; 61 | }}}} 62 | return s; 63 | }; 64 | 65 | /* **************************************************************************** 66 | L E X I C A L A N A L Y Z E R 67 | **************************************************************************** */ 68 | 69 | /** Scans the JS sequence of tokens from the string text and returns it as an 70 | array of objects {"s": s, "t": t, "l": l, "c": c} where s is the string 71 | representation of the token, t the token type (number, string or the token 72 | itself in other cases), l the line number and c the colum number where 73 | the token occurs inside text. */ 74 | let sjs_scan = function(text) 75 | { 76 | let ALPHA = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm_$"; 77 | let DIGIT = "1234567890"; 78 | let ALNUM = ALPHA + DIGIT; 79 | let KEYWORDS = ["do", "else", "false", "for", "function", "if", "in", 80 | "let", "new", "null", "return", "this", "throw", 81 | "true", "typeof", "undefined", "while"]; 82 | let OPERATORS = ["**", "<=", ">=", "==", "!=", "+=", "-=", 83 | "&&", "||", "++", "--"]; 84 | 85 | // Enclose the text to scan between "{" and "}". 86 | let toklist = [{s: "{", t: "{", l:0, c:0}]; // list of all scanned tokens 87 | 88 | let line = 1; // current line number 89 | let i_line = 0; // Index of 1st character of the current line 90 | 91 | /* skip_until(i, delims) = least j>i such that text[j] is in delims. 92 | If no such j exists then return text.length. Keep track of the 93 | newlines inside the text so to count line numbers correctly. */ 94 | let skip_until = function(i, delims) { 95 | for (; i < text.length && !delims.includes(text[i]); ++ i) { 96 | if (text[i] == "\n") { 97 | ++ line; 98 | i_line = i + 1; 99 | }} 100 | return i; 101 | }; 102 | /* skip_while(i, delims) = least j>i such that text[j] is not in delims. 103 | If no such j exists then return text.length. Keep track of the 104 | newlines inside the text so to count line numbers correctly. */ 105 | let skip_while = function(i, delims) { 106 | for (; i < text.length && delims.includes(text[i]); ++ i) { 107 | if (text[i] == "\n") { 108 | ++ line; 109 | i_line = i + 1; 110 | }} 111 | return i; 112 | }; 113 | let i = 0; 114 | while (i < text.length) { 115 | let c = text[i]; 116 | let c1 = text[i+1]; 117 | let cc1 = c + c1; 118 | if (c == "\n") { // Skip newlines 119 | ++ line; 120 | i_line = ++ i; 121 | } else 122 | if (" \r\t\f\v".includes(c)) { // Skip spaces 123 | ++ i; 124 | } else 125 | if (c == "/" && c1 == "/") { // Skip line comments 126 | i = skip_until(i, "\n"); 127 | ++ line; 128 | i_line = ++ i; 129 | } else 130 | if (c == "/" && c1 == "*") { // Skip comments 131 | let i0 = i + 2; 132 | do { 133 | i = 1 + skip_until(i + 1, "*"); 134 | } while (text[i] != "/"); 135 | ++ i; 136 | } else 137 | if (ALPHA.includes(c)) { // Names 138 | let i0 = i; 139 | i = skip_while(i0 + 1, ALNUM); 140 | let name = text.slice(i0, i); 141 | if (KEYWORDS.includes(name)) { 142 | toklist.push({s: name, t: name, l: line, c: i0 - i_line}); 143 | } else { 144 | toklist.push({s: text.slice(i0, i), t: "name", l: line, c: i0 - i_line}); 145 | } 146 | } else 147 | if (DIGIT.includes(c)) { // Numbers 148 | let i0 = i; 149 | i = skip_while(i0 + 1, DIGIT); 150 | if (text[i] == ".") { 151 | i = skip_while(i + 1, DIGIT); 152 | } 153 | toklist.push({s: Number(text.slice(i0, i)), t: "number", l: line, c: i0 - i_line}); 154 | } else 155 | if (c == "'" || c == '"') { // Strings 156 | let i0 = ++ i; 157 | i = skip_until(i, c); 158 | toklist.push({s: text.slice(i0, i), t: "string", l: line, c: i0 - i_line}); 159 | ++ i; 160 | } else 161 | if (OPERATORS.includes(cc1)) { 162 | toklist.push({s: cc1, t: cc1, l: line, c: i - i_line}); 163 | i += 2; 164 | } else 165 | if ("-+*/<>=!.,;:()[]{}".includes(c)) { 166 | toklist.push({s: c, t: c, l: line, c: i - i_line}); 167 | ++ i; 168 | } else { 169 | sjs_error(true, ": Syntax error: '" + c + "'", {l:line, c:(i - i_line)}); 170 | }} 171 | // Enclose the text to scan between "{" and "}". 172 | toklist.push({s: "}", t: "}", l:0, c:0}); 173 | 174 | return toklist; 175 | }; 176 | 177 | /* **************************************************************************** 178 | S Y N T A C T I C A N A L Y Z E R A N D C O D E G E N E R A T O R 179 | **************************************************************************** */ 180 | 181 | /** Compiler entry point: accepts a token list as produced by sjs_scan and 182 | returns a runtime environment with the compiled code and the stuff needed 183 | by the sjs_run function to actually run the code. On error an exception 184 | is thrown. */ 185 | let sjs_compile = function(tl) 186 | { 187 | let rt = sjs_runtime(); 188 | sjs_compile_block(tl, rt); 189 | return rt; 190 | }; 191 | 192 | /// Compile a sequence s of objects into rt.code. 193 | let sjs_compile_code = function(rt, s, t) 194 | { 195 | for (let i = 0; i < s.length; ++ i) { 196 | rt.code.push({i: s[i], l: t.l, c:t.c}); 197 | } 198 | }; 199 | 200 | /** Compile a value to be pushed on the stack at runtime. */ 201 | let sjs_compile_value = function(rt, v, t) 202 | { 203 | rt.code.push({i: rt.PUSH, l: t.l, c:t.c}); 204 | rt.code.push({i: v, l: t.l, c:t.c}); 205 | }; 206 | 207 | /** \note 208 | 209 | The following functions are used to analyze and compile JS expressions: 210 | during the compilation. Therein 211 | - the rt.code array is extended with the code generated; 212 | - the rt.ic value is increased each time a new function definition is 213 | parsed (so that rt.ic == 0 means we are not inside any function, so 214 | that "return" will raise an error). 215 | */ 216 | 217 | /** Compile a list of expressions from tl into an array which is pushed on the 218 | stack at runtime; the endchr is the one checked against the end of the list. 219 | This function is used with lists [x1,...,xn] or (x1,...,xn). */ 220 | let sjs_compile_array = function(tl, rt, endchr) 221 | { 222 | /* An empty array is created and elements are added to it 223 | [v1, ..., vk] is compiled as [] v1 ARRPUSH ... vk ARRPUSH */ 224 | let token = tl[0]; // probe but not scan the next token 225 | sjs_compile_value(rt, [], token); // compile "PUSH []" 226 | if (token.t == endchr) { 227 | // Empty list: drop the end of list token and return 228 | tl.shift(); 229 | } else { 230 | do { 231 | token = sjs_compile_expression(tl, rt); 232 | // a v ARRPUSH -> a[a.length] = v and a on the stack 233 | sjs_compile_code(rt, [rt.ARRPUSH], token); 234 | } while (token.t == ","); 235 | sjs_expected(token, endchr, token); 236 | } 237 | }; 238 | 239 | /** Compile a list of pairs key:value from tl into an object which is pushed 240 | on the stack at runtime. */ 241 | let sjs_compile_object = function(tl, rt) 242 | { 243 | /* An empty object is created and keys are added to it. 244 | {n1:v1, ..., nk:vk} is compiled as {} n1 v1 OBJADD ... nk vk OBJADD */ 245 | let token = tl[0]; // probe but not scan the next token 246 | sjs_compile_value(rt, {}, token); // compile "PUSH {}" 247 | if (token.t == "}") { 248 | // Empty object: drop the "}" token and return 249 | tl.shift(); 250 | } else { 251 | do { 252 | token = tl.shift(); 253 | // We expect a key: name or string 254 | sjs_error(token.t != "name" && token.t != "string", 255 | "Invalid object key: " + token.s, 256 | token); 257 | sjs_compile_value(rt, token.s, token); // OBJADD expect this 258 | sjs_expected(tl.shift(), ":"); 259 | token = sjs_compile_expression(tl, rt); 260 | // obj name value OBJADD -> obj[name] = value and obj on the stack 261 | sjs_compile_code(rt, [rt.OBJADD], token); 262 | } while (token.t == ","); 263 | sjs_expected(token, "}", token); 264 | } 265 | }; 266 | 267 | /** Compile a function() {...} into an object which is pushed on the stack 268 | at runtime. */ 269 | let sjs_compile_function = function(tl, rt) 270 | { 271 | /* A function(params) { body} is compiled as a sequence of three values: 272 | [CLOSURE(), params, body] where 273 | 274 | - CLOSURE is the function to be executed at runtime to create the 275 | closure and push it on the stack. 276 | - params is the array [x1,...,xn] of formal parameters 277 | - body is an array containing the compiled code of the closure body 278 | 279 | During runtime, the CLOSURE instruction creates a new environment, 280 | inserts into it the formal parameters as variables whose initial 281 | values are actual parameters and execute the closure body. */ 282 | 283 | // Parse the formal parameter list "(name, ..., name)" 284 | let parameters = []; 285 | sjs_expected(tl.shift(), "("); 286 | let token = tl.shift(); 287 | while (token.t != ")") { 288 | sjs_expected(token, "name"); 289 | parameters.push(token.s); 290 | token = tl.shift(); 291 | if (token.t == ",") { 292 | token = tl.shift(); 293 | } else { 294 | sjs_expected(token, ")"); 295 | }} 296 | // Parse and compile the function's body 297 | ++ rt.ic; // this means "inside a nested function" 298 | // Save the current code since we want to compile the body from scratch 299 | let code_saved = rt.code; 300 | rt.code = []; 301 | sjs_compile_block(tl, rt); 302 | // Save the compiled body and restore the code 303 | let body = rt.code; 304 | rt.code = code_saved; 305 | // If there's no final "return" add it! Needed at runtime. 306 | if (body[body.length - 1].i != rt.RET) { 307 | body.push({i: rt.PUSH, l: tl[0].l, c: tl[0].c}); 308 | body.push({i: undefined, l: tl[0].l, c: tl[0].c}); 309 | body.push({i: rt.RET, l: tl[0].l, c: tl[0].c}); 310 | } 311 | -- rt.ic; 312 | sjs_compile_code(rt, [rt.CLOSURE, parameters, body], token); 313 | }; 314 | 315 | /** Compile an operand into rt.code parsing it from the token list tl, which is 316 | consumed doing so. The first token of the operand is passed in the first 317 | argument, while the first token following the expression is returned as 318 | value. */ 319 | let sjs_compile_operand = function(token, tl, rt) 320 | { 321 | // Check against constants 322 | if (token.t == "number") { sjs_compile_value(rt, token.s, token); } 323 | else if (token.t == "string") { sjs_compile_value(rt, token.s, token); } 324 | else if (token.t == "false") { sjs_compile_value(rt, false, token); } 325 | else if (token.t == "true") { sjs_compile_value(rt, true, token); } 326 | else if (token.t == "null") { sjs_compile_value(rt, null, token); } 327 | else if (token.t == "undefined") { sjs_compile_value(rt, undefined, token); } 328 | // Check against literal objects 329 | else if (token.t == "function") { sjs_compile_function(tl, rt); } 330 | else if (token.t == "{") { 331 | //~ sjs_compile_code(rt, [rt.PUSHTHIS], token); 332 | sjs_compile_object(tl, rt); 333 | //~ sjs_compile_code(rt, [rt.POPTHIS], token); 334 | } 335 | else if (token.t == "[") { 336 | //~ sjs_compile_code(rt, [rt.PUSHTHIS], token); 337 | sjs_compile_array(tl, rt, "]"); 338 | //~ sjs_compile_code(rt, [rt.POPTHIS], token); 339 | } 340 | // Check against variable names 341 | else if (token.t == "name") { sjs_compile_code(rt, [rt.REF, token.s], token); } 342 | // If all else fails, we expect a subexpression "(expr)" 343 | else if (token.t == "(") { 344 | //~ sjs_compile_code(rt, [rt.PUSHTHIS], token); 345 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 346 | //~ sjs_compile_code(rt, [rt.POPTHIS], token); 347 | } 348 | else if (token.t == "this") { 349 | sjs_compile_code(rt, [rt.THIS], token); 350 | } 351 | else { sjs_error(true, "Syntax error: " + token.s, token); } 352 | return tl.shift(); 353 | }; 354 | 355 | /** Compiles an expression into rt.code parsing it from the token list tl, 356 | which is consumed doing so. The first token following the expression is 357 | returned as value. To implement operator precedence the function uses a 358 | classic bottom-up technique: each operator has a priority number 359 | associated. When an operator is parsed, if the operator on the stack had 360 | higher priority then the latter is compiled and the former pushed on the 361 | stack; else the former is pushed on the stack. When the expression has 362 | completely been parsed, the stack is unwinded by compiling each element 363 | until it is empty. */ 364 | let sjs_compile_expression = function(tl, rt) 365 | { 366 | let stack = []; // stack where operators are pushed before being compiled 367 | 368 | let PRIORITIES = { 369 | "fake": 0, // fake operator, needed in while condition in sjs_compile_stack. 370 | "=": 5, "+=": 5, "-=": 5, "||": 10, "&&": 15, "==": 20, "!=": 20, 371 | "<": 25, ">": 25, "<=": 25, ">=": 25, "in":25, "+": 30, "-": 30, 372 | "*": 40, "/": 40, "**": 42, "new": 45, "-NEG-": 50, "!": 50, "++": 50, "--": 50, "typeof": 50 373 | }; 374 | let OPERATORS = { 375 | "=": rt.SET, "+=": rt.SETADD, "-=": rt.SETSUB, "==": rt.EQ, "!=": rt.NE, 376 | "<": rt.LT, ">": rt.GT, "<=": rt.LE, ">=": rt.GE, "in": rt.IN, 377 | "+": rt.ADD, "-": rt.SUB, "*": rt.MUL, "/": rt.DIV, "**": rt.POW, "new": rt.NEW, 378 | "-NEG-": rt.NEG, "!": rt.NOT, "++": rt.INC, "--": rt.DEC, "typeof": rt.TYPEOF 379 | }; 380 | 381 | /// Compile all operators in the stack with priority > the priority of opt. 382 | let compile_stack = function(opt) { 383 | while (stack.length > 0 && PRIORITIES[stack[stack.length-1]] >= PRIORITIES[opt]) { 384 | let opt_to_dump = stack.pop(); 385 | if (opt_to_dump == "&&" || opt_to_dump == "||") { 386 | // pop from cstack where to write the jump. 387 | let i = cstack.pop(); 388 | rt.code[i].i = rt.code.length - i; 389 | } else { 390 | sjs_compile_code(rt, [OPERATORS[opt_to_dump]], tl[0]); 391 | }}}; 392 | 393 | let cstack = []; // Stack used for forward reference in && and || 394 | let token; // last parsed token from tl inside the do{...} loop 395 | let again; // do{...} loop iteration condition, defined inside the loop 396 | 397 | do { 398 | token = tl.shift(); // Prefix operator or operand expected 399 | 400 | // Is token a prefix operator? 401 | // (Notice: check tokens via token.t unless is a name, string, number). 402 | while (token.t == "!" || token.t == "-" || token.t == "--" || token.t == "++" || token.t == "typeof" || token.t == "new") { 403 | // A minus token means a negation 404 | let t = token.t == "-" && "-NEG-" || token.t; 405 | compile_stack(t); 406 | stack.push(t); 407 | token = tl.shift(); 408 | } 409 | // Operand expected in any case. 410 | token = sjs_compile_operand(token, tl, rt); 411 | 412 | // Is token a postfix operator? 413 | while (token.t == "." || token.t == "(" || token.t == "[") { 414 | if (token.t == "(") { // Actual parameter list 415 | // First of all takes the value of the function 416 | sjs_compile_code(rt, [rt.PUSHTHIS], token); 417 | sjs_compile_array(tl, rt, ")"); 418 | sjs_compile_code(rt, [rt.POPTHIS, rt.APPLY], token); 419 | } else 420 | if (token.t == ".") { // Member operator x.y 421 | token = tl.shift(); 422 | sjs_expected(token, "name"); 423 | sjs_compile_value(rt, token.s, token); 424 | sjs_compile_code(rt, [rt.DEREF], token); 425 | } else { 426 | // Assert token.t == "["; Member operator x[y] 427 | sjs_compile_code(rt, [rt.PUSHTHIS], token); 428 | token = sjs_compile_expression(tl, rt); 429 | sjs_expected(token, "]"); 430 | sjs_compile_code(rt, [rt.POPTHIS, rt.DEREF], token); 431 | } 432 | token = tl.shift(); 433 | } 434 | // Is there a binary operator? 435 | again = PRIORITIES[token.t]; // if undefined no! 436 | if (again) { 437 | // Compile elements in the stack with higher priorities 438 | compile_stack(token.t); 439 | stack.push(token.t); 440 | /* Shortcuts operator need more work: they also compile a JUMP 441 | and save the index of the element of code containing the 442 | jump address, to be written after the second operand will 443 | be compiled. */ 444 | if (token.t == "&&" || token.t == "||") { 445 | sjs_compile_code(rt, [token.t == "&&" && rt.DUPJPZ || rt.DUPJPNZ], token); 446 | /* The index where to jump shall be written at 447 | rt.code[rt.code.length] after the second operand will 448 | be compiled. Now save the position where to store it 449 | in the "control stack". */ 450 | cstack.push(rt.code.length); 451 | sjs_compile_code(rt, [0], token); // 0 overwritten in compile_stack. 452 | }} 453 | } while (again); 454 | 455 | // If there are operators on the stack, compile them all. 456 | compile_stack("fake"); 457 | 458 | return token; 459 | }; 460 | 461 | /** 462 | \note Analyze and compile JS statements. 463 | */ 464 | 465 | /** Compile the do {p} while (c) statement. Assume the "do" token 466 | to be already shifted from tl. */ 467 | let sjs_compile_do = function(tl, rt) 468 | { 469 | /* "do {p} while(c)" is compiled as "again: p c JPNZ again". */ 470 | sjs_compile_code(rt, [rt.PUSHENV], tl[0]); // New scope at runtime 471 | let again = rt.code.length; // where to jump to repeat the loop 472 | // Compile {p} appending it to rt.code 473 | sjs_compile_block(tl, rt); // compile {p} 474 | sjs_error(tl.shift().s != "while", "'while' expected", rt.code[rt.code.length-1]); 475 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 476 | // Compile a JPNZ to again to repeat the loop (notice the -1, is correct) 477 | sjs_compile_code(rt, [rt.JPNZ, again - rt.code.length - 1], tl[0]); 478 | sjs_compile_code(rt, [rt.POPENV], tl[0]); // Restore scope at runtime 479 | }; 480 | 481 | /** Compile the for (let s in e) {p} statement. 482 | Assume the "for (" tokens to be already shifted from tl. */ 483 | let sjs_compile_for_in = function(tl, rt) 484 | { 485 | /* for (let s in e) {p} is compiled as: 486 | PUSH s PUSH null LET Create variable s 487 | e OBJKEYS Push the array of e keys 488 | again: DUP ARRLEN JPZ leave If the array is empty then finish 489 | ARRSHIFT shift the first key from the array 490 | REF s SWAP SET DROP s = array[0] 491 | p execute body 492 | JP again repeat loop 493 | leave: DROP array no more needed */ 494 | tl.shift(); // Skip "let" 495 | let s = tl.shift().s; // skip s and take note of it 496 | tl.shift(); // skip "in" 497 | sjs_compile_code(rt, [rt.PUSHENV, 498 | rt.PUSH, s, 499 | rt.PUSH, undefined, 500 | rt.LET], tl[0]); 501 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 502 | sjs_compile_code(rt, [rt.OBJKEYS], tl[0]); 503 | let again = rt.code.length; 504 | let leave = again + 3; // index of the 0 placeholder after JPZ 505 | sjs_compile_code(rt, 506 | [rt.DUP, // DUP since JPZ destroys it 507 | rt.ARRLEN, // Lenght of remained keys to loop on 508 | rt.JPZ, 0, // JP after the loop, 0 is a placeholder 509 | rt.ARRSHIFT, // array -> its shifted 1st element 510 | rt.REF, s, 511 | rt.SWAP, // SET needs (... ref val) on the stack 512 | rt.SET, 513 | rt.DROP // SET leaves a value, discard it 514 | ], tl[0]); 515 | sjs_compile_block(tl, rt); // compile {p} 516 | // Compile a JP to again to repeat the loop (notice the -1, is correct) 517 | sjs_compile_code(rt, [rt.JP, again - rt.code.length - 1], tl[0]); 518 | rt.code[leave].i = rt.code.length - leave; 519 | sjs_compile_code(rt, [rt.DROP, rt.POPENV], tl[0]); 520 | }; 521 | 522 | /** Compile the for (a; c; i) {p} statement. 523 | Assume the "for (" tokens to be already shifted from tl. */ 524 | let sjs_compile_for = function(tl, rt) 525 | { 526 | /* for (a; c; i) {p} is compiled as: 527 | a again: c JPZ leave p i JP again leave: */ 528 | sjs_compile_code(rt, [rt.PUSHENV], tl[0]); 529 | // Compile a: it can be empty, an expression or a let statement 530 | if (tl[0].s == "let") { 531 | tl.shift(); // Skip "let" since compile_let expect this. 532 | sjs_compile_let(tl, rt); 533 | } else 534 | if (tl[0].s == ";") { // Empty assignment: skip the ";" 535 | tl.shift(); 536 | } else { 537 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 538 | sjs_compile_code(rt, [rt.DROP], tl[0]); // discard the value of the expression 539 | } 540 | // Compile c: it can be empty 541 | let again = rt.code.length; // where to jump to repeat the loop 542 | if (tl[0].s == ";") { // Empty expression: skip the ";" 543 | tl.shift(); 544 | } else { 545 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 546 | } 547 | // Compile JPZ leave 548 | sjs_compile_code(rt, [rt.JPZ], tl[0]); 549 | let leave = rt.code.length; // where to jump to leave the loop 550 | sjs_compile_code(rt, [0], tl[0]); // 0 is a placeholder here 551 | /* Now compile i that eventually will appear after p: to do that, 552 | mark the beginning of i (which is at leave + 1) and then insert 553 | the compiled body before it. */ 554 | if (tl[0].s == ")") { // Empty expression: nothing to do 555 | tl.shift(); 556 | sjs_compile_block(tl, rt); // compile {p} 557 | } else { 558 | // Compile i, cut it from rt.code, compile p and paste i after p 559 | sjs_expected(sjs_compile_expression(tl, rt), ")"); // compile i 560 | let i = rt.code.slice(leave + 1, rt.code.length); // save i 561 | rt.code = rt.code.slice(0, leave + 1); // drop i from code 562 | sjs_compile_block(tl, rt); // compile {p} 563 | // Append the increment/decrement part 564 | rt.code = rt.code.concat(i); 565 | // Expression c figures only for side effects: discard its value 566 | sjs_compile_code(rt, [rt.DROP], tl[0]); 567 | } 568 | // Compile the jump to repeat the loop 569 | sjs_compile_code(rt, [rt.JP, again - rt.code.length - 1], tl[0]); 570 | // Compile the offset for the JPZ leave here. 571 | rt.code[leave].i = rt.code.length - leave; 572 | // Back to the old scope 573 | sjs_compile_code(rt, [rt.POPENV], tl[0]); 574 | }; 575 | 576 | /** Compile if (e) {p1} else {p2}. Assume the "if" token to be 577 | already shifted from tl. */ 578 | let sjs_compile_if = function(tl, rt) 579 | { 580 | /* if (c) {p1} else {p2} is compiled as 581 | c JPZ other p1 JP after other: p2 after: 582 | if (c) {p1} is compiled as 583 | c JPZ other p1 other: */ 584 | // Compile the (c) condition 585 | sjs_expected(tl.shift(), "("); 586 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 587 | /* Now compile a JPZ to the first instruction after the if: 588 | that'll be determined after compiling the if-else, so we keep 589 | track of the index of the code element where this information 590 | will be stored after the if-else has been compiled. */ 591 | let other = rt.code.length + 1; // index of 0 in the following s 592 | // 0 after JPZ is a placeholder 593 | sjs_compile_code(rt, [rt.JPZ, 0, rt.PUSHENV], tl[0]); 594 | sjs_compile_block(tl, rt); // compile {p1} 595 | sjs_compile_code(rt,[rt.POPENV], tl[0]); 596 | if (tl[0].s != "else") { 597 | // The if (c) {p1} has been compiled: let JPZ jump here. 598 | rt.code[other].i = rt.code.length - other; 599 | } else { 600 | tl.shift(); // skip "else" 601 | let after = rt.code.length + 1; // index of 0 in the following s 602 | sjs_compile_code(rt, [rt.JP, 0], tl[0]); // to be overwritten later! 603 | // Let JPZ (after if) jump here. 604 | rt.code[other].i = rt.code.length - other; 605 | /* Now compile the p2 appending inside a new environment, appending 606 | it to rt.code. Instead of {p2} if may appear a if (...). */ 607 | if (tl[0].s == "if") { 608 | tl.shift(); 609 | sjs_compile_if(tl, rt, tl[0]); 610 | } else { 611 | sjs_compile_code(rt,[rt.PUSHENV], tl[0]); 612 | sjs_compile_block(tl, rt); 613 | sjs_compile_code(rt,[rt.POPENV], tl[0]); 614 | } 615 | // The if (c) {p1} else {p2} has been compiled: let JP jump here. 616 | rt.code[after].i = rt.code.length - after; 617 | } 618 | }; 619 | 620 | /** Compile a "let x1 = v1,...,xn=vn;" instruction from tl at rt. 621 | Assume the "let" token to be already shifted from tl. */ 622 | let sjs_compile_let = function(tl, rt) 623 | { 624 | let token; 625 | do { 626 | token = tl.shift(); 627 | sjs_expected(token, "name"); 628 | sjs_compile_value(rt, token.s, token); 629 | token = tl.shift(); 630 | if (token.t == "=") { 631 | token = sjs_compile_expression(tl, rt); 632 | } else { 633 | // variable default value 634 | sjs_compile_value(rt, undefined, token); 635 | } 636 | sjs_compile_code(rt,[rt.LET], tl[0]); 637 | } while (token.t == ","); 638 | sjs_expected(token, ";"); 639 | }; 640 | 641 | /** Compile while (c) {p}. Assume the "while" token to be 642 | already shifted from tl. */ 643 | let sjs_compile_while = function(tl, rt) 644 | { 645 | /* while (c) {p} is compiled as 646 | again: (c) JPZ leave p JP again leave: */ 647 | // New scope introduced at runtime 648 | sjs_compile_code(rt,[rt.PUSHENV], tl[0]); 649 | // Compile the (c) condition 650 | let again = rt.code.length; // jump here to repeat the loop 651 | sjs_expected(tl.shift(), "("); 652 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 653 | /* Now compile a JPZ to the first instruction after the while: 654 | that'll be determined after compiling the loop, so we keep 655 | track of the index of the code element where this information 656 | will be stored after the loop has been compiled. */ 657 | let leave = rt.code.length + 1; // index of 0 in the following s 658 | sjs_compile_code(rt, [rt.JPZ, 0], tl[0]); // 0 to be overwritten later! 659 | sjs_compile_block(tl, rt); // compile {p} 660 | // Compile a JP to again to repeat the loop (notice the -1, is correct) 661 | sjs_compile_code(rt, [rt.JP, again - rt.code.length - 1], tl[0]); 662 | // Compile the offset for the JPZ leave here. 663 | rt.code[leave].i = rt.code.length - leave; 664 | // Back to the old scope 665 | sjs_compile_code(rt,[rt.POPENV], tl[0]); 666 | }; 667 | 668 | /** Given a token list produce a runtime object containing the compiled code. 669 | The token list should ALWAYS be a block {...}. */ 670 | let sjs_compile_block = function(tl, rt) 671 | { 672 | let token = tl.shift(); 673 | sjs_expected(token, "{"); 674 | 675 | // Consume the token list as far as it is parsed 676 | while ((token = tl.shift()).s != "}") { 677 | if (token.t == ";") { /* Empty statement, nothing to compile! */ } 678 | else if (token.t == "do") { sjs_compile_do(tl, rt);} 679 | else if (token.t == "for") { 680 | sjs_expected(tl.shift(), "("); 681 | if (tl[0].t == "let" && tl[1].t == "name" && tl[2].t == "in") { 682 | sjs_compile_for_in(tl, rt); 683 | } else { 684 | sjs_compile_for(tl, rt); 685 | } 686 | } 687 | else if (token.t == "if") { sjs_compile_if(tl, rt); } 688 | else if (token.t == "let") { sjs_compile_let(tl, rt); } 689 | else if (token.t == "return") { 690 | sjs_error(rt.ic == 0, "Illegal return statement", token); 691 | if (tl[0].t != ";") { token = sjs_compile_expression(tl, rt); } 692 | else { 693 | sjs_compile_value(rt, undefined, token); 694 | token = tl.shift(); 695 | } 696 | sjs_compile_code(rt,[rt.RET], token); 697 | sjs_expected(token, ";"); 698 | } 699 | else if (token.t == "throw") { 700 | token = sjs_compile_expression(tl, rt); 701 | sjs_compile_code(rt,[rt.THROW], token); 702 | sjs_expected(token, ";"); 703 | } 704 | else if (token.t == "while") { sjs_compile_while(tl, rt); } 705 | else { 706 | // Expression statement 707 | tl.unshift(token); 708 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 709 | sjs_compile_code(rt,[rt.DROP], tl[0]); // discard the result of the expression 710 | }} 711 | sjs_expected(token, "}"); 712 | }; 713 | 714 | /* **************************************************************************** 715 | R U N T I M E S T U F F 716 | **************************************************************************** */ 717 | 718 | /** A runtime object rt, as returned by sjs_compile_block, is executed. 719 | If debug is not null, 0 nor undefined then execution is dumped on the 720 | console step by step. On error throws an exception, via sjs_error. */ 721 | let sjs_run = function(rt, debug) 722 | { 723 | // rt.env.$_outer_$ = environment at outer scope. 724 | /* The outmost scope is the window object of the JS engine executing 725 | executing the compiler ;-). */ 726 | rt.env = { $_outer_$: window }; 727 | rt.env.$_outer_$.$_outer_$ = null; 728 | rt.stack = []; 729 | rt.dump = []; 730 | rt.debug = debug; 731 | //~ rt.$_this_$ = rt.env.$_outer_$; 732 | rt.$_this_$ = undefined; 733 | sjs_execute(rt.code, rt); 734 | }; 735 | 736 | /// Executes code in runtime environment rt 737 | let sjs_execute = function(code, rt) 738 | { 739 | /// Returns a string representing the stack contents. 740 | let stackdump = function(rt) { 741 | let s = "Stack: ["; 742 | for (let i = 0; i < rt.stack.length; ++ i) { 743 | s += sjs_object2string(rt.stack[i]) + ", "; 744 | } 745 | return s + "]"; 746 | }; 747 | 748 | // Saves code and ic since sjs_execute can be recursively called. 749 | let code_saved = rt.code; 750 | let ic_saved = rt.ic; 751 | rt.code = code; 752 | rt.ic = 0; 753 | 754 | while (rt.ic < rt.code.length) { 755 | let ic = rt.ic; 756 | ++ rt.ic; 757 | if (rt.debug) { 758 | console.log(stackdump(rt)); 759 | console.log("this = " + sjs_object2string(rt.$_this_$)); 760 | if (rt.code[ic].i.$name) { 761 | console.log("[" + ic + "|" + rt.code[ic].l + ":" + rt.code[ic].c + "] " + rt.code[ic].i.$name); 762 | } else { 763 | console.log("[" + ic + "|" + rt.code[ic].l + ":" + rt.code[ic].c + "] " + rt.code[ic].i); 764 | }} 765 | rt.code[ic].i(rt); 766 | } 767 | // Restores original values 768 | rt.code = code_saved; 769 | rt.ic = ic_saved; 770 | }; 771 | 772 | // Create a runtime environment 773 | let sjs_runtime = function() 774 | { 775 | let rt = {code:[], ic:0, stack:[], dump:[]}; 776 | 777 | // AUXILIARY FUNCTIONS 778 | /** Pop a reference r, deference it and increases its value by v: used by 779 | DEC, INC, SETADD, SETSUB. */ 780 | rt.increase = function(v) { 781 | let r = rt.stack.pop(); 782 | sjs_error(!(r && r.$_ref_$), "Invalid LHS in assignment", rt.code[rt.ic]); 783 | r.$_ref_$[r.$_at_$] += v; 784 | rt.stack.push(r.$_ref_$[r.$_at_$]); 785 | }; 786 | 787 | /** Pop a value v: if v = {$_ref_$:r, $_at_$:a} then return r[a], 788 | else return v. */ 789 | rt.popval = function() { 790 | let v = rt.stack.pop(); 791 | if (v && v.$_ref_$ && v.$_at_$) { // reference 792 | v = v.$_ref_$[v.$_at_$]; 793 | } 794 | return v; 795 | }; 796 | 797 | /* 798 | VM INSTRUCTION IMPLEMENTATIONS 799 | 800 | Notice that they have rt as parameter: indeed it may be different 801 | from the rt defined in this function rt_runtime(). 802 | */ 803 | 804 | /// ADD() pop y, pop x, push x + y 805 | rt.ADD = function(rt) { 806 | let y = rt.popval(); 807 | rt.stack.push(rt.popval() + y); 808 | }; 809 | rt.ADD.$name = "ADD"; 810 | 811 | /// APPLY() pop a, pop f, apply function f to list a. 812 | rt.APPLY = function(rt) { 813 | let a = rt.popval(); 814 | let f = rt.popval(); 815 | 816 | if (f && f.apply) { // Native JS function 817 | // If the function is native then we check for its name as key of the in $_this_$ 818 | if (rt.$_this_$ && f.name in rt.$_this_$.__proto__) { 819 | rt.stack.push(f.apply(rt.$_this_$, a)); 820 | } else { 821 | rt.stack.push(f.apply(null, a)); 822 | } 823 | } else { 824 | /* User defined function: saves rt.env, rt.code, rt.ic on the 825 | dump stack, next set rt.env to the closure environment, rc.code 826 | to the closure code, set the values of actual parameters for the 827 | formal ones and execute the code. */ 828 | // f = {code: [...], env: e, length: 0, parameters: [x1,..,xn]} 829 | sjs_error(!(f && "parameters" in f), f + " is not a function", rt.code[rt.ic]); 830 | // Prepare the environment rt.env for the function call 831 | rt.dump.push(rt.env); 832 | rt.env = {$_outer_$:f.env}; // closure environment outer to new one 833 | // Assign actual parameters to formal parameters 834 | for (let i = 0; i < f.parameters.length; ++ i) { 835 | rt.env[f.parameters[i]] = a[i]; // undefined if i >= a.length 836 | } 837 | sjs_execute(f.code, rt); // Execute function body. 838 | // The return value, if any, is on top of rt.stack. 839 | rt.env = rt.dump.pop(); // Restore the environment from rt.dump. 840 | } 841 | rt.$_this_$ = undefined; 842 | }; 843 | rt.APPLY.$name = "APPLY"; 844 | 845 | /// pop a, push a.length 846 | rt.ARRLEN = function(rt) { rt.stack.push(rt.popval().length); }; 847 | rt.ARRLEN.$name = "ARRLEN"; 848 | 849 | /** pop v, pop a, { a[a.length] = v; ++ a.length; } push a */ 850 | rt.ARRPUSH = function(rt) { 851 | let v = rt.popval(); 852 | let a = rt.popval(); 853 | a.push(v); 854 | rt.stack.push(a.slice()); 855 | }; 856 | rt.ARRPUSH.$name = "ARRPUSH"; 857 | 858 | /// pop a, x = a.shift(), push a, push x 859 | rt.ARRSHIFT = function(rt) { 860 | let a = rt.stack[rt.stack.length - 1]; 861 | let x = a.shift(); 862 | rt.stack.push(x); 863 | }; 864 | rt.ARRSHIFT.$name = "ARRSHIFT"; 865 | 866 | /** parse x, parse y, creates a closure with parameters the elements of x 867 | (a list of strings), body the code list y and environment a new one 868 | containing the parameters and the current environment as $_outer_$. */ 869 | rt.CLOSURE = function(rt) { 870 | let closure = {length:0, 871 | env: {$_outer_$: rt.env}, 872 | parameters: rt.code[rt.ic].i}; 873 | ++ rt.ic; 874 | closure.code = rt.code[rt.ic].i; 875 | ++ rt.ic; 876 | rt.stack.push(closure); 877 | }; 878 | rt.CLOSURE.$name = "CLOSURE"; 879 | 880 | /// pop v, pop x, { x = v; }, push v 881 | rt.DEC = function(rt) { rt.increase(-1); }; 882 | rt.DEC.$name = "DEC"; 883 | 884 | /* pop i, pop a value or reference and add to the "at" key the value of i. 885 | The effect of DEREF is to dereferentiate an object by one of its keys, 886 | the result still is a referece. Thus, if on the stack there are 887 | {ref: r, at: a} i then after DEREF it'll be {ref: a, at: i} and 888 | rt.$_this_$ (thus the "this" variable of the runtime) will be set 889 | to a. */ 890 | rt.DEREF = function(rt) { 891 | let i = rt.popval(); 892 | let r = rt.stack.pop(); 893 | if (r && r.$_ref_$ && r.$_at_$) { // reference? 894 | /* Notice: we have r = {ref:e, at:s} and we want to change 895 | it int {ref:e[s], at:i}. */ 896 | r = {$_ref_$:r.$_ref_$[r.$_at_$], $_at_$:i}; 897 | rt.$_this_$ = r.$_ref_$; 898 | } else if (r && r[i]) { // object? 899 | r = {$_ref_$: r, $_at_$:i}; 900 | //~ if (typeof(r[i]) == "object") { 901 | rt.$_this_$ = r.$_ref_$; 902 | //~ } 903 | } else { 904 | r = undefined; 905 | rt.$_this_$ = undefined; 906 | } 907 | rt.stack.push(r); 908 | }; 909 | rt.DEREF.$name = "DEREF"; 910 | 911 | /// pop y, pop x, push x / y 912 | rt.DIV = function(rt) { 913 | let y = rt.popval(); 914 | rt.stack.push(rt.popval() / y); 915 | }; 916 | rt.DIV.$name = "MUL"; 917 | 918 | /// pop x, discard the value 919 | rt.DROP = function(rt) { rt.stack.pop(); }; 920 | rt.DROP.$name = "DROP"; 921 | 922 | /// pop x, push x, push x 923 | rt.DUP = function(rt) { rt.stack.push(rt.stack[rt.stack.length - 1]); }; 924 | rt.DUP.$name = "DUP"; 925 | 926 | /// pop x, if x != 0 push x and perform JP, else perform NOP. 927 | rt.DUPJPNZ = function(rt) { 928 | let r = rt.popval(); 929 | if (r) { 930 | rt.stack.push(r); 931 | rt.JP(rt); 932 | } else { 933 | ++ rt.ic; // skip the operand of DUPJPNZ 934 | } 935 | }; 936 | rt.DUPJPNZ.$name = "DUPJPNZ"; 937 | 938 | /// pop x, if x == 0 push x and perform JP, else perform NOP. 939 | rt.DUPJPZ = function(rt) { 940 | let r = rt.popval(); 941 | if (!r) { 942 | rt.stack.push(r); 943 | rt.JP(rt); 944 | } else { 945 | ++ rt.ic; // skip the operand of DUPJPNZ 946 | } 947 | }; 948 | rt.DUPJPZ.$name = "DUPJPZ"; 949 | 950 | /// pop y, pop x, push x == y 951 | rt.EQ = function(rt) { 952 | let y = rt.popval(); 953 | rt.stack.push(rt.popval() == y); 954 | }; 955 | rt.EQ.$name = "EQ"; 956 | 957 | /// pop y, pop x, push x >= y 958 | rt.GE = function(rt) { 959 | let y = rt.popval(); 960 | rt.stack.push(rt.popval() >= y); 961 | }; 962 | rt.GE.$name = "GE"; 963 | 964 | /// pop y, pop x, push x > y 965 | rt.GT = function(rt) { 966 | let y = rt.popval(); 967 | rt.stack.push(rt.popval() > y); 968 | }; 969 | rt.GT.$name = "GT"; 970 | 971 | /// pop x, { ++ x; }, push x 972 | rt.INC = function(rt) { rt.increase(1); }; 973 | rt.INC.$name = "INC"; 974 | 975 | /// pop y, pop x, push (x in y) 976 | rt.IN = function(rt) { 977 | let y = rt.popval(), x = rt.popval(); 978 | rt.stack.push(x in y); 979 | }; 980 | rt.IN.$name = "IN"; 981 | 982 | /// parse n, set rt.ic = n. 983 | rt.JP = function(rt) { rt.ic += rt.code[rt.ic].i; }; 984 | rt.JP.$name = "JP"; 985 | 986 | /// parse n, pop x, if x != 0 perform JP n. 987 | rt.JPNZ = function(rt) { 988 | if (rt.popval()) { 989 | rt.ic += rt.code[rt.ic].i; 990 | } else { 991 | ++ rt.ic; 992 | }}; 993 | rt.JPNZ.$name = "JPNZ"; 994 | 995 | /// parse n, pop x, if x == 0 perform JP n. 996 | rt.JPZ = function(rt) { 997 | if (rt.popval()) { 998 | ++ rt.ic; 999 | } else { 1000 | rt.ic += rt.code[rt.ic].i; 1001 | }}; 1002 | rt.JPZ.$name = "JPZ"; 1003 | 1004 | /// pop y, pop x, push x <= y 1005 | rt.LE = function(rt) { 1006 | let y = rt.popval(); 1007 | rt.stack.push(rt.popval() <= y); 1008 | }; 1009 | rt.LE.$name = "LE"; 1010 | 1011 | /** pop v, pop s and creates a variable with name s and value v. Notice that 1012 | a variable's value is always an object, with key "value" the actual value. 1013 | In this way we can refer to the variable when assigning a value to it. */ 1014 | rt.LET = function(rt) { 1015 | let v = rt.popval(); 1016 | let s = rt.stack.pop(); // string expected 1017 | rt.env[s] = v; 1018 | }; 1019 | rt.LET.$name = "LET"; 1020 | 1021 | /// pop y, pop x, push x < y 1022 | rt.LT = function(rt) { 1023 | let y = rt.popval(); 1024 | rt.stack.push(rt.popval() < y); 1025 | }; 1026 | rt.LT.$name = "LT"; 1027 | 1028 | /// pop y, pop x, push x * y 1029 | rt.MUL = function(rt) { 1030 | let y = rt.popval(); 1031 | rt.stack.push(rt.popval() * y); 1032 | }; 1033 | rt.MUL.$name = "MUL"; 1034 | 1035 | /// pop y, pop x, push x != y 1036 | rt.NE = function(rt) { 1037 | let y = rt.popval(); 1038 | rt.stack.push(rt.popval() != y); 1039 | }; 1040 | rt.NE.$name = "NE"; 1041 | 1042 | /// pop x, push -x 1043 | rt.NEG = function(rt) { rt.stack.push(-rt.popval()); }; 1044 | rt.NEG.$name = "NEG"; 1045 | 1046 | /// pop a, pop f, push new f(a). 1047 | rt.NEW = function(rt) { 1048 | // This is probably bugged. 1049 | rt.$_this_$ = Object({}); 1050 | rt.$_this_$ = rt.popval(); 1051 | rt.stack.push(rt.$_this_$); 1052 | rt.$_this_$ = undefined; 1053 | }; 1054 | rt.NEW.$name = "NEW"; 1055 | 1056 | /// pop x, push !x 1057 | rt.NOT = function(rt) { rt.stack.push(!rt.popval()); }; 1058 | rt.NOT.$name = "NOT"; 1059 | 1060 | /// pop v, pop n, pop a, { a[n] = v; } push a 1061 | rt.OBJADD = function(rt) { 1062 | let v = rt.popval(); 1063 | let n = rt.popval(); 1064 | let a = rt.popval(); 1065 | //~ console.log(a + "[" + n + "] = " + v); 1066 | a[n] = v; 1067 | rt.stack.push(a); 1068 | }; 1069 | rt.OBJADD.$name = "OBJADD"; 1070 | 1071 | /// pop x, push the list of keys of object x. 1072 | rt.OBJKEYS = function(rt) { 1073 | let x = rt.popval(); 1074 | rt.stack.push(Object.keys(x)); 1075 | }; 1076 | rt.OBJKEYS.$name = "OBJKEYS"; 1077 | 1078 | /** pop x, parse s, parse c, define a variable s, for each key a in x 1079 | set s = a and executes c. */ 1080 | rt.OBJSCAN = function(rt) { 1081 | let x = rt.popval(); 1082 | let s = rt.code[rt.ic].i; 1083 | ++ rt.ic; 1084 | let c = rt.code[rt.ic].i; 1085 | ++ rt.ic; 1086 | rt.env[s] = undefined; // in case x = {} 1087 | rt.dump.push({code:rt.code, ic:rt.ic}); 1088 | rt.code = c; 1089 | for (let a in x) { 1090 | rt.env[s] = a; 1091 | rt.ic = 0; 1092 | sjs_execute(rt); 1093 | } 1094 | let saved = rt.dump.pop(); 1095 | rt.code = saved.code; 1096 | rt.ic = saved.ic; 1097 | }; 1098 | 1099 | // restore old environment 1100 | rt.POPENV = function(rt) { rt.env = rt.env.$_outer_$; }; 1101 | rt.POPENV.$name = "POPENV"; 1102 | 1103 | // Restore a previous rt.$_this_$ value. 1104 | rt.POPTHIS = function(rt) { rt.$_this_$ = rt.dump.pop(); }; 1105 | rt.POPTHIS.$name = "POPTHIS"; 1106 | 1107 | rt.POW = function(rt) { 1108 | let x = rt.popval(), y= rt.popval(); 1109 | rt.stack.push(y**x); 1110 | }; 1111 | rt.POW.$name = "POW"; 1112 | 1113 | /// parse v, push v 1114 | rt.PUSH = function(rt) { 1115 | let v = rt.code[rt.ic].i; 1116 | ++ rt.ic; 1117 | /* WARNING: objects need to be cloned, else other instructions 1118 | referring to them will actually change the object following 1119 | rt.PUSH in rt.code, modifying the code itself!!! */ 1120 | if (v && v.constructor == Array) { 1121 | rt.stack.push(v.slice()); 1122 | } else 1123 | if (v && v.constructor == Object) { 1124 | rt.stack.push(Object.assign({}, v)); 1125 | } else { 1126 | rt.stack.push(v); 1127 | } 1128 | }; 1129 | rt.PUSH.$name = "PUSH"; 1130 | 1131 | // create a new environment 1132 | rt.PUSHENV = function(rt) { rt.env = {$_outer_$: rt.env}; }; 1133 | rt.PUSHENV.$name = "PUSHENV"; 1134 | 1135 | // Saves the current rt.$_this_$ value. 1136 | rt.PUSHTHIS = function(rt) { rt.dump.push(rt.$_this_$); }; 1137 | rt.PUSHTHIS.$name = "PUSHTHIS"; 1138 | 1139 | /** parse s, look for variable s in the environment, push on the stack 1140 | a reference to the value, thus an object {ref:e, at:[a} . */ 1141 | rt.REF = function(rt) { 1142 | let s = rt.code[rt.ic].i; 1143 | ++ rt.ic; 1144 | // Looks in the runtime environment 1145 | for (let e = rt.env; e != null; e = e.$_outer_$) { 1146 | if (s in e) { 1147 | rt.stack.push({$_ref_$: e, $_at_$:s}); 1148 | return; 1149 | }} 1150 | sjs_error(true, s + " is not defined", rt.code[rt.ic]); 1151 | }; 1152 | rt.REF.$name = "REF"; 1153 | 1154 | /// Ends the execution of the current rt.code 1155 | rt.RET = function(rt) { rt.ic = rt.code.length; }; 1156 | rt.RET.$name = "RET"; 1157 | 1158 | /// pop v, pop x, { x = v; }, push v 1159 | rt.SET = function(rt) { 1160 | let v = rt.popval(); 1161 | let r = rt.stack.pop(); 1162 | sjs_error(!(r && r.$_ref_$), "Invalid LHS in assignment", rt.code[rt.ic]); 1163 | r.$_ref_$[r.$_at_$] = v; 1164 | rt.stack.push(v); 1165 | rt.$_this_$ = undefined; 1166 | }; 1167 | rt.SET.$name = "SET"; 1168 | 1169 | /// pop v, pop x, { x += v; }, push v 1170 | rt.SETADD = function(rt) { rt.increase(rt.popval()); }; 1171 | rt.SETADD.$name = "SETADD"; 1172 | 1173 | /// pop v, pop x, { x -= v; }, push v 1174 | rt.SETSUB = function(rt) { rt.increase(-rt.popval()); }; 1175 | rt.SETSUB.$name = "SETSUB"; 1176 | 1177 | /// pop y, pop x, push x - y 1178 | rt.SUB = function(rt) { 1179 | let y = rt.popval(); 1180 | rt.stack.push(rt.popval() - y); 1181 | }; 1182 | rt.SUB.$name = "SUB"; 1183 | 1184 | /// pop x, pop y, push x, push y 1185 | rt.SWAP = function(rt) { 1186 | let x = rt.stack.pop(), y = rt.stack.pop(); 1187 | rt.stack.push(x); 1188 | rt.stack.push(y); 1189 | }; 1190 | rt.SWAP.$name = "SWAP"; 1191 | 1192 | /// push rt.$_this_$ 1193 | rt.THIS = function(rt) { rt.stack.push(rt.$_this_$); }; 1194 | rt.THIS.$name = "THIS"; 1195 | 1196 | /// pop x, throw exception x 1197 | rt.THROW = function(rt) { throw rt.popval(); }; 1198 | rt.THROW.$name = "THROW"; 1199 | 1200 | /// pop v, push the string corresponding to the type of v 1201 | rt.TYPEOF = function(rt) { rt.stack.push(typeof(rt.popval())); }; 1202 | rt.TYPEOF.$name = "TYPEOF"; 1203 | 1204 | return rt; 1205 | }; 1206 | -------------------------------------------------------------------------------- /S_test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* _________________________________________________________________________ 4 | 5 | HOUSEHOLD FUNCTIONS 6 | _________________________________________________________________________ */ 7 | 8 | /** Raises an error with message msg if cond is true, else it does nothing. 9 | If token is not omitted, then it is used to refer to line and column 10 | where the error occurred (see the sjs_scan() function. */ 11 | let sjs_error = function(cond, msg, token) 12 | { 13 | if (cond) { 14 | msg = "Error: " + msg; 15 | if (token) { msg += " at " + token.l + ":" + token.c; } 16 | alert(msg); 17 | throw msg; 18 | } 19 | }; 20 | 21 | /// Raise an error on token.t != expected_token. 22 | let sjs_expected = function(token, expected_token) 23 | { 24 | sjs_error(token.t != expected_token, "'" + expected_token + "' expected", token); 25 | }; 26 | 27 | /** Return a string with a shallow representation of an object o. 28 | Since an env can have cyclic references, we omit it when 29 | parsing it in an object. */ 30 | let sjs_object2string = function(o) { 31 | let s = o; 32 | if (o == null) { s = "null"; } 33 | else if (o == undefined) { s = "undefined"; } 34 | else { 35 | if (typeof(o) == "object" || typeof(o) == "function") { 36 | if (o && o["$name"]) { s = o.$name; } 37 | else { 38 | if (o.length) { 39 | s = "[ "; 40 | for (let x in o) { 41 | // Prevent some possible infinite loop 42 | if (x != "$_ref_$" && x != "env" && x != "$_outer_$") { 43 | s += sjs_object2string(o[x]) + " "; 44 | } else { 45 | s += "{...} "; 46 | }} 47 | s += "]"; 48 | } else { 49 | s = "{ "; 50 | for (let x in o) { 51 | // Prevent some possible infinite loop 52 | //~ if (x != "$$_ref_$" && x != "env" && x != "$$_outer_$") { 53 | //~ s += x + ":" + sjs_object2string(o[x]) + " "; 54 | //~ } else { 55 | if (typeof(o[x]) == "object") { 56 | s += x + ":{...} "; 57 | } else { 58 | s += x + ":" + sjs_object2string(o[x]) + " "; 59 | }} 60 | s += "}"; 61 | }}}} 62 | return s; 63 | }; 64 | 65 | /* **************************************************************************** 66 | L E X I C A L A N A L Y Z E R 67 | **************************************************************************** */ 68 | 69 | /** Scans the JS sequence of tokens from the string text and returns it as an 70 | array of objects {"s": s, "t": t, "l": l, "c": c} where s is the string 71 | representation of the token, t the token type (number, string or the token 72 | itself in other cases), l the line number and c the colum number where 73 | the token occurs inside text. */ 74 | let sjs_scan = function(text) 75 | { 76 | let ALPHA = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm_$"; 77 | let DIGIT = "1234567890"; 78 | let ALNUM = ALPHA + DIGIT; 79 | let KEYWORDS = ["do", "else", "false", "for", "function", "if", "in", 80 | "let", "new", "null", "return", "this", "throw", 81 | "true", "typeof", "undefined", "while"]; 82 | let OPERATORS = ["**", "<=", ">=", "==", "!=", "+=", "-=", 83 | "&&", "||", "++", "--"]; 84 | 85 | // Enclose the text to scan between "{" and "}". 86 | let toklist = [{s: "{", t: "{", l:0, c:0}]; // list of all scanned tokens 87 | 88 | let line = 1; // current line number 89 | let i_line = 0; // Index of 1st character of the current line 90 | 91 | /* skip_until(i, delims) = least j>i such that text[j] is in delims. 92 | If no such j exists then return text.length. Keep track of the 93 | newlines inside the text so to count line numbers correctly. */ 94 | let skip_until = function(i, delims) { 95 | for (; i < text.length && !delims.includes(text[i]); ++ i) { 96 | if (text[i] == "\n") { 97 | ++ line; 98 | i_line = i + 1; 99 | }} 100 | return i; 101 | }; 102 | /* skip_while(i, delims) = least j>i such that text[j] is not in delims. 103 | If no such j exists then return text.length. Keep track of the 104 | newlines inside the text so to count line numbers correctly. */ 105 | let skip_while = function(i, delims) { 106 | for (; i < text.length && delims.includes(text[i]); ++ i) { 107 | if (text[i] == "\n") { 108 | ++ line; 109 | i_line = i + 1; 110 | }} 111 | return i; 112 | }; 113 | let i = 0; 114 | while (i < text.length) { 115 | let c = text[i]; 116 | let c1 = text[i+1]; 117 | let cc1 = c + c1; 118 | if (c == "\n") { // Skip newlines 119 | ++ line; 120 | i_line = ++ i; 121 | } else 122 | if (" \r\t\f\v".includes(c)) { // Skip spaces 123 | ++ i; 124 | } else 125 | if (c == "/" && c1 == "/") { // Skip line comments 126 | i = skip_until(i, "\n"); 127 | ++ line; 128 | i_line = ++ i; 129 | } else 130 | if (c == "/" && c1 == "*") { // Skip comments 131 | let i0 = i + 2; 132 | do { 133 | i = 1 + skip_until(i + 1, "*"); 134 | } while (text[i] != "/"); 135 | ++ i; 136 | } else 137 | if (ALPHA.includes(c)) { // Names 138 | let i0 = i; 139 | i = skip_while(i0 + 1, ALNUM); 140 | let name = text.slice(i0, i); 141 | if (KEYWORDS.includes(name)) { 142 | toklist.push({s: name, t: name, l: line, c: i0 - i_line}); 143 | } else { 144 | toklist.push({s: text.slice(i0, i), t: "name", l: line, c: i0 - i_line}); 145 | } 146 | } else 147 | if (DIGIT.includes(c)) { // Numbers 148 | let i0 = i; 149 | i = skip_while(i0 + 1, DIGIT); 150 | if (text[i] == ".") { 151 | i = skip_while(i + 1, DIGIT); 152 | } 153 | toklist.push({s: Number(text.slice(i0, i)), t: "number", l: line, c: i0 - i_line}); 154 | } else 155 | if (c == "'" || c == '"') { // Strings 156 | let i0 = ++ i; 157 | i = skip_until(i, c); 158 | toklist.push({s: text.slice(i0, i), t: "string", l: line, c: i0 - i_line}); 159 | ++ i; 160 | } else 161 | if (OPERATORS.includes(cc1)) { 162 | toklist.push({s: cc1, t: cc1, l: line, c: i - i_line}); 163 | i += 2; 164 | } else 165 | if ("-+*/<>=!.,;:()[]{}".includes(c)) { 166 | toklist.push({s: c, t: c, l: line, c: i - i_line}); 167 | ++ i; 168 | } else { 169 | sjs_error(true, ": Syntax error: '" + c + "'", {l:line, c:(i - i_line)}); 170 | }} 171 | // Enclose the text to scan between "{" and "}". 172 | toklist.push({s: "}", t: "}", l:0, c:0}); 173 | 174 | return toklist; 175 | }; 176 | 177 | /* **************************************************************************** 178 | S Y N T A C T I C A N A L Y Z E R A N D C O D E G E N E R A T O R 179 | **************************************************************************** */ 180 | 181 | /** Compiler entry point: accepts a token list as produced by sjs_scan and 182 | returns a runtime environment with the compiled code and the stuff needed 183 | by the sjs_run function to actually run the code. On error an exception 184 | is thrown. */ 185 | let sjs_compile = function(tl) 186 | { 187 | let rt = sjs_runtime(); 188 | sjs_compile_block(tl, rt); 189 | return rt; 190 | }; 191 | 192 | /// Compile a sequence s of objects into rt.code. 193 | let sjs_compile_code = function(rt, s, t) 194 | { 195 | for (let i = 0; i < s.length; ++ i) { 196 | rt.code.push({i: s[i], l: t.l, c:t.c}); 197 | } 198 | }; 199 | 200 | /** Compile a value to be pushed on the stack at runtime. */ 201 | let sjs_compile_value = function(rt, v, t) 202 | { 203 | rt.code.push({i: rt.PUSH, l: t.l, c:t.c}); 204 | rt.code.push({i: v, l: t.l, c:t.c}); 205 | }; 206 | 207 | /** \note 208 | 209 | The following functions are used to analyze and compile JS expressions: 210 | during the compilation. Therein 211 | - the rt.code array is extended with the code generated; 212 | - the rt.ic value is increased each time a new function definition is 213 | parsed (so that rt.ic == 0 means we are not inside any function, so 214 | that "return" will raise an error). 215 | */ 216 | 217 | /** Compile a list of expressions from tl into an array which is pushed on the 218 | stack at runtime; the endchr is the one checked against the end of the list. 219 | This function is used with lists [x1,...,xn] or (x1,...,xn). */ 220 | let sjs_compile_array = function(tl, rt, endchr) 221 | { 222 | /* An empty array is created and elements are added to it 223 | [v1, ..., vk] is compiled as [] v1 ARRPUSH ... vk ARRPUSH */ 224 | let token = tl[0]; // probe but not scan the next token 225 | sjs_compile_value(rt, [], token); // compile "PUSH []" 226 | if (token.t == endchr) { 227 | // Empty list: drop the end of list token and return 228 | tl.shift(); 229 | } else { 230 | do { 231 | token = sjs_compile_expression(tl, rt); 232 | // a v ARRPUSH -> a[a.length] = v and a on the stack 233 | sjs_compile_code(rt, [rt.ARRPUSH], token); 234 | } while (token.t == ","); 235 | sjs_expected(token, endchr, token); 236 | } 237 | }; 238 | 239 | /** Compile a list of pairs key:value from tl into an object which is pushed 240 | on the stack at runtime. */ 241 | let sjs_compile_object = function(tl, rt) 242 | { 243 | /* An empty object is created and keys are added to it. 244 | {n1:v1, ..., nk:vk} is compiled as {} n1 v1 OBJADD ... nk vk OBJADD */ 245 | let token = tl[0]; // probe but not scan the next token 246 | sjs_compile_value(rt, {}, token); // compile "PUSH {}" 247 | if (token.t == "}") { 248 | // Empty object: drop the "}" token and return 249 | tl.shift(); 250 | } else { 251 | do { 252 | token = tl.shift(); 253 | // We expect a key: name or string 254 | sjs_error(token.t != "name" && token.t != "string", 255 | "Invalid object key: " + token.s, 256 | token); 257 | sjs_compile_value(rt, token.s, token); // OBJADD expect this 258 | sjs_expected(tl.shift(), ":"); 259 | token = sjs_compile_expression(tl, rt); 260 | // obj name value OBJADD -> obj[name] = value and obj on the stack 261 | sjs_compile_code(rt, [rt.OBJADD], token); 262 | } while (token.t == ","); 263 | sjs_expected(token, "}", token); 264 | } 265 | }; 266 | 267 | /** Compile a function() {...} into an object which is pushed on the stack 268 | at runtime. */ 269 | let sjs_compile_function = function(tl, rt) 270 | { 271 | /* A function(params) { body} is compiled as a sequence of three values: 272 | [CLOSURE(), params, body] where 273 | 274 | - CLOSURE is the function to be executed at runtime to create the 275 | closure and push it on the stack. 276 | - params is the array [x1,...,xn] of formal parameters 277 | - body is an array containing the compiled code of the closure body 278 | 279 | During runtime, the CLOSURE instruction creates a new environment, 280 | inserts into it the formal parameters as variables whose initial 281 | values are actual parameters and execute the closure body. */ 282 | 283 | // Parse the formal parameter list "(name, ..., name)" 284 | let parameters = []; 285 | sjs_expected(tl.shift(), "("); 286 | let token = tl.shift(); 287 | while (token.t != ")") { 288 | sjs_expected(token, "name"); 289 | parameters.push(token.s); 290 | token = tl.shift(); 291 | if (token.t == ",") { 292 | token = tl.shift(); 293 | } else { 294 | sjs_expected(token, ")"); 295 | }} 296 | // Parse and compile the function's body 297 | ++ rt.ic; // this means "inside a nested function" 298 | // Save the current code since we want to compile the body from scratch 299 | let code_saved = rt.code; 300 | rt.code = []; 301 | sjs_compile_block(tl, rt); 302 | // Save the compiled body and restore the code 303 | let body = rt.code; 304 | rt.code = code_saved; 305 | // If there's no final "return" add it! Needed at runtime. 306 | if (body[body.length - 1].i != rt.RET) { 307 | body.push({i: rt.PUSH, l: tl[0].l, c: tl[0].c}); 308 | body.push({i: undefined, l: tl[0].l, c: tl[0].c}); 309 | body.push({i: rt.RET, l: tl[0].l, c: tl[0].c}); 310 | } 311 | -- rt.ic; 312 | sjs_compile_code(rt, [rt.CLOSURE, parameters, body], token); 313 | }; 314 | 315 | /** Compile an operand into rt.code parsing it from the token list tl, which is 316 | consumed doing so. The first token of the operand is passed in the first 317 | argument, while the first token following the expression is returned as 318 | value. */ 319 | let sjs_compile_operand = function(token, tl, rt) 320 | { 321 | // Check against constants 322 | if (token.t == "number") { sjs_compile_value(rt, token.s, token); } 323 | else if (token.t == "string") { sjs_compile_value(rt, token.s, token); } 324 | else if (token.t == "false") { sjs_compile_value(rt, false, token); } 325 | else if (token.t == "true") { sjs_compile_value(rt, true, token); } 326 | else if (token.t == "null") { sjs_compile_value(rt, null, token); } 327 | else if (token.t == "undefined") { sjs_compile_value(rt, undefined, token); } 328 | // Check against literal objects 329 | else if (token.t == "function") { sjs_compile_function(tl, rt); } 330 | else if (token.t == "{") { 331 | //~ sjs_compile_code(rt, [rt.PUSHTHIS], token); 332 | sjs_compile_object(tl, rt); 333 | //~ sjs_compile_code(rt, [rt.POPTHIS], token); 334 | } 335 | else if (token.t == "[") { 336 | //~ sjs_compile_code(rt, [rt.PUSHTHIS], token); 337 | sjs_compile_array(tl, rt, "]"); 338 | //~ sjs_compile_code(rt, [rt.POPTHIS], token); 339 | } 340 | // Check against variable names 341 | else if (token.t == "name") { sjs_compile_code(rt, [rt.REF, token.s], token); } 342 | // If all else fails, we expect a subexpression "(expr)" 343 | else if (token.t == "(") { 344 | //~ sjs_compile_code(rt, [rt.PUSHTHIS], token); 345 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 346 | //~ sjs_compile_code(rt, [rt.POPTHIS], token); 347 | } 348 | else if (token.t == "this") { 349 | sjs_compile_code(rt, [rt.THIS], token); 350 | } 351 | else { sjs_error(true, "Syntax error: " + token.s, token); } 352 | return tl.shift(); 353 | }; 354 | 355 | /** Compiles an expression into rt.code parsing it from the token list tl, 356 | which is consumed doing so. The first token following the expression is 357 | returned as value. To implement operator precedence the function uses a 358 | classic bottom-up technique: each operator has a priority number 359 | associated. When an operator is parsed, if the operator on the stack had 360 | higher priority then the latter is compiled and the former pushed on the 361 | stack; else the former is pushed on the stack. When the expression has 362 | completely been parsed, the stack is unwinded by compiling each element 363 | until it is empty. */ 364 | let sjs_compile_expression = function(tl, rt) 365 | { 366 | let stack = []; // stack where operators are pushed before being compiled 367 | 368 | let PRIORITIES = { 369 | "fake": 0, // fake operator, needed in while condition in sjs_compile_stack. 370 | "=": 5, "+=": 5, "-=": 5, "||": 10, "&&": 15, "==": 20, "!=": 20, 371 | "<": 25, ">": 25, "<=": 25, ">=": 25, "in":25, "+": 30, "-": 30, 372 | "*": 40, "/": 40, "**": 42, "new": 45, "-NEG-": 50, "!": 50, "++": 50, "--": 50, "typeof": 50 373 | }; 374 | let OPERATORS = { 375 | "=": rt.SET, "+=": rt.SETADD, "-=": rt.SETSUB, "==": rt.EQ, "!=": rt.NE, 376 | "<": rt.LT, ">": rt.GT, "<=": rt.LE, ">=": rt.GE, "in": rt.IN, 377 | "+": rt.ADD, "-": rt.SUB, "*": rt.MUL, "/": rt.DIV, "**": rt.POW, "new": rt.NEW, 378 | "-NEG-": rt.NEG, "!": rt.NOT, "++": rt.INC, "--": rt.DEC, "typeof": rt.TYPEOF 379 | }; 380 | 381 | /// Compile all operators in the stack with priority > the priority of opt. 382 | let compile_stack = function(opt) { 383 | while (stack.length > 0 && PRIORITIES[stack[stack.length-1]] >= PRIORITIES[opt]) { 384 | let opt_to_dump = stack.pop(); 385 | if (opt_to_dump == "&&" || opt_to_dump == "||") { 386 | // pop from cstack where to write the jump. 387 | let i = cstack.pop(); 388 | rt.code[i].i = rt.code.length - i; 389 | } else { 390 | sjs_compile_code(rt, [OPERATORS[opt_to_dump]], tl[0]); 391 | }}}; 392 | 393 | let cstack = []; // Stack used for forward reference in && and || 394 | let token; // last parsed token from tl inside the do{...} loop 395 | let again; // do{...} loop iteration condition, defined inside the loop 396 | 397 | do { 398 | token = tl.shift(); // Prefix operator or operand expected 399 | 400 | // Is token a prefix operator? 401 | // (Notice: check tokens via token.t unless is a name, string, number). 402 | while (token.t == "!" || token.t == "-" || token.t == "--" || token.t == "++" || token.t == "typeof" || token.t == "new") { 403 | // A minus token means a negation 404 | let t = token.t == "-" && "-NEG-" || token.t; 405 | compile_stack(t); 406 | stack.push(t); 407 | token = tl.shift(); 408 | } 409 | // Operand expected in any case. 410 | token = sjs_compile_operand(token, tl, rt); 411 | 412 | // Is token a postfix operator? 413 | while (token.t == "." || token.t == "(" || token.t == "[") { 414 | if (token.t == "(") { // Actual parameter list 415 | // First of all takes the value of the function 416 | sjs_compile_code(rt, [rt.PUSHTHIS], token); 417 | sjs_compile_array(tl, rt, ")"); 418 | sjs_compile_code(rt, [rt.POPTHIS, rt.APPLY], token); 419 | } else 420 | if (token.t == ".") { // Member operator x.y 421 | token = tl.shift(); 422 | sjs_expected(token, "name"); 423 | sjs_compile_value(rt, token.s, token); 424 | sjs_compile_code(rt, [rt.DEREF], token); 425 | } else { 426 | // Assert token.t == "["; Member operator x[y] 427 | sjs_compile_code(rt, [rt.PUSHTHIS], token); 428 | token = sjs_compile_expression(tl, rt); 429 | sjs_expected(token, "]"); 430 | sjs_compile_code(rt, [rt.POPTHIS, rt.DEREF], token); 431 | } 432 | token = tl.shift(); 433 | } 434 | // Is there a binary operator? 435 | again = PRIORITIES[token.t]; // if undefined no! 436 | if (again) { 437 | // Compile elements in the stack with higher priorities 438 | compile_stack(token.t); 439 | stack.push(token.t); 440 | /* Shortcuts operator need more work: they also compile a JUMP 441 | and save the index of the element of code containing the 442 | jump address, to be written after the second operand will 443 | be compiled. */ 444 | if (token.t == "&&" || token.t == "||") { 445 | sjs_compile_code(rt, [token.t == "&&" && rt.DUPJPZ || rt.DUPJPNZ], token); 446 | /* The index where to jump shall be written at 447 | rt.code[rt.code.length] after the second operand will 448 | be compiled. Now save the position where to store it 449 | in the "control stack". */ 450 | cstack.push(rt.code.length); 451 | sjs_compile_code(rt, [0], token); // 0 overwritten in compile_stack. 452 | }} 453 | } while (again); 454 | 455 | // If there are operators on the stack, compile them all. 456 | compile_stack("fake"); 457 | 458 | return token; 459 | }; 460 | 461 | /** 462 | \note Analyze and compile JS statements. 463 | */ 464 | 465 | /** Compile the do {p} while (c) statement. Assume the "do" token 466 | to be already shifted from tl. */ 467 | let sjs_compile_do = function(tl, rt) 468 | { 469 | /* "do {p} while(c)" is compiled as "again: p c JPNZ again". */ 470 | sjs_compile_code(rt, [rt.PUSHENV], tl[0]); // New scope at runtime 471 | let again = rt.code.length; // where to jump to repeat the loop 472 | // Compile {p} appending it to rt.code 473 | sjs_compile_block(tl, rt); // compile {p} 474 | sjs_error(tl.shift().s != "while", "'while' expected", rt.code[rt.code.length-1]); 475 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 476 | // Compile a JPNZ to again to repeat the loop (notice the -1, is correct) 477 | sjs_compile_code(rt, [rt.JPNZ, again - rt.code.length - 1], tl[0]); 478 | sjs_compile_code(rt, [rt.POPENV], tl[0]); // Restore scope at runtime 479 | }; 480 | 481 | /** Compile the for (let s in e) {p} statement. 482 | Assume the "for (" tokens to be already shifted from tl. */ 483 | let sjs_compile_for_in = function(tl, rt) 484 | { 485 | /* for (let s in e) {p} is compiled as: 486 | PUSH s PUSH null LET Create variable s 487 | e OBJKEYS Push the array of e keys 488 | again: DUP ARRLEN JPZ leave If the array is empty then finish 489 | ARRSHIFT shift the first key from the array 490 | REF s SWAP SET DROP s = array[0] 491 | p execute body 492 | JP again repeat loop 493 | leave: DROP array no more needed */ 494 | tl.shift(); // Skip "let" 495 | let s = tl.shift().s; // skip s and take note of it 496 | tl.shift(); // skip "in" 497 | sjs_compile_code(rt, [rt.PUSHENV, 498 | rt.PUSH, s, 499 | rt.PUSH, undefined, 500 | rt.LET], tl[0]); 501 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 502 | sjs_compile_code(rt, [rt.OBJKEYS], tl[0]); 503 | let again = rt.code.length; 504 | let leave = again + 3; // index of the 0 placeholder after JPZ 505 | sjs_compile_code(rt, 506 | [rt.DUP, // DUP since JPZ destroys it 507 | rt.ARRLEN, // Lenght of remained keys to loop on 508 | rt.JPZ, 0, // JP after the loop, 0 is a placeholder 509 | rt.ARRSHIFT, // array -> its shifted 1st element 510 | rt.REF, s, 511 | rt.SWAP, // SET needs (... ref val) on the stack 512 | rt.SET, 513 | rt.DROP // SET leaves a value, discard it 514 | ], tl[0]); 515 | sjs_compile_block(tl, rt); // compile {p} 516 | // Compile a JP to again to repeat the loop (notice the -1, is correct) 517 | sjs_compile_code(rt, [rt.JP, again - rt.code.length - 1], tl[0]); 518 | rt.code[leave].i = rt.code.length - leave; 519 | sjs_compile_code(rt, [rt.DROP, rt.POPENV], tl[0]); 520 | }; 521 | 522 | /** Compile the for (a; c; i) {p} statement. 523 | Assume the "for (" tokens to be already shifted from tl. */ 524 | let sjs_compile_for = function(tl, rt) 525 | { 526 | /* for (a; c; i) {p} is compiled as: 527 | a again: c JPZ leave p i JP again leave: */ 528 | sjs_compile_code(rt, [rt.PUSHENV], tl[0]); 529 | // Compile a: it can be empty, an expression or a let statement 530 | if (tl[0].s == "let") { 531 | tl.shift(); // Skip "let" since compile_let expect this. 532 | sjs_compile_let(tl, rt); 533 | } else 534 | if (tl[0].s == ";") { // Empty assignment: skip the ";" 535 | tl.shift(); 536 | } else { 537 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 538 | sjs_compile_code(rt, [rt.DROP], tl[0]); // discard the value of the expression 539 | } 540 | // Compile c: it can be empty 541 | let again = rt.code.length; // where to jump to repeat the loop 542 | if (tl[0].s == ";") { // Empty expression: skip the ";" 543 | tl.shift(); 544 | } else { 545 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 546 | } 547 | // Compile JPZ leave 548 | sjs_compile_code(rt, [rt.JPZ], tl[0]); 549 | let leave = rt.code.length; // where to jump to leave the loop 550 | sjs_compile_code(rt, [0], tl[0]); // 0 is a placeholder here 551 | /* Now compile i that eventually will appear after p: to do that, 552 | mark the beginning of i (which is at leave + 1) and then insert 553 | the compiled body before it. */ 554 | if (tl[0].s == ")") { // Empty expression: nothing to do 555 | tl.shift(); 556 | sjs_compile_block(tl, rt); // compile {p} 557 | } else { 558 | // Compile i, cut it from rt.code, compile p and paste i after p 559 | sjs_expected(sjs_compile_expression(tl, rt), ")"); // compile i 560 | let i = rt.code.slice(leave + 1, rt.code.length); // save i 561 | rt.code = rt.code.slice(0, leave + 1); // drop i from code 562 | sjs_compile_block(tl, rt); // compile {p} 563 | // Append the increment/decrement part 564 | rt.code = rt.code.concat(i); 565 | // Expression c figures only for side effects: discard its value 566 | sjs_compile_code(rt, [rt.DROP], tl[0]); 567 | } 568 | // Compile the jump to repeat the loop 569 | sjs_compile_code(rt, [rt.JP, again - rt.code.length - 1], tl[0]); 570 | // Compile the offset for the JPZ leave here. 571 | rt.code[leave].i = rt.code.length - leave; 572 | // Back to the old scope 573 | sjs_compile_code(rt, [rt.POPENV], tl[0]); 574 | }; 575 | 576 | /** Compile if (e) {p1} else {p2}. Assume the "if" token to be 577 | already shifted from tl. */ 578 | let sjs_compile_if = function(tl, rt) 579 | { 580 | /* if (c) {p1} else {p2} is compiled as 581 | c JPZ other p1 JP after other: p2 after: 582 | if (c) {p1} is compiled as 583 | c JPZ other p1 other: */ 584 | // Compile the (c) condition 585 | sjs_expected(tl.shift(), "("); 586 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 587 | /* Now compile a JPZ to the first instruction after the if: 588 | that'll be determined after compiling the if-else, so we keep 589 | track of the index of the code element where this information 590 | will be stored after the if-else has been compiled. */ 591 | let other = rt.code.length + 1; // index of 0 in the following s 592 | // 0 after JPZ is a placeholder 593 | sjs_compile_code(rt, [rt.JPZ, 0, rt.PUSHENV], tl[0]); 594 | sjs_compile_block(tl, rt); // compile {p1} 595 | sjs_compile_code(rt,[rt.POPENV], tl[0]); 596 | if (tl[0].s != "else") { 597 | // The if (c) {p1} has been compiled: let JPZ jump here. 598 | rt.code[other].i = rt.code.length - other; 599 | } else { 600 | tl.shift(); // skip "else" 601 | let after = rt.code.length + 1; // index of 0 in the following s 602 | sjs_compile_code(rt, [rt.JP, 0], tl[0]); // to be overwritten later! 603 | // Let JPZ (after if) jump here. 604 | rt.code[other].i = rt.code.length - other; 605 | /* Now compile the p2 appending inside a new environment, appending 606 | it to rt.code. Instead of {p2} if may appear a if (...). */ 607 | if (tl[0].s == "if") { 608 | tl.shift(); 609 | sjs_compile_if(tl, rt, tl[0]); 610 | } else { 611 | sjs_compile_code(rt,[rt.PUSHENV], tl[0]); 612 | sjs_compile_block(tl, rt); 613 | sjs_compile_code(rt,[rt.POPENV], tl[0]); 614 | } 615 | // The if (c) {p1} else {p2} has been compiled: let JP jump here. 616 | rt.code[after].i = rt.code.length - after; 617 | } 618 | }; 619 | 620 | /** Compile a "let x1 = v1,...,xn=vn;" instruction from tl at rt. 621 | Assume the "let" token to be already shifted from tl. */ 622 | let sjs_compile_let = function(tl, rt) 623 | { 624 | let token; 625 | do { 626 | token = tl.shift(); 627 | sjs_expected(token, "name"); 628 | sjs_compile_value(rt, token.s, token); 629 | token = tl.shift(); 630 | if (token.t == "=") { 631 | token = sjs_compile_expression(tl, rt); 632 | } else { 633 | // variable default value 634 | sjs_compile_value(rt, undefined, token); 635 | } 636 | sjs_compile_code(rt,[rt.LET], tl[0]); 637 | } while (token.t == ","); 638 | sjs_expected(token, ";"); 639 | }; 640 | 641 | /** Compile while (c) {p}. Assume the "while" token to be 642 | already shifted from tl. */ 643 | let sjs_compile_while = function(tl, rt) 644 | { 645 | /* while (c) {p} is compiled as 646 | again: (c) JPZ leave p JP again leave: */ 647 | // New scope introduced at runtime 648 | sjs_compile_code(rt,[rt.PUSHENV], tl[0]); 649 | // Compile the (c) condition 650 | let again = rt.code.length; // jump here to repeat the loop 651 | sjs_expected(tl.shift(), "("); 652 | sjs_expected(sjs_compile_expression(tl, rt), ")"); 653 | /* Now compile a JPZ to the first instruction after the while: 654 | that'll be determined after compiling the loop, so we keep 655 | track of the index of the code element where this information 656 | will be stored after the loop has been compiled. */ 657 | let leave = rt.code.length + 1; // index of 0 in the following s 658 | sjs_compile_code(rt, [rt.JPZ, 0], tl[0]); // 0 to be overwritten later! 659 | sjs_compile_block(tl, rt); // compile {p} 660 | // Compile a JP to again to repeat the loop (notice the -1, is correct) 661 | sjs_compile_code(rt, [rt.JP, again - rt.code.length - 1], tl[0]); 662 | // Compile the offset for the JPZ leave here. 663 | rt.code[leave].i = rt.code.length - leave; 664 | // Back to the old scope 665 | sjs_compile_code(rt,[rt.POPENV], tl[0]); 666 | }; 667 | 668 | /** Given a token list produce a runtime object containing the compiled code. 669 | The token list should ALWAYS be a block {...}. */ 670 | let sjs_compile_block = function(tl, rt) 671 | { 672 | let token = tl.shift(); 673 | sjs_expected(token, "{"); 674 | 675 | // Consume the token list as far as it is parsed 676 | while ((token = tl.shift()).s != "}") { 677 | if (token.t == ";") { /* Empty statement, nothing to compile! */ } 678 | else if (token.t == "do") { sjs_compile_do(tl, rt);} 679 | else if (token.t == "for") { 680 | sjs_expected(tl.shift(), "("); 681 | if (tl[0].t == "let" && tl[1].t == "name" && tl[2].t == "in") { 682 | sjs_compile_for_in(tl, rt); 683 | } else { 684 | sjs_compile_for(tl, rt); 685 | } 686 | } 687 | else if (token.t == "if") { sjs_compile_if(tl, rt); } 688 | else if (token.t == "let") { sjs_compile_let(tl, rt); } 689 | else if (token.t == "return") { 690 | sjs_error(rt.ic == 0, "Illegal return statement", token); 691 | if (tl[0].t != ";") { token = sjs_compile_expression(tl, rt); } 692 | else { 693 | sjs_compile_value(rt, undefined, token); 694 | token = tl.shift(); 695 | } 696 | sjs_compile_code(rt,[rt.RET], token); 697 | sjs_expected(token, ";"); 698 | } 699 | else if (token.t == "throw") { 700 | token = sjs_compile_expression(tl, rt); 701 | sjs_compile_code(rt,[rt.THROW], token); 702 | sjs_expected(token, ";"); 703 | } 704 | else if (token.t == "while") { sjs_compile_while(tl, rt); } 705 | else { 706 | // Expression statement 707 | tl.unshift(token); 708 | sjs_expected(sjs_compile_expression(tl, rt), ";"); 709 | sjs_compile_code(rt,[rt.DROP], tl[0]); // discard the result of the expression 710 | }} 711 | sjs_expected(token, "}"); 712 | }; 713 | 714 | /* **************************************************************************** 715 | R U N T I M E S T U F F 716 | **************************************************************************** */ 717 | 718 | /** A runtime object rt, as returned by sjs_compile_block, is executed. 719 | If debug is not null, 0 nor undefined then execution is dumped on the 720 | console step by step. On error throws an exception, via sjs_error. */ 721 | let sjs_run = function(rt, debug) 722 | { 723 | // rt.env.$$_outer_$ = environment at outer scope. 724 | /* The outmost scope is the window object of the JS engine executing 725 | executing the compiler ;-). */ 726 | rt.env = { $$_outer_$: window }; 727 | rt.env.$$_outer_$.$$_outer_$ = null; 728 | rt.stack = []; 729 | rt.dump = []; 730 | rt.debug = debug; 731 | //~ rt.$$_this_$ = rt.env.$$_outer_$; 732 | rt.$$_this_$ = undefined; 733 | sjs_execute(rt.code, rt); 734 | }; 735 | 736 | /// Executes code in runtime environment rt 737 | let sjs_execute = function(code, rt) 738 | { 739 | /// Returns a string representing the stack contents. 740 | let stackdump = function(rt) { 741 | let s = "Stack: ["; 742 | for (let i = 0; i < rt.stack.length; ++ i) { 743 | s += sjs_object2string(rt.stack[i]) + ", "; 744 | } 745 | return s + "]"; 746 | }; 747 | 748 | // Saves code and ic since sjs_execute can be recursively called. 749 | let code_saved = rt.code; 750 | let ic_saved = rt.ic; 751 | rt.code = code; 752 | rt.ic = 0; 753 | 754 | while (rt.ic < rt.code.length) { 755 | let ic = rt.ic; 756 | ++ rt.ic; 757 | if (rt.debug) { 758 | console.log(stackdump(rt)); 759 | console.log("this = " + sjs_object2string(rt.$$_this_$)); 760 | if (rt.code[ic].i.$name) { 761 | console.log("[" + ic + "|" + rt.code[ic].l + ":" + rt.code[ic].c + "] " + rt.code[ic].i.$name); 762 | } else { 763 | console.log("[" + ic + "|" + rt.code[ic].l + ":" + rt.code[ic].c + "] " + rt.code[ic].i); 764 | }} 765 | rt.code[ic].i(rt); 766 | } 767 | // Restores original values 768 | rt.code = code_saved; 769 | rt.ic = ic_saved; 770 | }; 771 | 772 | // Create a runtime environment 773 | let sjs_runtime = function() 774 | { 775 | let rt = {code:[], ic:0, stack:[], dump:[]}; 776 | 777 | // AUXILIARY FUNCTIONS 778 | /** Pop a reference r, deference it and increases its value by v: used by 779 | DEC, INC, SETADD, SETSUB. */ 780 | rt.increase = function(v) { 781 | let r = rt.stack.pop(); 782 | sjs_error(!(r && r.$$_ref_$), "Invalid LHS in assignment", rt.code[rt.ic]); 783 | r.$$_ref_$[r.$$_at_$] += v; 784 | rt.stack.push(r.$$_ref_$[r.$$_at_$]); 785 | }; 786 | 787 | /** Pop a value v: if v = {$$_ref_$:r, $$_at_$:a} then return r[a], 788 | else return v. */ 789 | rt.popval = function() { 790 | let v = rt.stack.pop(); 791 | if (v && v.$$_ref_$ && v.$$_at_$) { // reference 792 | v = v.$$_ref_$[v.$$_at_$]; 793 | } 794 | return v; 795 | }; 796 | 797 | /* 798 | VM INSTRUCTION IMPLEMENTATIONS 799 | 800 | Notice that they have rt as parameter: indeed it may be different 801 | from the rt defined in this function rt_runtime(). 802 | */ 803 | 804 | /// ADD() pop y, pop x, push x + y 805 | rt.ADD = function(rt) { 806 | let y = rt.popval(); 807 | rt.stack.push(rt.popval() + y); 808 | }; 809 | rt.ADD.$name = "ADD"; 810 | 811 | /// APPLY() pop a, pop f, apply function f to list a. 812 | rt.APPLY = function(rt) { 813 | let a = rt.popval(); 814 | let f = rt.popval(); 815 | 816 | if (f && f.apply) { // Native JS function 817 | // If the function is native then we check for its name as key of the in $$_this_$ 818 | if (rt.$$_this_$ && f.name in rt.$$_this_$.__proto__) { 819 | rt.stack.push(f.apply(rt.$$_this_$, a)); 820 | } else { 821 | rt.stack.push(f.apply(null, a)); 822 | } 823 | } else { 824 | /* User defined function: saves rt.env, rt.code, rt.ic on the 825 | dump stack, next set rt.env to the closure environment, rc.code 826 | to the closure code, set the values of actual parameters for the 827 | formal ones and execute the code. */ 828 | // f = {code: [...], env: e, length: 0, parameters: [x1,..,xn]} 829 | sjs_error(!(f && "parameters" in f), f + " is not a function", rt.code[rt.ic]); 830 | // Prepare the environment rt.env for the function call 831 | rt.dump.push(rt.env); 832 | rt.env = {$$_outer_$:f.env}; // closure environment outer to new one 833 | // Assign actual parameters to formal parameters 834 | for (let i = 0; i < f.parameters.length; ++ i) { 835 | rt.env[f.parameters[i]] = a[i]; // undefined if i >= a.length 836 | } 837 | sjs_execute(f.code, rt); // Execute function body. 838 | // The return value, if any, is on top of rt.stack. 839 | rt.env = rt.dump.pop(); // Restore the environment from rt.dump. 840 | } 841 | rt.$$_this_$ = undefined; 842 | }; 843 | rt.APPLY.$name = "APPLY"; 844 | 845 | /// pop a, push a.length 846 | rt.ARRLEN = function(rt) { rt.stack.push(rt.popval().length); }; 847 | rt.ARRLEN.$name = "ARRLEN"; 848 | 849 | /** pop v, pop a, { a[a.length] = v; ++ a.length; } push a */ 850 | rt.ARRPUSH = function(rt) { 851 | let v = rt.popval(); 852 | let a = rt.popval(); 853 | a.push(v); 854 | rt.stack.push(a.slice()); 855 | }; 856 | rt.ARRPUSH.$name = "ARRPUSH"; 857 | 858 | /// pop a, x = a.shift(), push a, push x 859 | rt.ARRSHIFT = function(rt) { 860 | let a = rt.stack[rt.stack.length - 1]; 861 | let x = a.shift(); 862 | rt.stack.push(x); 863 | }; 864 | rt.ARRSHIFT.$name = "ARRSHIFT"; 865 | 866 | /** parse x, parse y, creates a closure with parameters the elements of x 867 | (a list of strings), body the code list y and environment a new one 868 | containing the parameters and the current environment as $$_outer_$. */ 869 | rt.CLOSURE = function(rt) { 870 | let closure = {length:0, 871 | env: {$$_outer_$: rt.env}, 872 | parameters: rt.code[rt.ic].i}; 873 | ++ rt.ic; 874 | closure.code = rt.code[rt.ic].i; 875 | ++ rt.ic; 876 | rt.stack.push(closure); 877 | }; 878 | rt.CLOSURE.$name = "CLOSURE"; 879 | 880 | /// pop v, pop x, { x = v; }, push v 881 | rt.DEC = function(rt) { rt.increase(-1); }; 882 | rt.DEC.$name = "DEC"; 883 | 884 | /* pop i, pop a value or reference and add to the "at" key the value of i. 885 | The effect of DEREF is to dereferentiate an object by one of its keys, 886 | the result still is a referece. Thus, if on the stack there are 887 | {ref: r, at: a} i then after DEREF it'll be {ref: a, at: i} and 888 | rt.$$_this_$ (thus the "this" variable of the runtime) will be set 889 | to a. */ 890 | rt.DEREF = function(rt) { 891 | let i = rt.popval(); 892 | let r = rt.stack.pop(); 893 | if (r && r.$$_ref_$ && r.$$_at_$) { // reference? 894 | /* Notice: we have r = {ref:e, at:s} and we want to change 895 | it int {ref:e[s], at:i}. */ 896 | r = {$$_ref_$:r.$$_ref_$[r.$$_at_$], $$_at_$:i}; 897 | rt.$$_this_$ = r.$$_ref_$; 898 | } else if (r && r[i]) { // object? 899 | r = {$$_ref_$: r, $$_at_$:i}; 900 | //~ if (typeof(r[i]) == "object") { 901 | rt.$$_this_$ = r.$$_ref_$; 902 | //~ } 903 | } else { 904 | r = undefined; 905 | rt.$$_this_$ = undefined; 906 | } 907 | rt.stack.push(r); 908 | }; 909 | rt.DEREF.$name = "DEREF"; 910 | 911 | /// pop y, pop x, push x / y 912 | rt.DIV = function(rt) { 913 | let y = rt.popval(); 914 | rt.stack.push(rt.popval() / y); 915 | }; 916 | rt.DIV.$name = "MUL"; 917 | 918 | /// pop x, discard the value 919 | rt.DROP = function(rt) { rt.stack.pop(); }; 920 | rt.DROP.$name = "DROP"; 921 | 922 | /// pop x, push x, push x 923 | rt.DUP = function(rt) { rt.stack.push(rt.stack[rt.stack.length - 1]); }; 924 | rt.DUP.$name = "DUP"; 925 | 926 | /// pop x, if x != 0 push x and perform JP, else perform NOP. 927 | rt.DUPJPNZ = function(rt) { 928 | let r = rt.popval(); 929 | if (r) { 930 | rt.stack.push(r); 931 | rt.JP(rt); 932 | } else { 933 | ++ rt.ic; // skip the operand of DUPJPNZ 934 | } 935 | }; 936 | rt.DUPJPNZ.$name = "DUPJPNZ"; 937 | 938 | /// pop x, if x == 0 push x and perform JP, else perform NOP. 939 | rt.DUPJPZ = function(rt) { 940 | let r = rt.popval(); 941 | if (!r) { 942 | rt.stack.push(r); 943 | rt.JP(rt); 944 | } else { 945 | ++ rt.ic; // skip the operand of DUPJPNZ 946 | } 947 | }; 948 | rt.DUPJPZ.$name = "DUPJPZ"; 949 | 950 | /// pop y, pop x, push x == y 951 | rt.EQ = function(rt) { 952 | let y = rt.popval(); 953 | rt.stack.push(rt.popval() == y); 954 | }; 955 | rt.EQ.$name = "EQ"; 956 | 957 | /// pop y, pop x, push x >= y 958 | rt.GE = function(rt) { 959 | let y = rt.popval(); 960 | rt.stack.push(rt.popval() >= y); 961 | }; 962 | rt.GE.$name = "GE"; 963 | 964 | /// pop y, pop x, push x > y 965 | rt.GT = function(rt) { 966 | let y = rt.popval(); 967 | rt.stack.push(rt.popval() > y); 968 | }; 969 | rt.GT.$name = "GT"; 970 | 971 | /// pop x, { ++ x; }, push x 972 | rt.INC = function(rt) { rt.increase(1); }; 973 | rt.INC.$name = "INC"; 974 | 975 | /// pop y, pop x, push (x in y) 976 | rt.IN = function(rt) { 977 | let y = rt.popval(), x = rt.popval(); 978 | rt.stack.push(x in y); 979 | }; 980 | rt.IN.$name = "IN"; 981 | 982 | /// parse n, set rt.ic = n. 983 | rt.JP = function(rt) { rt.ic += rt.code[rt.ic].i; }; 984 | rt.JP.$name = "JP"; 985 | 986 | /// parse n, pop x, if x != 0 perform JP n. 987 | rt.JPNZ = function(rt) { 988 | if (rt.popval()) { 989 | rt.ic += rt.code[rt.ic].i; 990 | } else { 991 | ++ rt.ic; 992 | }}; 993 | rt.JPNZ.$name = "JPNZ"; 994 | 995 | /// parse n, pop x, if x == 0 perform JP n. 996 | rt.JPZ = function(rt) { 997 | if (rt.popval()) { 998 | ++ rt.ic; 999 | } else { 1000 | rt.ic += rt.code[rt.ic].i; 1001 | }}; 1002 | rt.JPZ.$name = "JPZ"; 1003 | 1004 | /// pop y, pop x, push x <= y 1005 | rt.LE = function(rt) { 1006 | let y = rt.popval(); 1007 | rt.stack.push(rt.popval() <= y); 1008 | }; 1009 | rt.LE.$name = "LE"; 1010 | 1011 | /** pop v, pop s and creates a variable with name s and value v. Notice that 1012 | a variable's value is always an object, with key "value" the actual value. 1013 | In this way we can refer to the variable when assigning a value to it. */ 1014 | rt.LET = function(rt) { 1015 | let v = rt.popval(); 1016 | let s = rt.stack.pop(); // string expected 1017 | rt.env[s] = v; 1018 | }; 1019 | rt.LET.$name = "LET"; 1020 | 1021 | /// pop y, pop x, push x < y 1022 | rt.LT = function(rt) { 1023 | let y = rt.popval(); 1024 | rt.stack.push(rt.popval() < y); 1025 | }; 1026 | rt.LT.$name = "LT"; 1027 | 1028 | /// pop y, pop x, push x * y 1029 | rt.MUL = function(rt) { 1030 | let y = rt.popval(); 1031 | rt.stack.push(rt.popval() * y); 1032 | }; 1033 | rt.MUL.$name = "MUL"; 1034 | 1035 | /// pop y, pop x, push x != y 1036 | rt.NE = function(rt) { 1037 | let y = rt.popval(); 1038 | rt.stack.push(rt.popval() != y); 1039 | }; 1040 | rt.NE.$name = "NE"; 1041 | 1042 | /// pop x, push -x 1043 | rt.NEG = function(rt) { rt.stack.push(-rt.popval()); }; 1044 | rt.NEG.$name = "NEG"; 1045 | 1046 | /// pop a, pop f, push new f(a). 1047 | rt.NEW = function(rt) { 1048 | rt.$$_this_$ = Object({}); 1049 | rt.APPLY(); 1050 | rt.stack.push(rt.$$_this_$); 1051 | rt.$$_this_$ = undefined; 1052 | }; 1053 | rt.NEW.$name = "NEW"; 1054 | 1055 | /// pop x, push !x 1056 | rt.NOT = function(rt) { rt.stack.push(!rt.popval()); }; 1057 | rt.NOT.$name = "NOT"; 1058 | 1059 | /// pop v, pop n, pop a, { a[n] = v; } push a 1060 | rt.OBJADD = function(rt) { 1061 | let v = rt.popval(); 1062 | let n = rt.popval(); 1063 | let a = rt.popval(); 1064 | //~ console.log(a + "[" + n + "] = " + v); 1065 | a[n] = v; 1066 | rt.stack.push(a); 1067 | }; 1068 | rt.OBJADD.$name = "OBJADD"; 1069 | 1070 | /// pop x, push the list of keys of object x. 1071 | rt.OBJKEYS = function(rt) { 1072 | let x = rt.popval(); 1073 | rt.stack.push(Object.keys(x)); 1074 | }; 1075 | rt.OBJKEYS.$name = "OBJKEYS"; 1076 | 1077 | /** pop x, parse s, parse c, define a variable s, for each key a in x 1078 | set s = a and executes c. */ 1079 | rt.OBJSCAN = function(rt) { 1080 | let x = rt.popval(); 1081 | let s = rt.code[rt.ic].i; 1082 | ++ rt.ic; 1083 | let c = rt.code[rt.ic].i; 1084 | ++ rt.ic; 1085 | rt.env[s] = undefined; // in case x = {} 1086 | rt.dump.push({code:rt.code, ic:rt.ic}); 1087 | rt.code = c; 1088 | for (let a in x) { 1089 | rt.env[s] = a; 1090 | rt.ic = 0; 1091 | sjs_execute(rt); 1092 | } 1093 | let saved = rt.dump.pop(); 1094 | rt.code = saved.code; 1095 | rt.ic = saved.ic; 1096 | }; 1097 | 1098 | // restore old environment 1099 | rt.POPENV = function(rt) { rt.env = rt.env.$$_outer_$; }; 1100 | rt.POPENV.$name = "POPENV"; 1101 | 1102 | // Restore a previous rt.$$_this_$ value. 1103 | rt.POPTHIS = function(rt) { rt.$$_this_$ = rt.dump.pop(); }; 1104 | rt.POPTHIS.$name = "POPTHIS"; 1105 | 1106 | rt.POW = function(rt) { 1107 | let x = rt.popval(), y= rt.popval(); 1108 | rt.stack.push(y**x); 1109 | }; 1110 | rt.POW.$name = "POW"; 1111 | 1112 | /// parse v, push v 1113 | rt.PUSH = function(rt) { 1114 | let v = rt.code[rt.ic].i; 1115 | ++ rt.ic; 1116 | /* WARNING: objects need to be cloned, else other instructions 1117 | referring to them will actually change the object following 1118 | rt.PUSH in rt.code, modifying the code itself!!! */ 1119 | if (v && v.constructor == Array) { 1120 | rt.stack.push(v.slice()); 1121 | } else 1122 | if (v && v.constructor == Object) { 1123 | rt.stack.push(Object.assign({}, v)); 1124 | } else { 1125 | rt.stack.push(v); 1126 | } 1127 | }; 1128 | rt.PUSH.$name = "PUSH"; 1129 | 1130 | // create a new environment 1131 | rt.PUSHENV = function(rt) { rt.env = {$$_outer_$: rt.env}; }; 1132 | rt.PUSHENV.$name = "PUSHENV"; 1133 | 1134 | // Saves the current rt.$$_this_$ value. 1135 | rt.PUSHTHIS = function(rt) { rt.dump.push(rt.$$_this_$); }; 1136 | rt.PUSHTHIS.$name = "PUSHTHIS"; 1137 | 1138 | /** parse s, look for variable s in the environment, push on the stack 1139 | a reference to the value, thus an object {ref:e, at:[a} . */ 1140 | rt.REF = function(rt) { 1141 | let s = rt.code[rt.ic].i; 1142 | ++ rt.ic; 1143 | // Looks in the runtime environment 1144 | for (let e = rt.env; e != null; e = e.$$_outer_$) { 1145 | if (s in e) { 1146 | rt.stack.push({$$_ref_$: e, $$_at_$:s}); 1147 | return; 1148 | }} 1149 | sjs_error(true, s + " is not defined", rt.code[rt.ic]); 1150 | }; 1151 | rt.REF.$name = "REF"; 1152 | 1153 | /// Ends the execution of the current rt.code 1154 | rt.RET = function(rt) { rt.ic = rt.code.length; }; 1155 | rt.RET.$name = "RET"; 1156 | 1157 | /// pop v, pop x, { x = v; }, push v 1158 | rt.SET = function(rt) { 1159 | let v = rt.popval(); 1160 | let r = rt.stack.pop(); 1161 | sjs_error(!(r && r.$$_ref_$), "Invalid LHS in assignment", rt.code[rt.ic]); 1162 | r.$$_ref_$[r.$$_at_$] = v; 1163 | rt.stack.push(v); 1164 | rt.$$_this_$ = undefined; 1165 | }; 1166 | rt.SET.$name = "SET"; 1167 | 1168 | /// pop v, pop x, { x += v; }, push v 1169 | rt.SETADD = function(rt) { rt.increase(rt.popval()); }; 1170 | rt.SETADD.$name = "SETADD"; 1171 | 1172 | /// pop v, pop x, { x -= v; }, push v 1173 | rt.SETSUB = function(rt) { rt.increase(-rt.popval()); }; 1174 | rt.SETSUB.$name = "SETSUB"; 1175 | 1176 | /// pop y, pop x, push x - y 1177 | rt.SUB = function(rt) { 1178 | let y = rt.popval(); 1179 | rt.stack.push(rt.popval() - y); 1180 | }; 1181 | rt.SUB.$name = "SUB"; 1182 | 1183 | /// pop x, pop y, push x, push y 1184 | rt.SWAP = function(rt) { 1185 | let x = rt.stack.pop(), y = rt.stack.pop(); 1186 | rt.stack.push(x); 1187 | rt.stack.push(y); 1188 | }; 1189 | rt.SWAP.$name = "SWAP"; 1190 | 1191 | /// push rt.$$_this_$ 1192 | rt.THIS = function(rt) { rt.stack.push(rt.$$_this_$); }; 1193 | rt.THIS.$name = "THIS"; 1194 | 1195 | /// pop x, throw exception x 1196 | rt.THROW = function(rt) { throw rt.popval(); }; 1197 | rt.THROW.$name = "THROW"; 1198 | 1199 | /// pop v, push the string corresponding to the type of v 1200 | rt.TYPEOF = function(rt) { rt.stack.push(typeof(rt.popval())); }; 1201 | rt.TYPEOF.$name = "TYPEOF"; 1202 | 1203 | return rt; 1204 | }; 1205 | 1206 | let tl = sjs_scan("console.log(1+2*3);"); 1207 | console.log(tl); 1208 | let rt = sjs_compile(tl); 1209 | console.log(rt); 1210 | sjs_run(rt, true); 1211 | -------------------------------------------------------------------------------- /jsday2025.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pcaressa/sjs/4f760182cde2e7ea29f1330569aa1eb6f3c53f5e/jsday2025.pdf -------------------------------------------------------------------------------- /sjs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Self JavaScript Playground 6 | 7 | 8 | 9 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 | 55 |
56 | © 2024 by Paolo Caressa 57 | 58 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /test/01.js: -------------------------------------------------------------------------------- 1 | // Nested loops 2 | 3 | let i = 0; 4 | while (i < 10) { 5 | let j = 0; 6 | while (j < 10){ 7 | console.log("(" + i, "," + j + ")"); 8 | ++ j; 9 | } 10 | ++ i; 11 | } 12 | -------------------------------------------------------------------------------- /test/02.js: -------------------------------------------------------------------------------- 1 | let x = 10; 2 | 3 | if (x < 0) { 4 | alert("NEG"); 5 | } else 6 | if (x == 0) { 7 | alert("ZER"); 8 | } else 9 | if (x > 0) { 10 | alert("POS"); 11 | } else { 12 | alert("BUG"); 13 | } 14 | -------------------------------------------------------------------------------- /test/03.js: -------------------------------------------------------------------------------- 1 | for (let i = 0; i < 10; ++ i) { 2 | for (let j = 0; j < 10; ++ j) { 3 | console.log("(" + i + "," + j + ")"); 4 | } 5 | } -------------------------------------------------------------------------------- /test/04.js: -------------------------------------------------------------------------------- 1 | let x = 10; 2 | do { 3 | console.log(x); 4 | -- x; 5 | } while (x > 0); 6 | -------------------------------------------------------------------------------- /test/05.js: -------------------------------------------------------------------------------- 1 | // Simple condition 2 | 3 | let x = -10; 4 | 5 | if (x < 0) { 6 | console.log("negativo"); 7 | } 8 | else if (x == 0) { 9 | console.log("zero"); 10 | } 11 | else { console.log("positivo"); } 12 | -------------------------------------------------------------------------------- /test/06.js: -------------------------------------------------------------------------------- 1 | let f = function(x) { 2 | let g = function(y) { 3 | return x + y; 4 | }; 5 | return g; 6 | }; 7 | console.log(f); 8 | console.log(f(10)); 9 | console.log(f(10)(20)); -------------------------------------------------------------------------------- /test/07.js: -------------------------------------------------------------------------------- 1 | for (let i in [0,1,2,3,4,5]) { 2 | for (let j in {a:0, b:1, c:2, d:3, e:4, f:5}) { 3 | console.log("(" + i + "," + j + ")"); 4 | } 5 | } -------------------------------------------------------------------------------- /test/10.js: -------------------------------------------------------------------------------- 1 | // 01.js - gcd 2 | 3 | let n = Number(prompt("n = ?")); 4 | let m = Number(prompt("m = ?")); 5 | 6 | if (n != Math.trunc(n) || n <= 0 || m != Math.trunc(m) || m <= 0) { 7 | alert("Insert positive integer numbers!"); 8 | } else { 9 | while (n != m) { 10 | if (n < m) { 11 | m -= n; 12 | } else { 13 | n -= m; 14 | } 15 | } 16 | alert("gcd = " + n); 17 | } 18 | --------------------------------------------------------------------------------