Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Local 'JIT' compiled JavaScript #4

Closed
gfwilliams opened this issue Dec 19, 2014 · 7 comments
Closed

Local 'JIT' compiled JavaScript #4

gfwilliams opened this issue Dec 19, 2014 · 7 comments

Comments

@gfwilliams
Copy link
Member

Recently I've been wondering a bit about ways to get more 'native' speed out of Espruino. While it's fast enough for most things, sometimes you still notice problems - when doing things like manipulating images or decoding raw waveforms from the radio.

So, right now the solutions seem to fall into:

  • Download the Espruino source, compile it yourself and write C/C++ (works right now, perfectly)
  • Use the inline assembler (here now, although the assembler doesn't support a bunch of opcodes)
  • Have some way to compile C code in the Web IDE (not done, and raises issues about how to integrate it with native Espruino functionality)
  • Faster JS interpreter/JIT compile (none of which seem easy to get in the memory footprint we have)
  • ASM.JS

I've been talking to a few people about this, and another option has come up that's kind of a hybrid of the last 3 approaches... What if we could compile JavaScript to assembler inside the Web IDE, where there's more memory available?

It wouldn't have to be 100% full JS coverage - just enough that the stuff that needs to run quickly can do.

I just did a very quick proof of concept of this using acorn for the parsing. It's 150 lines so far, and if you type the following code:

  function foo(a,b) {
    "compiled";
    foo = foo + 1;
    return "Hello"+(a-b)+foo;
  }

Then when you click 'upload' it'll get parsed and will be converted to the following 'pseudo-assembler':

 ] MOVL r0, const_0
] BL jspeiFindInScopes
] PUSH {r0}
] MOVL r0, const_0
] BL jspeiFindInScopes
] PUSH {r0}
] MOVL r0, const_1
] BL jsvNewFromInteger
] PUSH {r0}
] POP {r0}
] POP {r1}
] MOVL r2, 43/* + */
] BL jsvMathsOp
] PUSH {r0}
] POP {r0}
] POP {r1}
] BL jspReplaceWith
] PUSH {r0}
] POP {r0}
] BL jsvUnLock
] MOVL r0, const_2
] BL jsvNewFromString
] PUSH {r0}
] MOVL r0, const_3
] BL jspeiFindInScopes
] PUSH {r0}
] MOVL r0, const_4
] BL jspeiFindInScopes
] PUSH {r0}
] POP {r0}
] POP {r1}
] MOVL r2, 45/* - */
] BL jsvMathsOp
] PUSH {r0}
] POP {r0}
] POP {r1}
] MOVL r2, 43/* + */
] BL jsvMathsOp
] PUSH {r0}
] MOVL r0, const_0
] BL jspeiFindInScopes
] PUSH {r0}
] POP {r0}
] POP {r1}
] MOVL r2, 43/* + */
] BL jsvMathsOp
] PUSH {r0}
] POP {r0}
] BX LR
] const_0:
]   .word 0x6f6f66 ; "foo\u0000"
] const_1:
]   .word 0x1 ; "\u0001\u0000\u0000\u0000"
] const_2:
]   .word 0x6c6c6548 ; "Hell"
]   .word 0x6f ; "o\u0000"
] const_3:
]   .word 0x61 ; "a\u0000"
] const_4:
]   .word 0x62 ; "b\u0000"

There's obviously still a bit to do before it's useful, but it's surprisingly close. While the generated code isn't super-fast, it'll still be a massive improvement over interpreting.

TODO

  • Work out a way of 'exporting' jsvMathsOp/jspeiFindInScopes/etc from the interpreter. This could just be a struct somewhere in memory with this stuff in a predefined order.
  • handle jsvUnLock properly when calling functions
  • Have a special case for function parameters, which will need to be taken out of reg0-3 and stored on the stack
  • Generate actual Thumb assembler that can be parsed by the assembler.js
  • Remove the original JS function declaration from the source and replace it with the assembler's output

And a few obvious optimisations

  • Peephole optimise push/pop to the same register
  • Make sure a referenced global variable is only looked up once
  • Peephole optimise constant expressions
  • Optimise stack push/pop into registers where possible
  • It should be possible to work out what variables are numbers/integers, and then to do the relevant operations right on them

Anyway, the hacky proof of concept is in https://github.com/espruino/EspruinoTools/tree/js_compiler. To use it, just add plugins/compiler.js to the Web IDE's main.html file and it'll spring into life

@gfwilliams
Copy link
Member Author

