If we had a Bitlash Best Hack of the Month award, this month's would surely go to Brett Hagman for his superb software prototype which demonstrates running Bitlash scripts off the uMMC shield. Check out Brett's post at Rogue Robotics, then please return here for some technical comments.
The prototype Bitlash_uMMC.pde is a clever (and very well-documented!) use of the new user-functions facility in Bitlash 1.1. The user function “exec()” opens a file from the storage card and executes the text there as Bitlash commands by passing one line at a time to the Bitlash doCommand() function.
What a clever combination of user functions + doCommand()! Unfortunately, while the prototype works (and bravo to Brett), danger lurks just beneath the surface, due to the fact that doCommand() is not reentrant.
Let's dig into the problem a bit before walking through a proposed solution. I hope I may safely quote the first line of the Wikipedia entry on reentrancy, which summarizes the issue well:
A computer program or routine is described as reentrant if it can be safely called again before its previous invocation has been completed…
Here is where the problem arises: when your user function is called, you are deep in the bowels of the interpreter stack somewhere under doCommand() . So calling doCommand() from a user function will end up calling doCommand() while it's already active on the stack.
Now, doCommand() was never designed to be reentrant since, among other things, it uses setjmp()/longjmp() to set the global error handling context. Have a look at the error handling in src/bitlash-error.c; you will see that all the errors end with a longjmp() back to the originating doCommand().
I suspect there would be no shortage of interesting debugging experiences if, for example, a macro running from eeprom runs an external script in the way the prototype does, and then throws an error after the script is done running (while the now-stale setjmp() context is deep in the stack). Wierdly nondeterministic behavior in the middle of an error case is never fun to track down.
This is why the documentation for Bitlash user functions recommends against calling doCommand() or runBitlash() from a user function.
So this approach is likely to be pretty fragile under error cases at least, without work to make doCommand() reentrant. Is there a cheaper approach?
The obvious and ideal approach would be to integrate the external storage as an input stream to the parser, the same way ram and eeprom are used today.
Implementing scripts on external storage as first-class citizens in the core would enable external scripts to call macros in eeprom, and vice versa. The net effect would be very powerful, but at the price of modifying the parser to enable parseid() to look up identifiers in the external store, and fetchc() to get characters from it like we do from the eeprom.
While this might be the “right” approach in the long run, there is somewhat risky work involved, and I have a proposal for a simpler approach which might meet Brett's requirements.
The basic idea is this: there is a place in the interpreter where Bitlash silently ignores references to undefined macros. (It is not a fatal error not to know a word.) Instead of ignoring the undefined reference, why not look it up and execute it from the external storage instead?
There is a second part, too. Since calling doCommand() from within Bitlash is a problem, let's organize the solution so the calls to Bitlash to execute the external script's commands from safely from outside Bitlash in the call tree.
The second part is easy using an approach one might call co-tasking: add a new procedure called runScriptTask() to handle the dirty work of calling doCommand() from outside Bitlash, in the loop() function, at the same level as runBitlash(). In honor of Brett's work we might call it Bhagman's Daemon:
loop(void) {
runBitlash(); // Bitlash gets time
runScriptTask(); // and so does our script running co-task
}
Think of runScriptTask() as a daemon waiting to process requests. This is where the fetch-execute loop from Brett's sdexec() function will go.
Now about those requests: we need a way to communicate script running requests to the runScriptTask(). Let's add an entry point named runExternalScript() to accept requests. The simplest implementation that comes to mind is a flag and buffer scheme something like this (warning uncompiled code):
#define IDLEN 12 // duplicating the Bitlash definition. sigh.
byte scriptRequest; // flag=1 when a script request is pending
char scriptName[IDLEN+1]; // IDLEN is 12 to allow "filename.txt" ;)
// script-run request hook
void runExternalScript(char *name) {
if (scriptRequest) Serial.println("OOPS can't nest external script calls");
else {
strcpy(scriptName, name); // capture the name from the bitlash buffer
scriptRequest = 1; // set flag for our top-level runScriptTask
}
}
// script running task
void runScriptTask(void) {
if (scriptRequest) {
/* ... this is where the execute loop goes ... */
/* ... open the file named in scriptName[] ... */
/* ... call doCommand for each line ... */
scriptRequest = 0; // clear the lock AFTER execution
}
}
loop(void) {
runBitlash(); // Bitlash gets time
runScriptTask(); // and so does our script running co-task
}
This could of course be improved on but I think the general idea is clear.
What remains is the matter of arranging for a callout to runExternalScript(). Take a deep breath and open the file src/bitlash-interpreter.c; all the way at the bottom you will find the procedure doMacroCall():
void doMacroCall(int macroaddress) {
char op = sym; // save sym for restore
if (macroaddress >= 0) {
/* ...code to handle eeprom macro call here... */
}
}
You'll note there is no else clause; this is where undefined macro references are silently ignored (when macroaddress < 0).
Now I have to let you in on some insider information. At this point in the code, for one brief moment, the parsed but as-yet-unrecognized identifier is in a global buffer named “idbuf[]”, which is defined in src/bitlash.h:
// Temporary buffer for ids #define IDLEN 12 extern char idbuf[IDLEN+1];
So the code change to pass the unknown identifier along for consideration by the scriptRunTask() is “else runExternalScript(idbuf);”:
void doMacroCall(int macroaddress) {
char op = sym; // save sym for restore
if (macroaddress >= 0) {
/* ...code to handle eeprom macro call here... */
}
else runExternalScript(idbuf); // unknown id: try to exec it from mmc
}
So, from the top, here is the proposed flow for the somewhat-safer method of running scripts from external storage:
Brett, this has gotten quite lengthy, hasn't it, but I hope you will find some useful thoughts here. I'd be happy to respond to comments here, or we can cross post back and forth.
Cheers!
-br
Discussion
Bill,
I'm seeing now how you want to externalize the calls to
doCommand(). I admit, I saw and understood about callingdoCommand()in the user function reference when I wrote the first sketch; I just figured that as long as no macro/script called theexec()function, everything would be fine, and I would have a proof of concept. I knew it would need further refinement.After reading what you were proposing above, I thought about taking it one step further. Right now, you're proposing that the function
runExternalScript()be defined before bitlash can compile.What about just making the hook entirely externalized? i.e. Optional processing of external identifiers.
If we create a struct to carry the identifier externally (in
src/bitlash.h):Then, in
src/bitlash-interpreter.c, add, much like you describe above, to the missing identifierelseindoMacroCall():Now our
loop()becomes:The only other change that needs to be made is putting this:
into the interface level
bitlash.h.I've tested this out and it works like a charm.
What do you think?
b
Brett:
Thanks for the detailed reply. I like how you have taken the implementation to the next level.
Of course, the thought of writing and maintaining a multi-gigabyte Bitlash program is a bit horrifying, but that is a separate point…
Cheers, Brett.
-br
You sound overwhelmed by your own creation!
If you get string variables implemented, I'll show you what you can do with Bitlash. You wouldn't use the multi-gigabytes for the script… you'd use it to read/write the data collected using Bitlash.
Right now, only having numerical types passed to user functions is a bit limiting. If you could add string variables, you'd be looking at a whole different ball game.
Dreaming of this:
>x = "String" >print x String >y = x + "Data" >print y StringData >y = x + 25 >print y String25 >a = 25 >y = a + "String" ^ expected number >x = "" + 25 >y = x + "String" >print y 25String >x = "Analog 0:" >y = x + a0 >print y Analog 0:246 >userFunc(y) I'm a user function. You passed me 'Analog 0:246'. >I could go on and on
And the result would be to be able to create a script like such:
(gets and stores samples every second)
Oooh! The possibilities! (I need to get out more)
I do realize that it is a complicated modification. I think it would be very worth it, though.
You could introduce a variant type. This might mean a lot of headache converting your variable storage and introducing malloc() - and how much fun would that be?
But for simplicity, maybe you could separate strings from numeric types and limit the number of available strings (e.g. a→w = numeric, x→z = string). Pre-allocate the string storage, limiting the string length to something like 80 chars or so?
Are you up to the task?
b
Well now Brett, as a matter of fact I am up to the task, and I have given it considerable thought. I have yet to be convinced of the need to burden Bitlash with the overhead of dynamic allocation. I would simply like it to be more deterministic than that.
I don't think your use cases are persuasive of a need for a string data type. Couldn't you generate the same output with an extension to the print command that would redirect the printed output to a file handling routine instead of a digital pin? Then you could use all the print primitives to generate output to MMC, no strings required.
I call this feature “Registered Output Handlers”; it might work something like user functions – you'd call a function like addOutputHandler() at startup to register your printed character handler function and then print to some special number or symbol like #d in this example:
The character consumer would need to handle buffering and flush to disk.
The way user functions work integrates reasonably well with this: printing from a user function respects any redirect that is in play in the print command where it is evaluated, provided you use the Bitlash output primitives sp() and spb(). So the same user function can generate output to the console port, to a digital pin, or, with this feature, to an alternate user sink.
I will be all smiles if someone wants to undertake the work to produce an extension to include a string type, and your thoughts about simplifying the problem by restricting the problem domain sound like a sensible way to proceed.
Short of that, I'd be interested in how well this alternate solution meets the actual need you have in mind.
Best,
-br
Hey Bill,
Sorry for the hiatus - life gets busy sometimes.
That would work great!
If I understand correctly, you'll have a way to register a character handler for the print function (much like streams in stdio.h). Thus, I would have a way to process the data in the string within my handler.
I would love to see this implemented. How soon can you get it done? I want it yesterday!
b
Roy, Bill
What a great job. Bitlash is just what I have been looking for - for my new Nanode project. Its a Mega328 with an ethernet controller on the same board for about $30.
http://hackerspaces.org/wiki/Nanode
With remote control of scripts with a telnet or browser session - bitlash makes this all very exciting.
Bill - particularly interested in your external MMC. Ive added an SPI 32Kx8 SRAM for external storage, but a MMC/SD could hold a huge amount of scripts and data.
Excellent work
Ken
London
Bill, Brett,
Sorry for the confusion over your names in the last post.
Excellent work both of you - I am really getting into bitlash!
I think it will fit my application perfectly.
Ken