A couple of times previously, I've posted some dynamic calculation hack in Python with Tk, where the calculation results are displayed as you type, and you can define several variables that depend on one another's values.
Here's the same thing, in DHTML for Firefox, with a small improvement --- most of the time, it tracks dependencies between formulas. It only works in Mozilla (Firefox or Seamonkey) because it depends on the specific format of the ReferenceError message and on .toSource(). This will be on the web at http://pobox.com/~kragen/sw/formulasheet/formulasheet.html for easier viewing. <html><head><title>Formula sheet sketch</title> <script type="text/javascript">//<![CDATA[ function meta_tracing_eval(expr, env) { var minimal_env = {} var err = null var val = null var exists = {} var minimal_env_exists = {} for (;;) { try { with(minimal_env) { val = eval(expr) } } catch (e) { if (e.constructor == ReferenceError) { var parsed = e.message.match(/(.*) is not defined/) var name = parsed[1] if (minimal_env_exists[name]) { // we already inserted it... err = e break } if (!env.exists(name)) { minimal_env_exists[name] = 1 err = e break } minimal_env[name] = env.get(name) minimal_env_exists[name] = 1 continue } else { err = e break } } break } return {vars: minimal_env_exists, error: err, value: val} } function tracing_eval(expr, env) { var exists = {} for (var name in env) exists[name] = 1 var meta_env = { exists: function(name) { return exists[name] }, get: function(name) { return env[name] }, } return meta_tracing_eval(expr, meta_env) } function keys(obj) { var rv = [] for (var name in obj) rv.push(name) return rv } // this needs to not hang: function hairy_example() { var env = { wrong: function() { return nonexistent_var } } tracing_eval('wrong()', env) } hairy_example() // this needs to not give an error: function falsity_example() { var env = { cond: 0, a: 3, b: 4 } var rv = tracing_eval('cond ? a : b', env) if (rv.error) alert('freevars test error: ' + rv.error) } falsity_example() // there was still a bug with the combination function combo_example() { var env = { funnyvar: 0, wrong: function() { return funnyvar } } tracing_eval('wrong()', env) } combo_example() function easy_example() { var env = { a: 3, b: 4 } var result = tracing_eval('a + b', env) if (result.error) alert('freevars easy test error: ' + result.error) if (result.value != 7) alert('freevars easy test result: ' + result.value) var vars = keys(result.vars) if (!result.vars['a'] || !result.vars['b'] || vars.length != 2) alert('freevars easy test vars: ' + vars.toSource()) } easy_example() function notfound_example() { var result = tracing_eval('nonexistent', {}) if (!result.vars['nonexistent']) alert('freevars notfound vars: ' + result.vars.toSource()) } notfound_example() function meta_example() { var meta_len_env = { exists: function() { return true }, get: function(name) { return name.length }, } var result = meta_tracing_eval('xxx + yy', meta_len_env) if (result.value != 5) alert('freevars meta example: ' + result.toSource()) } meta_example() // ]]> </script> <script type="text/javascript">// <![CDATA[ /* dynamic expression evaluation environment in the browser */ /* I didn't reinvent the wheel for fun here, exactly; I reinvented it because I wrote this on a machine without internet access or an already-downloaded copy of MochiKit. Not using jQuery is less defensible. */ /* basic wheel reinvention: iteration */ function forEach(array, callback) { for (var ii = 0; ii < array.length; ii++) callback(array[ii]) } /* basic wheel reinvention: DOM */ function $(id) { return document.getElementById(id) } function hasClass(node, className) { if (!node.className) return false var classes = node.className.split(/\s+/) for (var ii=0; ii < classes.length; ii++) { if (classes[ii] == className) return true } return false } function descendantsByClass(elem, className) { var rv = [] var stack = [elem] while (stack.length) { var node = stack.pop() if (hasClass(node, className)) rv.push(node) for (var ii = node.childNodes.length - 1; ii >= 0; ii--) { stack.push(node.childNodes[ii]) } } return rv } /* this one function may not be wheel reinvention. It finds the tree-nearest cousin with a particular class name, so given a formula and 'varname', it can find the varname, or given the varname and 'formula', it can find the corresponding formula. */ function cousin(elem, className) { while (elem != document) { elem = elem.parentNode var rv = descendantsByClass(elem, className) if (rv.length == 1) return rv[0] if (rv.length > 1) throw "too many cousins of " + className /* untested */ } throw "no cousin found: " + className /* error handling untested */ } function replaceChildren(node, newChild) { while (node.firstChild) node.removeChild(node.firstChild) node.appendChild(newChild) } function createDOM(tagname, attributes, contents) { var rv = document.createElement(tagname) for (var attr in attributes) { rv.setAttribute(attr, attributes[attr]) } if (contents) { forEach(contents, function(item) { if (item.constructor == String) item = document.createTextNode(item) rv.appendChild(item) /* XXX handle arrays */ }) } return rv } function createDOMSugar(tagname, args) { var attributes = args[0] var contents = [] for (ii = 1; ii < args.length; ii++) contents.push(args[ii]) return createDOM(tagname, attributes, contents) } function TR() { return createDOMSugar('TR', arguments) } function TD() { return createDOMSugar('TD', arguments) } function INPUT() { return createDOMSugar('INPUT', arguments) } function B() { return createDOMSugar('B', arguments) } /* basic wheel reinvention: repr, display a JavaScript value as text */ function toSource(val) { if (val == null) return 'null' else if (val.constructor == Number) return val.toString() else if (val.constructor == String) { /* cheating! */ var rv = [val].toSource() /* now we have leading [ and trailing ] */ return rv.substr(1, rv.length - 2) } else return val.toSource() } /* end of wheel reinvention; actual program starts here */ function Row(namefield, formulafield, result_loc) { this.namefield = namefield this.formulafield = formulafield this.result_loc = result_loc this.vars = {} } Row.prototype.eval = function(env) { var textresult var result = meta_tracing_eval('(' + this.formulafield.value + ')', env) this.vars = result.vars if (result.error) { textresult = result.error.toString() delete this.result } else { this.result = result.value textresult = toSource(result.value) } replaceChildren(this.result_loc, document.createTextNode(textresult)) return result } Row.prototype.name = function() { return this.namefield.value } Row.prototype.depends_on = function(name) { return this.vars[name] } function Env() { this.rows = [] this.vars = new VarFacade(this) } Env.prototype.updateRow = function(row) { var result = row.eval(this.vars) var name = row.name() if (!result.error && name != '') this.vars[name] = result.value var self = this if (!name) return forEach(this.rows, function(other_row) { if (other_row.depends_on(name)) setTimeout(function() { self.updateRow(other_row) }, 0) }) } Env.prototype.addRow = function(row) { this.rows.push(row) this.updateRow(row) } function VarFacade(env) { this.env = env } VarFacade.prototype.exists = function(name) { return this.getRow(name) } VarFacade.prototype.getRow = function(name) { for (var ii = 0; ii < this.env.rows.length; ii++) { var row = this.env.rows[ii] if (row.name() == name) return row } return null } VarFacade.prototype.get = function(name) { var row = this.getRow(name) // the exists() check in meta_tracing_eval should prevent this, but... if (!row) throw new ReferenceError('meta var ' + name) return row.result } function ancestorOfType(elem, type) { while (elem.tagName != type) elem = elem.parentNode return elem } function addRowEventHandler(env) { return function(ev) { var tr = ancestorOfType(ev.target, 'TR') var where = { elem: tr.parentNode, pos: tr } return addRow(env, where) } } function addRow(env, where, formula, name) { if (!formula) formula = "3 + 4" if (!name) name = '' var formula_f = INPUT({'class': 'formula', value: formula}) var name_f = INPUT({'class': 'varname', value: name}) var result_f = TD({'class': 'result'}) where.elem.insertBefore(TR({}, TD({}, name_f, B({}, ':')), TD({}, formula_f), result_f), where.pos) var row = new Row(name_f, formula_f, result_f) env.addRow(row) formula_f.addEventListener('keyup', function(ev) { env.updateRow(row) }, true) name_f.addEventListener('keyup', function(ev) { env.updateRow(row) }, true) setTimeout(function() { formula_f.focus() }, 0) // "permission denied"?! } global_env = null function init() { var env = new Env() global_env = env var rowadders = descendantsByClass(document, 'addrow') var tr = ancestorOfType(rowadders[0], 'TR') var where = { elem: tr.parentNode, pos: tr } addRow(env, where, '"These are days"', 'text') addRow(env, where, 'text.split(/\\s+/)', 'words') addRow(env, where, 'function (f, xs) { var rv = []; for (var ii = 0; ii < xs.length; ii++) rv.push(f(xs[ii])); return rv;}', 'map') addRow(env, where, 'map(function(x) { return x.toLowerCase() }, words)') addRow(env, where, 'text.length') addRow(env, where, 'words.length') forEach(rowadders, function(el) { el.addEventListener('click', addRowEventHandler(env), true) }) } window.addEventListener('load', init, true) /* TODO: (baby steps because i am feeling chicken) D move the + back to the bottom where it belongs! - don't have endless loops on circular refs X this is sort of solved now in the sense that it no longer hangs the browser, but it does use absurd amounts of CPU time D delete obsolete name bindings when names get edited D propagate values and update bindings when names change (not just when formulas change) - no, really, propagate values to dependents when name goes away D propagate errors to dependents (right now old values stick around) - don't update dependents when value didn't change D delete obsolete dependencies - is there a way to clean up the evaluation environment so things like "document" and "window" disappear? And especially things like "env"! - maybe use getter= instead of parsing ReferenceError? - maybe pack into a bookmarklet :) - adjust input width to fit text (is this even possible?) - make it not look like shit: - align text to top - errors in red, with an error icon - other values need better-laid-out representations (this is HTML, not ASR-33) - different columns in different colors? - nicer icon for "+" - enhance user control: - make it possible to rearrange row order - make it possible to delete old rows - continue displaying old values after an error; only show error on request - Do Something Cool, like: - a picture data type - numerical inputs with sliders - current mouse position - delayed mouse position. etc. - handle events other than keyup */ //]]> </script> <style type="text/css"> .formula, .varname { border: 0; font: inherit } .formula:hover, .varname:hover { background-color: #ffd } .varname { text-align: right } /* does not work: .varname:after { content: ":" } */ </style> </head><body> <h1>Formula sheet sketch</h1> <p>This is a very limited DHTML mockup of a Bicicleta feature. Here we have some names and some corresponding definitions, and we can see the current values of the names as computed from the definitions. If you alter a definition, all the values depending on it change correspondingly, in real time.</p> <p>There are a couple of "features" in it that are intended to be removed in the real Bicicleta:</p> <ul> <li>There's no distinction between things that have side effects and things that don't — it's easy to accidentally create something that has unwanted side effects.</li> <li>It has some difficulty telling what's inside a function that has free variables. If you define a function as "function(x) { return otherfunc(x) + 1}", it won't notice the "otherfunc" until it's too late. You can work around this by defining it as "otherfunc, function(x) { return otherfunc(x) + 1}".</li> <li>It's possible to construct a self-referential definition which will just keep on updating endlessly, although it takes a little work.</li> <li>It's straightforward to create a program that takes exponential time and has "glitches" where it temporarily has the wrong answer; define a: 1, a1: a, b: a+a1, b1: b, c: b+b1, c1: c, and so on through k, and the convergent data flow causes several seconds of slowness whenever you have a keyup event in the a field.</li> <li>You can only define functions in JavaScript, not in the environment itself.</li> </ul> <table id="where"> <tr><th align="right">name:</th><th>formula</th><th>value</th></tr> <tr><td class="addrow">+</td></tr> </table> </body></html>