Ok, now a mile better. Just grab EspruinoTools and run node compile.js. You'll need to sudo npm install -g acorn first.

This actually produces real assembled code now - not that it works or anything :)

Biggest thing right now is 'exporting' the function pointers. You'll have to compile your own Espruino firmware, look at the .lst file, grab the pointers out of it, and put them as .words under the label for them. Even then you'll have to change the kind of branch.

@gfwilliams
Copy link
Member Author

Ok, this now actually works (a bit). You need the js_compiler branch of Espruino too, then you add plugins/compiler.js to EspruinoWebIDE/main.html before assembler.js, and you can do:

function a(b) { 
  "compiled";
  return b+"World"; 
}
console.log(a("Hello "))
]   push {r7,lr}        ; Save registers that might get overwritten
]   push {r0}       ; push params onto stack - TODO: could do this in bulk
]   ldr r0, [sp,#0]     ; FIXME - need offset
]   push {r0}
]   adr r0, const_0
]   nop
]   ldr    r7, exports
]   ldr    r7, [r7, #20]        ; Get fn address
]   bl     trampoline       ; jsvNewFromString
]   push {r0}
]   movs r2, #43
]   pop {r1}
]   pop {r0}
]   ldr    r7, exports
]   ldr    r7, [r7, #8]     ; Get fn address
]   bl     trampoline       ; jsvMathsOp
]   push {r0}
]   pop {r0}
]   b end_of_fn
] end_of_fn:
]   pop {r3}        ; pop params off the stack - TODO: just add/sub stack ptr
]   pop {r7,lr}     ; Restore registers that might get overwritten
]   bx lr
] trampoline:
]   bx r7
]   nop
] exports:
]   .word 536871028     ; Function table pointer
] const_0:
]   .word 0x6c726f57        ; "Worl"
]   .word 0x64      ; "d\u0000"
var ASM_BASE=process.memory().stackEndAddres­s;
var ASM_BASE1=ASM_BASE+1/*thumb*/;
[0xb580,0xb401,0x9800,0xb401,0xa00b,0xbf00,0x4f09,0x697f,0xf000,0xf80e,0xb401,0x222b,0xbc02,0xbc01,0x4f05,0x68bf,0xf000,0xf806,0xb401,0xbc01,0xe7ff,0xbc08,0xbd80,0x4770,0x4738,0xbf00,0x74,0x2000,0x6f57,0x6c72,0x64,0x0].forEach(function(v) { poke16((ASM_BASE+=2)-2,v); });
var a = E.nativeCall(ASM_BASE1, "JsVar (JsVar)")
console.log(a("Hello "))
 _____                 _
|   __|___ ___ ___ _ _|_|___ ___
|   __|_ -| . |  _| | | |   | . |
|_____|___|  _|_| |___|_|_|_|___|
          |_| http://espruino.com
 1v72 Copyright 2014 G.Williams
>echo(0);
Hello World
> 

IF and >1 argument now added

TODO

  • handle jsvUnLock and jsvLockAgain properly when calling functions
  • local variables, and assigning to parameters
  • more control flow

@gfwilliams
Copy link
Member Author

See latest post: http://forum.espruino.com/conversations/259821/#comment12009165

Much better now, in fact probably useful - just need local variables and a few minor tweaks. Function calls/object access would be cool too

@gfwilliams
Copy link
Member Author

Some other bugs discovered by JumJum too. See above post.

@maks
Copy link

maks commented Feb 4, 2015

@gfwilliams am I missing something, as the js_compiler branch no longer seems to be in the Espruino repo here on GH?

@gfwilliams
Copy link
Member Author

@maks it's actually been merged into master/gh_pages now (Just check out the main branch in EspruinoWebIde/EsoruinoTools and you'll be fine).

To update:

  • There are now local variables
  • There's IF/FOR/WHILE
  • ++,+=,etc now work
  • You can call functions - but not anything that requires calling on an object (eg, digitalWrite is fine but LED1.set() isn't)
  • It turns out I used the wrong symbol table lookup function, so you can't access any built-in variables/functions right now. If you want one you have to create a variable first: var dw = digitalWrite;

@gfwilliams
Copy link
Member Author

Closing this now. Project has moved to https://github.com/gfwilliams/EspruinoCompiler - it's an online compiler, but honestly something like GCC is a much better bet for producing fast, reliable code.

Maybe at some point we'll be able to cross-compile ARM-GCC into JS with emscripten, and at that point it can move offline again :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants