608 lines
27 KiB
C#
608 lines
27 KiB
C#
using System.Diagnostics;
|
|
using Serilog;
|
|
using Serilog.Events;
|
|
|
|
namespace PineScriptV6
|
|
{
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 문 (Statement) 타입 정의
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>Pine Script 한 문(statement)의 추상 기반 클래스</summary>
|
|
public abstract record PineStmt;
|
|
|
|
/// <summary>var bool inLong = false → 최초 1회만 초기화</summary>
|
|
public record VarDeclStmt(string Name, string TypeHint, string Expr) : PineStmt;
|
|
|
|
/// <summary>sma4 = expr 또는 newLongSignal = false</summary>
|
|
public record AssignStmt(string Name, string Expr) : PineStmt;
|
|
|
|
/// <summary>inLong := true (상태 변수 재할당)</summary>
|
|
public record ReassignStmt(string Name, string Expr) : PineStmt;
|
|
|
|
/// <summary>plotshape(...), bgcolor(...) 등 반환값 없는 호출</summary>
|
|
public record ExprStmt(string Expr) : PineStmt;
|
|
|
|
/// <summary>if condition\n body</summary>
|
|
public record IfStmt(string Condition, List<PineStmt> ThenBody, List<PineStmt>? ElseBody) : PineStmt;
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 실행기
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
public class PineScriptExecutor
|
|
{
|
|
private readonly string _filePath;
|
|
private List<PineStmt>? _program; // 최초 1회 파싱
|
|
private DependencyAnalyzer.ExecutionPlan? _plan; // 최초 1회 분석
|
|
|
|
// 실시간 모드용 — WarmUp 이후 ProcessCandle에서 사용
|
|
private int _realtimeBarIndex = 0;
|
|
|
|
public PineScriptExecutor(string filePath = "./example.pine")
|
|
{
|
|
_filePath = filePath;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 배치 모드 (CSV 히스토리 전체 실행)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// 봉 데이터를 순서대로 주입하며 매 봉마다 스크립트를 실행합니다.
|
|
/// Two-Pass: Pass1(지표) → Pass2(상태머신) 순으로 실행.
|
|
/// signalFrom: 이 시각 이후 봉에만 상세 추적 로그를 출력합니다.
|
|
/// </summary>
|
|
public void ExecuteOnCandles(List<CandleData> candles,
|
|
DateTimeOffset? signalFrom = null)
|
|
{
|
|
ResetState();
|
|
PineEvaluator.ResetStats();
|
|
EnsurePlan();
|
|
|
|
if (_program!.Count == 0)
|
|
{
|
|
Log.Error("[Executor] 스크립트 로드 실패: {Path}", _filePath);
|
|
return;
|
|
}
|
|
|
|
Log.Information("[Executor] Two-Pass 실행 시작 | 총 {Count}봉 " +
|
|
"(Pass1={P1} Pass2={P2}) | 추적={From}",
|
|
candles.Count,
|
|
_plan!.Pass1Stmts.Count, _plan.Pass2Stmts.Count,
|
|
signalFrom.HasValue ? signalFrom.Value.ToString("yyyy-MM-dd") : "전체");
|
|
|
|
long swPass1Ms = 0, swPass2Ms = 0, swInjectMs = 0, swLogMs = 0;
|
|
var sw = Stopwatch.StartNew();
|
|
|
|
for (int i = 0; i < candles.Count; i++)
|
|
{
|
|
CandleData c = candles[i];
|
|
bool inSignalRange = !signalFrom.HasValue || c.Time >= signalFrom.Value;
|
|
|
|
var t0 = sw.ElapsedTicks;
|
|
InjectOhlcv(c, i);
|
|
var t1 = sw.ElapsedTicks;
|
|
|
|
// Pass 1 — 지표 계산 (O(1) 증분)
|
|
ExecuteStmts(_plan.Pass1Stmts);
|
|
var t2 = sw.ElapsedTicks;
|
|
|
|
// Pass 2 — 상태 머신 (순차)
|
|
ExecuteStmts(_plan.Pass2Stmts);
|
|
var t3 = sw.ElapsedTicks;
|
|
|
|
if (inSignalRange)
|
|
LogBarDetail(c, i + 1, candles.Count);
|
|
else
|
|
LogWarmupSnap(c, i + 1, candles.Count);
|
|
var t4 = sw.ElapsedTicks;
|
|
|
|
swInjectMs += t1 - t0;
|
|
swPass1Ms += t2 - t1;
|
|
swPass2Ms += t3 - t2;
|
|
swLogMs += t4 - t3;
|
|
}
|
|
|
|
double ticksPerMs = (double)Stopwatch.Frequency / 1000.0;
|
|
Log.Information("[Executor] 실행 완료 | {Count}봉 처리", candles.Count);
|
|
Log.Information("═══ Pass별 소요 시간 ═══════════════");
|
|
Log.Information(" Inject OHLCV : {Ms,8:F1} ms", swInjectMs / ticksPerMs);
|
|
Log.Information(" Pass1(지표) : {Ms,8:F1} ms", swPass1Ms / ticksPerMs);
|
|
Log.Information(" Pass2(상태) : {Ms,8:F1} ms", swPass2Ms / ticksPerMs);
|
|
Log.Information(" Logging : {Ms,8:F1} ms", swLogMs / ticksPerMs);
|
|
Log.Information("════════════════════════════════════");
|
|
PineEvaluator.DumpStats();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 실시간 모드 (스트리밍)
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// 실시간 연결 전, 과거 봉으로 지표 상태를 초기화합니다.
|
|
/// warmupCandles: 최근 WarmupBars개 봉 (지표 최대 윈도우 이상이어야 함).
|
|
/// </summary>
|
|
public void WarmUp(List<CandleData> warmupCandles)
|
|
{
|
|
ResetState();
|
|
EnsurePlan();
|
|
|
|
Log.Information("[Executor] WarmUp 시작 | {Count}봉", warmupCandles.Count);
|
|
|
|
for (int i = 0; i < warmupCandles.Count; i++)
|
|
{
|
|
InjectOhlcv(warmupCandles[i], i);
|
|
ExecuteStmts(_plan!.Pass1Stmts);
|
|
ExecuteStmts(_plan.Pass2Stmts);
|
|
}
|
|
|
|
_realtimeBarIndex = warmupCandles.Count;
|
|
Log.Information("[Executor] WarmUp 완료 | 다음 bar_index={Idx}", _realtimeBarIndex);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 실시간 단일 봉 처리.
|
|
/// WarmUp() 이후에 호출해야 합니다.
|
|
/// 반환값: 이 봉에서 발생한 신호 목록 (없으면 빈 리스트).
|
|
/// </summary>
|
|
public List<TradeSignalEvent> ProcessCandle(CandleData candle)
|
|
{
|
|
EnsurePlan();
|
|
|
|
int prevCount = TradingSignal.Results.Count;
|
|
|
|
InjectOhlcv(candle, _realtimeBarIndex++);
|
|
ExecuteStmts(_plan!.Pass1Stmts);
|
|
ExecuteStmts(_plan.Pass2Stmts);
|
|
|
|
// 이번 봉에서 새로 추가된 신호만 반환
|
|
return TradingSignal.Results.Count > prevCount
|
|
? TradingSignal.Results.GetRange(prevCount,
|
|
TradingSignal.Results.Count - prevCount)
|
|
: new List<TradeSignalEvent>();
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 내부 헬퍼
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
private void ResetState()
|
|
{
|
|
PineContext.Reset();
|
|
PineStateVars.Clear();
|
|
TradingSignal.ClearResults();
|
|
IndicatorEngine.Reset();
|
|
_realtimeBarIndex = 0;
|
|
}
|
|
|
|
private void EnsurePlan()
|
|
{
|
|
_program ??= LoadScript();
|
|
if (_plan != null) return;
|
|
_plan = DependencyAnalyzer.Analyze(_program);
|
|
Log.Information("[Executor] 의존성 분석 완료 | Pass1={P1}문 Pass2={P2}문 | " +
|
|
"Pass1변수={Vars}개 StateVars={State}개",
|
|
_plan.Pass1Stmts.Count, _plan.Pass2Stmts.Count,
|
|
_plan.Pass1Vars.Count, _plan.StateVars.Count);
|
|
}
|
|
|
|
private static void InjectOhlcv(CandleData c, int barIndex)
|
|
{
|
|
Pine.open = (double)c.Open;
|
|
Pine.high = (double)c.High;
|
|
Pine.low = (double)c.Low;
|
|
Pine.close = (double)c.Close;
|
|
Pine.volume = (double)c.Volume;
|
|
Pine.time = c.Time.ToUnixTimeMilliseconds();
|
|
PineContext.CurrentBarIndex = barIndex;
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 추적 로그
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>분석 구간 매 봉: 핵심 변수 한 줄 + 신호·NearMiss 상세</summary>
|
|
private static void LogBarDetail(CandleData c, int barNum, int total)
|
|
{
|
|
bool debugOn = Log.IsEnabled(LogEventLevel.Debug);
|
|
|
|
// ── ① 신호 발생 여부 최우선 확인 (4개 조회) ─────────────
|
|
bool sigL = PineEvaluator.ToBool(PineStateVars.Get("newLongSignal"));
|
|
bool sigLS = PineEvaluator.ToBool(PineStateVars.Get("longSellSignal"));
|
|
bool sigS = PineEvaluator.ToBool(PineStateVars.Get("newShortSignal"));
|
|
bool sigSS = PineEvaluator.ToBool(PineStateVars.Get("shortSellSignal"));
|
|
|
|
if (sigL || sigLS || sigS || sigSS)
|
|
{
|
|
Log.Information(
|
|
"★[SIGNAL] {Time} Bar={B}/{T} | " +
|
|
"NewLong={SL} LongSell={SLS} NewShort={SS} ShortSell={SSS} | " +
|
|
"Close={C} sma72={S72} cloudTop={CT}",
|
|
c.Time.ToString("yyyy-MM-dd HH:mm"), barNum, total,
|
|
sigL, sigLS, sigS, sigSS,
|
|
FV(c.Close),
|
|
FV(PineStateVars.Get("sma72")),
|
|
FV(PineStateVars.Get("cloudTopNow")));
|
|
return;
|
|
}
|
|
|
|
// ── ② rawNew* 확인 (NearMiss) ─────────────────────────────
|
|
bool rawL = PineEvaluator.ToBool(PineStateVars.Get("rawNewLong"));
|
|
bool rawS = PineEvaluator.ToBool(PineStateVars.Get("rawNewShort"));
|
|
|
|
// ── ③ NearMiss도 없고 Debug도 꺼져있으면 즉시 리턴 ───────
|
|
// → 나머지 10개 Dict 조회·포맷팅 완전 생략
|
|
if (!rawL && !rawS && !debugOn) return;
|
|
|
|
// ── ④ 나머지 변수 (필요한 경우에만 조회) ─────────────────
|
|
bool inL = PineEvaluator.ToBool(PineStateVars.Get("inLong"));
|
|
bool inS = PineEvaluator.ToBool(PineStateVars.Get("inShort"));
|
|
bool sidewaysL = PineEvaluator.ToBool(PineStateVars.Get("isSidewaysLong"));
|
|
bool sidewaysS = PineEvaluator.ToBool(PineStateVars.Get("isSidewaysShort"));
|
|
bool touchL = PineEvaluator.ToBool(PineStateVars.Get("isLongRecentTouch"));
|
|
bool touchS = PineEvaluator.ToBool(PineStateVars.Get("isShortRecentTouch"));
|
|
|
|
if (rawL || rawS)
|
|
{
|
|
Log.Information(
|
|
"△[NEAR ] {Time} | rawL={RL} rawS={RS} inL={IL} inS={IS} | " +
|
|
"sideL={SL} sideS={SS} touchL={TL} touchS={TS}",
|
|
c.Time.ToString("yyyy-MM-dd HH:mm"),
|
|
rawL, rawS, inL, inS, sidewaysL, sidewaysS, touchL, touchS);
|
|
}
|
|
|
|
// ── ⑤ 매 봉 상세: Debug 레벨 (기본 꺼짐 → 스킵) ─────────
|
|
if (!debugOn) return;
|
|
|
|
string sma72 = FV(PineStateVars.Get("sma72"));
|
|
string sma4M = FV(PineStateVars.Get("sma4Middle"));
|
|
string cloudTop = FV(PineStateVars.Get("cloudTopNow"));
|
|
string rangeTh = FV(PineStateVars.Get("rangeThresholdLongPercent"));
|
|
bool rawLS = PineEvaluator.ToBool(PineStateVars.Get("rawLongSell"));
|
|
bool rawSS = PineEvaluator.ToBool(PineStateVars.Get("rawShortSell"));
|
|
|
|
Log.Debug(
|
|
"[BAR ] {Time} | C={C} s72={S72} s4M={S4M} cTop={CT} rTh={RT} | " +
|
|
"sideL={SL} sideS={SS} tL={TL} tS={TS} | " +
|
|
"rawL={RL} rawS={RS} rawLS={RLS} rawSS={RSS} | inL={IL} inS={IS}",
|
|
c.Time.ToString("yyyy-MM-dd HH:mm"),
|
|
FV(c.Close), sma72, sma4M, cloudTop, rangeTh,
|
|
B(sidewaysL), B(sidewaysS), B(touchL), B(touchS),
|
|
B(rawL), B(rawS), B(rawLS), B(rawSS),
|
|
B(inL), B(inS));
|
|
}
|
|
|
|
/// <summary>워밍업 구간: 50봉마다 지표 준비 상황만 출력</summary>
|
|
private static void LogWarmupSnap(CandleData c, int barNum, int total)
|
|
{
|
|
if (barNum % 50 != 0 && barNum != 1) return;
|
|
|
|
Log.Information(
|
|
"[WARM] Bar={B}/{T} {Time} | close={C} sma72={S72} cloudTop={CT} rangeTh={RT}",
|
|
barNum, total, c.Time.ToString("yyyy-MM-dd HH:mm"),
|
|
FV(c.Close),
|
|
FV(PineStateVars.Get("sma72")),
|
|
FV(PineStateVars.Get("cloudTopNow")),
|
|
FV(PineStateVars.Get("rangeThresholdLongPercent")));
|
|
}
|
|
|
|
// ── 포맷 헬퍼 ────────────────────────────────────────────────────
|
|
private static string FV(object? v)
|
|
{
|
|
if (v == null) return "na";
|
|
if (v is double d) return d.ToString("F4");
|
|
if (v is decimal m) return m.ToString("F4");
|
|
return v.ToString() ?? "na";
|
|
}
|
|
private static string FV(decimal m) => m.ToString("F4");
|
|
private static string B(bool b) => b ? "T" : "F";
|
|
|
|
// ── 스크립트 로드 & 파싱 ──────────────────────────────────────
|
|
|
|
private List<PineStmt> LoadScript()
|
|
{
|
|
if (!File.Exists(_filePath))
|
|
{
|
|
Log.Error("[Executor] 파일 없음: {Path}", _filePath);
|
|
return new List<PineStmt>();
|
|
}
|
|
|
|
// 들여쓰기 보존 → (indent, content) 쌍으로 수집
|
|
var lines = new List<(int Indent, string Raw)>();
|
|
foreach (string raw in File.ReadAllLines(_filePath))
|
|
{
|
|
string trimEnd = raw.TrimEnd();
|
|
if (string.IsNullOrWhiteSpace(trimEnd)) continue;
|
|
|
|
string content = trimEnd.TrimStart();
|
|
|
|
// 주석·버전 어노테이션 제외
|
|
if (content.StartsWith("//") || content.StartsWith("//@")) continue;
|
|
|
|
int indent = trimEnd.Length - content.Length;
|
|
lines.Add((indent, content));
|
|
}
|
|
|
|
Log.Information("[Executor] 스크립트 로드 | 유효 라인 {Count}개 | {Path}",
|
|
lines.Count, _filePath);
|
|
|
|
int pos = 0;
|
|
var stmts = BuildBlock(lines, ref pos, -1);
|
|
Log.Information("[Executor] 문(Statement) 파싱 완료 | {Count}개", stmts.Count);
|
|
return stmts;
|
|
}
|
|
|
|
// ── 블록 빌더 (재귀) ─────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// parentIndent 보다 큰 들여쓰기 수준의 라인들을 문 목록으로 변환합니다.
|
|
/// </summary>
|
|
private List<PineStmt> BuildBlock(List<(int Indent, string Raw)> lines,
|
|
ref int pos, int parentIndent)
|
|
{
|
|
var stmts = new List<PineStmt>();
|
|
|
|
while (pos < lines.Count)
|
|
{
|
|
var (indent, raw) = lines[pos];
|
|
|
|
// 상위 블록으로 복귀
|
|
if (indent <= parentIndent) break;
|
|
|
|
// ── if 처리 ──
|
|
if (raw.StartsWith("if ", StringComparison.OrdinalIgnoreCase) ||
|
|
raw.StartsWith("if(", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string condition = ExtractCondition(raw);
|
|
int myIndent = indent;
|
|
pos++;
|
|
|
|
// then 블록: 다음 라인들 중 indent > myIndent
|
|
var thenBody = pos < lines.Count && lines[pos].Indent > myIndent
|
|
? BuildBlock(lines, ref pos, myIndent)
|
|
: new List<PineStmt>();
|
|
|
|
// else 블록
|
|
List<PineStmt>? elseBody = null;
|
|
if (pos < lines.Count
|
|
&& lines[pos].Indent == myIndent
|
|
&& lines[pos].Raw.Equals("else", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
pos++;
|
|
elseBody = pos < lines.Count && lines[pos].Indent > myIndent
|
|
? BuildBlock(lines, ref pos, myIndent)
|
|
: new List<PineStmt>();
|
|
}
|
|
|
|
stmts.Add(new IfStmt(condition, thenBody, elseBody));
|
|
continue; // pos는 BuildBlock 내부에서 이미 이동
|
|
}
|
|
|
|
// ── 일반 문 ──
|
|
var stmt = ParseStatement(raw);
|
|
if (stmt != null) stmts.Add(stmt);
|
|
pos++;
|
|
}
|
|
|
|
return stmts;
|
|
}
|
|
|
|
// ── 문 파싱 ───────────────────────────────────────────────────
|
|
|
|
private static PineStmt? ParseStatement(string line)
|
|
{
|
|
// 1) var 선언: var bool inLong = false
|
|
if (line.StartsWith("var ", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string rest = line.Substring(4).Trim();
|
|
|
|
// type hint 제거
|
|
string typeHint = "";
|
|
string[] types = { "float[]", "int[]", "bool[]", "string[]",
|
|
"bool", "int", "float", "string" };
|
|
foreach (var t in types)
|
|
{
|
|
if (rest.StartsWith(t + " ", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
typeHint = t;
|
|
rest = rest.Substring(t.Length).Trim();
|
|
break;
|
|
}
|
|
}
|
|
|
|
int eqIdx = FindAssignEq(rest);
|
|
if (eqIdx > 0)
|
|
{
|
|
string name = rest.Substring(0, eqIdx).Trim();
|
|
string expr = rest.Substring(eqIdx + 1).Trim();
|
|
return new VarDeclStmt(name, typeHint, expr);
|
|
}
|
|
// var foo (초기화 없음)
|
|
string justName = rest.Split(' ')[0].Trim();
|
|
return new VarDeclStmt(justName, typeHint, "na");
|
|
}
|
|
|
|
// 2) := 재할당: inLong := true
|
|
{
|
|
int cIdx = FindReassignOp(line);
|
|
if (cIdx > 0)
|
|
{
|
|
string name = line.Substring(0, cIdx).Trim();
|
|
string expr = line.Substring(cIdx + 2).Trim();
|
|
if (IsSimpleIdent(name))
|
|
return new ReassignStmt(name, expr);
|
|
}
|
|
}
|
|
|
|
// 3) 대입: sma4 = expr
|
|
{
|
|
int eqIdx = FindAssignEq(line);
|
|
if (eqIdx > 0)
|
|
{
|
|
string lhs = line.Substring(0, eqIdx).Trim();
|
|
string rhs = line.Substring(eqIdx + 1).Trim();
|
|
if (IsSimpleIdent(lhs) && !string.IsNullOrEmpty(rhs))
|
|
return new AssignStmt(lhs, rhs);
|
|
}
|
|
}
|
|
|
|
// 4) 표현식 문: plotshape(...), plot(...) 등
|
|
if (!string.IsNullOrEmpty(line))
|
|
return new ExprStmt(line);
|
|
|
|
return null;
|
|
}
|
|
|
|
// ── 문 실행 ───────────────────────────────────────────────────
|
|
|
|
private static void ExecuteStmts(List<PineStmt> stmts)
|
|
{
|
|
foreach (var stmt in stmts)
|
|
ExecuteOne(stmt);
|
|
}
|
|
|
|
private static void ExecuteOne(PineStmt stmt)
|
|
{
|
|
switch (stmt)
|
|
{
|
|
// ── var 선언 (최초 1회만 초기화) ──
|
|
case VarDeclStmt vd:
|
|
{
|
|
if (!PineStateVars.Has(vd.Name))
|
|
{
|
|
object? val = PineEvaluator.Evaluate(vd.Expr);
|
|
PineStateVars.Declare(vd.Name, val);
|
|
Log.Debug("[Exec] var {Name} = {Val}", vd.Name, val);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// ── 일반 대입 (매 봉마다 재평가) ──
|
|
case AssignStmt a:
|
|
{
|
|
object? val = PineEvaluator.Evaluate(a.Expr);
|
|
PineStateVars.Set(a.Name, val);
|
|
|
|
// double 값은 PineContext 시리즈에도 Push (ta.sma 등이 읽음)
|
|
double dv = PineEvaluator.ToDouble(val);
|
|
if (!double.IsNaN(dv))
|
|
PineContext.SetVariable(a.Name, dv);
|
|
|
|
Log.Debug("[Exec] {Name} = {Val}", a.Name, val);
|
|
break;
|
|
}
|
|
|
|
// ── := 재할당 ──
|
|
case ReassignStmt r:
|
|
{
|
|
object? val = PineEvaluator.Evaluate(r.Expr);
|
|
PineStateVars.Set(r.Name, val);
|
|
|
|
double dv = PineEvaluator.ToDouble(val);
|
|
if (!double.IsNaN(dv))
|
|
PineContext.SetVariable(r.Name, dv);
|
|
|
|
Log.Debug("[Exec] {Name} := {Val}", r.Name, val);
|
|
break;
|
|
}
|
|
|
|
// ── 표현식 문 (side-effect 목적) ──
|
|
case ExprStmt e:
|
|
{
|
|
PineEvaluator.Evaluate(e.Expr);
|
|
break;
|
|
}
|
|
|
|
// ── if / else ──
|
|
case IfStmt ifs:
|
|
{
|
|
bool cond = PineEvaluator.EvaluateBool(ifs.Condition);
|
|
Log.Debug("[Exec] if ({Cond}) → {Result}", ifs.Condition, cond);
|
|
if (cond)
|
|
ExecuteStmts(ifs.ThenBody);
|
|
else if (ifs.ElseBody != null)
|
|
ExecuteStmts(ifs.ElseBody);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ════════════════════════════════════════════════════════════════
|
|
// 파싱 헬퍼
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>if 라인에서 조건 문자열 추출</summary>
|
|
private static string ExtractCondition(string line)
|
|
{
|
|
// "if " or "if(" 이후 텍스트
|
|
string rest = line.Length > 3 ? line.Substring(3).Trim() : "";
|
|
// 바깥 괄호 제거: "(cond)" → "cond"
|
|
if (rest.StartsWith("(") && rest.EndsWith(")"))
|
|
{
|
|
// 매칭 괄호 확인
|
|
int depth = 0;
|
|
bool matched = false;
|
|
for (int i = 0; i < rest.Length; i++)
|
|
{
|
|
if (rest[i] == '(') depth++;
|
|
else if (rest[i] == ')') { depth--; if (depth == 0 && i == rest.Length - 1) { matched = true; break; } }
|
|
}
|
|
if (matched) rest = rest.Substring(1, rest.Length - 2).Trim();
|
|
}
|
|
return rest;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 순수 대입 '=' 위치 반환.
|
|
/// ==, !=, <=, >=, := 는 제외.
|
|
/// 괄호/대괄호 안은 탐색 제외.
|
|
/// </summary>
|
|
private static int FindAssignEq(string line)
|
|
{
|
|
int depth = 0;
|
|
for (int i = 1; i < line.Length; i++)
|
|
{
|
|
char c = line[i];
|
|
if (c == '(' || c == '[') { depth++; continue; }
|
|
if (c == ')' || c == ']') { depth--; continue; }
|
|
if (depth > 0) continue;
|
|
|
|
if (c == '=')
|
|
{
|
|
char prev = line[i - 1];
|
|
char next = i + 1 < line.Length ? line[i + 1] : '\0';
|
|
bool compound = prev is '!' or '<' or '>' or '=' or ':';
|
|
if (!compound && next != '=') return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>:= 위치 반환 (없으면 -1), 괄호 밖만 탐색</summary>
|
|
private static int FindReassignOp(string line)
|
|
{
|
|
int depth = 0;
|
|
for (int i = 0; i < line.Length - 1; i++)
|
|
{
|
|
char c = line[i];
|
|
if (c == '(' || c == '[') { depth++; continue; }
|
|
if (c == ')' || c == ']') { depth--; continue; }
|
|
if (depth > 0) continue;
|
|
if (c == ':' && line[i + 1] == '=') return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/// <summary>단순 변수명 여부 (영문자·숫자·밑줄만, 첫 글자는 영문자/밑줄)</summary>
|
|
private static bool IsSimpleIdent(string s)
|
|
{
|
|
if (string.IsNullOrEmpty(s)) return false;
|
|
if (!char.IsLetter(s[0]) && s[0] != '_') return false;
|
|
foreach (char c in s)
|
|
if (!char.IsLetterOrDigit(c) && c != '_') return false;
|
|
return true;
|
|
}
|
|
}
|
|
}
|