Homemmm​.s‑ol.nu

fun experiments with oscilloscopes
an error occured while rendering this view:

I finally added a better type-conversion-path-finding algorithm [deb21aa]. The old algorithm only took into consideration how many steps it took to get from type A (stored on disk) to type B (requested). This could lead to problems, for example in this situation:

On Disk: text/moonscript -> fn -> mmm/dom (a moonscript file that contains a function that returns a bit of UI)
Requested: mmm/dom
Known Conversions: 1. text/moonscript to mmm/dom (displays the source code with highlighting) 2. text/moonscript -> ??? to ??? (evaluates the moonscript file) 3. fn -> ??? to ??? (calls the function, providing access to children and other facets)

Since conversion 1 only takes a single step, it would have been preferred by the old algorithm (although there were workarounds for this). The new algorithm adds the concept of conversion-cost, that has to be specified for each conversion. The conversions 2 and 3 now have a cost of 1, while conversion 1 has a cost of 5. The conversions are simply added up and the path with the lowest cost is chosen. Like in other pathfinding applications like digital games, the cost metric is also used by the algorithm to enhance the search itself, it prioritises searching further on the path with the least current cost.

To implement this optimization I implemented a priority queue:

-- a priority queue with an index
-- only one element with a given key may exist at a time
-- when an element with an existing key is added,
-- the element with lower priority survives.
class Queue
  new: =>
    @values = {}
    @index = {}

  -- add a value with a given priority to the queue
  -- if no key is specified, assume the element is uniq
  add: (value, priority, key) =>
    entry = { :value, :key, :priority }

    if key
      if old_entry = @index[key]
        -- already have an entry for this key
        -- if it is lower priority, we leave it there and do nothing
        if old_entry.priority < priority
          return

        -- otherwise we remove the old one and continue as normal
        -- find the index of the old entry
        local i
        for ii, entry in ipairs @values
          if entry == old_entry
            i = ii
            break

        -- remove it
        table.remove @values, i

      -- store this entry in the index
      @index[key] = entry

    -- store lowest priority last
    for i, v in ipairs @values
      if v.priority < priority
        -- i is the first key that is lower,
        -- we want to insert right before it
        table.insert @values, i, entry
        return

    -- couldn't find a key with a lower priority,
    -- so insert at the end
    table.insert @values, entry

  peek: =>
    entry = @values[#@values]
    if entry
      { :value, :priority, :key } = entry
      @index[key] = nil if key
      value, priority

  pop: =>
    entry = table.remove @values
    if entry
      { :value, :priority, :key } = entry
      @index[key] = nil if key
      value, priority

  -- iterator, yields (value, priority), low priority first
  poll: => @.pop, @

{
  :Queue
}

This priority queue behaves mostly as expected, but I added an extra feature to make sure that only the best conversion path to a specific type is considered for further searching: When adding a new element to the Queue, an extra key can be passed. If a key is passed, the Queue makes sure that there is only ever one element with that key in the queue. When a second element would be introduced, the queue discards whichever element has a higher priority value.

This way I can use the type that a conversion path leads to as the key, and the queue will automatically discard a worse solution if a better one is found that leads to the same type result.

To make sure the Queue implementation was solid, I also added unit tests for it: spec/queue_spec.moon