Skip to content

Add #-prefixed custom operators with inferred arity (0–3) to parser and tree renderer#2

Merged
psaegert merged 4 commits into
masterfrom
copilot/add-custom-operators-support
Apr 21, 2026
Merged

Add #-prefixed custom operators with inferred arity (0–3) to parser and tree renderer#2
psaegert merged 4 commits into
masterfrom
copilot/add-custom-operators-support

Conversation

Copilot AI commented Apr 21, 2026

Copy link
Copy Markdown

This PR adds user-defined/custom operators using #name syntax 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)

    • Added custom token support: #name ([A-Za-z_][A-Za-z0-9_]*)
    • Added comma tokenization for argument separation
    • Added custom token shape in postfix: { type: 'custom', name, arity }
  • Validation path for custom operators

    • Added structural validation for expressions containing custom tokens (instead of math.parse)
    • Supports:
      • arity 0 leaf form: #pi
      • arity 1–3 call form: #f(x), #f(x,y), #f(x,y,z)
      • nested custom/operator/function expressions in arguments
    • Rejects malformed forms:
      • bad name: #1foo(a)
      • empty args: #foo(,a), #foo(a,), #foo()
      • arity > 3 (rendering cap)
  • Shunting-yard extension

    • Arity-0 custom ops emit directly as leaves
    • Custom calls push function token to operator stack
    • Comma handling pops until ( to delimit argument subexpressions
    • On ), custom function token is emitted with inferred arity = commas + 1
  • Tree model + construction/rendering (tree.js)

    • Generalized Node to children[] with left/right aliases retained
    • Replaced postfix→tree logic with standard stack-based arity-driven construction
    • Updated coordinate assignment/drawing to iterate children in order
    • Supports unary/binary/ternary layouts
    • Custom labels render without the # prefix (prefix is syntax-only)
    • Node radius is fixed; font size scales down for longer labels instead of growing the circle
    • Increased horizontal spacing between sibling subtrees to prevent node overlap
  • Docs/UI updates

    • Updated input placeholder and examples for custom operators
    • Updated README syntax/examples for #pi, #sign(a), #add(a,b), #clamp(x,a,b)
    • Updated invalid-expression help text to include custom syntax and arity cap
infixToPostfix('sin(#add(a,b))*#pi')
// => ['a', 'b', { type: 'custom', name: 'add', arity: 2 }, 'sin', { type: 'custom', name: 'pi', arity: 0 }, '*']

UI screenshot

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 arguments

Arity 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

  • Prefix: Custom operator names are always preceded by # to distinguish them from built-in functions (sin, cos, …) and variables (az).
  • Name charset: Python identifier rules — [A-Za-z_][A-Za-z0-9_]*. Must match exactly after the #.
  • Arity 0: Written as just #name with no parentheses (e.g., #pi, #e). It's a leaf node, used exactly like a variable.
  • Arity ≥ 1: Written as #name(arg1, arg2, ...). Each argument is a full sub-expression (can contain operators, built-in functions, nested custom operators, etc.).
  • Whitespace around commas and inside parens should be tolerated.

Files involved

  • infixToPostfix.js — tokenizer, validator, Shunting-yard
  • tree.js — Node model, postfix→tree construction, rendering
  • index.html / README.md — examples/placeholder text

Required 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.
  • The comma , character → emit an argument-separator token (only meaningful inside a custom operator's argument list).

Update the character whitelist / regex in isValidExpression to allow #, _, digits (inside identifiers only), and ,.

2. Validator (isValidExpression)

  • Custom operators cannot be validated via math.parse, so short-circuit: if the expression contains any #name tokens, do a structural validation instead:
    • Every #name followed by ( must have a matching ), with well-formed comma-separated arguments.
    • Every #name not followed by ( is arity 0 and treated as a leaf (like a variable).
    • Each argument sub-expression must itself be a valid expression (recurse).
    • Disallow empty arguments (#f(,x), #f(x,), #f() with arity 0 written as #f() — arity 0 MUST be written without parens).
    • Identifier after # must match ^[A-Za-z_][A-Za-z0-9_]*$.
  • Expressions with no # continue through the existing math.parse path unchanged.

3. Shunting-yard (infixToPostfix)

Extend the current function-aware Shunting-yard:

  • Arity 0 custom op (#pi): behaves exactly like a variable — push directly to output.
  • Arity ≥ 1 custom op (#f(...)): push the custom-op token onto the op stack (like the built-in unary functions).
  • Comma ,: pop operators from op stack to output until ( is on top (do not pop the (). This delimits argument sub-expressions correctly.
  • Closing ): 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 constructTree knows how many children to pop. Options:

  • (Preferred) Make custom tokens rich objects: { 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.
  • Alternative: emit arity as a separate postfix token right after the name (simpler but uglier). Use the object approach.

4. Node model & tree construction (tree.js)

Generalize children. Currently Node has left/right. Change to a children: [] array while keeping left/right as getters/aliases for children[0] and children[1] so the rest of tree.js (drawEdge, drawTree, setCoordinates, print_coords, etc.) continues to work for binary/unary cases without invasive edits.

Add an arity property on Node (0 for leaves, 1 for unary, 2 for binary, or N for custom).

constructTree(postfix) rewrite. Replace the right-to-left shift-flag algorithm with the standard left-to-right stack-based postfix→tree algorithm:

for token in po...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI changed the title [WIP] Add support for custom operators and user-defined functions Add #-prefixed custom operators with inferred arity (0–3) to parser and tree renderer Apr 21, 2026
Copilot AI requested a review from psaegert April 21, 2026 09:23
@psaegert

Copy link
Copy Markdown
Owner

@copilot It seems to work judging by your screenshot. But the names in the nodes should be displayed without the #, and the overall formatting could be improved. In the screenshot everything is a little close and overlaps. And I think it would be better if the font size decreased instead of increasing the node size for longer names.

Copilot AI commented Apr 21, 2026

Copy link
Copy Markdown
Author

@copilot It seems to work judging by your screenshot. But the names in the nodes should be displayed without the #, and the overall formatting could be improved. In the screenshot ev...

Dropped the # from displayed labels, switched to fixed node radius with font scaling for longer names, and bumped horizontal spacing so siblings no longer overlap (bf0c48c).

Updated rendering

@psaegert psaegert marked this pull request as ready for review April 21, 2026 09:30
Copilot AI review requested due to automatic review settings April 21, 2026 09:30
@psaegert psaegert merged commit 7be9762 into master Apr 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread infixToPostfix.js
Comment on lines 55 to +60
tokens.push(current)
continue
}
if (current === ',') {
tokens.push(current)
continue
Comment thread canvas.js
Comment on lines 21 to 24
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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants