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 &mdash; 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>

Reply via email to