diff --git a/src/Kusto.Language.AOT/KqlNative.cs b/src/Kusto.Language.AOT/KqlNative.cs new file mode 100644 index 00000000..7f05f6c8 --- /dev/null +++ b/src/Kusto.Language.AOT/KqlNative.cs @@ -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('}'); + } +} diff --git a/src/Kusto.Language.AOT/Kusto.Language.AOT.csproj b/src/Kusto.Language.AOT/Kusto.Language.AOT.csproj new file mode 100644 index 00000000..4e033a15 --- /dev/null +++ b/src/Kusto.Language.AOT/Kusto.Language.AOT.csproj @@ -0,0 +1,11 @@ + + + Library + net9.0 + true + true + + + + + diff --git a/src/kql_parse_sample.py b/src/kql_parse_sample.py new file mode 100644 index 00000000..8270e138 --- /dev/null +++ b/src/kql_parse_sample.py @@ -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))