Skip to content

feat: implement UDO - #539

Open
NicoLaval wants to merge 12 commits into
developfrom
feat/udo
Open

feat: implement UDO#539
NicoLaval wants to merge 12 commits into
developfrom
feat/udo

Conversation

@NicoLaval

Copy link
Copy Markdown
Collaborator

No description provided.

@hadrienk hadrienk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Few comments here and there. Given the scope, I'd question the LLM about how can we control the base implementation and validate the new UdoFunctionExpression and wiring until we know it works. Ie: unit tests on the model first with hard coded udo we replace later by dynamically resolved instances.

Comment thread vtl-engine/specs/udo/02-model.md Outdated
**Source of truth:** engine-side `UdoDefinition` in `ENGINE_SCOPE` bindings.
**Dispatch hook:** trampoline `java.lang.reflect.Method` registered under the same name (not a second source of truth).

No `vtl-model` DTO in P0.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why not?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Because the body is an ANTLR ExprContext (and we keep an engine handle for re-entry). That would pull the parser into vtl-model. Same as rulesets: engine-side artefact in bindings. Clarified in ffd5f0d.

Comment thread vtl-engine/specs/udo/02-model.md Outdated

## Body representation

**Parse subtree** (`ExprContext`). Define does not evaluate the body. Invoke re-enters `ExpressionVisitor` with a child map (params + outer bindings). Free vars resolve at **invoke time** ([08 §1](./08-open-questions.md)).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Free vars?

@NicoLaval NicoLaval Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Names in the body that aren’t parameters (e.g. y in max_with_y). P0 looks them up at invoke time; params shadow outer names. Not a lexical closure — snapshot-at-define can come later. Clarified in ffd5f0d.

Comment thread vtl-engine/specs/udo/03-define.md Outdated
| Action | Why |
|--------|-----|
| `bindings.put(name, udo)` | Source of truth for invoke |
| `registerMethod(name, trampoline)` | Enables `FunctionExpression` / `Method.invoke` path |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not sure. Might need to "resolve" the function id (like a variable) and then call it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Registering the method with registerMethod goes around the bindings and make closure scoping moot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

You’re right. registerMethod was only there to reuse FunctionExpression / Method.invoke.

Now: define → bindings.put only (08be7cf). Call → resolve the id in the current bindings; if UdoDefinition, evaluate as a ResolvableExpression (02a258b, 8dcae61).

Comment thread vtl-engine/specs/udo/04-invoke.md Outdated

```java
String name = ctx.operatorID().getText();
Object binding = engine.getBindings(ENGINE_SCOPE).get(name);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Might need to check what the scope mechanisms gives us here. Maybe create a scoped view of the bindings.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For closure binding

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes. Call lookup uses the visitor map, not always ENGINE_SCOPE (8dcae61). P0 is still invoke-time free-var lookup, not a snapshot at define.

Comment thread vtl-engine/specs/udo/04-invoke.md Outdated
5. If `returns` declared → assignability check (`integer` ⊆ `number` allowed).
6. Return result.

ThreadLocal CallSite is the P0 bridge (no per-UDO bytecode). Acceptable for the locked pattern; replace later only if needed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Double check later feasibility

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Dropped with the trampoline. UdoFunctionExpression.resolve evals the body with a child map (02a258b, 08be7cf).

Comment thread vtl-engine/specs/udo/06-types.md Outdated
| scalar actual → opaque `dataset` formal | **reject** |
| opaque `dataset` ↔ opaque `dataset` | allow (`instanceof Dataset` / `DatasetExpression`) |

Do not invent a parallel assignability table for UDOs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Probably the most important. Not sure about the value of this file. We have a type system, it will probably just work. Ask LLM about potential changes it foresees.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agree, no parallel type system. 06 is now only which signature syntax we reject at define (component, constraints, …). Assignability should follow existing TypeChecking (ffd5f0d). Not recoding that in this pass.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd put that as the first step. Absolute requirement.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Needs more work. Maybe leverage the plan in prov? Tests written in vtl itself + unit testing at the engine level?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes. Hardcoded expression unit tests first (02a258b), plus the existing script-level UserDefinedOperatorTest. .vtl fixtures like the prov corpus can come later, not blocking this PR.

Comment thread vtl-engine/specs/udo/07-testing.md Outdated
| Tag | IDs |
|-----|-----|
| Doc | D1–D4 |
| Métier | DS1–DS5 |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

French word.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in ffd5f0d — “Métier” → “Recipes”.


## 7. Component-level calls

Commented grammar (`genericOperatorsComponent` / `callComponent`) suggests component-level UDO invoke is unfinished upstream. **Out of scope** until grammar is revived.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Make sure to implement so that it work when compExpr is removed from grammar.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

UDO calls go through the normal expression path (visitCallDataset / ResolvableExpression), not callComponent (already commented out in Vtl.g4). 08 rephrased in ffd5f0d.

* Resolves call-site arguments (defaults / {@code _}) then returns a {@link UdoFunctionExpression}
* so evaluation goes through {@code FunctionExpression} → {@code Method.invoke}.
*/
public final class UdoInvokeExecutor {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Weird. With the resolvable expr this should be simpler. It should also implement the general expression contract.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agree. The UDO is now a ResolvableExpression that binds formals and visits the body (02a258b). UdoInvokeExecutor is only argument wiring (defaults / _ need the raw parameter ctx). Trampoline removed in 08be7cf.

@NicoLaval
NicoLaval requested a review from hadrienk August 14, 2026 14:10
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.

2 participants