Json Schema for LLMs

This commit is contained in:
Michael Schimmel
2026-01-06 20:55:11 +01:00
parent 4618573d6c
commit 8a29cf7f74
8 changed files with 263 additions and 179 deletions
+5 -10
View File
@@ -98,7 +98,8 @@ type
function Run(const ANode: IAstNode; const Params: TArray<IIdentifierNode> = []; const Args: TArray<TDataValue> = []): TDataValue;
procedure Define(const Name: String; const AScript: IAstNode);
// UPDATED: Now supports Documentation
procedure Define(const Name: String; const AScript: IAstNode; const Doc: string = '');
property Environment: IEnvironment read FEnvironment;
property RootScope: IExecutionScope read GetRootScope;
@@ -261,11 +262,12 @@ begin
Result := FEnvironment.CreateEnvironment;
end;
procedure TAstEnvironment.Define(const Name: String; const AScript: IAstNode);
procedure TAstEnvironment.Define(const Name: String; const AScript: IAstNode; const Doc: string);
begin
var compiled := Compile(AScript);
var linked := Link(compiled);
RootScope.Define(Name, linked.Func([]), compiled.StaticType);
// Pass the documentation to the scope
RootScope.Define(Name, linked.Func([]), compiled.StaticType, Doc);
end;
function TAstEnvironment.ExpandMacros(const Node: IAstNode): IAstNode;
@@ -425,9 +427,6 @@ begin
if lg.HasErrors then
raise ECompilationFailed.Create(lg.GetEntries);
// Note: If types are Unknown but no errors were logged (edge case), we proceed.
// But Binder/TypeChecker logic ensures errors are logged for Unknowns that matter.
var specialized := Specialize(typedNode);
Result := specialized.AsLambdaExpression;
@@ -471,7 +470,6 @@ begin
var cExecutionStrategy := FExecutionStrategy;
// The Glue: This callback performs the full compilation pipeline for a macro argument expression.
// It binds the node to the layout of the temporary macro scope and then executes it.
var macroEvaluator: TMacroEvaluatorProc :=
function(const Scope: IExecutionScope; const ANode: IAstNode): TDataValue
var
@@ -485,15 +483,12 @@ begin
tempLog := TCompilerLog.Create;
// 1. Binding
// Note: Scope is dynamic (from TMacroExpander), so Descriptor.Layout describes current variables.
boundSubAst := TAstBinder.Bind(Scope.Descriptor.Layout, ANode, tmpLayout, tempLog);
// Macro execution is strictly fail-fast. If we can't bind arguments, we can't run the macro.
if tempLog.HasErrors then
raise EMacroException.Create('Macro Argument Error: ' + tempLog.GetEntries[0].Message);
// 2. Scope Matching
// Create a child scope to ensure correct depth resolution (Binder sees Layout at depth 0 relative to itself)
scratchScope := TScope.CreateScope(Scope, nil, nil);
// 3. Execution
+109 -116
View File
@@ -14,23 +14,17 @@ uses
Myc.Data.Value,
Myc.Ast.Nodes,
Myc.Ast.Attributes,
Myc.Ast.RTL,
Myc.Ast.RTL.Core,
Myc.Ast.Scope,
Myc.Ast.Types,
Myc.Ast.Script,
Myc.Ast.Json;
Myc.Ast.Json,
Myc.Ast;
type
{ Scans AST, RTL and Scope metadata to generate a context-aware LLM system prompt. }
TAstSchema = class
strict private
type
TRtlMeta = record
Doc: string;
Signatures: TArray<string>;
end;
TFieldInfo = record
Idx: Integer;
Name: string;
@@ -39,18 +33,17 @@ type
class function GetTsType(Kind: TFieldKind): string; static;
class function TypeToTs(const AType: IStaticType): string; static;
class function GetRtlMetadata: TDictionary<string, TRtlMeta>; static;
public
// Generates the TypeScript definition for AST nodes
// Generates the TypeScript definition for AST nodes using RTTI on the Node Interfaces
class function GenerateTypeScriptDefinition: string; static;
// Unified section: Lists only symbols available in the current scope with their docs/types
// Unified section: Lists symbols available in the given scope with their docs and types.
class function GenerateAvailableSymbols(const AScope: IExecutionScope): string; static;
// Documentation for all RTL functions (used as fallback or for general prompts)
// Generates documentation for the standard library by creating a temporary scope.
class function GenerateAllRtlDocumentation: string; static;
// Detailed generation rules
// Detailed generation rules for the LLM
class function GenerateGenerationRules: string; static;
// The final consolidated system prompt for the AI
@@ -81,37 +74,106 @@ begin
exit('any');
case AType.Kind of
stUnknown: Result := 'any';
stVoid: Result := 'void';
stOrdinal, stFloat: Result := 'number';
stBoolean: Result := 'boolean';
stDateTime: Result := 'datetime';
stText: Result := 'string';
stKeyword: Result := 'keyword';
stSeries: Result := Format('Series<%s>', [TypeToTs(AType.AsSeries.ElementType)]);
stRecord:
begin
var def := AType.AsRecord.Definition;
var fields := TList<string>.Create;
try
for var i := 0 to def.Count - 1 do
fields.Add(Format('%s: %s', [def.Keywords[i].Name, def[i].ToString.ToLower]));
begin
var k := def.Keywords[i].Name;
var tStr: string;
case def[i] of
TScalar.TKind.Ordinal, TScalar.TKind.Float: tStr := 'number';
TScalar.TKind.Boolean: tStr := 'boolean';
TScalar.TKind.DateTime: tStr := 'datetime';
TScalar.TKind.Keyword: tStr := 'keyword';
else
tStr := 'any';
end;
fields.Add(Format('%s: %s', [k, tStr]));
end;
Result := '{ ' + string.Join(', ', fields.ToArray) + ' }';
finally
fields.Free;
end;
end;
stGenericRecord:
begin
var def := AType.AsGenericRecord.GenericDefinition;
var fields := TList<string>.Create;
try
for var i := 0 to def.Count - 1 do
fields.Add(Format('%s: %s', [def.Keywords[i].Name, TypeToTs(def[i])]));
Result := '{ ' + string.Join(', ', fields.ToArray) + ' }';
finally
fields.Free;
end;
end;
stRecordSeries: Result := 'Stream<' + TypeToTs(TTypes.CreateRecord(AType.AsRecord.Definition)) + '>';
stTuple:
begin
var elems := AType.AsTuple.Elements;
var elemStrings := TList<string>.Create;
try
for var t in elems do
elemStrings.Add(TypeToTs(t));
Result := '[' + string.Join(', ', elemStrings.ToArray) + ']';
finally
elemStrings.Free;
end;
end;
stVector: Result := TypeToTs(AType.AsVector.ElementType) + '[]';
stMatrix:
begin
var dims := AType.AsMatrix.Dimensions;
Result := TypeToTs(AType.AsMatrix.ElementType);
for var k := 0 to High(dims) do
Result := Result + '[]';
end;
stMethod:
begin
if Length(AType.AsMethod.Signatures) = 0 then
var signatures := AType.AsMethod.Signatures;
if Length(signatures) = 0 then
exit('function');
var sig := AType.AsMethod.Signatures[0];
var args := TList<string>.Create;
var sigStrings := TList<string>.Create;
try
for var p in sig.ParamTypes do
args.Add(TypeToTs(p));
Result := Format('(%s) => %s', [string.Join(', ', args.ToArray), TypeToTs(sig.ReturnType)]);
for var sig in signatures do
begin
var args := TList<string>.Create;
try
for var p in sig.ParamTypes do
args.Add(TypeToTs(p));
var s := Format('(%s) => %s', [string.Join(', ', args.ToArray), TypeToTs(sig.ReturnType)]);
// Deduplicate: If multiple RTL overloads map to the same TS signature, only show it once.
if not sigStrings.Contains(s) then
sigStrings.Add(s);
finally
args.Free;
end;
end;
Result := string.Join(' | ', sigStrings.ToArray);
finally
args.Free;
sigStrings.Free;
end;
end;
else
@@ -122,56 +184,6 @@ begin
Result := Result + ' | null';
end;
class function TAstSchema.GetRtlMetadata: TDictionary<string, TRtlMeta>;
var
ctx: TRttiContext;
typ: TRttiType;
meth: TRttiMethod;
attr: TCustomAttribute;
meta: TRtlMeta;
rtlName: string;
sigs: TList<string>;
begin
Result := TDictionary<string, TRtlMeta>.Create;
sigs := TList<string>.Create;
try
typ := ctx.GetType(TypeInfo(TRtlFunctions));
for meth in typ.GetMethods do
begin
rtlName := '';
meta.Doc := '';
sigs.Clear;
for attr in meth.GetAttributes do
begin
if attr is AstDocAttribute then
meta.Doc := AstDocAttribute(attr).Description;
if attr is TRtlExportAttribute then
rtlName := TRtlExportAttribute(attr).Name;
if attr is AstSignatureAttribute then
sigs.Add(AstSignatureAttribute(attr).Signature);
end;
if rtlName <> '' then
begin
meta.Signatures := sigs.ToArray;
if Result.ContainsKey(rtlName) then
begin
var existing := Result[rtlName];
var combinedSigs := TList<string>.Create;
combinedSigs.AddRange(existing.Signatures);
combinedSigs.AddRange(meta.Signatures);
existing.Signatures := combinedSigs.ToArray;
combinedSigs.Free;
Result[rtlName] := existing;
end
else
Result.Add(rtlName, meta);
end;
end;
finally
sigs.Free;
end;
end;
class function TAstSchema.GenerateTypeScriptDefinition: string;
var
ctx: TRttiContext;
@@ -215,6 +227,7 @@ begin
if exampleList = nil then
exampleList := TList<string>.Create;
try
// Parse example script to ensure valid JSON output in documentation
var node := TAstScript.Parse(AstScriptExampleAttribute(attr).Script);
var jsonVal := jsonConv.Serialize(node);
try
@@ -224,7 +237,7 @@ begin
end;
except
on E: Exception do
exampleList.Add('/* Error in example */');
exampleList.Add('/* Error in example: ' + E.Message + ' */');
end;
end;
if attr is AstFieldAttribute then
@@ -309,87 +322,67 @@ end;
class function TAstSchema.GenerateAvailableSymbols(const AScope: IExecutionScope): string;
var
sb: TStringBuilder;
rtlMeta: TDictionary<string, TRtlMeta>;
layout: IScopeLayout;
descriptor: IScopeDescriptor;
symbols: TArray<string>;
name: string;
meta: TRtlMeta;
name, doc: string;
typ: IStaticType;
slot: Integer;
begin
if not Assigned(AScope) then
exit('');
rtlMeta := GetRtlMetadata;
sb := TStringBuilder.Create;
try
layout := AScope.Descriptor.Layout;
descriptor := AScope.Descriptor;
layout := descriptor.Layout;
symbols := layout.GetSymbols;
TArray.Sort<string>(symbols);
sb.AppendLine('### 3. Available Symbols (Environment & Library)');
sb.AppendLine('### Available Symbols (Environment & Library)');
sb.AppendLine('The following symbols are currently defined and can be used in your script:');
sb.AppendLine;
for name in symbols do
begin
// Filter internal compiler symbols
if name.StartsWith('<') or name.EndsWith('>') then
continue;
slot := layout.FindSlot(name);
typ := descriptor.GetSymbolType(slot);
doc := layout.GetSymbolDoc(name);
sb.Append(Format('- `%s`', [name]));
if rtlMeta.TryGetValue(name, meta) then
begin
if Length(meta.Signatures) > 0 then
sb.Append(': ' + string.Join(' | ', meta.Signatures));
if meta.Doc <> '' then
sb.Append(' — ' + meta.Doc);
end
else
begin
typ := AScope.Descriptor.GetSymbolType(layout.FindSlot(name));
if (typ <> nil) then
sb.Append(': ' + TypeToTs(typ));
end;
if doc <> '' then
sb.Append(' — ' + doc);
sb.AppendLine;
end;
Result := sb.ToString;
finally
sb.Free;
rtlMeta.Free;
end;
end;
class function TAstSchema.GenerateAllRtlDocumentation: string;
var
rtlMeta: TDictionary<string, TRtlMeta>;
sb: TStringBuilder;
pair: TPair<string, TRtlMeta>;
begin
rtlMeta := GetRtlMetadata;
sb := TStringBuilder.Create;
try
sb.AppendLine('### 3. Runtime Library (Global Functions)');
sb.AppendLine;
for pair in rtlMeta do
begin
sb.Append(Format('- `%s`', [pair.Key]));
if Length(pair.Value.Signatures) > 0 then
sb.Append(': ' + string.Join(' | ', pair.Value.Signatures));
if pair.Value.Doc <> '' then
sb.Append(' — ' + pair.Value.Doc);
sb.AppendLine;
end;
Result := sb.ToString;
finally
sb.Free;
rtlMeta.Free;
end;
// Create a temporary root scope.
// The True parameter triggers the registration of all standard libraries (RTL).
// This populates the scope with function definitions and their documentation strings.
var tempScope := TAst.CreateScope(nil, nil, True);
Result := GenerateAvailableSymbols(tempScope);
end;
class function TAstSchema.GenerateGenerationRules: string;
begin
Result :=
'''
**⚠️ CRITICAL GENERATION RULES:**
**CRITICAL GENERATION RULES:**
1. **NO JSON OBJECTS:** Do not use `{}`. Every node MUST be a JSON Array `["Tag", Arg1, Arg2]`.
2. **EXPLICIT TUPLES:** Fields defined as `Tuple` MUST be wrapped in `["Tuple", [...]]`.
@@ -412,12 +405,12 @@ begin
You are a compiler frontend for a domain-specific scripting language.
Your task is to generate a valid Abstract Syntax Tree (AST) in a specific Compact JSON format.
### 1. AST Schema (TypeScript Definition)
### AST Schema (TypeScript Definition)
'''
+ sLineBreak
+ GenerateTypeScriptDefinition
+ sLineBreak
+ '### 2. Constraints & Rules'
+ '### Constraints & Rules'
+ sLineBreak
+ GenerateGenerationRules
+ sLineBreak;
+7 -8
View File
@@ -3,12 +3,17 @@ unit Myc.Ast.RTL.Core;
interface
uses
system.sysutils,
System.SysUtils,
System.Math,
System.DateUtils,
System.Generics.Collections,
Myc.Utils,
Myc.Data.Scalar,
Myc.Data.Value,
Myc.Ast.RTL,
Myc.Ast.Attributes;
Myc.Ast.Attributes,
Myc.Data.Keyword,
Myc.Data.Series;
type
{ Contains the native implementations for the Myc standard library. }
@@ -325,12 +330,6 @@ type
implementation
uses
System.Generics.Collections,
System.Math,
System.Dateutils,
Myc.Data.Decimal;
{ TRtlFunctions - Operator Implementations }
class function TRtlFunctions.Add(const Args: TArray<TDataValue>): TDataValue;
+6 -5
View File
@@ -56,11 +56,13 @@ type
// --- Factories ---
// Registers a factory function. T must be registered via RegisterType<T> first.
// UPDATED: Now supports documentation parameter.
class procedure RegisterFactory<T: IInterface>(
const Scope: IExecutionScope;
const FactoryName: string;
const FactoryArgs: TArray<IStaticType>;
const FactoryDelegate: TInterfaceFactoryDelegate<T>
const FactoryDelegate: TInterfaceFactoryDelegate<T>;
const Doc: string = ''
);
// --- Resolution ---
@@ -176,8 +178,6 @@ begin
var rIntf := rType as TRttiInterfaceType;
// If the Interface has an "Invoke" method and is marked as reference-to (or is generic TFunc/TProc),
// map it directly to stMethod.
// Note: RTTI for "IsReferenceTo" is not always directly available,
// but we can check if there is exactly one method named "Invoke".
var method := rIntf.GetMethod('Invoke');
if Assigned(method) and (Length(rIntf.GetMethods) = 1) then
begin
@@ -496,7 +496,8 @@ class procedure TRtlTypeRegistry.RegisterFactory<T>(
const Scope: IExecutionScope;
const FactoryName: string;
const FactoryArgs: TArray<IStaticType>;
const FactoryDelegate: TInterfaceFactoryDelegate<T>
const FactoryDelegate: TInterfaceFactoryDelegate<T>;
const Doc: string
);
var
staticType: IStaticType;
@@ -518,7 +519,7 @@ begin
end;
var factorySig := TTypes.CreateMethod(FactoryArgs, staticType);
Scope.Define(FactoryName, TDataValue(factoryWrapper), factorySig);
Scope.Define(FactoryName, TDataValue(factoryWrapper), factorySig, Doc);
end;
end.
+17 -11
View File
@@ -60,6 +60,7 @@ type
public
DynamicWrapper: TDataValue.TFunc;
Signatures: TList<IMethodSignature>;
Doc: string; // Documentation string from AstDoc attribute
constructor Create;
destructor Destroy; override;
end;
@@ -145,6 +146,7 @@ uses
System.TypInfo,
System.Hash,
System.StrUtils,
Myc.Ast.Attributes,
Myc.Ast.RTL.Core;
type
@@ -228,6 +230,7 @@ begin
inherited Create;
Signatures := TList<IMethodSignature>.Create;
DynamicWrapper := nil;
Doc := '';
end;
destructor TRtlFunctionInfo.Destroy;
@@ -262,6 +265,7 @@ var
rttiParams: TArray<TRttiParameter>;
isDynamic: Boolean;
i: Integer;
docString: string;
begin
// 1. Create global caches
FStaticBootstrap := TStaticBootstrapCache.Create(TStaticSignatureKeyComparer.Create);
@@ -278,13 +282,14 @@ begin
continue;
exportAttr := nil;
docString := '';
for attribute in method.GetAttributes do
begin
if attribute is TRtlExportAttribute then
begin
exportAttr := attribute as TRtlExportAttribute;
break;
end;
exportAttr := attribute as TRtlExportAttribute
else if attribute is AstDocAttribute then
docString := AstDocAttribute(attribute).Description;
end;
if not Assigned(exportAttr) then
@@ -298,6 +303,10 @@ begin
FStaticFuncMap.Add(rtlName, funcInfo);
end;
// Update documentation (if not already set or override)
if (funcInfo.Doc = '') and (docString <> '') then
funcInfo.Doc := docString;
// Check Signature Type (Dynamic vs Static)
rttiParams := method.GetParameters;
isDynamic :=
@@ -346,10 +355,6 @@ begin
if (not Assigned(funcInfo.DynamicWrapper)) and (Length(argTypes) = 1) and (argTypes[0].Kind = stUnknown) then
begin
funcInfo.DynamicWrapper := wrapper;
end
else if (not Assigned(funcInfo.DynamicWrapper)) and (Length(argTypes) = 2) and (argTypes[0].Kind = stUnknown) then
begin
// Potentially handle 2-arg scalar wrappers here too if needed
end;
end
else
@@ -492,7 +497,8 @@ begin
var wrapper := funcInfo.DynamicWrapper; // Default is Void
AScope.Define(rtlName, wrapper, staticType);
// Pass documentation to scope definition
AScope.Define(rtlName, wrapper, staticType, funcInfo.Doc);
end;
end;
@@ -766,8 +772,8 @@ end;
procedure RegisterRtlFunctions(const AScope: IExecutionScope);
begin
AScope.Define('true', TDataValue(TScalar.FromBoolean(True)), TTypes.Boolean);
AScope.Define('false', TDataValue(TScalar.FromBoolean(False)), TTypes.Boolean);
AScope.Define('true', TDataValue(TScalar.FromBoolean(True)), TTypes.Boolean, 'Boolean true.');
AScope.Define('false', TDataValue(TScalar.FromBoolean(False)), TTypes.Boolean, 'Boolean false.');
TRtlRegistry.RegisterAll(AScope);
end;
+108 -20
View File
@@ -59,6 +59,7 @@ type
{$endregion}
function FindSlot(const Name: string): Integer;
function GetSymbolDoc(const Name: string): string;
// Returns all defined symbols in this layout (for debugging/reflection)
function GetSymbols: TArray<string>;
@@ -69,7 +70,7 @@ type
// 2. The Builder (Mutable).
IScopeBuilder = interface(IScopeLayout)
function Define(const Name: string): Integer;
function Define(const Name: string; const Doc: string = ''): Integer;
function Build: IScopeLayout;
end;
@@ -95,7 +96,13 @@ type
procedure SetValues(const Address: TResolvedAddress; const Value: TDataValue);
{$endregion}
function Define(const Name: string; const Value: TDataValue; const AStaticType: IStaticType = nil): TResolvedAddress;
function Define(
const Name: string;
const Value: TDataValue;
const AStaticType: IStaticType = nil;
const ADoc: string = ''
): TResolvedAddress;
procedure DefineBoxed(SlotIndex: Integer; const Value: TDataValue);
function Capture(const Address: TResolvedAddress): IValueCell;
@@ -129,18 +136,25 @@ uses
System.SyncObjs;
type
TSymbolMeta = record
Slot: Integer;
Doc: string;
constructor Create(ASlot: Integer; const ADoc: string);
end;
// --- Concrete Layout (Immutable) ---
TScopeLayout = class(TInterfacedObject, IScopeLayout)
private
FParent: IScopeLayout;
FMap: TDictionary<string, Integer>;
FSlotCount: Integer; // Explicit slot count
FMap: TDictionary<string, TSymbolMeta>;
FSlotCount: Integer;
function GetParent: IScopeLayout;
function GetSlotCount: Integer;
public
constructor Create(const AParent: IScopeLayout; AMap: TDictionary<string, Integer>; ASlotCount: Integer);
constructor Create(const AParent: IScopeLayout; AMap: TDictionary<string, TSymbolMeta>; ASlotCount: Integer);
destructor Destroy; override;
function FindSlot(const Name: string): Integer;
function GetSymbolDoc(const Name: string): string;
function GetSymbols: TArray<string>;
end;
@@ -148,16 +162,17 @@ type
TScopeBuilder = class(TInterfacedObject, IScopeBuilder, IScopeLayout)
private
FParentLayout: IScopeLayout;
FMap: TDictionary<string, Integer>;
FNextSlot: Integer; // Counter for sequential slot allocation
FMap: TDictionary<string, TSymbolMeta>;
FNextSlot: Integer;
function GetParent: IScopeLayout;
function GetSlotCount: Integer;
public
constructor Create(const AParentLayout: IScopeLayout);
destructor Destroy; override;
function Define(const Name: string): Integer;
function Define(const Name: string; const Doc: string): Integer;
function FindSlot(const Name: string): Integer;
function GetSymbolDoc(const Name: string): string;
function GetSymbols: TArray<string>;
function Build: IScopeLayout;
end;
@@ -202,6 +217,7 @@ type
public
constructor Create(AOwner: TExecutionScope);
function FindSlot(const Name: string): Integer;
function GetSymbolDoc(const Name: string): string;
function GetSymbols: TArray<string>;
end;
@@ -224,9 +240,12 @@ type
FSlotTypes: TArray<IStaticType>;
FValues: TArray<TScopeItem>;
FCapturedUpvalues: TArray<IValueCell>;
// Dynamic Name Mapping
FNames: TDictionary<string, Integer>;
FNameStrings: TList<string>;
FNameToIndex: TDictionary<Integer, Integer>;
FDocs: TDictionary<string, string>; // Stores docs for dynamic symbols
procedure DumpScope(const ABuilder: TStringBuilder; AIndent: Integer);
procedure NeedNameToIndex;
@@ -242,7 +261,12 @@ type
function GetNameID(const Name: String): Integer;
function Resolve(const Name: string): TResolvedAddress;
function Dump: string;
function Define(const Name: string; const Value: TDataValue; const AStaticType: IStaticType = nil): TResolvedAddress;
function Define(
const Name: string;
const Value: TDataValue;
const AStaticType: IStaticType = nil;
const ADoc: string = ''
): TResolvedAddress;
procedure DefineBoxed(SlotIndex: Integer; const Value: TDataValue);
function Capture(const Address: TResolvedAddress): IValueCell;
@@ -300,9 +324,17 @@ begin
Dest.StaticType := TTypes.Unknown;
end;
{ TSymbolMeta }
constructor TSymbolMeta.Create(ASlot: Integer; const ADoc: string);
begin
Slot := ASlot;
Doc := ADoc;
end;
{ TScopeLayout }
constructor TScopeLayout.Create(const AParent: IScopeLayout; AMap: TDictionary<string, Integer>; ASlotCount: Integer);
constructor TScopeLayout.Create(const AParent: IScopeLayout; AMap: TDictionary<string, TSymbolMeta>; ASlotCount: Integer);
begin
inherited Create;
FParent := AParent;
@@ -317,11 +349,25 @@ begin
end;
function TScopeLayout.FindSlot(const Name: string): Integer;
var
meta: TSymbolMeta;
begin
if not FMap.TryGetValue(Name, Result) then
if FMap.TryGetValue(Name, meta) then
Result := meta.Slot
else
Result := -1;
end;
function TScopeLayout.GetSymbolDoc(const Name: string): string;
var
meta: TSymbolMeta;
begin
if FMap.TryGetValue(Name, meta) then
Result := meta.Doc
else
Result := '';
end;
function TScopeLayout.GetSymbols: TArray<string>;
begin
Result := FMap.Keys.ToArray;
@@ -343,7 +389,7 @@ constructor TScopeBuilder.Create(const AParentLayout: IScopeLayout);
begin
inherited Create;
FParentLayout := AParentLayout;
FMap := TDictionary<string, Integer>.Create;
FMap := TDictionary<string, TSymbolMeta>.Create;
FNextSlot := 0;
end;
@@ -353,25 +399,38 @@ begin
inherited;
end;
function TScopeBuilder.Define(const Name: string): Integer;
function TScopeBuilder.Define(const Name: string; const Doc: string): Integer;
begin
// Rule: Shadowing / Redefinition in the same scope is forbidden.
// The Client (Binder) must ensure this name is not already taken before calling Define.
Assert(not FMap.ContainsKey(Name), 'Scope Error: Variable "' + Name + '" is already defined in this scope builder.');
// Allocate a new slot
Result := FNextSlot;
Inc(FNextSlot);
FMap.Add(Name, Result);
FMap.Add(Name, TSymbolMeta.Create(Result, Doc));
end;
function TScopeBuilder.FindSlot(const Name: string): Integer;
var
meta: TSymbolMeta;
begin
if not FMap.TryGetValue(Name, Result) then
if FMap.TryGetValue(Name, meta) then
Result := meta.Slot
else
Result := -1;
end;
function TScopeBuilder.GetSymbolDoc(const Name: string): string;
var
meta: TSymbolMeta;
begin
if FMap.TryGetValue(Name, meta) then
Result := meta.Doc
else
Result := '';
end;
function TScopeBuilder.GetSymbols: TArray<string>;
begin
Result := FMap.Keys.ToArray;
@@ -380,7 +439,7 @@ end;
function TScopeBuilder.Build: IScopeLayout;
begin
// Pass FNextSlot as the total slot count
Result := TScopeLayout.Create(FParentLayout, TDictionary<string, Integer>.Create(FMap), FNextSlot);
Result := TScopeLayout.Create(FParentLayout, TDictionary<string, TSymbolMeta>.Create(FMap), FNextSlot);
end;
function TScopeBuilder.GetParent: IScopeLayout;
@@ -436,6 +495,12 @@ begin
Result := -1;
end;
function TExecutionScope.TDynamicLayout.GetSymbolDoc(const Name: string): string;
begin
if (FOwner.FDocs = nil) or (not FOwner.FDocs.TryGetValue(Name, Result)) then
Result := '';
end;
function TExecutionScope.TDynamicLayout.GetSymbols: TArray<string>;
begin
Result := FOwner.FNames.Keys.ToArray;
@@ -532,6 +597,8 @@ begin
FNameStrings := TList<string>.Create;
end;
FDocs := nil; // Initialized on demand in Define
if Assigned(FLayoutIntf) then
SetLength(FValues, FLayoutIntf.SlotCount);
end;
@@ -544,6 +611,7 @@ begin
FNameStrings.Free;
FNames.Free;
end;
FDocs.Free;
inherited Destroy;
end;
@@ -564,6 +632,13 @@ procedure TExecutionScope.Clear;
begin
FValues := [];
FreeAndNil(FNameToIndex);
// Docs are tied to Names which are persistent/shared, so FDocs should clear if this is the owner?
// Actually FDocs is local to this scope instance if we define dynamic vars.
// If we clear values, we should probably clear doc mappings too if they are tied to those values.
// But Resolve uses FNames/FNameToIndex. If we clear FNameToIndex, existing name lookups fail.
// FDocs is separate. Let's clear it too.
if FDocs <> nil then
FDocs.Clear;
end;
function TExecutionScope.Capture(const Address: TResolvedAddress): IValueCell;
@@ -602,7 +677,12 @@ begin
end;
end;
function TExecutionScope.Define(const Name: string; const Value: TDataValue; const AStaticType: IStaticType): TResolvedAddress;
function TExecutionScope.Define(
const Name: string;
const Value: TDataValue;
const AStaticType: IStaticType;
const ADoc: string
): TResolvedAddress;
var
id: Integer;
index: Integer;
@@ -630,6 +710,14 @@ begin
end;
FNameToIndex.Add(id, index);
if ADoc <> '' then
begin
if FDocs = nil then
FDocs := TDictionary<string, string>.Create;
FDocs.AddOrSetValue(Name, ADoc);
end;
Result.Kind := akLocalOrParent;
Result.ScopeDepth := 0;
Result.SlotIndex := index;
@@ -755,7 +843,7 @@ begin
begin
var map := (FStaticDescriptor.Layout as TScopeLayout).FMap;
for var item in map do
FNameToIndex.AddOrSetValue(GetNameId(item.Key), item.Value);
FNameToIndex.AddOrSetValue(GetNameId(item.Key), item.Value.Slot);
end;
end;
end;
@@ -843,7 +931,7 @@ end;
class function TScope.CreateRootLayout: IScopeLayout;
begin
Result := TScopeLayout.Create(nil, TDictionary<string, Integer>.Create, 0);
Result := TScopeLayout.Create(nil, TDictionary<string, TSymbolMeta>.Create, 0);
end;
end.
+4 -5
View File
@@ -16,7 +16,8 @@ uses
type
TSymProc = reference to procedure(const Symbol: String; Amount: Integer);
// Das Interface, das im AST-Skript verfügbar sein wird.
// Das Interface, das im AST-Skript als Record-Struktur verfügbar sein wird.
// Die Methodennamen dienen als Dokumentation.
IMycBroker = interface
['{D8E9F1A2-B3C4-4D5E-F6A7-B8C9D0E1F2A3}']
@@ -27,7 +28,6 @@ type
function GetPosition(const Symbol: string): Integer;
// Iteriert über alle Positionen und ruft für jede den Callback auf.
// Benötigt Registrierung von TProc<string>.
procedure ListPositions(const Callback: TSymProc);
property Cash: Double read GetCash;
@@ -138,8 +138,6 @@ end;
procedure RegisterBroker(const Scope: IExecutionScope);
begin
// 1. Registrierung des Callback-Typs.
// Dies ermöglicht dem TypeRegistry-Marshaller, die Signatur von Invoke(string, integer) zu analysieren
// und AST-Lambdas (TDataValue.TFunc) in native TProc<string> zu wrappen.
TRtlTypeRegistry.RegisterType<TSymProc>;
// 2. Registrierung des Broker-Interfaces
@@ -152,7 +150,8 @@ begin
Scope,
'create-broker',
argTypes,
function(const Args: TArray<TDataValue>): IMycBroker begin Result := TMockBroker.Create(Args[0].AsScalar.Value.AsDouble); end
function(const Args: TArray<TDataValue>): IMycBroker begin Result := TMockBroker.Create(Args[0].AsScalar.Value.AsDouble); end,
'Creates a new mock broker instance with the specified initial cash.' // Doc für das Symbol selbst
);
end;
+7 -4
View File
@@ -85,7 +85,8 @@ begin
// Für den Compiler ist es eine RecordSeries (Typ), zur Laufzeit ein Stream (Wert)
StaticType := TTypes.CreateRecordSeries(Def);
Scope.Define(VarName, Stream, StaticType);
// UPDATED: Dokumentation für die Stream-Variable hinzufügen
Scope.Define(VarName, Stream, StaticType, 'Financial OHLC Data Stream (Time, Open, High, Low, Close, Vol).');
end;
procedure TFinanceSimulator.AddRandomCandle(const Scope: IExecutionScope; const VarName: string);
@@ -169,7 +170,8 @@ begin
Sim.CreateOHLCSeries(Scope, Args[0].AsText);
Result := TDataValue.Void;
end,
Sig
Sig,
'Creates a new OHLC data stream with the specified name.' // UPDATED: Doc
);
Scope.Define(
@@ -181,10 +183,11 @@ begin
Sim.AddRandomCandle(Scope, Args[0].AsText);
Result := TDataValue.Void;
end,
Sig
Sig,
'Simulates and pushes a new 5-minute candle to the specified stream.' // UPDATED: Doc
);
// Some Demo-Stream
// Some Demo-Stream (This will now also be documented in the scope)
Sim.CreateOHLCSeries(Scope, 'btc');
end;