Jacscript is a programming language for scripting Jacdac services.
It has JavaScript-like syntax and is compiled to a custom VM bytecode, which can run in very constrained environments (VM itself compiles to 10kB of code, with the floating point library and Jacdac SDK adding further 30kB). The langauge is typically edited using Block-based drag-and-drop interface.
This repository contains:
- jacdac-c submodule, including sources for Jacdac client libraries and Jacscript VM
compiler/- sources for Jacscript compilervm/- glue files to build Jacscript VM as WASM module using emscripten;vm/dist/contain pre-built filessamples/- sample Jacscript programsposix/- implementation of Jacdac SDK HAL for grown-up POSIX-like operating systems (as opposed to embedded platforms)
You can just use the devcontainer to build.
If you want to build locally, you need to install emscripten, as well node.js, GNU Make and C compiler.
- start
jacdac devtools(the npm version) and let is running - open this folder in VSCode; use "Reopen in Container" if needed
- start Terminal in VSCode
- run
makein terminal
Now you have options:
- run
./built/jdcli 8082- this will run the POSIX/native Jacscript server, which can be accessed from the devtools dashboard - run
node run.js samples/something.js- this will execute given Jacscript program using the WASM binary - run
node run.js -c samples/something.js- this will execute given Jacscript program using the POSIX/native binary
- secure - can predictably execute untrusted code (random bytes)
- easy to analyze - should be possible to statically determine the set of APIs used
- small memory (RAM) footprint
- small code (flash) footprint
- leave space for extensions in future
Global and local variables are supported (only var, no const or let).
All variables are numbers (64-bit IEEE floating point).
Numeric binary and unary expressions are supported.
Comparisons return 0.0 or 1.0 (in particular comparing anything to NaN returns 0.0).
0.0 and NaN are considered falsy.
TODO should compare with NaN return NaN?
var x, y
x = pot.reading.read()
if (x < 0.3) {
y = x * 3
} else {
y = -x / 7
}Some builtin functions only take literal arguments (especially strings, and time values).
The only jump statement supported is currently return.
Only while loop is supported.
The console.log() takes zero or more arguments, each of which is a string or a number.
Compiler internally constructs a format string (see below).
console.log("Hello world")
console.log("X is", x, "and Y is", y)
console.log("X=", x, "Y=", y)The compiler is smart about adding spaces (the second and third examples will print X is 7 and Y is 12
and X=7 Y=12).
You can also use the format() function directly, either with console.log() or
when setting string registers.
Arguments are {0}, {1}, ..., {9}, {A}, {B}, ..., {F}.
A second digit can be supplied to specify precision (though this doesn't work so well yet):
console.log(format("X is {0} and Y is {1}", x, y))
console.log(format("X = {04}", x))
charDisplay.message.write(format("X is {0}", x))Roles are defined by referencing a service name (in roles namespace).
The same role can be referenced multiple times, and runtime makes sure not to assign
multiple roles to the same service instance.
var btnA = roles.button()
var btnB = roles.button()
var pot = roles.potentiometer()
var lamp = roles.lightBulb()You can check if role is currently assigned, and react to it being assigned or unassigned:
if (heater.isConnected())
heater.active.write(1)
heater.onConnected(() => {
// ...
})
heater.onDisconnected(() => {
// ...
})Events are referenced as myRole.eventName. They currently have two member functions, .wait() and .subscribe().
btnA.down.subscribe(() => {
console.log("button down!")
})
// ...
btnA.up.wait()
// ...While handler registration (for events, register changes, device connect, etc.) looks dynamic, it is implemented statically. Thus handlers can be only registered at the top-level and un-conditionally.
Every handler runs in its own fiber (lightweight thread).
The scheduler is non-preemptive, meaning
a fiber executes without interruption until it returns or hits an asynchronous operation,
upon which point it's suspended.
Example async operations are wait() and register read.
Only one fiber executes at a time, while the other fibers are suspended.
This is similar to modern JavaScript, but there's no await keyword.
When the executor is woken up (typically due to an incoming packet or a timer expiring), it will execute all viable fibers until they become suspended. Executing a fiber may start another viable fiber, which would be also executed until suspension, before any more packets are processed.
At any given time, there is at most one fiber (which could be suspended) executing a given handler.
If this is not desired, bg(() => { ... }) syntax can be used to queue code execution
in background, without limits of how many instances of it are running (TODO not impl yet).
If a handler is triggered again, while it is still executing, a boolean flag is set on it,
so that it starts again (once) after the current execution finishes.
Registers are referenced as myRole.regName, where regName can also be the system-wide name,
so both pot.position and pot.reading will work.
TODO should we drop this, and only leave pot.position ?
Registers have following methods - .onChange(), .read() and .write().
If register contains multiple fields, a tuple (array) is returned.
var x
x = pot.position.read()
x = pot.reading.read() // equivalent
lamp.brightness.write(0.7)
var r, g, b
[r, g, b] = colorSensor.color.read()
myLed.color.write(0.3, 1, 0.7)The .onChange() handler can be registered to execute whenever the value of the register changes
by at least the specified value.
It is executed once when the value is first determined, and then whenever the current value
is different by at least the specified value from the value at previous handler execution.
pot.position.onChange(0.02, () => {
lamp.brightness.write(pot.position.read())
})Buffers can be statically allocated, read and written. This can be used to conserve memory (regular variables always take 8 bytes) and create arrays (with fixed upper limit).
var mybuf = buffer(12) // 12 byte buffer
mybuf.setAt(10, "u16", 123)
mybuf.setAt(3, "u22.10", 173.282)
var z = mybuf.getAt(3, "u22.10")There is a special buffer called packet which represents a buffer to be passed to next
command or register write.
It supports packets.setLength() function (unlike regular buffers),
and can be passed to any command or register write.
For example lamp.brightness.write(0.7) is equivalent to:
packet.setLength(2)
packet.setAt(0, "u0.16", 0.7)
lamp.brightness.write(packet)User-defined functions are allowed at the top-level, using function foo(x, y) { ... } syntax.
They are also allowed as event handlers using arrow syntax (see above).
Nested functions and real first-class functions are not supported.
Functions can return values.
A plain return is equivalent to return NaN.
Unused function are not compiled (and not checked for errors).
Function parameters are numbers by default. A role can be also passed using syntax:
function beep(/** @type BuzzerRole */ bz, len) {
bz.playNote(440, 0.7, len)
}
var b = roles.buzzer()
beep(b, 10)Commands can be marked as client in the spec.
These need to be implemented by assigning to properties of the prototype of the role.
For example:
BuzzerRole.prototype.playNote = function (frequency, volume, duration) {
var p = 1000000 / frequency
volume = clamp(0, volume, 1)
this.playTone(p, p * volume * 0.5, duration)
}Send a label + 0 or more numeric values.
cloud.upload("potval", pot.reading.read())
cloud.upload("color", r * 256, g * 256, b * 256)Respond to a request from the cloud. Normally, status code 200 is returned to the cloud once the handler finishes. If the method is not found, 404 is returned. If the method is already running 429 is returned.
cloud.onMethod("set_lights", (a, b) => {
lightA.brightness.write(a)
lightB.brightness.write(a)
})
cloud.onMethod("get_temp_hum", () => {
return [temp.temperature.read(), hum.humidity.read()]
})Run a function periodically (0.3s in the example below; 20ms minimum):
every(0.3, () => {
// ...
})Wait given number of seconds:
wait(0.3)Arithmetic operators are supported: +, -, *, /, **, as well as unary - and +.
Comparison operators <, <=, >, >=, ==, ===, !=, !== are supported (and return doubles).
The operators && and || are supported, and are properly lazy.
The boolean negation ! is supported (returning 0 or 1).
The bitwise operators are now supported: |, &, ^, ~, <<, >>, >>>.
The following math functions and constants are supported:
Math.floorMath.roundMath.ceilMath.logMath.randomMath.maxMath.minMath.powMath.sqrtMath.cbrtMath.expMath.log10Math.log2Math.idiv(integer division)Math.imul(integer multiplication)Math.EMath.PIMath.LN10Math.LN2Math.LOG2EMath.LOG10EMath.SQRT1_2Math.SQRT2isNaNNaN
All of the above should have the same semantics as JavaScript.
The panic() function takes a numeric error code and terminates or restarts the program.
reboot() is similar, but doesn't print error message.
panic(348)
reboot()Main dynamic memory usage - function activation records (and fibers).
BG_MAX1call frames can be only allocated once- whatever they call may need additional frames
- can collect all register gets and estimate memory for them (do we need a size limit on these?)
-
add
Date.now()? -
hang properties off roles -
high,lastHighfor bar graph eg -
more dynamic buffers? (offline store of pixels)
-
role.control-> control service of device that has this role ? -
role for control service of the brain (to set status light, reset, etc)
-
disallow top-level code?
-
add opcode to cache current packet (in onChanged())
-
extend format strings to include numfmt
-
shift buffer opcode?
-
somehow deal with events with legit args (button and barcode reader currently) - doesn't work so well in handler-pending model
-
add
role.waitConnected()or something? -
add
bg(() => { ... }), alsobg1()? -
do fiber round-robin for yields?
-
some testing framework? (depends on services?)
- this generally doesn't work with handler-pending model
- opcode to send current packet
- opcode to set the command
- opcode to set service number
- some way of building announce packets
- handler when a packet is received
- support 1 or more devices per VM instance?
- add
try_againreport in addition tocommand_not_implemented?
- specific uploads:
hum.autoUpload(5, 1) // 5s, 1% - auto-upload of everything
- fiber list, locals, globals
- setting breakpoints - breakpoint instruction? (based on source code location)
- more "debug" info in compiled program - role names, etc for error messages?
This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact [email protected] with any additional questions or comments.
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.