diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
new file mode 100644
index 0000000..6202524
--- /dev/null
+++ b/.github/workflows/benchmarks.yml
@@ -0,0 +1,208 @@
+name: benchmarks
+
+on:
+ push:
+ branches: main
+ paths:
+ - 'benchmarks/**'
+ - 'rust/links-notation-benchmark/**'
+ - '.github/workflows/benchmarks.yml'
+ pull_request:
+ paths:
+ - 'benchmarks/**'
+ - 'rust/links-notation-benchmark/**'
+ - '.github/workflows/benchmarks.yml'
+ workflow_dispatch:
+
+env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+jobs:
+ findChangedBenchmarkFiles:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ outputs:
+ isBenchmarkFilesChanged: ${{ steps.setIsBenchmarkFilesChangedOutput.outputs.isBenchmarkFilesChanged }}
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ - name: Get changed files using defaults
+ id: changed-files
+ uses: tj-actions/changed-files@v47
+ - name: Set output isBenchmarkFilesChanged
+ id: setIsBenchmarkFilesChangedOutput
+ run: |
+ isBenchmarkFilesChanged='false'
+ echo "Changed files: ${{ steps.changed-files.outputs.all_changed_files }}"
+ for changedFile in ${{ steps.changed-files.outputs.all_changed_files }}; do
+ if [[ $changedFile == benchmarks/* ]] || [[ $changedFile == rust/links-notation-benchmark/* ]] || [[ $changedFile == .github/workflows/benchmarks.yml ]]; then
+ echo "isBenchmarkFilesChanged='true'"
+ isBenchmarkFilesChanged='true'
+ break
+ fi
+ done
+ echo "isBenchmarkFilesChanged=${isBenchmarkFilesChanged}" >> $GITHUB_OUTPUT
+ echo "isBenchmarkFilesChanged: ${isBenchmarkFilesChanged}"
+
+ run-benchmark:
+ needs: [findChangedBenchmarkFiles]
+ if: ${{ needs.findChangedBenchmarkFiles.outputs.isBenchmarkFilesChanged == 'true' }}
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ submodules: true
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache Cargo registry
+ uses: actions/cache@v5
+ with:
+ path: ~/.cargo/registry
+ key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Cache Cargo index
+ uses: actions/cache@v5
+ with:
+ path: ~/.cargo/git
+ key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Cache Cargo build
+ uses: actions/cache@v5
+ with:
+ path: rust/target
+ key: ${{ runner.os }}-cargo-build-target-benchmark-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Build benchmark
+ working-directory: rust
+ run: cargo build -p links-notation-benchmark --release
+
+ - name: Run benchmark tests
+ working-directory: rust
+ run: cargo test -p links-notation-benchmark
+
+ - name: Run benchmark and generate report
+ working-directory: rust
+ run: cargo run -p links-notation-benchmark --release
+
+ - name: Check for changes in benchmark results
+ id: check-changes
+ run: |
+ if git diff --quiet benchmarks/BENCHMARK_RESULTS.md benchmarks/benchmark_results.json; then
+ echo "No changes in benchmark results"
+ echo "has_changes=false" >> $GITHUB_OUTPUT
+ else
+ echo "Benchmark results have changed"
+ echo "has_changes=true" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Commit updated benchmark results
+ if: ${{ steps.check-changes.outputs.has_changes == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
+ run: |
+ git config --local user.email "github-actions[bot]@users.noreply.github.com"
+ git config --local user.name "github-actions[bot]"
+ git add benchmarks/BENCHMARK_RESULTS.md benchmarks/benchmark_results.json
+ git diff --staged --quiet || git commit -m "Update benchmark results [skip ci]"
+ git push
+
+ validate-all-benchmarks:
+ needs: [run-benchmark]
+ runs-on: ubuntu-latest
+ timeout-minutes: 30
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ submodules: true
+
+ # Rust benchmark (already tested in run-benchmark job)
+ - name: Setup Rust
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Cache Cargo
+ uses: actions/cache@v5
+ with:
+ path: |
+ ~/.cargo/registry
+ ~/.cargo/git
+ rust/target
+ key: ${{ runner.os }}-cargo-validate-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Validate Rust benchmark
+ working-directory: rust
+ run: |
+ cargo build -p links-notation-benchmark --release
+ cargo run -p links-notation-benchmark --release
+ echo "Rust benchmark: PASSED"
+
+ # JavaScript benchmark
+ - name: Setup Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '20'
+
+ - name: Validate JavaScript benchmark
+ run: |
+ node benchmarks/js/benchmark.mjs
+ echo "JavaScript benchmark: PASSED"
+
+ # Python benchmark
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.11'
+
+ - name: Validate Python benchmark
+ run: |
+ python3 benchmarks/python/benchmark.py
+ echo "Python benchmark: PASSED"
+
+ # C# benchmark
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '8.0.x'
+
+ - name: Validate C# benchmark
+ run: |
+ dotnet run --project benchmarks/csharp/Benchmark.csproj
+ echo "C# benchmark: PASSED"
+
+ # Go benchmark
+ - name: Setup Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: '1.21'
+
+ - name: Validate Go benchmark
+ working-directory: benchmarks/go
+ run: |
+ go run benchmark.go
+ echo "Go benchmark: PASSED"
+
+ # Java benchmark
+ - name: Setup Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: 'temurin'
+ java-version: '17'
+ cache: 'maven'
+
+ - name: Validate Java benchmark
+ working-directory: benchmarks/java
+ run: |
+ mvn compile exec:java -q
+ echo "Java benchmark: PASSED"
+
+ - name: Summary
+ run: |
+ echo "=== All benchmarks validated successfully ==="
+ echo "- Rust: PASSED"
+ echo "- JavaScript: PASSED"
+ echo "- Python: PASSED"
+ echo "- C#: PASSED"
+ echo "- Go: PASSED"
+ echo "- Java: PASSED"
diff --git a/benchmarks/BENCHMARK_RESULTS.md b/benchmarks/BENCHMARK_RESULTS.md
new file mode 100644
index 0000000..d40eebc
--- /dev/null
+++ b/benchmarks/BENCHMARK_RESULTS.md
@@ -0,0 +1,96 @@
+# Links Notation Character Count Benchmark
+
+This benchmark compares the UTF-8 character count of Links Notation (lino) against JSON, YAML, and XML.
+
+## Summary
+
+| Format | Total Characters | vs Lino |
+|--------|------------------|----------|
+| **Lino** | **734** | - |
+| JSON | 1332 | +81.5% |
+| YAML | 920 | +25.3% |
+| XML | 1882 | +156.4% |
+
+## Average Savings with Lino
+
+- **vs JSON**: 47.9% fewer characters
+- **vs YAML**: 21.5% fewer characters
+- **vs XML**: 61.5% fewer characters
+
+## Detailed Results
+
+| Test Case | Description | Lino | JSON | YAML | XML | Lino vs JSON | Lino vs YAML | Lino vs XML |
+|-----------|-------------|------|------|------|-----|--------------|--------------|-------------|
+| employees | Employee records with nested structure | 177 | 251 | 142 | 302 | 29.5% | -24.6% | 41.4% |
+| simple_doublets | Simple doublet links (2-tuples) | 62 | 155 | 112 | 215 | 60.0% | 44.6% | 71.2% |
+| triplets | Triplet relations (3-tuples) | 54 | 229 | 183 | 371 | 76.4% | 70.5% | 85.4% |
+| nested_structure | Deeply nested company structure | 254 | 400 | 282 | 615 | 36.5% | 9.9% | 58.7% |
+| config | Application configuration | 187 | 297 | 201 | 379 | 37.0% | 7.0% | 50.7% |
+
+## Test Cases
+
+### employees
+
+Employee records with nested structure
+
+| Format | Characters |
+|--------|------------|
+| Lino | 177 |
+| JSON | 251 |
+| YAML | 142 |
+| XML | 302 |
+
+### simple_doublets
+
+Simple doublet links (2-tuples)
+
+| Format | Characters |
+|--------|------------|
+| Lino | 62 |
+| JSON | 155 |
+| YAML | 112 |
+| XML | 215 |
+
+### triplets
+
+Triplet relations (3-tuples)
+
+| Format | Characters |
+|--------|------------|
+| Lino | 54 |
+| JSON | 229 |
+| YAML | 183 |
+| XML | 371 |
+
+### nested_structure
+
+Deeply nested company structure
+
+| Format | Characters |
+|--------|------------|
+| Lino | 254 |
+| JSON | 400 |
+| YAML | 282 |
+| XML | 615 |
+
+### config
+
+Application configuration
+
+| Format | Characters |
+|--------|------------|
+| Lino | 187 |
+| JSON | 297 |
+| YAML | 201 |
+| XML | 379 |
+
+## Methodology
+
+This benchmark counts UTF-8 characters (not bytes) in equivalent data representations across all formats.
+The "savings" percentage indicates how much smaller the Lino representation is compared to each format.
+
+A positive savings percentage means Lino uses fewer characters.
+
+---
+
+*Generated automatically by links-notation-benchmark*
diff --git a/benchmarks/README.md b/benchmarks/README.md
new file mode 100644
index 0000000..279178d
--- /dev/null
+++ b/benchmarks/README.md
@@ -0,0 +1,133 @@
+# Links Notation Benchmarks
+
+This directory contains UTF-8 character count benchmarks comparing Links Notation (lino) against JSON, YAML, and XML formats.
+
+## Overview
+
+The benchmarks measure the UTF-8 character count efficiency of Links Notation compared to other popular data serialization formats. This is useful for understanding the data size impact when using Links Notation, particularly in contexts where character count matters (e.g., LLM context windows, storage optimization).
+
+## Benchmark Results
+
+See [BENCHMARK_RESULTS.md](BENCHMARK_RESULTS.md) for the latest benchmark results.
+
+## Test Cases
+
+The benchmark includes 5 test cases representing different data structures:
+
+| Test Case | Description |
+|-----------|-------------|
+| employees | Employee records with nested structure |
+| simple_doublets | Simple doublet links (2-tuples) |
+| triplets | Triplet relations (3-tuples) |
+| nested_structure | Deeply nested company structure |
+| config | Application configuration |
+
+Test data files are located in the `data/` directory.
+
+## Running Benchmarks
+
+### Rust (Primary - Used in CI/CD)
+
+```bash
+cd rust
+cargo run -p links-notation-benchmark --release
+```
+
+### JavaScript
+
+```bash
+node benchmarks/js/benchmark.mjs
+```
+
+### Python
+
+```bash
+python3 benchmarks/python/benchmark.py
+```
+
+### C#
+
+```bash
+dotnet run --project benchmarks/csharp/Benchmark.csproj
+```
+
+### Go
+
+```bash
+cd benchmarks/go
+go run benchmark.go
+```
+
+### Java
+
+```bash
+cd benchmarks/java
+mvn compile exec:java
+```
+
+## CI/CD Integration
+
+The benchmarks are automatically run on push to the `main` branch when:
+- Files in `benchmarks/` are changed
+- Files in `rust/links-notation-benchmark/` are changed
+- The `.github/workflows/benchmarks.yml` workflow is changed
+
+When running on the `main` branch, the workflow will:
+1. Run the Rust benchmark
+2. Compare results with existing `BENCHMARK_RESULTS.md`
+3. If results have changed, commit the updated markdown file
+
+## Directory Structure
+
+```
+benchmarks/
+├── README.md # This file
+├── BENCHMARK_RESULTS.md # Generated benchmark results (auto-updated by CI)
+├── benchmark_results.json # JSON report from Rust benchmark
+├── data/ # Test data files
+│ ├── *.lino # Links Notation format
+│ ├── *.json # JSON format
+│ ├── *.yaml # YAML format
+│ └── *.xml # XML format
+├── js/ # JavaScript benchmark
+│ └── benchmark.mjs
+├── python/ # Python benchmark
+│ └── benchmark.py
+├── csharp/ # C# benchmark
+│ ├── Benchmark.csproj
+│ └── Program.cs
+├── go/ # Go benchmark
+│ ├── go.mod
+│ └── benchmark.go
+└── java/ # Java benchmark
+ ├── pom.xml
+ └── src/main/java/.../Benchmark.java
+```
+
+## Methodology
+
+The benchmark counts UTF-8 characters (not bytes) in equivalent data representations across all formats:
+- **Lino**: Links Notation format
+- **JSON**: Standard JSON format
+- **YAML**: YAML 1.2 format
+- **XML**: Standard XML format
+
+The "savings" percentage indicates how much smaller the Lino representation is compared to each format. A positive savings percentage means Lino uses fewer characters.
+
+## Adding New Test Cases
+
+1. Create equivalent representations in all four formats:
+ - `data/{name}.lino`
+ - `data/{name}.json`
+ - `data/{name}.yaml`
+ - `data/{name}.xml`
+
+2. Add the test case to each benchmark implementation:
+ - Rust: `rust/links-notation-benchmark/src/main.rs`
+ - JavaScript: `benchmarks/js/benchmark.mjs`
+ - Python: `benchmarks/python/benchmark.py`
+ - C#: `benchmarks/csharp/Program.cs`
+ - Go: `benchmarks/go/benchmark.go`
+ - Java: `benchmarks/java/src/.../Benchmark.java`
+
+3. Run the benchmarks to verify and update results.
diff --git a/benchmarks/benchmark_results.json b/benchmarks/benchmark_results.json
new file mode 100644
index 0000000..7af0e97
--- /dev/null
+++ b/benchmarks/benchmark_results.json
@@ -0,0 +1,68 @@
+{
+ "summary": {
+ "total_lino_chars": 734,
+ "total_json_chars": 1332,
+ "total_yaml_chars": 920,
+ "total_xml_chars": 1882,
+ "avg_lino_vs_json": 47.88766454479672,
+ "avg_lino_vs_yaml": 21.476205048227925,
+ "avg_lino_vs_xml": 61.47141614170713
+ },
+ "results": [
+ {
+ "name": "employees",
+ "description": "Employee records with nested structure",
+ "lino_chars": 177,
+ "json_chars": 251,
+ "yaml_chars": 142,
+ "xml_chars": 302,
+ "lino_vs_json": 29.482071713147413,
+ "lino_vs_yaml": -24.647887323943664,
+ "lino_vs_xml": 41.390728476821195
+ },
+ {
+ "name": "simple_doublets",
+ "description": "Simple doublet links (2-tuples)",
+ "lino_chars": 62,
+ "json_chars": 155,
+ "yaml_chars": 112,
+ "xml_chars": 215,
+ "lino_vs_json": 60.0,
+ "lino_vs_yaml": 44.642857142857146,
+ "lino_vs_xml": 71.16279069767441
+ },
+ {
+ "name": "triplets",
+ "description": "Triplet relations (3-tuples)",
+ "lino_chars": 54,
+ "json_chars": 229,
+ "yaml_chars": 183,
+ "xml_chars": 371,
+ "lino_vs_json": 76.41921397379913,
+ "lino_vs_yaml": 70.49180327868852,
+ "lino_vs_xml": 85.44474393530997
+ },
+ {
+ "name": "nested_structure",
+ "description": "Deeply nested company structure",
+ "lino_chars": 254,
+ "json_chars": 400,
+ "yaml_chars": 282,
+ "xml_chars": 615,
+ "lino_vs_json": 36.5,
+ "lino_vs_yaml": 9.929078014184398,
+ "lino_vs_xml": 58.69918699186992
+ },
+ {
+ "name": "config",
+ "description": "Application configuration",
+ "lino_chars": 187,
+ "json_chars": 297,
+ "yaml_chars": 201,
+ "xml_chars": 379,
+ "lino_vs_json": 37.03703703703704,
+ "lino_vs_yaml": 6.965174129353234,
+ "lino_vs_xml": 50.65963060686016
+ }
+ ]
+}
\ No newline at end of file
diff --git a/benchmarks/csharp/Benchmark.csproj b/benchmarks/csharp/Benchmark.csproj
new file mode 100644
index 0000000..ae4ddfd
--- /dev/null
+++ b/benchmarks/csharp/Benchmark.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ LinksNotation.Benchmark
+
+
+
+
+
+
+
diff --git a/benchmarks/csharp/Program.cs b/benchmarks/csharp/Program.cs
new file mode 100644
index 0000000..72d4057
--- /dev/null
+++ b/benchmarks/csharp/Program.cs
@@ -0,0 +1,355 @@
+// UTF-8 Character Count Benchmark for Links Notation vs JSON, YAML, and XML
+//
+// This benchmark measures the UTF-8 character count efficiency of Links Notation
+// compared to other popular data serialization formats.
+
+using System.Globalization;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace LinksNotation.Benchmark;
+
+public record BenchmarkCase(
+ string Name,
+ string Description,
+ string Lino,
+ string Json,
+ string Yaml,
+ string Xml
+);
+
+public record BenchmarkResult(
+ string Name,
+ string Description,
+ int LinoChars,
+ int JsonChars,
+ int YamlChars,
+ int XmlChars,
+ double LinoVsJson,
+ double LinoVsYaml,
+ double LinoVsXml
+);
+
+public record AggregatedResults(
+ int TotalLinoChars,
+ int TotalJsonChars,
+ int TotalYamlChars,
+ int TotalXmlChars,
+ double AvgLinoVsJson,
+ double AvgLinoVsYaml,
+ double AvgLinoVsXml
+);
+
+public class Report
+{
+ [JsonPropertyName("language")]
+ public string Language { get; set; } = "C#";
+
+ [JsonPropertyName("summary")]
+ public SummaryData? Summary { get; set; }
+
+ [JsonPropertyName("results")]
+ public List? Results { get; set; }
+}
+
+public class SummaryData
+{
+ [JsonPropertyName("total_lino_chars")]
+ public int TotalLinoChars { get; set; }
+
+ [JsonPropertyName("total_json_chars")]
+ public int TotalJsonChars { get; set; }
+
+ [JsonPropertyName("total_yaml_chars")]
+ public int TotalYamlChars { get; set; }
+
+ [JsonPropertyName("total_xml_chars")]
+ public int TotalXmlChars { get; set; }
+
+ [JsonPropertyName("avg_lino_vs_json")]
+ public double AvgLinoVsJson { get; set; }
+
+ [JsonPropertyName("avg_lino_vs_yaml")]
+ public double AvgLinoVsYaml { get; set; }
+
+ [JsonPropertyName("avg_lino_vs_xml")]
+ public double AvgLinoVsXml { get; set; }
+}
+
+public class ResultData
+{
+ [JsonPropertyName("name")]
+ public string Name { get; set; } = "";
+
+ [JsonPropertyName("description")]
+ public string Description { get; set; } = "";
+
+ [JsonPropertyName("lino_chars")]
+ public int LinoChars { get; set; }
+
+ [JsonPropertyName("json_chars")]
+ public int JsonChars { get; set; }
+
+ [JsonPropertyName("yaml_chars")]
+ public int YamlChars { get; set; }
+
+ [JsonPropertyName("xml_chars")]
+ public int XmlChars { get; set; }
+
+ [JsonPropertyName("lino_vs_json")]
+ public double LinoVsJson { get; set; }
+
+ [JsonPropertyName("lino_vs_yaml")]
+ public double LinoVsYaml { get; set; }
+
+ [JsonPropertyName("lino_vs_xml")]
+ public double LinoVsXml { get; set; }
+}
+
+public static class Program
+{
+ ///
+ /// Count UTF-8 characters in a string.
+ /// In C#, we use StringInfo.LengthInTextElements to get the count of text elements (grapheme clusters),
+ /// which corresponds to what users perceive as "characters".
+ ///
+ public static int CountUtf8Chars(string text)
+ {
+ return new StringInfo(text).LengthInTextElements;
+ }
+
+ ///
+ /// Calculate the percentage savings of Lino vs another format.
+ ///
+ public static double CalculateSavings(int linoChars, int otherChars)
+ {
+ if (otherChars == 0) return 0.0;
+ return ((double)(otherChars - linoChars) / otherChars) * 100;
+ }
+
+ ///
+ /// Find the data directory by checking multiple possible paths.
+ ///
+ public static string? FindDataDir()
+ {
+ var baseDir = AppDomain.CurrentDomain.BaseDirectory;
+ var cwd = Directory.GetCurrentDirectory();
+
+ var possiblePaths = new[]
+ {
+ Path.Combine(baseDir, "../../../data"), // Running from bin/Debug/net8.0/
+ Path.Combine(baseDir, "../../../../data"), // Running from deeper nested
+ Path.Combine(cwd, "benchmarks/data"), // CWD is repo root
+ Path.Combine(cwd, "../data"), // CWD is benchmarks/
+ Path.Combine(cwd, "data"), // CWD is benchmarks/
+ Path.Combine(cwd, "../../benchmarks/data"), // CWD is benchmarks/csharp/
+ };
+
+ foreach (var path in possiblePaths)
+ {
+ var resolved = Path.GetFullPath(path);
+ if (Directory.Exists(resolved))
+ {
+ return resolved;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// Find the output directory for reports.
+ ///
+ public static string FindOutputDir()
+ {
+ var baseDir = AppDomain.CurrentDomain.BaseDirectory;
+ var cwd = Directory.GetCurrentDirectory();
+
+ var possiblePaths = new[]
+ {
+ Path.Combine(baseDir, "../../.."), // Running from bin/Debug/net8.0/
+ Path.Combine(cwd, "benchmarks"), // CWD is repo root
+ cwd, // Fallback to current directory
+ };
+
+ foreach (var path in possiblePaths)
+ {
+ var resolved = Path.GetFullPath(path);
+ if (Directory.Exists(resolved))
+ {
+ return resolved;
+ }
+ }
+
+ return cwd;
+ }
+
+ ///
+ /// Load all benchmark test cases from the data directory.
+ ///
+ public static List LoadBenchmarkCases(string dataDir)
+ {
+ var casesConfig = new[]
+ {
+ ("employees", "Employee records with nested structure"),
+ ("simple_doublets", "Simple doublet links (2-tuples)"),
+ ("triplets", "Triplet relations (3-tuples)"),
+ ("nested_structure", "Deeply nested company structure"),
+ ("config", "Application configuration"),
+ };
+
+ var cases = new List();
+
+ foreach (var (name, description) in casesConfig)
+ {
+ try
+ {
+ var lino = File.ReadAllText(Path.Combine(dataDir, $"{name}.lino"), Encoding.UTF8);
+ var json = File.ReadAllText(Path.Combine(dataDir, $"{name}.json"), Encoding.UTF8);
+ var yaml = File.ReadAllText(Path.Combine(dataDir, $"{name}.yaml"), Encoding.UTF8);
+ var xml = File.ReadAllText(Path.Combine(dataDir, $"{name}.xml"), Encoding.UTF8);
+
+ cases.Add(new BenchmarkCase(name, description, lino, json, yaml, xml));
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Warning: Could not load {name}: {ex.Message}");
+ }
+ }
+
+ return cases;
+ }
+
+ ///
+ /// Run the benchmark for a single test case.
+ ///
+ public static BenchmarkResult RunBenchmark(BenchmarkCase testCase)
+ {
+ var linoChars = CountUtf8Chars(testCase.Lino);
+ var jsonChars = CountUtf8Chars(testCase.Json);
+ var yamlChars = CountUtf8Chars(testCase.Yaml);
+ var xmlChars = CountUtf8Chars(testCase.Xml);
+
+ return new BenchmarkResult(
+ testCase.Name,
+ testCase.Description,
+ linoChars,
+ jsonChars,
+ yamlChars,
+ xmlChars,
+ CalculateSavings(linoChars, jsonChars),
+ CalculateSavings(linoChars, yamlChars),
+ CalculateSavings(linoChars, xmlChars)
+ );
+ }
+
+ ///
+ /// Aggregate results across all benchmark cases.
+ ///
+ public static AggregatedResults AggregateResults(List results)
+ {
+ var totalLino = results.Sum(r => r.LinoChars);
+ var totalJson = results.Sum(r => r.JsonChars);
+ var totalYaml = results.Sum(r => r.YamlChars);
+ var totalXml = results.Sum(r => r.XmlChars);
+
+ var avgVsJson = results.Average(r => r.LinoVsJson);
+ var avgVsYaml = results.Average(r => r.LinoVsYaml);
+ var avgVsXml = results.Average(r => r.LinoVsXml);
+
+ return new AggregatedResults(
+ totalLino,
+ totalJson,
+ totalYaml,
+ totalXml,
+ avgVsJson,
+ avgVsYaml,
+ avgVsXml
+ );
+ }
+
+ public static void Main(string[] args)
+ {
+ var dataDir = FindDataDir();
+ if (dataDir == null)
+ {
+ Console.Error.WriteLine("Error: Could not find benchmarks/data directory");
+ Console.Error.WriteLine("Please run from the repository root or benchmarks directory");
+ Environment.Exit(1);
+ }
+
+ Console.WriteLine($"Loading benchmark cases from {dataDir}...");
+ var cases = LoadBenchmarkCases(dataDir);
+
+ if (cases.Count == 0)
+ {
+ Console.Error.WriteLine("Error: No benchmark cases found");
+ Environment.Exit(1);
+ }
+
+ Console.WriteLine($"Running {cases.Count} benchmark cases...\n");
+
+ var results = cases.Select(RunBenchmark).ToList();
+ var aggregated = AggregateResults(results);
+
+ // Print summary to console
+ Console.WriteLine("=== Links Notation Character Count Benchmark (C#) ===\n");
+ Console.WriteLine("Summary:");
+ Console.WriteLine($" Total Lino characters: {aggregated.TotalLinoChars}");
+ Console.WriteLine($" Total JSON characters: {aggregated.TotalJsonChars}");
+ Console.WriteLine($" Total YAML characters: {aggregated.TotalYamlChars}");
+ Console.WriteLine($" Total XML characters: {aggregated.TotalXmlChars}");
+ Console.WriteLine();
+ Console.WriteLine("Average savings with Lino:");
+ Console.WriteLine($" vs JSON: {aggregated.AvgLinoVsJson:F1}% fewer characters");
+ Console.WriteLine($" vs YAML: {aggregated.AvgLinoVsYaml:F1}% fewer characters");
+ Console.WriteLine($" vs XML: {aggregated.AvgLinoVsXml:F1}% fewer characters");
+ Console.WriteLine();
+
+ // Generate JSON report
+ var report = new Report
+ {
+ Language = "C#",
+ Summary = new SummaryData
+ {
+ TotalLinoChars = aggregated.TotalLinoChars,
+ TotalJsonChars = aggregated.TotalJsonChars,
+ TotalYamlChars = aggregated.TotalYamlChars,
+ TotalXmlChars = aggregated.TotalXmlChars,
+ AvgLinoVsJson = aggregated.AvgLinoVsJson,
+ AvgLinoVsYaml = aggregated.AvgLinoVsYaml,
+ AvgLinoVsXml = aggregated.AvgLinoVsXml,
+ },
+ Results = results.Select(r => new ResultData
+ {
+ Name = r.Name,
+ Description = r.Description,
+ LinoChars = r.LinoChars,
+ JsonChars = r.JsonChars,
+ YamlChars = r.YamlChars,
+ XmlChars = r.XmlChars,
+ LinoVsJson = r.LinoVsJson,
+ LinoVsYaml = r.LinoVsYaml,
+ LinoVsXml = r.LinoVsXml,
+ }).ToList(),
+ };
+
+ var outputDir = FindOutputDir();
+ var jsonPath = Path.Combine(outputDir, "benchmark_results_csharp.json");
+
+ try
+ {
+ var options = new JsonSerializerOptions { WriteIndented = true };
+ var jsonContent = JsonSerializer.Serialize(report, options);
+ File.WriteAllText(jsonPath, jsonContent, Encoding.UTF8);
+ Console.WriteLine($"JSON report written to {jsonPath}");
+ }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Warning: Could not write JSON report: {ex.Message}");
+ }
+
+ Console.WriteLine("\nBenchmark completed successfully!");
+ }
+}
diff --git a/benchmarks/data/config.json b/benchmarks/data/config.json
new file mode 100644
index 0000000..92ff098
--- /dev/null
+++ b/benchmarks/data/config.json
@@ -0,0 +1,19 @@
+{
+ "server": {
+ "host": "localhost",
+ "port": 8080,
+ "ssl": true,
+ "timeout": 30
+ },
+ "database": {
+ "driver": "postgres",
+ "host": "db.example.com",
+ "port": 5432,
+ "name": "myapp"
+ },
+ "features": {
+ "cache": true,
+ "logging": true,
+ "metrics": false
+ }
+}
diff --git a/benchmarks/data/config.lino b/benchmarks/data/config.lino
new file mode 100644
index 0000000..719fce6
--- /dev/null
+++ b/benchmarks/data/config.lino
@@ -0,0 +1,14 @@
+server
+ host localhost
+ port 8080
+ ssl true
+ timeout 30
+database
+ driver postgres
+ host db.example.com
+ port 5432
+ name myapp
+features
+ cache true
+ logging true
+ metrics false
diff --git a/benchmarks/data/config.xml b/benchmarks/data/config.xml
new file mode 100644
index 0000000..bf18078
--- /dev/null
+++ b/benchmarks/data/config.xml
@@ -0,0 +1,19 @@
+
+
+ localhost
+ 8080
+ true
+ 30
+
+
+ postgres
+ db.example.com
+ 5432
+ myapp
+
+
+ true
+ true
+ false
+
+
diff --git a/benchmarks/data/config.yaml b/benchmarks/data/config.yaml
new file mode 100644
index 0000000..fd2946e
--- /dev/null
+++ b/benchmarks/data/config.yaml
@@ -0,0 +1,14 @@
+server:
+ host: localhost
+ port: 8080
+ ssl: true
+ timeout: 30
+database:
+ driver: postgres
+ host: db.example.com
+ port: 5432
+ name: myapp
+features:
+ cache: true
+ logging: true
+ metrics: false
diff --git a/benchmarks/data/employees.json b/benchmarks/data/employees.json
new file mode 100644
index 0000000..342ba8f
--- /dev/null
+++ b/benchmarks/data/employees.json
@@ -0,0 +1,18 @@
+{
+ "empInfo": {
+ "employees": [
+ {
+ "name": "James Kirk",
+ "age": 40
+ },
+ {
+ "name": "Jean-Luc Picard",
+ "age": 45
+ },
+ {
+ "name": "Wesley Crusher",
+ "age": 27
+ }
+ ]
+ }
+}
diff --git a/benchmarks/data/employees.lino b/benchmarks/data/employees.lino
new file mode 100644
index 0000000..1064109
--- /dev/null
+++ b/benchmarks/data/employees.lino
@@ -0,0 +1,14 @@
+empInfo
+ employees:
+ (
+ name (James Kirk)
+ age 40
+ )
+ (
+ name (Jean-Luc Picard)
+ age 45
+ )
+ (
+ name (Wesley Crusher)
+ age 27
+ )
diff --git a/benchmarks/data/employees.xml b/benchmarks/data/employees.xml
new file mode 100644
index 0000000..1cbe076
--- /dev/null
+++ b/benchmarks/data/employees.xml
@@ -0,0 +1,16 @@
+
+
+
+ James Kirk
+ 40
+
+
+ Jean-Luc Picard
+ 45
+
+
+ Wesley Crusher
+ 27
+
+
+
diff --git a/benchmarks/data/employees.yaml b/benchmarks/data/employees.yaml
new file mode 100644
index 0000000..916a792
--- /dev/null
+++ b/benchmarks/data/employees.yaml
@@ -0,0 +1,8 @@
+empInfo:
+ employees:
+ - name: James Kirk
+ age: 40
+ - name: Jean-Luc Picard
+ age: 45
+ - name: Wesley Crusher
+ age: 27
diff --git a/benchmarks/data/nested_structure.json b/benchmarks/data/nested_structure.json
new file mode 100644
index 0000000..6cfdcc3
--- /dev/null
+++ b/benchmarks/data/nested_structure.json
@@ -0,0 +1,21 @@
+{
+ "company": {
+ "name": "TechCorp",
+ "departments": [
+ {
+ "name": "Engineering",
+ "teams": [
+ { "name": "Frontend", "size": 5 },
+ { "name": "Backend", "size": 8 }
+ ]
+ },
+ {
+ "name": "Marketing",
+ "teams": [
+ { "name": "Digital", "size": 3 },
+ { "name": "Content", "size": 4 }
+ ]
+ }
+ ]
+ }
+}
diff --git a/benchmarks/data/nested_structure.lino b/benchmarks/data/nested_structure.lino
new file mode 100644
index 0000000..e42733e
--- /dev/null
+++ b/benchmarks/data/nested_structure.lino
@@ -0,0 +1,15 @@
+company
+ name TechCorp
+ departments:
+ (
+ name Engineering
+ teams:
+ (name Frontend size 5)
+ (name Backend size 8)
+ )
+ (
+ name Marketing
+ teams:
+ (name Digital size 3)
+ (name Content size 4)
+ )
diff --git a/benchmarks/data/nested_structure.xml b/benchmarks/data/nested_structure.xml
new file mode 100644
index 0000000..9ba9e15
--- /dev/null
+++ b/benchmarks/data/nested_structure.xml
@@ -0,0 +1,31 @@
+
+ TechCorp
+
+
+ Engineering
+
+
+ Frontend
+ 5
+
+
+ Backend
+ 8
+
+
+
+
+ Marketing
+
+
+ Digital
+ 3
+
+
+ Content
+ 4
+
+
+
+
+
diff --git a/benchmarks/data/nested_structure.yaml b/benchmarks/data/nested_structure.yaml
new file mode 100644
index 0000000..6ec115f
--- /dev/null
+++ b/benchmarks/data/nested_structure.yaml
@@ -0,0 +1,15 @@
+company:
+ name: TechCorp
+ departments:
+ - name: Engineering
+ teams:
+ - name: Frontend
+ size: 5
+ - name: Backend
+ size: 8
+ - name: Marketing
+ teams:
+ - name: Digital
+ size: 3
+ - name: Content
+ size: 4
diff --git a/benchmarks/data/simple_doublets.json b/benchmarks/data/simple_doublets.json
new file mode 100644
index 0000000..95b4a2e
--- /dev/null
+++ b/benchmarks/data/simple_doublets.json
@@ -0,0 +1,7 @@
+{
+ "links": [
+ { "from": "papa", "to": "loves mama" },
+ { "from": "son", "to": "loves mama" },
+ { "from": "daughter", "to": "loves mama" }
+ ]
+}
diff --git a/benchmarks/data/simple_doublets.lino b/benchmarks/data/simple_doublets.lino
new file mode 100644
index 0000000..897f22e
--- /dev/null
+++ b/benchmarks/data/simple_doublets.lino
@@ -0,0 +1,3 @@
+papa (lovesMama: loves mama)
+son lovesMama
+daughter lovesMama
diff --git a/benchmarks/data/simple_doublets.xml b/benchmarks/data/simple_doublets.xml
new file mode 100644
index 0000000..1bc47af
--- /dev/null
+++ b/benchmarks/data/simple_doublets.xml
@@ -0,0 +1,14 @@
+
+
+ papa
+ loves mama
+
+
+ son
+ loves mama
+
+
+ daughter
+ loves mama
+
+
diff --git a/benchmarks/data/simple_doublets.yaml b/benchmarks/data/simple_doublets.yaml
new file mode 100644
index 0000000..6d94b9c
--- /dev/null
+++ b/benchmarks/data/simple_doublets.yaml
@@ -0,0 +1,7 @@
+links:
+ - from: papa
+ to: loves mama
+ - from: son
+ to: loves mama
+ - from: daughter
+ to: loves mama
diff --git a/benchmarks/data/triplets.json b/benchmarks/data/triplets.json
new file mode 100644
index 0000000..4ba2648
--- /dev/null
+++ b/benchmarks/data/triplets.json
@@ -0,0 +1,7 @@
+{
+ "relations": [
+ { "subject": "papa", "predicate": "has", "object": "car" },
+ { "subject": "mama", "predicate": "has", "object": "house" },
+ { "subject": "papa and mama", "predicate": "are", "object": "happy" }
+ ]
+}
diff --git a/benchmarks/data/triplets.lino b/benchmarks/data/triplets.lino
new file mode 100644
index 0000000..c4c91b4
--- /dev/null
+++ b/benchmarks/data/triplets.lino
@@ -0,0 +1,3 @@
+papa has car
+mama has house
+(papa and mama) are happy
diff --git a/benchmarks/data/triplets.xml b/benchmarks/data/triplets.xml
new file mode 100644
index 0000000..5660e40
--- /dev/null
+++ b/benchmarks/data/triplets.xml
@@ -0,0 +1,17 @@
+
+
+ papa
+ has
+
+
+
+ mama
+ has
+
+
+
+ papa and mama
+ are
+
+
+
diff --git a/benchmarks/data/triplets.yaml b/benchmarks/data/triplets.yaml
new file mode 100644
index 0000000..69bb2e9
--- /dev/null
+++ b/benchmarks/data/triplets.yaml
@@ -0,0 +1,10 @@
+relations:
+ - subject: papa
+ predicate: has
+ object: car
+ - subject: mama
+ predicate: has
+ object: house
+ - subject: papa and mama
+ predicate: are
+ object: happy
diff --git a/benchmarks/go/benchmark.go b/benchmarks/go/benchmark.go
new file mode 100644
index 0000000..8f72a2e
--- /dev/null
+++ b/benchmarks/go/benchmark.go
@@ -0,0 +1,292 @@
+// UTF-8 Character Count Benchmark for Links Notation vs JSON, YAML, and XML
+//
+// This benchmark measures the UTF-8 character count efficiency of Links Notation
+// compared to other popular data serialization formats.
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+ "unicode/utf8"
+)
+
+// BenchmarkCase represents a single benchmark test case
+type BenchmarkCase struct {
+ Name string
+ Description string
+ Lino string
+ JSON string
+ YAML string
+ XML string
+}
+
+// BenchmarkResult represents the results of a single benchmark
+type BenchmarkResult struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ LinoChars int `json:"lino_chars"`
+ JSONChars int `json:"json_chars"`
+ YAMLChars int `json:"yaml_chars"`
+ XMLChars int `json:"xml_chars"`
+ LinoVsJSON float64 `json:"lino_vs_json"`
+ LinoVsYAML float64 `json:"lino_vs_yaml"`
+ LinoVsXML float64 `json:"lino_vs_xml"`
+}
+
+// AggregatedResults represents aggregated results across all benchmarks
+type AggregatedResults struct {
+ TotalLinoChars int `json:"total_lino_chars"`
+ TotalJSONChars int `json:"total_json_chars"`
+ TotalYAMLChars int `json:"total_yaml_chars"`
+ TotalXMLChars int `json:"total_xml_chars"`
+ AvgLinoVsJSON float64 `json:"avg_lino_vs_json"`
+ AvgLinoVsYAML float64 `json:"avg_lino_vs_yaml"`
+ AvgLinoVsXML float64 `json:"avg_lino_vs_xml"`
+}
+
+// Report represents the full benchmark report
+type Report struct {
+ Language string `json:"language"`
+ Summary AggregatedResults `json:"summary"`
+ Results []BenchmarkResult `json:"results"`
+}
+
+// countUTF8Chars counts UTF-8 characters (runes) in a string
+func countUTF8Chars(s string) int {
+ return utf8.RuneCountInString(s)
+}
+
+// calculateSavings calculates the percentage savings of Lino vs another format
+func calculateSavings(linoChars, otherChars int) float64 {
+ if otherChars == 0 {
+ return 0.0
+ }
+ return (float64(otherChars-linoChars) / float64(otherChars)) * 100
+}
+
+// findDataDir finds the data directory by checking multiple possible paths
+func findDataDir() string {
+ // Get executable directory
+ execPath, err := os.Executable()
+ execDir := ""
+ if err == nil {
+ execDir = filepath.Dir(execPath)
+ }
+
+ cwd, _ := os.Getwd()
+
+ possiblePaths := []string{
+ filepath.Join(execDir, "../data"), // Running from benchmarks/go/
+ filepath.Join(execDir, "../../benchmarks/data"), // Running from deeper nested
+ filepath.Join(cwd, "benchmarks/data"), // CWD is repo root
+ filepath.Join(cwd, "../data"), // CWD is benchmarks/
+ filepath.Join(cwd, "data"), // CWD is benchmarks/
+ filepath.Join(cwd, "../../benchmarks/data"), // CWD is benchmarks/go/
+ }
+
+ for _, path := range possiblePaths {
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ continue
+ }
+ if info, err := os.Stat(absPath); err == nil && info.IsDir() {
+ return absPath
+ }
+ }
+
+ return ""
+}
+
+// findOutputDir finds the output directory for reports
+func findOutputDir() string {
+ execPath, err := os.Executable()
+ execDir := ""
+ if err == nil {
+ execDir = filepath.Dir(execPath)
+ }
+
+ cwd, _ := os.Getwd()
+
+ possiblePaths := []string{
+ filepath.Join(execDir, ".."), // Running from benchmarks/go/
+ filepath.Join(cwd, "benchmarks"), // CWD is repo root
+ cwd, // Fallback to current directory
+ }
+
+ for _, path := range possiblePaths {
+ absPath, err := filepath.Abs(path)
+ if err != nil {
+ continue
+ }
+ if info, err := os.Stat(absPath); err == nil && info.IsDir() {
+ return absPath
+ }
+ }
+
+ return cwd
+}
+
+// loadBenchmarkCases loads all benchmark test cases from the data directory
+func loadBenchmarkCases(dataDir string) []BenchmarkCase {
+ casesConfig := []struct {
+ name string
+ description string
+ }{
+ {"employees", "Employee records with nested structure"},
+ {"simple_doublets", "Simple doublet links (2-tuples)"},
+ {"triplets", "Triplet relations (3-tuples)"},
+ {"nested_structure", "Deeply nested company structure"},
+ {"config", "Application configuration"},
+ }
+
+ var cases []BenchmarkCase
+
+ for _, cfg := range casesConfig {
+ lino, err := os.ReadFile(filepath.Join(dataDir, cfg.name+".lino"))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: Could not load %s.lino: %v\n", cfg.name, err)
+ continue
+ }
+
+ jsonData, err := os.ReadFile(filepath.Join(dataDir, cfg.name+".json"))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: Could not load %s.json: %v\n", cfg.name, err)
+ continue
+ }
+
+ yaml, err := os.ReadFile(filepath.Join(dataDir, cfg.name+".yaml"))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: Could not load %s.yaml: %v\n", cfg.name, err)
+ continue
+ }
+
+ xml, err := os.ReadFile(filepath.Join(dataDir, cfg.name+".xml"))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: Could not load %s.xml: %v\n", cfg.name, err)
+ continue
+ }
+
+ cases = append(cases, BenchmarkCase{
+ Name: cfg.name,
+ Description: cfg.description,
+ Lino: string(lino),
+ JSON: string(jsonData),
+ YAML: string(yaml),
+ XML: string(xml),
+ })
+ }
+
+ return cases
+}
+
+// runBenchmark runs the benchmark for a single test case
+func runBenchmark(testCase BenchmarkCase) BenchmarkResult {
+ linoChars := countUTF8Chars(testCase.Lino)
+ jsonChars := countUTF8Chars(testCase.JSON)
+ yamlChars := countUTF8Chars(testCase.YAML)
+ xmlChars := countUTF8Chars(testCase.XML)
+
+ return BenchmarkResult{
+ Name: testCase.Name,
+ Description: testCase.Description,
+ LinoChars: linoChars,
+ JSONChars: jsonChars,
+ YAMLChars: yamlChars,
+ XMLChars: xmlChars,
+ LinoVsJSON: calculateSavings(linoChars, jsonChars),
+ LinoVsYAML: calculateSavings(linoChars, yamlChars),
+ LinoVsXML: calculateSavings(linoChars, xmlChars),
+ }
+}
+
+// aggregateResults aggregates results across all benchmark cases
+func aggregateResults(results []BenchmarkResult) AggregatedResults {
+ var totalLino, totalJSON, totalYAML, totalXML int
+ var sumVsJSON, sumVsYAML, sumVsXML float64
+
+ for _, r := range results {
+ totalLino += r.LinoChars
+ totalJSON += r.JSONChars
+ totalYAML += r.YAMLChars
+ totalXML += r.XMLChars
+ sumVsJSON += r.LinoVsJSON
+ sumVsYAML += r.LinoVsYAML
+ sumVsXML += r.LinoVsXML
+ }
+
+ n := float64(len(results))
+ return AggregatedResults{
+ TotalLinoChars: totalLino,
+ TotalJSONChars: totalJSON,
+ TotalYAMLChars: totalYAML,
+ TotalXMLChars: totalXML,
+ AvgLinoVsJSON: sumVsJSON / n,
+ AvgLinoVsYAML: sumVsYAML / n,
+ AvgLinoVsXML: sumVsXML / n,
+ }
+}
+
+func main() {
+ dataDir := findDataDir()
+ if dataDir == "" {
+ fmt.Fprintln(os.Stderr, "Error: Could not find benchmarks/data directory")
+ fmt.Fprintln(os.Stderr, "Please run from the repository root or benchmarks directory")
+ os.Exit(1)
+ }
+
+ fmt.Printf("Loading benchmark cases from %s...\n", dataDir)
+ cases := loadBenchmarkCases(dataDir)
+
+ if len(cases) == 0 {
+ fmt.Fprintln(os.Stderr, "Error: No benchmark cases found")
+ os.Exit(1)
+ }
+
+ fmt.Printf("Running %d benchmark cases...\n\n", len(cases))
+
+ var results []BenchmarkResult
+ for _, testCase := range cases {
+ results = append(results, runBenchmark(testCase))
+ }
+ aggregated := aggregateResults(results)
+
+ // Print summary to console
+ fmt.Println("=== Links Notation Character Count Benchmark (Go) ===")
+ fmt.Println()
+ fmt.Println("Summary:")
+ fmt.Printf(" Total Lino characters: %d\n", aggregated.TotalLinoChars)
+ fmt.Printf(" Total JSON characters: %d\n", aggregated.TotalJSONChars)
+ fmt.Printf(" Total YAML characters: %d\n", aggregated.TotalYAMLChars)
+ fmt.Printf(" Total XML characters: %d\n", aggregated.TotalXMLChars)
+ fmt.Println()
+ fmt.Println("Average savings with Lino:")
+ fmt.Printf(" vs JSON: %.1f%% fewer characters\n", aggregated.AvgLinoVsJSON)
+ fmt.Printf(" vs YAML: %.1f%% fewer characters\n", aggregated.AvgLinoVsYAML)
+ fmt.Printf(" vs XML: %.1f%% fewer characters\n", aggregated.AvgLinoVsXML)
+ fmt.Println()
+
+ // Generate JSON report
+ report := Report{
+ Language: "Go",
+ Summary: aggregated,
+ Results: results,
+ }
+
+ outputDir := findOutputDir()
+ jsonPath := filepath.Join(outputDir, "benchmark_results_go.json")
+
+ jsonData, err := json.MarshalIndent(report, "", " ")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: Could not marshal JSON report: %v\n", err)
+ } else {
+ if err := os.WriteFile(jsonPath, jsonData, 0644); err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: Could not write JSON report: %v\n", err)
+ } else {
+ fmt.Printf("JSON report written to %s\n", jsonPath)
+ }
+ }
+
+ fmt.Println("\nBenchmark completed successfully!")
+}
diff --git a/benchmarks/go/go.mod b/benchmarks/go/go.mod
new file mode 100644
index 0000000..1818723
--- /dev/null
+++ b/benchmarks/go/go.mod
@@ -0,0 +1,3 @@
+module github.com/link-foundation/links-notation/benchmarks/go
+
+go 1.21
diff --git a/benchmarks/java/pom.xml b/benchmarks/java/pom.xml
new file mode 100644
index 0000000..90138e7
--- /dev/null
+++ b/benchmarks/java/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+
+ io.github.link-foundation
+ links-notation-benchmark
+ 0.1.0
+ jar
+
+ Links Notation Benchmark
+ UTF-8 Character Count Benchmark for Links Notation vs JSON, YAML, and XML
+
+
+ 17
+ 17
+ UTF-8
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ io.github.linkfoundation.benchmark.Benchmark
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ io.github.linkfoundation.benchmark.Benchmark
+
+
+
+
+
diff --git a/benchmarks/java/src/main/java/io/github/linkfoundation/benchmark/Benchmark.java b/benchmarks/java/src/main/java/io/github/linkfoundation/benchmark/Benchmark.java
new file mode 100644
index 0000000..0b2b63e
--- /dev/null
+++ b/benchmarks/java/src/main/java/io/github/linkfoundation/benchmark/Benchmark.java
@@ -0,0 +1,271 @@
+package io.github.linkfoundation.benchmark;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * UTF-8 Character Count Benchmark for Links Notation vs JSON, YAML, and XML
+ *
+ * This benchmark measures the UTF-8 character count efficiency of Links Notation
+ * compared to other popular data serialization formats.
+ */
+public class Benchmark {
+
+ /**
+ * Represents a single benchmark test case.
+ */
+ record BenchmarkCase(
+ String name,
+ String description,
+ String lino,
+ String json,
+ String yaml,
+ String xml
+ ) {}
+
+ /**
+ * Represents the results of a single benchmark.
+ */
+ record BenchmarkResult(
+ String name,
+ String description,
+ int lino_chars,
+ int json_chars,
+ int yaml_chars,
+ int xml_chars,
+ double lino_vs_json,
+ double lino_vs_yaml,
+ double lino_vs_xml
+ ) {}
+
+ /**
+ * Represents aggregated results across all benchmarks.
+ */
+ record AggregatedResults(
+ int total_lino_chars,
+ int total_json_chars,
+ int total_yaml_chars,
+ int total_xml_chars,
+ double avg_lino_vs_json,
+ double avg_lino_vs_yaml,
+ double avg_lino_vs_xml
+ ) {}
+
+ /**
+ * Represents the full benchmark report.
+ */
+ record Report(
+ String language,
+ AggregatedResults summary,
+ List results
+ ) {}
+
+ /**
+ * Count UTF-8 characters in a string.
+ * In Java, we use codePointCount to get the number of Unicode code points.
+ */
+ public static int countUtf8Chars(String text) {
+ return text.codePointCount(0, text.length());
+ }
+
+ /**
+ * Calculate the percentage savings of Lino vs another format.
+ */
+ public static double calculateSavings(int linoChars, int otherChars) {
+ if (otherChars == 0) return 0.0;
+ return ((double)(otherChars - linoChars) / otherChars) * 100;
+ }
+
+ /**
+ * Find the data directory by checking multiple possible paths.
+ */
+ public static Path findDataDir() {
+ Path cwd = Paths.get(System.getProperty("user.dir"));
+
+ List possiblePaths = List.of(
+ cwd.resolve("benchmarks/data"), // CWD is repo root
+ cwd.resolve("../data"), // CWD is benchmarks/
+ cwd.resolve("data"), // CWD is benchmarks/
+ cwd.resolve("../../benchmarks/data"), // CWD is benchmarks/java/
+ cwd.resolve("../../../benchmarks/data") // CWD is benchmarks/java/target/
+ );
+
+ for (Path path : possiblePaths) {
+ Path resolved = path.toAbsolutePath().normalize();
+ if (Files.isDirectory(resolved)) {
+ return resolved;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Find the output directory for reports.
+ */
+ public static Path findOutputDir() {
+ Path cwd = Paths.get(System.getProperty("user.dir"));
+
+ List possiblePaths = List.of(
+ cwd.resolve("benchmarks"), // CWD is repo root
+ cwd.resolve(".."), // CWD is benchmarks/java/
+ cwd // Fallback to current directory
+ );
+
+ for (Path path : possiblePaths) {
+ Path resolved = path.toAbsolutePath().normalize();
+ if (Files.isDirectory(resolved)) {
+ return resolved;
+ }
+ }
+
+ return cwd;
+ }
+
+ /**
+ * Load a file as a UTF-8 string.
+ */
+ private static String readFile(Path path) throws IOException {
+ return Files.readString(path, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * Load all benchmark test cases from the data directory.
+ */
+ public static List loadBenchmarkCases(Path dataDir) {
+ record CaseConfig(String name, String description) {}
+
+ List casesConfig = List.of(
+ new CaseConfig("employees", "Employee records with nested structure"),
+ new CaseConfig("simple_doublets", "Simple doublet links (2-tuples)"),
+ new CaseConfig("triplets", "Triplet relations (3-tuples)"),
+ new CaseConfig("nested_structure", "Deeply nested company structure"),
+ new CaseConfig("config", "Application configuration")
+ );
+
+ List cases = new ArrayList<>();
+
+ for (CaseConfig cfg : casesConfig) {
+ try {
+ String lino = readFile(dataDir.resolve(cfg.name + ".lino"));
+ String json = readFile(dataDir.resolve(cfg.name + ".json"));
+ String yaml = readFile(dataDir.resolve(cfg.name + ".yaml"));
+ String xml = readFile(dataDir.resolve(cfg.name + ".xml"));
+
+ cases.add(new BenchmarkCase(cfg.name, cfg.description, lino, json, yaml, xml));
+ } catch (IOException e) {
+ System.err.println("Warning: Could not load " + cfg.name + ": " + e.getMessage());
+ }
+ }
+
+ return cases;
+ }
+
+ /**
+ * Run the benchmark for a single test case.
+ */
+ public static BenchmarkResult runBenchmark(BenchmarkCase testCase) {
+ int linoChars = countUtf8Chars(testCase.lino);
+ int jsonChars = countUtf8Chars(testCase.json);
+ int yamlChars = countUtf8Chars(testCase.yaml);
+ int xmlChars = countUtf8Chars(testCase.xml);
+
+ return new BenchmarkResult(
+ testCase.name,
+ testCase.description,
+ linoChars,
+ jsonChars,
+ yamlChars,
+ xmlChars,
+ calculateSavings(linoChars, jsonChars),
+ calculateSavings(linoChars, yamlChars),
+ calculateSavings(linoChars, xmlChars)
+ );
+ }
+
+ /**
+ * Aggregate results across all benchmark cases.
+ */
+ public static AggregatedResults aggregateResults(List results) {
+ int totalLino = results.stream().mapToInt(BenchmarkResult::lino_chars).sum();
+ int totalJson = results.stream().mapToInt(BenchmarkResult::json_chars).sum();
+ int totalYaml = results.stream().mapToInt(BenchmarkResult::yaml_chars).sum();
+ int totalXml = results.stream().mapToInt(BenchmarkResult::xml_chars).sum();
+
+ double avgVsJson = results.stream().mapToDouble(BenchmarkResult::lino_vs_json).average().orElse(0);
+ double avgVsYaml = results.stream().mapToDouble(BenchmarkResult::lino_vs_yaml).average().orElse(0);
+ double avgVsXml = results.stream().mapToDouble(BenchmarkResult::lino_vs_xml).average().orElse(0);
+
+ return new AggregatedResults(
+ totalLino,
+ totalJson,
+ totalYaml,
+ totalXml,
+ avgVsJson,
+ avgVsYaml,
+ avgVsXml
+ );
+ }
+
+ public static void main(String[] args) {
+ Path dataDir = findDataDir();
+ if (dataDir == null) {
+ System.err.println("Error: Could not find benchmarks/data directory");
+ System.err.println("Please run from the repository root or benchmarks directory");
+ System.exit(1);
+ }
+
+ System.out.println("Loading benchmark cases from " + dataDir + "...");
+ List cases = loadBenchmarkCases(dataDir);
+
+ if (cases.isEmpty()) {
+ System.err.println("Error: No benchmark cases found");
+ System.exit(1);
+ }
+
+ System.out.println("Running " + cases.size() + " benchmark cases...\n");
+
+ List results = cases.stream()
+ .map(Benchmark::runBenchmark)
+ .toList();
+ AggregatedResults aggregated = aggregateResults(results);
+
+ // Print summary to console
+ System.out.println("=== Links Notation Character Count Benchmark (Java) ===\n");
+ System.out.println("Summary:");
+ System.out.println(" Total Lino characters: " + aggregated.total_lino_chars());
+ System.out.println(" Total JSON characters: " + aggregated.total_json_chars());
+ System.out.println(" Total YAML characters: " + aggregated.total_yaml_chars());
+ System.out.println(" Total XML characters: " + aggregated.total_xml_chars());
+ System.out.println();
+ System.out.println("Average savings with Lino:");
+ System.out.printf(" vs JSON: %.1f%% fewer characters%n", aggregated.avg_lino_vs_json());
+ System.out.printf(" vs YAML: %.1f%% fewer characters%n", aggregated.avg_lino_vs_yaml());
+ System.out.printf(" vs XML: %.1f%% fewer characters%n", aggregated.avg_lino_vs_xml());
+ System.out.println();
+
+ // Generate JSON report
+ Report report = new Report("Java", aggregated, results);
+
+ Path outputDir = findOutputDir();
+ Path jsonPath = outputDir.resolve("benchmark_results_java.json");
+
+ Gson gson = new GsonBuilder().setPrettyPrinting().create();
+ try {
+ Files.writeString(jsonPath, gson.toJson(report), StandardCharsets.UTF_8);
+ System.out.println("JSON report written to " + jsonPath);
+ } catch (IOException e) {
+ System.err.println("Warning: Could not write JSON report: " + e.getMessage());
+ }
+
+ System.out.println("\nBenchmark completed successfully!");
+ }
+}
diff --git a/benchmarks/js/benchmark.mjs b/benchmarks/js/benchmark.mjs
new file mode 100644
index 0000000..1009fb2
--- /dev/null
+++ b/benchmarks/js/benchmark.mjs
@@ -0,0 +1,225 @@
+#!/usr/bin/env node
+/**
+ * UTF-8 Character Count Benchmark for Links Notation vs JSON, YAML, and XML
+ *
+ * This benchmark measures the UTF-8 character count efficiency of Links Notation
+ * compared to other popular data serialization formats.
+ */
+
+import { readFileSync, writeFileSync, existsSync } from 'fs';
+import { join, dirname } from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+/**
+ * Count UTF-8 characters in a string
+ * @param {string} str - Input string
+ * @returns {number} Character count
+ */
+function countUtf8Chars(str) {
+ // In JavaScript, string.length gives UTF-16 code units
+ // For proper UTF-8 character counting, we use the spread operator
+ return [...str].length;
+}
+
+/**
+ * Calculate savings percentage
+ * @param {number} linoChars - Links Notation character count
+ * @param {number} otherChars - Other format character count
+ * @returns {number} Savings percentage
+ */
+function calculateSavings(linoChars, otherChars) {
+ if (otherChars === 0) return 0;
+ return ((otherChars - linoChars) / otherChars) * 100;
+}
+
+/**
+ * Find the data directory by checking multiple possible paths
+ * @returns {string|null} Path to data directory or null
+ */
+function findDataDir() {
+ const possiblePaths = [
+ join(__dirname, '../data'), // Running from benchmarks/js/
+ join(__dirname, '../../benchmarks/data'), // Running from repo root
+ join(process.cwd(), 'benchmarks/data'), // CWD is repo root
+ join(process.cwd(), '../data'), // CWD is benchmarks/
+ join(process.cwd(), 'data'), // CWD is benchmarks/
+ ];
+
+ for (const path of possiblePaths) {
+ if (existsSync(path)) {
+ return path;
+ }
+ }
+ return null;
+}
+
+/**
+ * Find the output directory for reports
+ * @returns {string} Path to output directory
+ */
+function findOutputDir() {
+ const possiblePaths = [
+ join(__dirname, '..'), // Running from benchmarks/js/
+ join(process.cwd(), 'benchmarks'), // CWD is repo root
+ process.cwd(), // Fallback to current directory
+ ];
+
+ for (const path of possiblePaths) {
+ if (existsSync(path)) {
+ return path;
+ }
+ }
+ return process.cwd();
+}
+
+/**
+ * Load all benchmark test cases
+ * @param {string} dataDir - Path to data directory
+ * @returns {Array} Array of benchmark cases
+ */
+function loadBenchmarkCases(dataDir) {
+ const cases = [
+ { name: 'employees', description: 'Employee records with nested structure' },
+ { name: 'simple_doublets', description: 'Simple doublet links (2-tuples)' },
+ { name: 'triplets', description: 'Triplet relations (3-tuples)' },
+ { name: 'nested_structure', description: 'Deeply nested company structure' },
+ { name: 'config', description: 'Application configuration' },
+ ];
+
+ return cases.map(testCase => {
+ try {
+ const lino = readFileSync(join(dataDir, `${testCase.name}.lino`), 'utf-8');
+ const json = readFileSync(join(dataDir, `${testCase.name}.json`), 'utf-8');
+ const yaml = readFileSync(join(dataDir, `${testCase.name}.yaml`), 'utf-8');
+ const xml = readFileSync(join(dataDir, `${testCase.name}.xml`), 'utf-8');
+
+ return {
+ ...testCase,
+ lino,
+ json,
+ yaml,
+ xml,
+ };
+ } catch (error) {
+ console.warn(`Warning: Could not load ${testCase.name}: ${error.message}`);
+ return null;
+ }
+ }).filter(Boolean);
+}
+
+/**
+ * Run benchmark for a single case
+ * @param {Object} testCase - Benchmark case
+ * @returns {Object} Benchmark result
+ */
+function runBenchmark(testCase) {
+ const linoChars = countUtf8Chars(testCase.lino);
+ const jsonChars = countUtf8Chars(testCase.json);
+ const yamlChars = countUtf8Chars(testCase.yaml);
+ const xmlChars = countUtf8Chars(testCase.xml);
+
+ return {
+ name: testCase.name,
+ description: testCase.description,
+ lino_chars: linoChars,
+ json_chars: jsonChars,
+ yaml_chars: yamlChars,
+ xml_chars: xmlChars,
+ lino_vs_json: calculateSavings(linoChars, jsonChars),
+ lino_vs_yaml: calculateSavings(linoChars, yamlChars),
+ lino_vs_xml: calculateSavings(linoChars, xmlChars),
+ };
+}
+
+/**
+ * Aggregate results across all benchmark cases
+ * @param {Array} results - Array of benchmark results
+ * @returns {Object} Aggregated results
+ */
+function aggregateResults(results) {
+ const totalLinoChars = results.reduce((sum, r) => sum + r.lino_chars, 0);
+ const totalJsonChars = results.reduce((sum, r) => sum + r.json_chars, 0);
+ const totalYamlChars = results.reduce((sum, r) => sum + r.yaml_chars, 0);
+ const totalXmlChars = results.reduce((sum, r) => sum + r.xml_chars, 0);
+
+ const avgLinoVsJson = results.reduce((sum, r) => sum + r.lino_vs_json, 0) / results.length;
+ const avgLinoVsYaml = results.reduce((sum, r) => sum + r.lino_vs_yaml, 0) / results.length;
+ const avgLinoVsXml = results.reduce((sum, r) => sum + r.lino_vs_xml, 0) / results.length;
+
+ return {
+ total_lino_chars: totalLinoChars,
+ total_json_chars: totalJsonChars,
+ total_yaml_chars: totalYamlChars,
+ total_xml_chars: totalXmlChars,
+ avg_lino_vs_json: avgLinoVsJson,
+ avg_lino_vs_yaml: avgLinoVsYaml,
+ avg_lino_vs_xml: avgLinoVsXml,
+ };
+}
+
+/**
+ * Main function
+ */
+function main() {
+ const dataDir = findDataDir();
+ if (!dataDir) {
+ console.error('Error: Could not find benchmarks/data directory');
+ console.error('Please run from the repository root or benchmarks directory');
+ process.exit(1);
+ }
+
+ console.log(`Loading benchmark cases from ${dataDir}...`);
+ const cases = loadBenchmarkCases(dataDir);
+
+ if (cases.length === 0) {
+ console.error('Error: No benchmark cases found');
+ process.exit(1);
+ }
+
+ console.log(`Running ${cases.length} benchmark cases...\n`);
+
+ const results = cases.map(runBenchmark);
+ const aggregated = aggregateResults(results);
+
+ // Print summary to console
+ console.log('=== Links Notation Character Count Benchmark (JavaScript) ===\n');
+ console.log('Summary:');
+ console.log(` Total Lino characters: ${aggregated.total_lino_chars}`);
+ console.log(` Total JSON characters: ${aggregated.total_json_chars}`);
+ console.log(` Total YAML characters: ${aggregated.total_yaml_chars}`);
+ console.log(` Total XML characters: ${aggregated.total_xml_chars}`);
+ console.log();
+ console.log('Average savings with Lino:');
+ console.log(` vs JSON: ${aggregated.avg_lino_vs_json.toFixed(1)}% fewer characters`);
+ console.log(` vs YAML: ${aggregated.avg_lino_vs_yaml.toFixed(1)}% fewer characters`);
+ console.log(` vs XML: ${aggregated.avg_lino_vs_xml.toFixed(1)}% fewer characters`);
+ console.log();
+
+ // Generate JSON report
+ const report = {
+ language: 'JavaScript',
+ summary: aggregated,
+ results: results,
+ };
+
+ const outputDir = findOutputDir();
+ const jsonPath = join(outputDir, 'benchmark_results_js.json');
+
+ try {
+ writeFileSync(jsonPath, JSON.stringify(report, null, 2));
+ console.log(`JSON report written to ${jsonPath}`);
+ } catch (error) {
+ console.warn(`Warning: Could not write JSON report: ${error.message}`);
+ }
+
+ console.log('\nBenchmark completed successfully!');
+}
+
+// Run main function
+main();
+
+// Export for testing
+export { countUtf8Chars, calculateSavings, runBenchmark, aggregateResults };
diff --git a/benchmarks/python/benchmark.py b/benchmarks/python/benchmark.py
new file mode 100644
index 0000000..75bc405
--- /dev/null
+++ b/benchmarks/python/benchmark.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+"""
+UTF-8 Character Count Benchmark for Links Notation vs JSON, YAML, and XML
+
+This benchmark measures the UTF-8 character count efficiency of Links Notation
+compared to other popular data serialization formats.
+"""
+
+import json
+import os
+import sys
+from dataclasses import dataclass
+from pathlib import Path
+from typing import List, Optional
+
+
+@dataclass
+class BenchmarkCase:
+ """Represents a single benchmark test case."""
+ name: str
+ description: str
+ lino: str
+ json_content: str
+ yaml: str
+ xml: str
+
+
+@dataclass
+class BenchmarkResult:
+ """Represents the results of a single benchmark."""
+ name: str
+ description: str
+ lino_chars: int
+ json_chars: int
+ yaml_chars: int
+ xml_chars: int
+ lino_vs_json: float
+ lino_vs_yaml: float
+ lino_vs_xml: float
+
+
+@dataclass
+class AggregatedResults:
+ """Represents aggregated results across all benchmarks."""
+ total_lino_chars: int
+ total_json_chars: int
+ total_yaml_chars: int
+ total_xml_chars: int
+ avg_lino_vs_json: float
+ avg_lino_vs_yaml: float
+ avg_lino_vs_xml: float
+
+
+def count_utf8_chars(text: str) -> int:
+ """Count UTF-8 characters in a string.
+
+ In Python 3, len(str) already returns the number of Unicode code points,
+ which is what we want for UTF-8 character counting.
+ """
+ return len(text)
+
+
+def calculate_savings(lino_chars: int, other_chars: int) -> float:
+ """Calculate the percentage savings of Lino vs another format."""
+ if other_chars == 0:
+ return 0.0
+ return ((other_chars - lino_chars) / other_chars) * 100
+
+
+def find_data_dir() -> Optional[Path]:
+ """Find the data directory by checking multiple possible paths."""
+ script_dir = Path(__file__).parent
+ cwd = Path.cwd()
+
+ possible_paths = [
+ script_dir / "../data", # Running from benchmarks/python/
+ script_dir / "../../benchmarks/data", # Running from repo root
+ cwd / "benchmarks/data", # CWD is repo root
+ cwd / "../data", # CWD is benchmarks/
+ cwd / "data", # CWD is benchmarks/
+ ]
+
+ for path in possible_paths:
+ resolved = path.resolve()
+ if resolved.exists():
+ return resolved
+
+ return None
+
+
+def find_output_dir() -> Path:
+ """Find the output directory for reports."""
+ script_dir = Path(__file__).parent
+ cwd = Path.cwd()
+
+ possible_paths = [
+ script_dir / "..", # Running from benchmarks/python/
+ cwd / "benchmarks", # CWD is repo root
+ cwd, # Fallback to current directory
+ ]
+
+ for path in possible_paths:
+ resolved = path.resolve()
+ if resolved.exists():
+ return resolved
+
+ return cwd
+
+
+def load_benchmark_cases(data_dir: Path) -> List[BenchmarkCase]:
+ """Load all benchmark test cases from the data directory."""
+ cases_config = [
+ ("employees", "Employee records with nested structure"),
+ ("simple_doublets", "Simple doublet links (2-tuples)"),
+ ("triplets", "Triplet relations (3-tuples)"),
+ ("nested_structure", "Deeply nested company structure"),
+ ("config", "Application configuration"),
+ ]
+
+ cases = []
+ for name, description in cases_config:
+ try:
+ lino = (data_dir / f"{name}.lino").read_text(encoding="utf-8")
+ json_content = (data_dir / f"{name}.json").read_text(encoding="utf-8")
+ yaml = (data_dir / f"{name}.yaml").read_text(encoding="utf-8")
+ xml = (data_dir / f"{name}.xml").read_text(encoding="utf-8")
+
+ cases.append(BenchmarkCase(
+ name=name,
+ description=description,
+ lino=lino,
+ json_content=json_content,
+ yaml=yaml,
+ xml=xml,
+ ))
+ except FileNotFoundError as e:
+ print(f"Warning: Could not load {name}: {e}", file=sys.stderr)
+
+ return cases
+
+
+def run_benchmark(case: BenchmarkCase) -> BenchmarkResult:
+ """Run the benchmark for a single test case."""
+ lino_chars = count_utf8_chars(case.lino)
+ json_chars = count_utf8_chars(case.json_content)
+ yaml_chars = count_utf8_chars(case.yaml)
+ xml_chars = count_utf8_chars(case.xml)
+
+ return BenchmarkResult(
+ name=case.name,
+ description=case.description,
+ lino_chars=lino_chars,
+ json_chars=json_chars,
+ yaml_chars=yaml_chars,
+ xml_chars=xml_chars,
+ lino_vs_json=calculate_savings(lino_chars, json_chars),
+ lino_vs_yaml=calculate_savings(lino_chars, yaml_chars),
+ lino_vs_xml=calculate_savings(lino_chars, xml_chars),
+ )
+
+
+def aggregate_results(results: List[BenchmarkResult]) -> AggregatedResults:
+ """Aggregate results across all benchmark cases."""
+ total_lino = sum(r.lino_chars for r in results)
+ total_json = sum(r.json_chars for r in results)
+ total_yaml = sum(r.yaml_chars for r in results)
+ total_xml = sum(r.xml_chars for r in results)
+
+ avg_vs_json = sum(r.lino_vs_json for r in results) / len(results)
+ avg_vs_yaml = sum(r.lino_vs_yaml for r in results) / len(results)
+ avg_vs_xml = sum(r.lino_vs_xml for r in results) / len(results)
+
+ return AggregatedResults(
+ total_lino_chars=total_lino,
+ total_json_chars=total_json,
+ total_yaml_chars=total_yaml,
+ total_xml_chars=total_xml,
+ avg_lino_vs_json=avg_vs_json,
+ avg_lino_vs_yaml=avg_vs_yaml,
+ avg_lino_vs_xml=avg_vs_xml,
+ )
+
+
+def main():
+ """Main function to run the benchmark."""
+ data_dir = find_data_dir()
+ if data_dir is None:
+ print("Error: Could not find benchmarks/data directory", file=sys.stderr)
+ print("Please run from the repository root or benchmarks directory", file=sys.stderr)
+ sys.exit(1)
+
+ print(f"Loading benchmark cases from {data_dir}...")
+ cases = load_benchmark_cases(data_dir)
+
+ if not cases:
+ print("Error: No benchmark cases found", file=sys.stderr)
+ sys.exit(1)
+
+ print(f"Running {len(cases)} benchmark cases...\n")
+
+ results = [run_benchmark(case) for case in cases]
+ aggregated = aggregate_results(results)
+
+ # Print summary to console
+ print("=== Links Notation Character Count Benchmark (Python) ===\n")
+ print("Summary:")
+ print(f" Total Lino characters: {aggregated.total_lino_chars}")
+ print(f" Total JSON characters: {aggregated.total_json_chars}")
+ print(f" Total YAML characters: {aggregated.total_yaml_chars}")
+ print(f" Total XML characters: {aggregated.total_xml_chars}")
+ print()
+ print("Average savings with Lino:")
+ print(f" vs JSON: {aggregated.avg_lino_vs_json:.1f}% fewer characters")
+ print(f" vs YAML: {aggregated.avg_lino_vs_yaml:.1f}% fewer characters")
+ print(f" vs XML: {aggregated.avg_lino_vs_xml:.1f}% fewer characters")
+ print()
+
+ # Generate JSON report
+ report = {
+ "language": "Python",
+ "summary": {
+ "total_lino_chars": aggregated.total_lino_chars,
+ "total_json_chars": aggregated.total_json_chars,
+ "total_yaml_chars": aggregated.total_yaml_chars,
+ "total_xml_chars": aggregated.total_xml_chars,
+ "avg_lino_vs_json": aggregated.avg_lino_vs_json,
+ "avg_lino_vs_yaml": aggregated.avg_lino_vs_yaml,
+ "avg_lino_vs_xml": aggregated.avg_lino_vs_xml,
+ },
+ "results": [
+ {
+ "name": r.name,
+ "description": r.description,
+ "lino_chars": r.lino_chars,
+ "json_chars": r.json_chars,
+ "yaml_chars": r.yaml_chars,
+ "xml_chars": r.xml_chars,
+ "lino_vs_json": r.lino_vs_json,
+ "lino_vs_yaml": r.lino_vs_yaml,
+ "lino_vs_xml": r.lino_vs_xml,
+ }
+ for r in results
+ ],
+ }
+
+ output_dir = find_output_dir()
+ json_path = output_dir / "benchmark_results_python.json"
+
+ try:
+ with open(json_path, "w", encoding="utf-8") as f:
+ json.dump(report, f, indent=2)
+ print(f"JSON report written to {json_path}")
+ except IOError as e:
+ print(f"Warning: Could not write JSON report: {e}", file=sys.stderr)
+
+ print("\nBenchmark completed successfully!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rust/Cargo.toml b/rust/Cargo.toml
index b421c9e..9da1bfe 100644
--- a/rust/Cargo.toml
+++ b/rust/Cargo.toml
@@ -2,5 +2,6 @@
members = [
"links-notation",
"links-notation-macro",
+ "links-notation-benchmark",
]
resolver = "2"
diff --git a/rust/links-notation-benchmark/Cargo.toml b/rust/links-notation-benchmark/Cargo.toml
new file mode 100644
index 0000000..51c71b5
--- /dev/null
+++ b/rust/links-notation-benchmark/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "links-notation-benchmark"
+version = "0.1.0"
+edition = "2021"
+description = "UTF-8 character count benchmarks comparing Links Notation with JSON, YAML, and XML"
+license = "Unlicense"
+repository = "https://github.com/link-foundation/links-notation"
+publish = false
+
+[[bin]]
+name = "benchmark"
+path = "src/main.rs"
+
+[dependencies]
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
diff --git a/rust/links-notation-benchmark/src/main.rs b/rust/links-notation-benchmark/src/main.rs
new file mode 100644
index 0000000..5864c0e
--- /dev/null
+++ b/rust/links-notation-benchmark/src/main.rs
@@ -0,0 +1,391 @@
+//! UTF-8 Character Count Benchmark for Links Notation vs JSON, YAML, and XML
+//!
+//! This benchmark measures the UTF-8 character count efficiency of Links Notation
+//! compared to other popular data serialization formats.
+
+use serde::Serialize;
+use std::fs;
+use std::path::Path;
+
+/// Represents a single benchmark test case with all format representations
+#[derive(Debug, Clone)]
+struct BenchmarkCase {
+ name: String,
+ description: String,
+ lino: String,
+ json: String,
+ yaml: String,
+ xml: String,
+}
+
+/// Represents the character count results for a benchmark case
+#[derive(Debug, Clone, Serialize)]
+struct BenchmarkResult {
+ name: String,
+ description: String,
+ lino_chars: usize,
+ json_chars: usize,
+ yaml_chars: usize,
+ xml_chars: usize,
+ lino_vs_json: f64,
+ lino_vs_yaml: f64,
+ lino_vs_xml: f64,
+}
+
+/// Represents aggregated results across all benchmark cases
+#[derive(Debug, Clone, Serialize)]
+struct AggregatedResults {
+ total_lino_chars: usize,
+ total_json_chars: usize,
+ total_yaml_chars: usize,
+ total_xml_chars: usize,
+ avg_lino_vs_json: f64,
+ avg_lino_vs_yaml: f64,
+ avg_lino_vs_xml: f64,
+}
+
+fn count_utf8_chars(s: &str) -> usize {
+ s.chars().count()
+}
+
+fn calculate_savings(lino_chars: usize, other_chars: usize) -> f64 {
+ if other_chars == 0 {
+ 0.0
+ } else {
+ ((other_chars as f64 - lino_chars as f64) / other_chars as f64) * 100.0
+ }
+}
+
+fn load_benchmark_cases(data_dir: &Path) -> Vec {
+ let cases = vec![
+ ("employees", "Employee records with nested structure"),
+ ("simple_doublets", "Simple doublet links (2-tuples)"),
+ ("triplets", "Triplet relations (3-tuples)"),
+ ("nested_structure", "Deeply nested company structure"),
+ ("config", "Application configuration"),
+ ];
+
+ cases
+ .into_iter()
+ .filter_map(|(name, desc)| {
+ let lino_path = data_dir.join(format!("{}.lino", name));
+ let json_path = data_dir.join(format!("{}.json", name));
+ let yaml_path = data_dir.join(format!("{}.yaml", name));
+ let xml_path = data_dir.join(format!("{}.xml", name));
+
+ let lino = fs::read_to_string(&lino_path).ok()?;
+ let json = fs::read_to_string(&json_path).ok()?;
+ let yaml = fs::read_to_string(&yaml_path).ok()?;
+ let xml = fs::read_to_string(&xml_path).ok()?;
+
+ Some(BenchmarkCase {
+ name: name.to_string(),
+ description: desc.to_string(),
+ lino,
+ json,
+ yaml,
+ xml,
+ })
+ })
+ .collect()
+}
+
+fn run_benchmark(case: &BenchmarkCase) -> BenchmarkResult {
+ let lino_chars = count_utf8_chars(&case.lino);
+ let json_chars = count_utf8_chars(&case.json);
+ let yaml_chars = count_utf8_chars(&case.yaml);
+ let xml_chars = count_utf8_chars(&case.xml);
+
+ BenchmarkResult {
+ name: case.name.clone(),
+ description: case.description.clone(),
+ lino_chars,
+ json_chars,
+ yaml_chars,
+ xml_chars,
+ lino_vs_json: calculate_savings(lino_chars, json_chars),
+ lino_vs_yaml: calculate_savings(lino_chars, yaml_chars),
+ lino_vs_xml: calculate_savings(lino_chars, xml_chars),
+ }
+}
+
+fn aggregate_results(results: &[BenchmarkResult]) -> AggregatedResults {
+ let total_lino_chars: usize = results.iter().map(|r| r.lino_chars).sum();
+ let total_json_chars: usize = results.iter().map(|r| r.json_chars).sum();
+ let total_yaml_chars: usize = results.iter().map(|r| r.yaml_chars).sum();
+ let total_xml_chars: usize = results.iter().map(|r| r.xml_chars).sum();
+
+ let avg_lino_vs_json: f64 =
+ results.iter().map(|r| r.lino_vs_json).sum::() / results.len() as f64;
+ let avg_lino_vs_yaml: f64 =
+ results.iter().map(|r| r.lino_vs_yaml).sum::() / results.len() as f64;
+ let avg_lino_vs_xml: f64 =
+ results.iter().map(|r| r.lino_vs_xml).sum::() / results.len() as f64;
+
+ AggregatedResults {
+ total_lino_chars,
+ total_json_chars,
+ total_yaml_chars,
+ total_xml_chars,
+ avg_lino_vs_json,
+ avg_lino_vs_yaml,
+ avg_lino_vs_xml,
+ }
+}
+
+fn generate_markdown_report(results: &[BenchmarkResult], aggregated: &AggregatedResults) -> String {
+ let mut md = String::new();
+
+ md.push_str("# Links Notation Character Count Benchmark\n\n");
+ md.push_str("This benchmark compares the UTF-8 character count of Links Notation (lino) against JSON, YAML, and XML.\n\n");
+ md.push_str("## Summary\n\n");
+ md.push_str("| Format | Total Characters | vs Lino |\n");
+ md.push_str("|--------|------------------|----------|\n");
+ md.push_str(&format!(
+ "| **Lino** | **{}** | - |\n",
+ aggregated.total_lino_chars
+ ));
+ md.push_str(&format!(
+ "| JSON | {} | +{:.1}% |\n",
+ aggregated.total_json_chars,
+ ((aggregated.total_json_chars as f64 / aggregated.total_lino_chars as f64) - 1.0) * 100.0
+ ));
+ md.push_str(&format!(
+ "| YAML | {} | +{:.1}% |\n",
+ aggregated.total_yaml_chars,
+ ((aggregated.total_yaml_chars as f64 / aggregated.total_lino_chars as f64) - 1.0) * 100.0
+ ));
+ md.push_str(&format!(
+ "| XML | {} | +{:.1}% |\n",
+ aggregated.total_xml_chars,
+ ((aggregated.total_xml_chars as f64 / aggregated.total_lino_chars as f64) - 1.0) * 100.0
+ ));
+
+ md.push_str("\n## Average Savings with Lino\n\n");
+ md.push_str(&format!(
+ "- **vs JSON**: {:.1}% fewer characters\n",
+ aggregated.avg_lino_vs_json
+ ));
+ md.push_str(&format!(
+ "- **vs YAML**: {:.1}% fewer characters\n",
+ aggregated.avg_lino_vs_yaml
+ ));
+ md.push_str(&format!(
+ "- **vs XML**: {:.1}% fewer characters\n",
+ aggregated.avg_lino_vs_xml
+ ));
+
+ md.push_str("\n## Detailed Results\n\n");
+ md.push_str("| Test Case | Description | Lino | JSON | YAML | XML | Lino vs JSON | Lino vs YAML | Lino vs XML |\n");
+ md.push_str("|-----------|-------------|------|------|------|-----|--------------|--------------|-------------|\n");
+
+ for result in results {
+ md.push_str(&format!(
+ "| {} | {} | {} | {} | {} | {} | {:.1}% | {:.1}% | {:.1}% |\n",
+ result.name,
+ result.description,
+ result.lino_chars,
+ result.json_chars,
+ result.yaml_chars,
+ result.xml_chars,
+ result.lino_vs_json,
+ result.lino_vs_yaml,
+ result.lino_vs_xml
+ ));
+ }
+
+ md.push_str("\n## Test Cases\n\n");
+
+ for result in results {
+ md.push_str(&format!("### {}\n\n", result.name));
+ md.push_str(&format!("{}\n\n", result.description));
+ md.push_str(&format!(
+ "| Format | Characters |\n|--------|------------|\n| Lino | {} |\n| JSON | {} |\n| YAML | {} |\n| XML | {} |\n\n",
+ result.lino_chars, result.json_chars, result.yaml_chars, result.xml_chars
+ ));
+ }
+
+ md.push_str("## Methodology\n\n");
+ md.push_str("This benchmark counts UTF-8 characters (not bytes) in equivalent data representations across all formats.\n");
+ md.push_str("The \"savings\" percentage indicates how much smaller the Lino representation is compared to each format.\n\n");
+ md.push_str("A positive savings percentage means Lino uses fewer characters.\n\n");
+ md.push_str("---\n\n");
+ md.push_str("*Generated automatically by links-notation-benchmark*\n");
+
+ md
+}
+
+fn generate_json_report(results: &[BenchmarkResult], aggregated: &AggregatedResults) -> String {
+ #[derive(Serialize)]
+ struct Report {
+ summary: AggregatedResults,
+ results: Vec,
+ }
+
+ let report = Report {
+ summary: aggregated.clone(),
+ results: results.to_vec(),
+ };
+
+ serde_json::to_string_pretty(&report).unwrap_or_default()
+}
+
+fn main() {
+ // Determine the data directory - try multiple possible locations
+ let possible_paths = [
+ "benchmarks/data", // Running from repo root
+ "../benchmarks/data", // Running from rust/
+ "../../benchmarks/data", // Running from rust/links-notation-benchmark/
+ "../../../benchmarks/data", // Running from rust/links-notation-benchmark/src/
+ ];
+
+ let data_dir = possible_paths
+ .iter()
+ .find(|p| Path::new(p).exists())
+ .map(Path::new);
+
+ let data_dir = match data_dir {
+ Some(path) => path,
+ None => {
+ eprintln!("Error: Could not find benchmarks/data directory");
+ eprintln!("Searched in: {:?}", possible_paths);
+ eprintln!("Please run from the repository root");
+ std::process::exit(1);
+ }
+ };
+
+ println!("Loading benchmark cases from {:?}...", data_dir);
+ let cases = load_benchmark_cases(data_dir);
+
+ if cases.is_empty() {
+ eprintln!("Error: No benchmark cases found in {:?}", data_dir);
+ std::process::exit(1);
+ }
+
+ println!("Running {} benchmark cases...\n", cases.len());
+
+ let results: Vec = cases.iter().map(run_benchmark).collect();
+ let aggregated = aggregate_results(&results);
+
+ // Print summary to console
+ println!("=== Links Notation Character Count Benchmark ===\n");
+ println!("Summary:");
+ println!(" Total Lino characters: {}", aggregated.total_lino_chars);
+ println!(" Total JSON characters: {}", aggregated.total_json_chars);
+ println!(" Total YAML characters: {}", aggregated.total_yaml_chars);
+ println!(" Total XML characters: {}", aggregated.total_xml_chars);
+ println!();
+ println!("Average savings with Lino:");
+ println!(
+ " vs JSON: {:.1}% fewer characters",
+ aggregated.avg_lino_vs_json
+ );
+ println!(
+ " vs YAML: {:.1}% fewer characters",
+ aggregated.avg_lino_vs_yaml
+ );
+ println!(
+ " vs XML: {:.1}% fewer characters",
+ aggregated.avg_lino_vs_xml
+ );
+ println!();
+
+ // Generate reports
+ let markdown_report = generate_markdown_report(&results, &aggregated);
+ let json_report = generate_json_report(&results, &aggregated);
+
+ // Determine output directory using the same search logic
+ let output_possible_paths = [
+ "benchmarks", // Running from repo root
+ "../benchmarks", // Running from rust/
+ "../../benchmarks", // Running from rust/links-notation-benchmark/
+ ];
+
+ let output_dir = output_possible_paths
+ .iter()
+ .find(|p| Path::new(p).exists())
+ .map(|p| Path::new(*p))
+ .unwrap_or(Path::new("."));
+
+ // Write markdown report
+ let md_path = output_dir.join("BENCHMARK_RESULTS.md");
+ if let Err(e) = fs::write(&md_path, &markdown_report) {
+ eprintln!(
+ "Warning: Could not write markdown report to {:?}: {}",
+ md_path, e
+ );
+ } else {
+ println!("Markdown report written to {:?}", md_path);
+ }
+
+ // Write JSON report
+ let json_path = output_dir.join("benchmark_results.json");
+ if let Err(e) = fs::write(&json_path, &json_report) {
+ eprintln!(
+ "Warning: Could not write JSON report to {:?}: {}",
+ json_path, e
+ );
+ } else {
+ println!("JSON report written to {:?}", json_path);
+ }
+
+ println!("\nBenchmark completed successfully!");
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_count_utf8_chars() {
+ assert_eq!(count_utf8_chars("hello"), 5);
+ assert_eq!(count_utf8_chars("hello world"), 11);
+ assert_eq!(count_utf8_chars(""), 0);
+ // Test with unicode characters
+ assert_eq!(count_utf8_chars("привет"), 6);
+ assert_eq!(count_utf8_chars("你好"), 2);
+ assert_eq!(count_utf8_chars("🎉"), 1);
+ }
+
+ #[test]
+ fn test_calculate_savings() {
+ assert_eq!(calculate_savings(100, 200), 50.0);
+ assert_eq!(calculate_savings(50, 100), 50.0);
+ assert_eq!(calculate_savings(100, 100), 0.0);
+ assert_eq!(calculate_savings(0, 0), 0.0);
+ }
+
+ #[test]
+ fn test_aggregate_results() {
+ let results = vec![
+ BenchmarkResult {
+ name: "test1".to_string(),
+ description: "Test 1".to_string(),
+ lino_chars: 100,
+ json_chars: 150,
+ yaml_chars: 120,
+ xml_chars: 200,
+ lino_vs_json: 33.33,
+ lino_vs_yaml: 16.67,
+ lino_vs_xml: 50.0,
+ },
+ BenchmarkResult {
+ name: "test2".to_string(),
+ description: "Test 2".to_string(),
+ lino_chars: 50,
+ json_chars: 80,
+ yaml_chars: 60,
+ xml_chars: 100,
+ lino_vs_json: 37.5,
+ lino_vs_yaml: 16.67,
+ lino_vs_xml: 50.0,
+ },
+ ];
+
+ let aggregated = aggregate_results(&results);
+ assert_eq!(aggregated.total_lino_chars, 150);
+ assert_eq!(aggregated.total_json_chars, 230);
+ assert_eq!(aggregated.total_yaml_chars, 180);
+ assert_eq!(aggregated.total_xml_chars, 300);
+ }
+}