Add #-prefixed custom operators with inferred arity (0–3) to parser and tree renderer#2
Conversation
Agent-Logs-Url: https://github.com/psaegert/expression-tree-gen/sessions/b0a59eb7-ed41-437f-beb7-60d427c204bd Co-authored-by: psaegert <36567814+psaegert@users.noreply.github.com>
Agent-Logs-Url: https://github.com/psaegert/expression-tree-gen/sessions/b0a59eb7-ed41-437f-beb7-60d427c204bd Co-authored-by: psaegert <36567814+psaegert@users.noreply.github.com>
#-prefixed custom operators with inferred arity (0–3) to parser and tree renderer
|
@copilot It seems to work judging by your screenshot. But the names in the nodes should be displayed without the |
Agent-Logs-Url: https://github.com/psaegert/expression-tree-gen/sessions/9641a83b-5b89-4e77-8331-bbb358b7a92e Co-authored-by: psaegert <36567814+psaegert@users.noreply.github.com>
Dropped the |
There was a problem hiding this comment.
Pull request overview
Adds #name-prefixed custom operators/functions to the expression pipeline, inferring arity (0–3) from comma-separated arguments at the call site, and updates the tree renderer to handle N-ary nodes.
Changes:
- Extended tokenization, validation, and shunting-yard conversion to support custom tokens (
#name) and commas for argument separation. - Updated postfix → tree construction to be arity-driven and generalized tree nodes to
children[](supporting unary/binary/ternary/custom). - Updated UI placeholder, examples, and documentation to include custom operator syntax.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| infixToPostfix.js | Adds custom-token parsing/validation and arity inference in shunting-yard; emits { type: 'custom', name, arity } into postfix. |
| tree.js | Generalizes node structure to children[] and renders custom tokens/variable arity nodes; adjusts layout and label sizing. |
| canvas.js | Adds sample expressions using custom operators and updates invalid-expression help text. |
| index.html | Updates input placeholder to demonstrate custom operator usage. |
| README.md | Documents custom operator syntax and provides examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| tokens.push(current) | ||
| continue | ||
| } | ||
| if (current === ',') { | ||
| tokens.push(current) | ||
| continue |
| var expression = document.getElementById('expression-input').value | ||
| if (typeof expression !== 'undefined' && null != expression) { | ||
| expression = expression.replace(/\s+/g, '') | ||
| expression = expression.toLowerCase() | ||
| var postfix = infixToPostfix(expression); |

This PR adds user-defined/custom operators using
#namesyntax with arity inferred from call-site arguments, including nested custom calls. It extends parsing, postfix generation, and tree rendering while preserving existing arithmetic + built-in unary behavior.Tokenizer + syntax model (
infixToPostfix.js)#name([A-Za-z_][A-Za-z0-9_]*){ type: 'custom', name, arity }Validation path for custom operators
math.parse)#pi#f(x),#f(x,y),#f(x,y,z)#1foo(a)#foo(,a),#foo(a,),#foo()Shunting-yard extension
(to delimit argument subexpressions), custom function token is emitted with inferredarity = commas + 1Tree model + construction/rendering (
tree.js)Nodetochildren[]withleft/rightaliases retained#prefix (prefix is syntax-only)Docs/UI updates
#pi,#sign(a),#add(a,b),#clamp(x,a,b)Original prompt
Goal
Add support for custom operators / user-defined functions with arbitrary arity using a dedicated syntax prefixed with
#:#pi— arity 0 (constant / leaf)#sign(x)— arity 1#add(x, y)— arity 2#my_func(x, y, z)— arity 3#clamp(x, #min(a,b), c)— nested custom operators and arbitrary expressions as argumentsArity is inferred from the number of comma-separated arguments at the call site (no
[n]annotation in the syntax). The same name used with different argument counts is treated as a different operator — this is fine and doesn't need special handling beyond the per-call arity.This builds on the unary-functions PR that was just merged to
master.Syntax rules
#to distinguish them from built-in functions (sin,cos, …) and variables (a–z).[A-Za-z_][A-Za-z0-9_]*. Must match exactly after the#.#namewith no parentheses (e.g.,#pi,#e). It's a leaf node, used exactly like a variable.#name(arg1, arg2, ...). Each argument is a full sub-expression (can contain operators, built-in functions, nested custom operators, etc.).Files involved
infixToPostfix.js— tokenizer, validator, Shunting-yardtree.js— Node model, postfix→tree construction, renderingindex.html/README.md— examples/placeholder textRequired changes
1. Tokenizer (
infixToPostfix.js)Extend the tokenizer to recognize:
#followed by a Python-style identifier → emit a single custom-operator token. Represent it as an object or tagged string so downstream code can read back the name and (later) arity — e.g.,{ type: 'custom', name: 'my_func' }or the string#my_func.,character → emit an argument-separator token (only meaningful inside a custom operator's argument list).Update the character whitelist / regex in
isValidExpressionto allow#,_, digits (inside identifiers only), and,.2. Validator (
isValidExpression)math.parse, so short-circuit: if the expression contains any#nametokens, do a structural validation instead:#namefollowed by(must have a matching), with well-formed comma-separated arguments.#namenot followed by(is arity 0 and treated as a leaf (like a variable).#f(,x),#f(x,),#f()with arity 0 written as#f()— arity 0 MUST be written without parens).#must match^[A-Za-z_][A-Za-z0-9_]*$.#continue through the existingmath.parsepath unchanged.3. Shunting-yard (
infixToPostfix)Extend the current function-aware Shunting-yard:
#pi): behaves exactly like a variable — push directly to output.#f(...)): push the custom-op token onto the op stack (like the built-in unary functions).,: pop operators from op stack to output until(is on top (do not pop the(). This delimits argument sub-expressions correctly.): after popping until(, if the token below(is a function (built-in or custom), also pop it to output — existing behavior, just make sure custom tokens are recognized.Arity tracking. The postfix output needs to carry arity for custom operators so
constructTreeknows how many children to pop. Options:{ type: 'custom', name: 'add', arity: 2 }, with arity computed during Shunting-yard by counting commas + 1 between the matching parens. A simple way: when you push a custom-op token on the op stack, also push a counter; increment on each,encountered while that function is the active frame; on), attach the final arity (counter value, or 1 if no commas were seen but args were present, or 0 if the call had no parens at all) to the token before emitting it.4. Node model & tree construction (
tree.js)Generalize children. Currently
Nodehasleft/right. Change to achildren: []array while keepingleft/rightas getters/aliases forchildren[0]andchildren[1]so the rest oftree.js(drawEdge,drawTree,setCoordinates,print_coords, etc.) continues to work for binary/unary cases without invasive edits.Add an
arityproperty onNode(0 for leaves, 1 for unary, 2 for binary, or N for custom).constructTree(postfix)rewrite. Replace the right-to-leftshift-flag algorithm with the standard left-to-right stack-based postfix→tree algorithm: