-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
general variables
function
#1
Conversation
src/extract.js
Outdated
if (node.operator === ":") { | ||
return right; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This special case is needed to handle Mavo's group(key1: value1, key2: value2, ...)
operator. I'm thinking about the best way to be able to get rid of this special case by adding the second options
parameter to the function. My first thought is to include the field modifier
inside of options, i.e.
options: {
modifier: (node) => void; // applies some sort of modification to the node
// other options fields
}
This would then enable us (within Mavo) to call extract like this:
const expression = "group(key1: 'foo', key2: 'bar')";
const ast = Mavo.Script.parse(expression);
const modifier = (node) => {
if (node.type === "BinaryExpression" && node.operator === ":") {
node = node.right; // ignore the left side of `key : value` BinaryExpressions
}
};
const extractResult = Vastly.extract(ast, {modifier})
Wanted to get feedback to see if anyone has any better thoughts on this, or the options
parameter in general
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, Mavo specific stuff has no place here, but it's totally fine for Mavo to be passing specific options to these functions, so the general idea is on the right track: we design vastly to be flexible enough to accommodate Mavo, then Mavo can just pass certain options when using vastly methods.
Some issues with this proposed API:
- Only allowing a single function forces calling code to couple all their modifications in the same place
- The modifier being an arbitrary function will make it harder for you to adapt
extract()
to use this parameter. You can't add handling code to individual cases, so where does it go? Before or after the switch? - Speaking of ordering, it is also not clear to users when this gets executed: before, after, or instead of our code?
- I imagine a lot of use cases will want to specify these overrides once, rather than having to pass them in every single function call, so it would be nice if that was supported. As a rule of thumb, a good design principle for all UI design (including API design) is to try and make common things easy and complex things possible.
I have some thoughts on what an alternative API could look like, but I think there's value in you iterating on it before I share my thoughts. 😊
Love the tests! How did it feel to specify them using this syntax? More or less tedious than other testing frameworks you have used? Thinking some more and looking at the complications the recent change introduced, I wonder if the distinction between functions and variables is actually arbitrary, and we are guilty of overfitting to our primary use case. What if other code wanted to draw a different distinction, or none at all? Thought experiment: If this function just extracted top level identifiers, what would the code look like to tell them apart after (remember that each node has a pointer to its parent, and we also have Yes, that would be slower (without thinking about it much, I think the current solution is O(N) on the number of AST nodes, this one would be O(Nk), where k is depth), but usually these kinds of things are not hugely performance-critical (e.g. rewriting expressions happens once per expression for the lifetime of the app) so trading off some performance to get more flexibility could be a worthy tradeoff. |
It was definitely much easier! I might be biased because I typically write tests with a react app, and testing components is annoying, but still, it was great
Hmm this is a good point! We could provide this general function, which returns identifiers (we would have to rename this to something like
I agree, especially since the typical expression is mostly likely pretty simple, and wouldn't have many nodes anyways |
Important distinction: top-level identifiers, i.e. basically what you're extracting now, but in one array. Just extracting identifiers wouldn't be that useful (you can do it already with a simple walker)
We won't know until we see what the code for it would look like! (if it's up to the vastly user) let variables = extractVariables(expression);
let functions = [], data = [];
for (let variable of variables) {
if (/* ??? */) {
functions.push(variable);
}
else {
data.push(variable);
}
} Can you fill in the |
Yes
Where does const variables = extractVariables(expr);
const functions = [], data = [];
for (const variable of variables) {
if (variable.parent && variable.parent.type === "CallExpression") {
functions.push(variable);
} else {
data.push(variable);
}
} but when I ran a simple test, the parent field was not populated. |
Huh. Right now from Just thinking out loud here: Rather than having this be a side effect of one of the other functions, it should probably be its own function. Note that functions cannot always do this on an as-needed basis: if e.g.
That’s not the main issue with this:
Another way would be to treat the nodes as entirely untouchable, and have a
|
Also thinking out loud: what if instead of returning a list of nodes, this function returns a list of objects with this form? {
node: NodeObj;
parentProperty: "callee" | "left" | "right" | ... ;
} That way the node wouldn't be mutated, and someone who uses this function would be able to tell a variable's relationship to its parent. The downside is that this adds memory per each node in the output, but what do you think? |
Coming back to this after a few days, my above comment doesn't make sense given that we'd potentially need to walk up the tree multiple levels to determine if an identifier is a function or not. Perhaps there's some sort of way we might consider making a new function called Then we could call I'm struggling to think of a great way to solve this, but does this seem like a solution that could be reasonable? The memory complexity is again not good, but I can't think of a better solution that's not overfitting to Mavo's use case... |
I started a new issue for the broader architectural discussion on parent references: #3 You don't need to block on it: since your function accepts whole ASTs, you can just call This is somewhat orthogonal to being able to tell which top-level identifiers are what, because even with being able to get parents it's still nontrivial. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi there, see comments
src/extract.js
Outdated
} | ||
} | ||
|
||
return _extractIdentifiers(node); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need a separate function here? It seems to take the same arguments?
Other functions use this pattern when they need to pass more arguments to the recursive function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did this because it was easier to only have to pass in 1 argument in recursive subcalls rather than all of them. Plus, then parents.setAll
only needs to be called once at the start of the function (I know that even if it gets called multiple times it will only walk the tree once because it uses the heuristic of if a node has a parent pointer then it doesn't walk its subtree, but still)
If you don't like this and would prefer it be simpler, I can unnest it
src/variables.js
Outdated
* Recursively traverse the AST and return all top-level identifiers | ||
* @param {object} node | ||
* @param {object} [options] | ||
* @param {function(object): boolean} [options.filter] A function that returns true if the node should be included |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd leave options out until we have use cases that need them.
src/variables.js
Outdated
* @param {object} node | ||
* @param {object} [options] | ||
* @param {function(object): boolean} [options.filter] A function that returns true if the node should be included | ||
* @param {boolean} [options.addParents] If true, add a parent property to each node |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's leave that out until we know what we're doing in #3
test/variables.js
Outdated
name: "variables()", | ||
run (expression) { | ||
const ast = jsep(expression); | ||
let identifiers = variables(ast); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: indentation
src/variables.js
Outdated
const propertyChildren = property.type === "Identifier" ? [] : variables(property); | ||
return variables(object).concat(propertyChildren); | ||
// Rest of the cases contain a single variable | ||
// Also check for filter condition |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Also check for filter condition |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See minor comments, but overall looks good!
I don't think it should hold the PR up, but the tests are very Mavo-specific, and it's unclear all are testing sufficiently different things.
Got it, I will write some more robust tests |
Keep in mind that's pretty low priority — everything else we discussed is far more important! |
Yes, I'm just going to write a handful more then merge in ~1 hour |
Re: your most recent commit, that's a good first step, but I think we also need to remove some. It's not clear what each test is actually testing, and I suspect there is a lot of duplication. |
Ok, I can make a follow-up PR to deduplicate and get better coverage |
No description provided.