Files
PineSript/PineScript/DependencyAnalyzer.cs
2026-04-25 17:32:49 +09:00

132 lines
5.7 KiB
C#

namespace PineScriptV6
{
/// <summary>
/// Pine Script 문(Statement) 목록을 정적 분석하여
/// Pass1(지표 사전계산) / Pass2(상태머신)로 자동 분류합니다.
///
/// Pass1 조건: 순수 AssignStmt이며, 참조 변수가 모두
/// OHLCV 또는 이전에 분류된 다른 Pass1 변수여야 합니다.
/// Pass2 조건: var 선언, := 재할당, if 블록, ExprStmt(side-effect),
/// 또는 상태 변수를 참조하는 AssignStmt.
///
/// Pine Script 문은 이미 위상 정렬된 순서로 작성되므로
/// 별도의 위상 정렬 없이 선형 스캔으로 분류할 수 있습니다.
/// </summary>
public static class DependencyAnalyzer
{
// ── OHLCV 및 내장 변수 (항상 Pass1에서 접근 가능) ─────────────
private static readonly HashSet<string> _builtins =
new(StringComparer.OrdinalIgnoreCase)
{
"close", "open", "high", "low", "volume",
"bar_index", "time"
};
// ── 표현식 내 무시할 키워드/리터럴 ───────────────────────────
private static readonly HashSet<string> _keywords =
new(StringComparer.OrdinalIgnoreCase)
{
"true", "false", "na", "and", "or", "not"
};
// ════════════════════════════════════════════════════════════════
// 공개 API
// ════════════════════════════════════════════════════════════════
/// <summary>
/// 분석 결과를 담는 레코드.
/// Pass1Stmts → 증분 지표 계산 (매 봉, 순서 유지)
/// Pass2Stmts → 상태 머신 (매 봉, 순서 유지)
/// </summary>
public record ExecutionPlan(
List<PineStmt> Pass1Stmts,
List<PineStmt> Pass2Stmts,
HashSet<string> Pass1Vars,
HashSet<string> StateVars
);
public static ExecutionPlan Analyze(List<PineStmt> stmts)
{
// ── Step 1: 상태 변수 수집 ──────────────────────────────────
var stateVars = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
CollectStateVars(stmts, stateVars);
// ── Step 2: 순서대로 분류 ─────────────────────────────────
var pass1Vars = new HashSet<string>(_builtins, StringComparer.OrdinalIgnoreCase);
var pass1 = new List<PineStmt>();
var pass2 = new List<PineStmt>();
foreach (var stmt in stmts)
{
if (TryPass1(stmt, stateVars, pass1Vars, out string? varName))
{
pass1.Add(stmt);
if (varName != null) pass1Vars.Add(varName);
}
else
{
pass2.Add(stmt);
}
}
return new ExecutionPlan(pass1, pass2, pass1Vars, stateVars);
}
// ════════════════════════════════════════════════════════════════
// 내부 구현
// ════════════════════════════════════════════════════════════════
/// <summary>모든 var 선언과 := 재할당 변수를 재귀적으로 수집합니다.</summary>
private static void CollectStateVars(List<PineStmt> stmts, HashSet<string> result)
{
foreach (var stmt in stmts)
{
switch (stmt)
{
case VarDeclStmt vd:
result.Add(vd.Name);
break;
case ReassignStmt r:
result.Add(r.Name);
break;
case IfStmt ifs:
CollectStateVars(ifs.ThenBody, result);
if (ifs.ElseBody != null)
CollectStateVars(ifs.ElseBody, result);
break;
}
}
}
/// <summary>
/// stmt가 Pass1 조건을 만족하면 true 반환.
/// varName에는 이 문이 생성하는 변수명이 들어옵니다 (없으면 null).
/// </summary>
private static bool TryPass1(
PineStmt stmt,
HashSet<string> stateVars,
HashSet<string> pass1Vars,
out string? varName)
{
varName = null;
// Pass1 대상은 순수 대입문(AssignStmt)만
if (stmt is not AssignStmt a) return false;
// 변수 자체가 상태 변수이면 Pass2
if (stateVars.Contains(a.Name)) return false;
// 표현식 내 참조 변수가 모두 Pass1 범위 안이어야 함
var refs = PineEvaluator.GetReferencedVars(a.Expr);
foreach (var r in refs)
{
if (_keywords.Contains(r)) continue;
if (!pass1Vars.Contains(r)) return false; // 상태변수 or 미분류 → Pass2
}
varName = a.Name;
return true;
}
}
}