Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Overview

Power Fx is a low-code general-purpose programming language based on spreadsheet-like formulas. This is the open-source C# implementation used across Microsoft Power Platform. The codebase contains a complete compiler and interpreter.

## Build Commands

```bash
# Build (from repo root)
dotnet build src/Microsoft.PowerFx.sln

# Build specific configuration (Debug, Release, DebugAll, ReleaseAll, Debug462, Debug70)
dotnet build src/Microsoft.PowerFx.sln -p:Configuration=Release

# Run all tests
dotnet test src/Microsoft.PowerFx.sln

# Run tests for a specific project
dotnet test src/tests/Microsoft.PowerFx.Core.Tests.Shared/Microsoft.PowerFx.Core.Tests.Shared.csproj

# Run a single test by name
dotnet test src/Microsoft.PowerFx.sln --filter "FullyQualifiedName~YourTestName"

# Run tests by category
dotnet test src/Microsoft.PowerFx.sln --filter "Category=ExpressionTest"

# Build local NuGet packages (from src/ directory, outputs to src/outputpackages/)
.\src\buildLocalPackages.cmd [Configuration]

# Run tests on both net462 and net7.0 via VSTest (from src/ directory)
.\src\runLocalTests.cmd [Configuration]
```

**Build configurations**: `Debug`/`Release` (single-target), `Debug462`/`Release462` (net462 only), `Debug70`/`Release70` (net7.0 only), `DebugAll`/`ReleaseAll` (multi-target, used by local build scripts).

## Architecture

### Compiler Pipeline

```
Expression Text -> [Lexer] -> Tokens -> [Parser] -> AST -> [Binder] -> Typed AST -> [IR Translator] -> IR -> [Interpreter/Backend] -> Result
```

### Core Libraries

1. **Microsoft.PowerFx.Core** - Compiler only (no evaluation)
- `Lexer/` - Tokenization with culture-aware parsing
- `Parser/` - Recursive descent parser producing AST nodes (`Syntax/Nodes/`)
- `Binding/Binder.cs` (~240KB) - Heart of semantic analysis, symbol resolution, type checking
- `Types/DType.cs` (~160KB) - Internal discriminated union type representation (via `DKind` enum)
- `IR/` - Intermediate Representation with explicit coercion nodes, normalized operators
- `Texl/Builtins/` - Function signatures and type checking only (no implementations)
- `Public/Engine.cs` - Main entry point for compilation
- `Public/CheckResult.cs` - Lazy compilation pipeline result

2. **Microsoft.PowerFx.Interpreter** - Execution engine
- `EvalVisitor.cs` - Walks IR tree to compute results
- `Functions/Library*.cs` (~144KB) - Function implementations
- `RecalcEngine.cs` - Extends `Engine` with evaluation and reactive formulas

3. **Microsoft.PowerFx.Connectors** - OpenAPI connector support for external APIs
4. **Microsoft.PowerFx.Json** - JSON serialization/deserialization
5. **Microsoft.PowerFx.LanguageServerProtocol** - LSP for IDE integration
6. **Microsoft.PowerFx.Repl** - Read-Eval-Print-Loop

### Key Architecture Patterns

**Separation of Compilation and Execution**: Core compiles to IR with zero evaluation code. Multiple backends (JavaScript, SQL, etc.) can consume the same IR independently.

**Symbol Table Composition**: Layered symbol tables (Config -> Engine -> Parameters) for flexible scoping:
- `SymbolTable` (mutable) / `ReadOnlySymbolTable` (immutable, composable) for definitions
- `SymbolValues` for runtime values paired with a SymbolTable

**CheckResult Workflow**:
```csharp
var check = engine.Check(expressionText, parameterType);
check.ThrowOnErrors();
var result = check.Eval(); // Interpreter only
```

**Type System**: `DType` is the internal representation, `FormulaType` is the public API wrapper. `CoercionMatrix.cs` defines valid conversions, `BinaryOpMatrix.cs` defines operator type rules.

**Display Names vs Logical Names**: Dual tracking throughout - display names are user-facing/localized (e.g., "First Name"), logical names are internal identifiers (e.g., "nwind_firstname").

### Naming: "Texl" = Power Fx

"Texl" is the internal codename (from Excel heritage). `TexlFunction`, `TexlLexer`, `TexlParser` all refer to Power Fx components.

## Adding a New Built-in Function

1. **Define signature** in `src/libraries/Microsoft.PowerFx.Core/Texl/Builtins/NewFunction.cs`:
- Extend `TexlFunction`, implement `CheckInvocation` for type checking
- Register in `BuiltinFunctionsCore._library`

2. **Implement logic** in `src/libraries/Microsoft.PowerFx.Interpreter/Functions/Library*.cs`:
- Return `FormulaValue` results
- Register in appropriate `Library` category

3. **Add tests** in `src/tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/NewFunction.txt`

## Expression Test Cases

Tests use a `.txt` format in `tests/Microsoft.PowerFx.Core.Tests.Shared/ExpressionTestCases/`:

```
>> If(true, "yes", "no")
"yes"

>> 1+1
2

>> 1/0
Error({Kind:ErrorKind.Div0})
```

- `BaseRunner.cs` is the test harness infrastructure
- Tests run across multiple backends/configurations
- Special markers: `#skip`, `#error`, `#novalue`
- 60-second timeout per test

## Important Concepts

- **Cooperative Cancellation**: Interpreter checks `CancellationToken` and calls `Governor.Poll()` in loops to prevent runaway evaluation.
- **Immutability**: Most core data structures (DType, IR nodes, bound trees) are immutable. Symbol tables are mutable but have version hashes to detect concurrent mutations.
- **DPath**: Represents paths through type structure (e.g., "record.field.subfield"), used for type navigation and error reporting.
- **GuardSingleThreaded**: Detects concurrent mutations of mutable structures in development builds.
- **Feature Flags**: `Features` class controls language behavior. `Features.PowerFxV1` is the current standard. Configured via `PowerFxConfig`.
- **UDFs**: User-defined functions support recursion with stack depth tracking. Added via `Engine.AddUserDefinedFunction()`.

## Code Quality

- **StyleCop analyzers** enforce code style (configured in `src/PowerFx.ruleset`, relaxed in `src/PowerFx.Tests.ruleset`)
- **Warnings treated as errors** in Release builds
- C# 10.0 language version, .editorconfig enforces formatting (4-space indent, CRLF, UTF-8 BOM)
- Multi-targets: .NET Standard 2.0, .NET Framework 4.6.2, .NET 7.0
- Tests use **xUnit** with aggressive parallelization
- Test projects organized into `.Net4.6.2/` and `.Net7.0/` folders with `.Shared/` projects for shared code
- Versioning via Nerdbank.GitVersioning (NBGV), config in `version.json` (base version: 1.8)
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using Microsoft.PowerFx.Syntax;
using Microsoft.PowerFx.Types;
using static Microsoft.PowerFx.Core.Localization.TexlStrings;
using Attribute = Microsoft.PowerFx.Core.Parser.Attribute;
using CallNode = Microsoft.PowerFx.Syntax.CallNode;

namespace Microsoft.PowerFx.Core.Functions
Expand All @@ -40,6 +41,9 @@ internal class UserDefinedFunction : TexlFunction, IExternalPageableSymbol
private readonly IEnumerable<UDFArg> _args;
private TexlBinding _binding;
private readonly IdentToken _returnTypeName;
private readonly IReadOnlyList<Attribute> _attributes;

internal IReadOnlyList<Attribute> Attributes => _attributes;

public override bool IsAsync => _binding.IsAsync(UdfBody);

Expand Down Expand Up @@ -84,6 +88,18 @@ public override bool SupportsPaging(CallNode callNode, TexlBinding binding)

public TexlBinding Binding => _binding;

internal IReadOnlyList<NamedFormulaType> GetPublicParameters()
{
var result = new List<NamedFormulaType>();
foreach (var arg in _args.OrderBy(a => a.ArgIndex))
{
var formulaType = FormulaType.Build(ParamTypes[arg.ArgIndex]);
result.Add(new NamedFormulaType(arg.NameIdent.Name.Value, formulaType));
}

return result;
}

