Skip to content
Merged
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
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,25 @@ This separation allows the same parser to support:
* language servers
* syntax highlighting and other editor tooling

### Structural access

Expressif distinguishes record fields from elements of ordered values:

```text
.name named field of the current record
.0 positional field of the current record
$0 first element of the current tuple or array
$1 second element of the current tuple or array
$^0 last element of the current tuple or array
$^1 second-to-last element of the current tuple or array
```

Element positions are zero-based. `$n` counts from the beginning and `$^n`
counts from the end. The parser represents both tuple and array access with the
same `positional_element_access` node; downstream binders decide whether the
runtime value supports positional access and how invalid or out-of-range access
is handled.

## Repository structure

```text
Expand Down
32 changes: 32 additions & 0 deletions bindings/csharp/Expressif.Syntax.Tests/SyntaxBindingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ public void TemporalFormsRemainDistinct(string source, Type expected)
Assert.That(root.Value, Is.TypeOf(expected).With.Property(nameof(SyntaxNode.Text)).EqualTo(source));
}

[TestCase("$0", 0, false)]
[TestCase("$1", 1, false)]
[TestCase("$^0", 0, true)]
[TestCase("$^1", 1, true)]
public void PositionalElementAccessExposesDirectionAndIndex(string source, int index, bool fromEnd)
{
var root = (ClosedExpressionSyntax)ExpressifSyntax.Parse(source);
var access = (PositionalElementAccessSyntax)root.Value;

Assert.Multiple(() =>
{
Assert.That(access.Index, Is.EqualTo(index));
Assert.That(access.FromEnd, Is.EqualTo(fromEnd));
Assert.That(access.Text, Is.EqualTo(source));
});
}

[Test]
public void PositionalElementAccessCanBeAnArgumentAndPipelineSource()
{
var argumentRoot = (OpenExpressionSyntax)ExpressifSyntax.Parse("select($1)");
var pipelineRoot = (ClosedExpressionSyntax)ExpressifSyntax.Parse("$^0 | upper");

Assert.Multiple(() =>
{
Assert.That(argumentRoot.Pipeline.Single().Arguments.Single().Value,
Is.TypeOf<PositionalElementAccessSyntax>());
Assert.That(pipelineRoot.Value, Is.TypeOf<PositionalElementAccessSyntax>());
Assert.That(pipelineRoot.Pipeline.Single().Name, Is.EqualTo("upper"));
});
}

[Test]
public void SourceTextAndRangesAreLossless()
{
Expand Down
1 change: 1 addition & 0 deletions bindings/csharp/Expressif.Syntax/ExpressifSyntax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private static PositionalArgumentSyntax BindArgument(TsNode node)

private static ValueSyntax BindValue(TsNode node) => node.Type switch
{
"positional_element_access" => new PositionalElementAccessSyntax(Span(node), node.Text),
"numeric_literal" => new NumericLiteralSyntax(Span(node), node.Text),
"boolean_literal" => new BooleanLiteralSyntax(Span(node), node.Text),
"double_quoted_literal" => new QuotedLiteralSyntax(Span(node), node.Text, QuotingStyle.DoubleQuote),
Expand Down
14 changes: 14 additions & 0 deletions bindings/csharp/Expressif.Syntax/SyntaxNodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public enum SyntaxKind
DateLiteral,
DateTimeLiteral,
TimeLiteral,
PositionalElementAccess,
}

public readonly record struct SourceSpan(int Start, int Length)
Expand Down Expand Up @@ -108,6 +109,19 @@ protected ValueSyntax(SyntaxKind kind, SourceSpan span, string text)
: base(kind, span, text) { }
}

public sealed class PositionalElementAccessSyntax : ValueSyntax
{
internal PositionalElementAccessSyntax(SourceSpan span, string text)
: base(SyntaxKind.PositionalElementAccess, span, text)
{
FromEnd = text[1] == '^';
Index = int.Parse(text.AsSpan(FromEnd ? 2 : 1), System.Globalization.CultureInfo.InvariantCulture);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -F '"positional_element_access"' src/grammar.json
rg -n -F '\\$\\^?(?:0|[1-9][0-9]*)' src/grammar.json

if rg -n 'Index\s*=\s*int\.Parse' \
    bindings/csharp/Expressif.Syntax/SyntaxNodes.cs; then
  echo "Found an unguarded Int32 parse for positional access."
  exit 1
fi

Repository: Seddryck/Expressif.Syntax

Length of output: 464


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- grammar ---'
sed -n '425,442p' src/grammar.json

printf '%s\n' '--- README contract ---'
sed -n '65,84p' README.md

printf '%s\n' '--- syntax node context ---'
sed -n '90,130p' bindings/csharp/Expressif.Syntax/SyntaxNodes.cs

printf '%s\n' '--- binding exception and positional index usage ---'
rg -n -C 3 'ExpressifBindingException|PositionalElementAccess|Index\b|FromEnd' \
  bindings/csharp src tests 2>/dev/null || true

printf '%s\n' '--- relevant files ---'
git ls-files | rg '(^|/)(README\.md|.*(SyntaxNodes|Binding|Positional|ElementAccess|Test).*)$' | head -200

Repository: Seddryck/Expressif.Syntax

Length of output: 11134


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- parser and binding flow ---'
sed -n '1,145p' bindings/csharp/Expressif.Syntax/ExpressifSyntax.cs

printf '%s\n' '--- current positional-access tests ---'
sed -n '60,125p' bindings/csharp/Expressif.Syntax.Tests/SyntaxBindingTests.cs

printf '%s\n' '--- exception assertions and parse API usage ---'
rg -n -C 4 'Assert\.Throws|ExpressifSyntax\.Parse|Errors|SyntaxError|OverflowException|out-of-range|out of range' \
  bindings/csharp/Expressif.Syntax.Tests bindings/csharp/Expressif.Syntax README.md

printf '%s\n' '--- source/project metadata ---'
cat bindings/csharp/Expressif.Syntax/Expressif.Syntax.csproj
cat bindings/csharp/Expressif.Syntax.Tests/Expressif.Syntax.Tests.csproj

printf '%s\n' '--- behavioral probe for grammar and Int32 conversion ---'
python3 - <<'PY'
import json
import re

grammar = json.load(open("src/grammar.json", encoding="utf-8"))
pattern = re.compile(grammar["positional_element_access"]["value"])
for text in ("$0", "$^0", "$2147483647", "$2147483648", "$^9223372036854775808"):
    match = bool(pattern.fullmatch(text))
    try:
        value = int(text[2:] if text.startswith("$^") else text[1:])
        fits_int32 = -(2**31) <= value <= 2**31 - 1
    except ValueError:
        fits_int32 = False
    print(f"{text}: grammar_match={match}, int32_fit={fits_int32}")
PY

Repository: Seddryck/Expressif.Syntax

Length of output: 26252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- grammar JSON shape and all positional definitions ---'
python3 - <<'PY'
import json
from pathlib import Path

data = json.loads(Path("src/grammar.json").read_text(encoding="utf-8"))

def walk(value, path=()):
    if isinstance(value, dict):
        if value.get("name") == "positional_element_access" or value.get("value") == r"\$\^?(?:0|[1-9][0-9]*)":
            print("path:", ".".join(path) or "<root>")
            print(value)
        for key, child in value.items():
            walk(child, path + (str(key),))
    elif isinstance(value, list):
        for index, child in enumerate(value):
            walk(child, path + (str(index),))

walk(data)
PY

printf '%s\n' '--- grammar.js and generated parser references ---'
rg -n -C 5 'positional_element_access|\$\\\^|[Pp]ositional' grammar.js src/parser.c src 2>/dev/null || true

printf '%s\n' '--- exact Int32 boundary cases represented by the grammar ---'
python3 - <<'PY'
import re

pattern = re.compile(r"\$\^?(?:0|[1-9][0-9]*)")
for source in ("$2147483647", "$2147483648", "$^2147483647", "$^2147483648"):
    digits = source[2:] if source.startswith("$^") else source[1:]
    value = int(digits)
    print(source, "grammar_match=", bool(pattern.fullmatch(source)),
          "int32_fit=", 0 <= value <= 2**31 - 1)
PY

Repository: Seddryck/Expressif.Syntax

Length of output: 31221


Convert oversized positional indexes to ExpressifBindingException.

The grammar accepts $2147483648, but int.Parse throws OverflowException during syntax binding. This prevents downstream binders from applying their out-of-range policy. Add boundary tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/csharp/Expressif.Syntax/SyntaxNodes.cs` at line 118, Update the
positional-index parsing in the relevant syntax-node constructor or binder
around the Index assignment to catch overflow from int.Parse and convert it to
ExpressifBindingException, preserving the existing binding flow so downstream
binders can apply their out-of-range policy. Add boundary tests covering the
maximum valid index and an oversized value such as $2147483648.

}

public int Index { get; }
public bool FromEnd { get; }
}

public sealed class NumericLiteralSyntax : ValueSyntax
{
internal NumericLiteralSyntax(SourceSpan span, string text) : base(SyntaxKind.NumericLiteral, span, text) { }
Expand Down
3 changes: 3 additions & 0 deletions grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default grammar({
$.variable,
$.property_reference,
$.index_reference,
$.positional_element_access,
$.numeric_literal,
$.boolean_literal,
$.quoted_literal,
Expand Down Expand Up @@ -111,6 +112,8 @@ export default grammar({

index_reference: (_) => /\[(?:0|[1-9][0-9]*)\]/,

positional_element_access: (_) => /\$\^?(?:0|[1-9][0-9]*)/,

boolean_literal: (_) => choice("#true", "#false"),

quoted_literal: ($) => choice(
Expand Down
8 changes: 8 additions & 0 deletions src/grammar.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions src/node-types.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading