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
124 changes: 124 additions & 0 deletions src/Kusto.Language.AOT/KqlNative.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Kusto.Language;

namespace Kusto.Language.AOT;

public static class KqlNative
{
[UnmanagedCallersOnly(EntryPoint = "kql_parse")]
public static IntPtr Parse(IntPtr queryPtr)
{
var query = Marshal.PtrToStringUTF8(queryPtr) ?? string.Empty;
var code = KustoCode.Parse(query);
var diagnostics = code.GetDiagnostics();
var result = diagnostics.Count == 0 ? "OK" : diagnostics[0].Message;
return Marshal.StringToCoTaskMemUTF8(result);
}

[UnmanagedCallersOnly(EntryPoint = "kql_get_diagnostics")]
public static IntPtr GetDiagnostics(IntPtr queryPtr)
{
var query = Marshal.PtrToStringUTF8(queryPtr) ?? string.Empty;
var code = KustoCode.Parse(query);
var diagnostics = code.GetDiagnostics();

if (diagnostics.Count == 0)
return Marshal.StringToCoTaskMemUTF8("[]");

var sb = new StringBuilder();
sb.Append('[');
for (int i = 0; i < diagnostics.Count; i++)
{
if (i > 0) sb.Append(',');
var d = diagnostics[i];
sb.Append($"{{\"start\":{d.Start},\"length\":{d.Length},\"severity\":\"{d.Severity}\",\"message\":\"{Escape(d.Message)}\"}}");
}
sb.Append(']');
return Marshal.StringToCoTaskMemUTF8(sb.ToString());
}

[UnmanagedCallersOnly(EntryPoint = "kql_get_syntax_tree")]
public static IntPtr GetSyntaxTree(IntPtr queryPtr)
{
var query = Marshal.PtrToStringUTF8(queryPtr) ?? string.Empty;
var code = KustoCode.Parse(query);

var sb = new StringBuilder();
WriteSyntaxNode(code.Syntax, sb, indent: 0);
return Marshal.StringToCoTaskMemUTF8(sb.ToString());
}

[UnmanagedCallersOnly(EntryPoint = "kql_get_syntax_json")]
public static IntPtr GetSyntaxJson(IntPtr queryPtr)
{
var query = Marshal.PtrToStringUTF8(queryPtr) ?? string.Empty;
var code = KustoCode.Parse(query);

var sb = new StringBuilder();
WriteSyntaxJson(code.Syntax, sb);
return Marshal.StringToCoTaskMemUTF8(sb.ToString());
}

[UnmanagedCallersOnly(EntryPoint = "kql_get_syntax_kind")]
public static IntPtr GetSyntaxKind(IntPtr queryPtr)
{
var query = Marshal.PtrToStringUTF8(queryPtr) ?? string.Empty;
var code = KustoCode.Parse(query);
return Marshal.StringToCoTaskMemUTF8(code.Kind.ToString());
}

[UnmanagedCallersOnly(EntryPoint = "kql_free")]
public static void Free(IntPtr ptr)
{
Marshal.FreeCoTaskMem(ptr);
}

private static void WriteSyntaxNode(Syntax.SyntaxElement node, StringBuilder sb, int indent)
{
sb.Append(' ', indent * 2);
sb.Append(node.Kind);
if (node.Width > 0 && node.ChildCount == 0)
sb.Append($" \"{Escape(node.ToString(Syntax.IncludeTrivia.Minimal))}\"");
sb.AppendLine();
for (int i = 0; i < node.ChildCount; i++)
{
var child = node.GetChild(i);
if (child != null)
WriteSyntaxNode(child, sb, indent + 1);
}
}

private static string Escape(string s) =>
s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");

private static void WriteSyntaxJson(Syntax.SyntaxElement node, StringBuilder sb)
{
sb.Append('{');
sb.Append($"\"kind\":\"{node.Kind}\"");
sb.Append($",\"start\":{node.TextStart}");
sb.Append($",\"length\":{node.Width}");

if (node.Width > 0 && node.ChildCount == 0)
sb.Append($",\"text\":\"{Escape(node.ToString(Syntax.IncludeTrivia.Minimal))}\"");

if (node.ChildCount > 0)
{
sb.Append(",\"children\":[");
bool first = true;
for (int i = 0; i < node.ChildCount; i++)
{
var child = node.GetChild(i);
if (child == null) continue;
if (!first) sb.Append(',');
first = false;
WriteSyntaxJson(child, sb);
}
sb.Append(']');
}

sb.Append('}');
}
}
11 changes: 11 additions & 0 deletions src/Kusto.Language.AOT/Kusto.Language.AOT.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net9.0</TargetFramework>
<PublishAot>true</PublishAot>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Kusto.Language\Kusto.Language.csproj" />
</ItemGroup>
</Project>
114 changes: 114 additions & 0 deletions src/kql_parse_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""
Sample: Using the Kusto.Language native AOT library from Python.

This script demonstrates calling the KQL parser through its C FFI surface
using ctypes. No .NET runtime is required — just the compiled native DLL.

Exports used:
kql_parse(query) -> "OK" or first error message
kql_get_diagnostics(query) -> JSON array of diagnostics
kql_get_syntax_tree(query) -> indented syntax tree dump
kql_get_syntax_kind(query) -> top-level syntax kind
kql_free(ptr) -> free memory returned by the above
"""

import ctypes
import json
import os
import sys

# ---------------------------------------------------------------------------
# Load the native DLL
# ---------------------------------------------------------------------------

DLL_NAME = "Kusto.Language.AOT.dll"
DLL_DIR = os.path.join(
os.path.dirname(__file__),
"Kusto.Language.AOT", "bin", "Release", "net9.0", "win-x64", "publish",
)
DLL_PATH = os.path.join(DLL_DIR, DLL_NAME)

if not os.path.exists(DLL_PATH):
sys.exit(f"DLL not found at {DLL_PATH}\n"
f"Run: dotnet publish src/Kusto.Language.AOT -r win-x64 -c Release /p:NativeLib=Shared")

lib = ctypes.CDLL(DLL_PATH)

# Declare signatures
lib.kql_parse.argtypes = [ctypes.c_char_p]
lib.kql_parse.restype = ctypes.c_void_p

lib.kql_get_diagnostics.argtypes = [ctypes.c_char_p]
lib.kql_get_diagnostics.restype = ctypes.c_void_p

lib.kql_get_syntax_tree.argtypes = [ctypes.c_char_p]
lib.kql_get_syntax_tree.restype = ctypes.c_void_p

lib.kql_get_syntax_kind.argtypes = [ctypes.c_char_p]
lib.kql_get_syntax_kind.restype = ctypes.c_void_p

lib.kql_free.argtypes = [ctypes.c_void_p]
lib.kql_free.restype = None


# ---------------------------------------------------------------------------
# Helper: call a string->string export, auto-freeing the result
# ---------------------------------------------------------------------------

def _call_str(func, query: str) -> str:
ptr = func(query.encode("utf-8"))
try:
return ctypes.string_at(ptr).decode("utf-8")
finally:
lib.kql_free(ptr)


def kql_parse(query: str) -> str:
"""Return 'OK' if the query is syntactically valid, or the first error."""
return _call_str(lib.kql_parse, query)


def kql_diagnostics(query: str) -> list[dict]:
"""Return a list of diagnostics (start, length, severity, message)."""
return json.loads(_call_str(lib.kql_get_diagnostics, query))


def kql_syntax_tree(query: str) -> str:
"""Return an indented dump of the syntax tree."""
return _call_str(lib.kql_get_syntax_tree, query)


def kql_syntax_kind(query: str) -> str:
"""Return the top-level syntax kind of the query."""
return _call_str(lib.kql_get_syntax_kind, query)


# ---------------------------------------------------------------------------
# Demo
# ---------------------------------------------------------------------------

if __name__ == "__main__":
queries = [
# Valid queries
"StormEvents | where State == 'TEXAS' | count",
"T | project a = a + b | where a > 10.0",
"T | summarize count() by bin(Timestamp, 1h)",
# Invalid query
"T | where",
]

for query in queries:
print(f"{'='*60}")
print(f"Query: {query}")
print(f"Kind: {kql_syntax_kind(query)}")

result = kql_parse(query)
if result == "OK":
print(f"Valid: ✓")
else:
print(f"Error: {result}")
for diag in kql_diagnostics(query):
print(f" [{diag['severity']}] offset {diag['start']}: {diag['message']}")

print(f"\nSyntax tree:")
print(kql_syntax_tree(query))
Loading