public bool TryGetExternalDataSource(out IExternalDataSource dataSource)
{
return ArgValidators.DelegatableDataSourceInfoValidator.TryGetValidValue(_binding.Top, _binding, out dataSource);
Expand Down Expand Up @@ -128,12 +144,14 @@ public override bool CheckTypes(CheckTypesContext context, TexlNode[] args, DTyp
/// <param name="args"></param>
/// <param name="argTypes">Array of argTypes in order.</param>
/// <param name="returnTypeName">Name of the type in the decleration, used by error messages.</param>
public UserDefinedFunction(string functionName, DType returnType, TexlNode body, bool isImperative, ISet<UDFArg> args, DType[] argTypes, IdentToken returnTypeName)
/// <param name="attributes">Attributes applied to this UDF.</param>
public UserDefinedFunction(string functionName, DType returnType, TexlNode body, bool isImperative, ISet<UDFArg> args, DType[] argTypes, IdentToken returnTypeName, IReadOnlyList<Attribute> attributes = null)
: base(DPath.Root, functionName, functionName, SG(functionName), FunctionCategories.UserDefined, returnType, 0, args.Count, args.Count, argTypes)
{
this._args = args;
this._isImperative = isImperative;
this._returnTypeName = returnTypeName;
this._attributes = attributes ?? Array.Empty<Attribute>();

this.UdfBody = body;
}
Expand Down Expand Up @@ -300,7 +318,7 @@ public UserDefinedFunction WithBinding(INameResolver nameResolver, IBinderGlue b
throw new ArgumentNullException(nameof(binderGlue));
}

var func = new UserDefinedFunction(Name, ReturnType, UdfBody, _isImperative, new HashSet<UDFArg>(_args), ParamTypes, _returnTypeName);
var func = new UserDefinedFunction(Name, ReturnType, UdfBody, _isImperative, new HashSet<UDFArg>(_args), ParamTypes, _returnTypeName, _attributes);
binding = func.BindBody(nameResolver, binderGlue, bindingConfig, features, rule, updateDisplayNames);

return func;
Expand Down Expand Up @@ -369,7 +387,7 @@ public static IEnumerable<UserDefinedFunction> CreateFunctions(IEnumerable<UDF>
errors.Add(new TexlError(udf.Ident, DocumentErrorSeverity.Warning, TexlStrings.WrnUDF_ShadowingBuiltInFunction, udfName));
}

var func = new UserDefinedFunction(udfName.Value, returnType, udf.Body, udf.IsImperative, udf.Args, parameterTypes, udf.ReturnType);
var func = new UserDefinedFunction(udfName.Value, returnType, udf.Body, udf.IsImperative, udf.Args, parameterTypes, udf.ReturnType, udf.Attributes);

texlFunctionSet.Add(func);
userDefinedFunctions.Add(func);
Expand Down Expand Up @@ -427,7 +445,7 @@ internal static UserDefinedFunction CreatePartialFunction(UDF udf, INameResolver

var dummyref = 0;
var udfBody = udf.Body ?? new PowerFx.Syntax.ErrorNode(ref dummyref, new CommentToken("dummy token", new Span(0, 0)), "dummy error");
var func = new UserDefinedFunction(udfName.Value, returnType, udfBody, udf.IsImperative, udf.Args, parameterTypes, udf.ReturnType);
var func = new UserDefinedFunction(udfName.Value, returnType, udfBody, udf.IsImperative, udf.Args, parameterTypes, udf.ReturnType, udf.Attributes);

return func;
}
Expand Down Expand Up @@ -542,11 +560,12 @@ public bool HasSameDefintion(string definitionsScript, UserDefinedFunction targe
Contracts.AssertValue(targetUDF);

if (Name != targetUDF.Name ||
UdfBody.GetCompleteSpan().GetFragment(definitionsScript) != targetUDFbody ||
UdfBody.GetCompleteSpan().GetFragment(definitionsScript) != targetUDFbody ||
_args.Count() != targetUDF._args.Count() ||
ReturnType.AssociatedDataSources.SetEquals(targetUDF.ReturnType.AssociatedDataSources) == false ||
ReturnType != targetUDF.ReturnType ||
_isImperative != targetUDF._isImperative)
_isImperative != targetUDF._isImperative ||
!AttributesEqual(_attributes, targetUDF._attributes))
{
return false;
}
Expand All @@ -568,6 +587,25 @@ public bool HasSameDefintion(string definitionsScript, UserDefinedFunction targe
return true;
}

private static bool AttributesEqual(IReadOnlyList<Attribute> a, IReadOnlyList<Attribute> b)
{
if (a.Count != b.Count)
{
return false;
}

for (var i = 0; i < a.Count; i++)
{
if (a[i].Name.Name != b[i].Name.Name ||
!a[i].Arguments.SequenceEqual(b[i].Arguments))
{
return false;
}
}

return true;
}

/// <summary>
/// NameResolver that combines global named resolver and params for user defined function.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,9 @@ internal static class TexlStrings
public static ErrorResourceKey ErrOperationDoesntMatch = new ErrorResourceKey("ErrOperationDoesntMatch");
public static ErrorResourceKey ErrUnknownPartialOp = new ErrorResourceKey("ErrUnknownPartialOp");

public static ErrorResourceKey ErrUnknownAttribute = new ErrorResourceKey("ErrUnknownAttribute");
public static ErrorResourceKey ErrAttributeArgCount = new ErrorResourceKey("ErrAttributeArgCount");

public static ErrorResourceKey ErrTruncatedArgWarning = new ErrorResourceKey("ErrTruncatedArgWarning");

public static ErrorResourceKey ErrNeedPrimitive = new ErrorResourceKey("ErrNeedPrimitive");
Expand Down
28 changes: 28 additions & 0 deletions src/libraries/Microsoft.PowerFx.Core/Parser/Attribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System.Collections.Generic;
using System.Linq;
using Microsoft.PowerFx.Syntax;

namespace Microsoft.PowerFx.Core.Parser
{
internal sealed class Attribute
{
public readonly IdentToken Name;

public readonly IReadOnlyList<string> Arguments;

public readonly IReadOnlyList<Token> ArgumentTokens;

public readonly Token OpenBracket;

public Attribute(IdentToken name, IReadOnlyList<Token> argumentTokens, Token openBracket)
{
Name = name;
Arguments = argumentTokens.Select(t => t.As<StrLitToken>().Value).ToList();
ArgumentTokens = argumentTokens;
OpenBracket = openBracket;
}
}
}
8 changes: 5 additions & 3 deletions src/libraries/Microsoft.PowerFx.Core/Parser/NamedFormula.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.PowerFx.Core.Utils;
using Microsoft.PowerFx.Syntax;

Expand All @@ -14,12 +16,12 @@ internal class NamedFormula

internal int StartingIndex { get; }

internal PartialAttribute Attribute { get; }
internal IReadOnlyList<Attribute> Attributes { get; }

// used by the pretty printer to get the proper operator in the output
internal bool ColonEqual { get; }

public NamedFormula(IdentToken ident, Formula formula, int startingIndex, bool colonEqual, PartialAttribute attribute = null)
public NamedFormula(IdentToken ident, Formula formula, int startingIndex, bool colonEqual, IReadOnlyList<Attribute> attributes = null)
{
Contracts.AssertValue(ident);
Contracts.AssertValue(formula);
Expand All @@ -28,7 +30,7 @@ public NamedFormula(IdentToken ident, Formula formula, int startingIndex, bool c
Formula = formula;
StartingIndex = startingIndex;
ColonEqual = colonEqual;
Attribute = attribute;
Attributes = attributes ?? Array.Empty<Attribute>();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ internal class UDF
/// </summary>
internal bool IsParseValid { get; }

/// <summary>
/// Gets the attributes applied to this UDF.
/// </summary>
internal IReadOnlyList<Attribute> Attributes { get; }

/// <summary>
/// Initializes a new instance of the <see cref="UDF"/> class with the specified properties.
/// </summary>
Expand All @@ -170,7 +175,8 @@ internal class UDF
/// <param name="isImperative">A value indicating whether the UDF is imperative.</param>
/// <param name="numberIsFloat">A value indicating whether numbers are treated as floats in the UDF.</param>
/// <param name="isValid">A value indicating whether the UDF is parse valid.</param>
public UDF(IdentToken ident, Token colonToken, IdentToken returnType, HashSet<UDFArg> args, TexlNode body, bool isImperative, bool numberIsFloat, bool isValid)
/// <param name="attributes">The attributes applied to this UDF.</param>
public UDF(IdentToken ident, Token colonToken, IdentToken returnType, HashSet<UDFArg> args, TexlNode body, bool isImperative, bool numberIsFloat, bool isValid, IReadOnlyList<Attribute> attributes = null)
{
Ident = ident;
ReturnType = returnType;
Expand All @@ -180,6 +186,7 @@ public UDF(IdentToken ident, Token colonToken, IdentToken returnType, HashSet<UD
NumberIsFloat = numberIsFloat;
IsParseValid = isValid;
ReturnTypeColonToken = colonToken;
Attributes = attributes ?? Array.Empty<Attribute>();
}
}

Expand Down
Loading
Loading