Skip to content

fix(transpiler): user-defined functions named after a constant namespace - #310

Merged
alexgrover merged 2 commits into
devfrom
fix/udf-namespace-collision
Sep 17, 2026
Merged

alexgrover merged 2 commits into
devfrom
fix/udf-namespace-collision

Conversation

@alexgrover

@alexgrover alexgrover commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

Problem

Pine allows a user-defined function to share its name with a constant namespace, and to keep using that namespace's members in the same script. This valid script fails on dev today:

//@version=6
indicator("My script")
position(x)=>close+x
plot(position(14))

→ TypeError: position is not a function

Two transpiler passes are involved:

  1. Codegen collision pass (codegen.ts, renameVariableRefsInAST): the declaration is renamed to position_$0, but bare call sites are deliberately left alone. That assumption is right for variable collisions (fill = 3 next to the built-in fill(p1, p2)) but wrong for function collisions — a constants namespace is not callable, so position(14) can only mean the user function.

    function position_$0(x) { ... }
    plot(position(14));   // ← still the bare name → resolves to the constants object
  2. Parser name → name_var rewrite (parser.ts, ~L1749): once a function position() exists, any non-call reference to position is treated as a variable sharing the function's name. That fires on the namespace base of position.top_right → position_var.top_right → ReferenceError: position_var is not defined.

This affects every entry of NAMESPACE_COLLISION_NAMES (position, font, order, currency, size, format, …) and was introduced with the 0.9.32 "user variables named after a constant namespace" fix, which only considered variables. It also means adding a name to that list can turn a working script into a broken one — which is exactly what happens to scale(x)=>close+x with #305 as-is (works on dev, TypeError: scale is not a function on the PR branch).

Fix

  • codegen.ts: track collision names that were declared as a FunctionDeclaration (userFunctionCollisions) and rename their bare callees, alongside the existing JS_RESERVED_WORDS callee rename. Variable collisions are unchanged (guarded by a test).
  • parser.ts: the _var rewrite skips an identifier that is a collision name and is immediately followed by . (namespace member access).

Both changes are in the pineToJS stage only; the Phase-2 transpiler is untouched.

Tests

tests/transpiler/namespace-identifier-collision.test.ts, new describe block, 7 cases:

  • position(x)=>… bare call resolves to the user function
  • position(x)=>… coexisting with table.new(position.top_right, …)
  • font(x)=>… coexisting with font.family_monospace as a named argument
  • UDF call nested in math.max(...) and in arithmetic
  • UDF called by name from inside another UDF
  • guard: a variable named fill is still renamed while the built-in fill(...) keeps reaching the namespace
  • forward guard: scale(x)=>… works (must keep working once scale becomes a collision name)

5 of these fail on dev for the stated reasons (is not a function ×4, position_var is not defined ×1); all pass with the fix.

Regression check

  • Full suite on this branch: 1845 passed | 5 skipped | 16 todo — dev baseline is 1838, +7 new tests, 0 failures.
  • tsc --emitDeclarationOnly -p tsconfig.dts.json: clean.
  • Merged together with Fix scale and settlement_as_close namespaces not resolving in scripts #305 (conflict-free): all 40 tests in the collision + constants files pass, including scale(x)=>close+x and indicator(scale = scale.right) in the same script.

Relation to #305

#305 is correct in what it adds, but on its own it regresses scale(x)=>…. With this PR merged first, #305 becomes safe to merge as-is.


Note

Medium Risk
Changes pineToJS identifier renaming for every NAMESPACE_COLLISION_NAMES entry when used as UDFs; mistakes could mis-route calls vs namespace access, but behavior is narrowly scoped and heavily regression-tested.

Overview
Fixes valid Pine scripts that declare a user function with the same name as a constant namespace (position(x) => …, font(x) => …) while still using namespace members like position.top_right.

The pineToJS codegen collision pass now tracks collision names declared as functions (userFunctionCollisions) and renames bare call sites name(...) to name_$N, not only the declaration. Variable collisions stay unchanged: fill = 3 plus built-in fill(p1, p2) still targets the namespace at the call.

The parser no longer applies the name → name_var rewrite when a collision-name identifier is immediately followed by ., so position.top_right stays a namespace access instead of becoming position_var.top_right.

Regression coverage is in tests/transpiler/namespace-identifier-collision.test.ts (UDF calls, mixed namespace use, nested/indirect calls, and the variable-collision guard). This also unblocks safely extending NAMESPACE_COLLISION_NAMES (e.g. scale in #305) for scripts that use those names as functions.

Reviewed by Cursor Bugbot for commit 3b451e6. Bugbot is set up for automated code reviews on this repo. Configure here.

Pine allows a script to declare a function whose name matches a constant
namespace (`position(x) => close + x`) and to keep using that namespace's
members in the same script. Two transpiler passes broke this:

- The pineToJS codegen collision pass renamed the declaration to
  `position_$N` but left bare call sites alone (it assumed a bare callee is
  always the built-in, which is only true for VARIABLE collisions such as
  `fill = 3` alongside `fill(p1, p2)`), so `plot(position(14))` resolved to
  the constants object -> `TypeError: position is not a function`. The pass
  now records which collision names were declared as functions and renames
  their bare callees too.

- The parser's `name -> name_var` rewrite (for a variable sharing a user
  function's name) fired on the namespace base of `position.top_right` once
  a `position()` function existed -> `ReferenceError: position_var is not
  defined`. It now skips a collision-name identifier followed by `.`.

Affects every entry of NAMESPACE_COLLISION_NAMES and makes adding names to
that list (e.g. `scale`, #305) safe for scripts that use them as functions.

Tests: tests/transpiler/namespace-identifier-collision.test.ts (7 new cases:
bare call, member-access coexistence, argument position, nested/indirect
calls, variable-collision guard, `scale` forward guard).

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Sep 14, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_78b89a92-4aa8-4ca1-908e-844b50faa144)

@cursor

cursor Bot commented Sep 17, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_8d502aea-c463-4d6c-9bd9-cd78f2c1e669)

@alexgrover
alexgrover merged commit 73bfeb1 into dev Sep 17, 2026
2 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant