Skip to content

Configurable named types for UDF/UDT and configurable Number type#3014

Draft
gregli-msft wants to merge 10 commits intomainfrom
gregli/udt-decimal-2
Draft

Configurable named types for UDF/UDT and configurable Number type#3014
gregli-msft wants to merge 10 commits intomainfrom
gregli/udt-decimal-2

Conversation

@gregli-msft
Copy link
Contributor

@gregli-msft gregli-msft commented Jan 28, 2026

Currently, there is a named types table in the Engine, that contains Number, Text, Boolean, etc. The problem is that there isn't one list of named types that are appropriate for consumers of the Engine: for example, Canvas doesn't support Decimal, while Dataverse formula columns support DateTimeNoTZ but nobody else does.

This is the key change in the Engine class (Engine.cs):
image

  1. Where there used to be a fixed set of primitives for all hosts, this is now handed to the Engine by the host.
  2. It is handed in the Engine constructor, hence this is only { get; }.
  3. This is scoped to the type names available for UDTs and UDFs. It is not a general list of all primitives supported by this host.

This change pulls the named types out and makes that something that needs to be setup in order to use UDF/UDTs. RecalcEngine does this automatically, with the correct types, for all consumers of the C# interpreter. Dataverse doesn't need to worry about it as UDF/UDTs aren't usable there. Canvas will require an update.

In addition, the Number type name needs to be added to the symbol table of the Engine as an alias for Float or Decimal as appropriate. Number should be setup with the default numeric type, allowing UDT/UDFs to be type agnostic for situations where it doesn't matter. Using Float or Decimal explicitly is always specific. Setting this up is in addition to passing the NumberIsFloat flag into the Parser. As Canvas doesn't have Decimal yet, and has only ever had Float, and it is the only product with UDT/UDFs today, this is not a breaking change.

Error messages associate with numeric types have been simplified. Instead of perhaps saying that a type could be one of Number, Decimal, Float, these three will be folded into a single Number, . Which has several advantages:

  1. We'll make it clear that Number is a placeholder for either Decimal or Float where available and covers those two. Decimal always coerces to Float and vice versa.
  2. It is shorter with less cognitive load. At the level of this example, it doesn't matter if those are floats or decimals.
  3. Most makers will never need to use anything other than Number. We should be guiding makers in this direction anyway.
  4. It makes the engineering much simpler. We don't need to plumb down to the error messages which numeric types are supported.

Finally, the list of reserved type names has been updated, attempting a small amount of future proofing.

@gregli-msft gregli-msft requested a review from a team as a code owner January 28, 2026 21:19
@gregli-msft gregli-msft marked this pull request as draft January 28, 2026 21:19

if (!nameResolver.LookupType(returnTypeToken.Name, out var returnTypeFormulaType))
// only the return type from a UDF can be a void type, it is not valid in other type contexts
if (returnTypeToken.Name == FormulaType.Void.Name)
Copy link
Contributor

Choose a reason for hiding this comment

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

if (returnTypeToken.Name == FormulaType.Void.Name)

Will this brake if someone added "Void" named Type in resolver?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Things could go haywire if someone added Void as a type in the builtinnamedtypes or as a UDT, but there are checks to prevent both from happening now. Is there another way you think it could be done?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we put this inside the nameResolver If? (I know very edge case/ silly case) but what if we allowed SymbolTable to have "Void" named type that is not actually a VoidType.

return argIndex == 1;
}

// This list is effectively intersected with the BuiltInNamedTypes in the Engine, with outliers generating errors before this list is checked.
Copy link
Contributor

Choose a reason for hiding this comment

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

intersected

If it's intersected, do we need DType.Float?

Copy link
Contributor Author

@gregli-msft gregli-msft Feb 2, 2026

Choose a reason for hiding this comment

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

I'm not sure I understand the question exactly. We need DType.Number (Float) in this list so that if a host supports Float, then it would be OK as a supported JSON type. DType.Decimal is a good example where it exists in this list, but wouldn't work in Canvas today.

@jas-valgotar
Copy link
Contributor

✅ No public API change.

@jas-valgotar
Copy link
Contributor

✅ No public API change.

@jas-valgotar
Copy link
Contributor

✅ No public API change.

@@ -469,17 +469,23 @@ internal void AddTypes(IEnumerable<KeyValuePair<DName, FormulaType>> types)
/// Helper to create a symbol table with primitive types.
Copy link
Contributor

Choose a reason for hiding this comment

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

primitive types.

nit: is it restricted to primitive types still?

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