Search code examples
algorithmdata-structuresinterpolation

Algorithm / data structure for resolving nested interpolated values in this example?


I am working on a compiler and one aspect currently is how to wait for interpolated variable names to be resolved. So I am wondering how to take a nested interpolated variable string and build some sort of simple data model/schema for unwrapping the evaluated string so to speak. Let me demonstrate.

Say we have a string like this:

foo{a{x}-{y}}-{baz{one}-{two}}-foo{c}

That has 1, 2, and 3 levels of nested interpolations in it. So essentially it should resolve something like this:

  1. wait for x, y, one, two, and c to resolve.
  2. when both x and y resolve, then resolve a{x}-{y} immediately.
  3. when both one and two resolve, resolve baz{one}-{two}.
  4. when a{x}-{y}, baz{one}-{two}, and c all resolve, then finally resolve the whole expression.

I am shaky on my understanding of the logic flow for handling something like this, wondering if you could help solidify/clarify the general algorithm (high level pseudocode or something like that). Mainly just looking for how I would structure the data model and algorithm so as to progressively evaluate when the pieces are ready.

I'm starting out trying and it's not clear what to do next:

{
  dependencies: [
    {
      path: [x]
    },
    {
      path: [y]
    }
  ],
  parent: {
    dependency: a{x}-{y} // interpolated term
    parent: {
      dependencies: [
        {

        }
      ]
    }
  }
}

Some sort of tree is probably necessary, but I am having trouble figuring out what it might look like, wondering if you could shed some light on that with some pseudocode (or JavaScript even).

  • watch the leaf nodes at first
  • then, when the children of a node are completed, propagate upward to resolving the next parent node. This would mean once x and y are done, it could resolve a{x}-{y}, but then wait until the other nodes are ready before doing the final top-level evaluation.

You can just simulate it by sending "events" to the system theoretically, like:

ready('y')
ready('c')
ready('x')
ready('a{x}-{y}')

function ready(variable) {
  if ()
}

...actually that may not work, not sure how to handle the interpolated nodes in a hacky way like that. But even a high level description of how to solve this would be helpful.

export type SiteDependencyObserverParentType = {
  observer: SiteDependencyObserverType
  remaining: number
}

export type SiteDependencyObserverType = {
  children: Array<SiteDependencyObserverType>
  node: LinkNodeType
  parent?: SiteDependencyObserverParentType
  path: Array<string>
}

(What I'm currently thinking, some TypeScript)


Solution

  • Here is an approach in JavaScript:

    • Parse the input string to create a Node instance for each {} term, and create parent-child dependencies between the nodes.
    • Collect the leaf Nodes of this tree as the tree is being constructed: group these leaf nodes by their identifier. Note that the same identifier could occur multiple times in the input string, leading to multiple Nodes. If a variable x is resolved, then all Nodes with that name (the group) will be resolved.
    • Each node has a resolve method to set its final value
    • Each node has a notify method that any of its child nodes can call in order to notify it that the child has been resolved with a value. This may (or may not yet) lead to a cascading call of resolve.
    • In a demo, a timer is set up that at every tick will resolve a randomly picked variable to some number

    I think that in your example, foo, and a might be functions that need to be called, but I didn't elaborate on that, and just considered them as literal text that does not need further treatment. It should not be difficult to extend the algorithm with such function-calling features.

    class Node {
        constructor(parent) {
            this.source = ""; // The slice of the input string that maps to this node
            this.texts = []; // Literal text that's not part of interpolation
            this.children = []; // Node instances corresponding to interpolation
            this.parent = parent; // Link to parent that should get notified when this node resolves
            this.value = undefined; // Not yet resolved
        }
        isResolved() {
            return this.value !== undefined;
        }
        resolve(value) {
            if (this.isResolved()) return; // A node is not allowed to resolve twice: ignore
            console.log(`Resolving "${this.source}" to "${value}"`);
            this.value = value;
            if (this.parent) this.parent.notify();
        }
        notify() {
            // Check if all dependencies have been resolved
            let value = "";
            for (let i = 0; i < this.children.length; i++) {
                const child = this.children[i];
                if (!child.isResolved()) { // Not ready yet
                    console.log(`"${this.source}" is getting notified, but not all dependecies are ready yet`);
                    return; 
                }
                value += this.texts[i] + child.value;
            }
            console.log(`"${this.source}" is getting notified, and all dependecies are ready:`);
            this.resolve(value + this.texts.at(-1));
        }
    }
    
    function makeTree(s) {
        const leaves = {}; // nodes keyed by atomic names (like "x" "y" in the example)
        const tokens = s.split(/([{}])/);
        let i = 0; // Index in s
        
        function dfs(parent=null) {
            const node = new Node(parent);
            const start = i;
            while (tokens.length) {
                const token = tokens.shift();
                i += token.length;
                if (token == "}") break;
                if (token == "{") {
                    node.children.push(dfs(node));
                } else {
                    node.texts.push(token);
                }
            }
            node.source = s.slice(start, i - (tokens.length ? 1 : 0));
            if (node.children.length == 0) { // It's a leaf
                const label = node.texts[0];
                leaves[label] ??= []; // Define as empty array if not yet defined
                leaves[label].push(node);
            }
            return node;
        }
        
        dfs();
        
        return leaves;
    }
    
    // ------------------- DEMO --------------------
    let s = "foo{a{x}-{y}}-{baz{one}-{two}}-foo{c}";
    const leaves = makeTree(s);
    
    // Create a random order in which to resolve the atomic variables:
    function shuffle(array) {
        for (var i = array.length - 1; i > 0; i--) {
            var j = Math.floor(Math.random() * (i + 1));
            [array[j], array[i]] = [array[i], array[j]];
        }
        return array;
    }
    const names = shuffle(Object.keys(leaves));
    
    // Use a timer to resolve the variables one by one in the given random order
    let index = 0;
    function resolveRandomVariable() {
        if (index >= names.length) return; // all done
        console.log("\n---------------- timer tick --------------");
        const name = names[index++];
        console.log(`Variable ${name} gets a value: "${index}". Calling resolve() on the connected node instance(s):`);
        for (const node of leaves[name]) node.resolve(index);
        setTimeout(resolveRandomVariable, 1000);
    }    
    setTimeout(resolveRandomVariable, 1000);