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

Time:01-11

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)

CodePudding user response:

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);

CodePudding user response:

your idea of building a dependency tree it's really likeable. Anyway I tryed to find a solution as simplest possible. Even if it already works, there are many optimizations possible, take this just as proof of concept.

The background idea it's produce a List of Strings which you can read in order where each element it's what you need to solve progressively. Each element might be mandatory to solve something that come next in the List, hence for the overall expression. Once you solved all the chunks you have all pieces to solve your original expression.

It's written in Java, I hope it's understandable.

    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.Objects;

    public class StackOverflow {

        public static void main(String[] args) {

            String exp = "foo{a{x}-{y}}-{baz{one}-{two}}-foo{c}";
            List<String> chunks = expToChunks(exp);

            //it just reverse the order of the list
            Collections.reverse(chunks);
            System.out.println(chunks);
            //output -> [c, two, one, baz{one}-{two}, y, x, a{x}-{y}]

        }

        public static List<String> expToChunks(String exp) {
            List<String> chunks = new ArrayList<>();


            //this first piece just find the first inner open parenthesys and its relative close parenthesys
            int begin = exp.indexOf("{")   1;

            int numberOfParenthesys = 1;
            int end = -1;
            for(int i = begin; i < exp.length(); i  ) {
                char c = exp.charAt(i);
                if (c == '{') numberOfParenthesys   ;
                if (c == '}') numberOfParenthesys --;
                if (numberOfParenthesys == 0) {
                    end = i;
                    break;
                }
            }

            //this if put an end to recursive calls
            if(begin > 0 && begin < exp.length() && end > 0) {
                //add the chunk to the final list
                String substring = exp.substring(begin, end);
                chunks.add(substring);

                //remove from the starting expression the already considered chunk
                String newExp = exp.replace("{"   substring   "}", "");

                //recursive call for inner element on the chunk found
                chunks.addAll(Objects.requireNonNull(expToChunks(substring)));

                //calculate other chunks on the remained expression
                chunks.addAll(Objects.requireNonNull(expToChunks(newExp)));
            }


            return chunks;
        }


    }

Some details on the code:

The following piece find the begin and the end index of the first outer chunk of expression. The background idea is: in a valid expression the number of open parenthesys must be equal to the number of closing parenthesys. The count of open( 1) and close(-1) parenthesys can't ever be negative. So using that simple loop once I find the count of parenthesys to be 0, I also found the first chunk of the expression.

    int begin = exp.indexOf("{")   1;

    int numberOfParenthesys = 1;
    int end = -1;
    for(int i = begin; i < exp.length(); i  ) {
         char c = exp.charAt(i);
         if (c == '{') numberOfParenthesys   ;
         if (c == '}') numberOfParenthesys --;
         if (numberOfParenthesys == 0) {
             end = i;
             break;
         }
    }

The if condition provide validation on the begin and end indexes and stop the recursive call when no more chunks can be found on the remained expression.

    if(begin > 0 && begin < exp.length() && end > 0) {
        ...
    }
  • Related