diff --git a/midterm/README.md b/midterm/README.md new file mode 100644 index 0000000..40a7456 --- /dev/null +++ b/midterm/README.md @@ -0,0 +1,75 @@ +# TIP 언어 파서 · CFG/ICFG · Interval Analysis + +TIP (Tiny Imperative Programming) 언어를 파싱하여 AST를 생성하고, CFG/ICFG를 구성한 뒤 Interval Analysis(Widening/Narrowing 포함)를 수행합니다. 결과는 Graphviz DOT(및 선택적 PDF)와 JSON으로 출력됩니다. + +## 구현 내용 + +- [x] Flow-sensitive 분석 +- [x] Interval analysis +- [x] Widening +- [x] Narrowing +- [x] Control-sensitive analysis +- [x] Inter-procedural analysis +- [-] Context-sensitive analysis (k-callsite sensitivity w/ parameterized k) (구현 못함) + +## 파일 구조 + +- `parser.ts` - TIP 파서 (Ohm.js 기반), 소스 → AST(`Program`) +- `tip-cfg-converter.ts` - 함수별 CFG 생성기 (DOT 내보내기 지원) +- `tip-icfg-converter.ts` - 프로그램 단일 ICFG 생성기 (함수 간 call/return 엣지 포함) +- `interval-analysis.ts` - ICFG 기반 Interval Analysis + - threshold 기반 widen/normalize, 후속 narrowing 단계 포함 + - 분기 조건(`>`, `==`)에 대한 보수적 정제 지원 +- `tip-all-in-one.ts` - 통합 실행 스크립트 + - AST 생성 및 저장: `output/ast.json` + - CFG DOT(+PDF) 생성: `output/cfg/*.dot(.pdf)` + - ICFG DOT(+PDF) 생성: `output/icfg/main.dot(.pdf)` + - Interval 결과: `output/intervals.json`, `output/intervals_trace.json` +- `grammar.ohm` - TIP 문법 정의 +- `types.ts` - AST 타입 정의 +- `tip_code.txt` - 실행 입력 TIP 코드 +- `test/` - 예제/평가용 입력 (`1. widening_narrowing.txt`, `2. control_sensitive.txt`, `3. interprocedural.txt`) + +## 요구 사항 + +- Node.js (권장: v18+) +- `npm install`로 의존성 설치 +- Graphviz(선택) 설치 시 DOT → PDF 자동 변환 + - macOS: `brew install graphviz` + - Ubuntu: `sudo apt-get install graphviz` + - Windows: `https://graphviz.org/download/` + +## 실행 방법 (통합) + +```bash +# 의존성 설치 +npm install + +# 입력 코드 편집 +# - 분석할 TIP 코드를 midterm/tip_code.txt에 작성 + +# 통합 실행 (AST → CFG/ICFG → Interval → DOT/PDF 변환까지) +npm run tip-all + +# 생성물은 midterm/output/ 아래에 저장됩니다. +``` + +Graphviz 미설치 시 DOT 파일만 생성되며, 다음과 같이 수동 변환할 수 있습니다. + +```bash +dot -Tpdf output/cfg/.dot -o output/cfg/.pdf +dot -Tpdf output/icfg/main.dot -o output/icfg/main.pdf +``` + +## 출력물 요약 + +- `output/ast.json`: 파싱된 AST +- `output/cfg/`: 함수별 CFG DOT(+PDF) +- `output/icfg/main.dot(.pdf)`: 단일 ICFG +- `output/intervals.json`: 노드별 추정 구간 환경(Top/±inf 포함) +- `output/intervals_trace.json`: 고정점 수렴 과정(와이덴/내로잉) 추적 로그 + +## 참고 + +- `package.json`의 권장 스크립트: `tip-all` + - 기타 과거 스크립트(`parser`, `cfg`, `normal`, `all`)는 현재 통합 흐름과 무관합니다. diff --git a/midterm/grammar.ohm b/midterm/grammar.ohm new file mode 100644 index 0000000..2e76fef --- /dev/null +++ b/midterm/grammar.ohm @@ -0,0 +1,73 @@ +TIP { + // 프로그램: 함수들의 시퀀스 + Program = Function+ + + // 함수: x(x, ..., x){ (var x, ..., x;)? s return e; } + Function = identifier "(" Params? ")" "{" VarDecl? Statement* ReturnStmt "}" + + Params = identifier ("," identifier)* + VarDecl = "var" identifier ("," identifier)* ";" + + // 구문들 + Statement = AssignmentStmt + | OutputStmt + | IfStmt + | WhileStmt + | CallStmt + | AssertStmt + + + AssignmentStmt = identifier "=" Expression ";" + OutputStmt = "output" Expression ";" + CallStmt = identifier "(" Args? ")" ";" + IfStmt = "if" "(" Expression ")" Block ElseClause? + ElseClause = "else" Block + WhileStmt = "while" "(" Expression ")" Block + ReturnStmt = "return" Expression ";" + + AssertStmt = "assert" "(" Expression ")" ";" + + Block = "{" BlockStatement* "}" + BlockStatement = Statement + + + // 표현식들 (우선순위 순서대로) + Expression = ComparisonExpr + + ComparisonExpr = ComparisonExpr ">" ArithExpr -- greater + | ComparisonExpr "==" ArithExpr -- equal + | ArithExpr + + ArithExpr = ArithExpr "+" MulExpr -- add + | ArithExpr "-" MulExpr -- sub + | MulExpr + + MulExpr = MulExpr "*" UnaryExpr -- mul + | MulExpr "/" UnaryExpr -- div + | UnaryExpr + + UnaryExpr = PrimaryExpr + + PrimaryExpr = "input" -- input + | "(" Expression ")" -- paren + | FunctionCallOrAccess + | number -- number + | identifier -- identifier + + FunctionCallOrAccess = FunctionCallOrAccess "(" Args? ")" -- call + | identifier -- base + + + + Args = Expression ("," Expression)* + + + + // 기본 토큰들 + identifier = letter (alnum | "_")* + number = digit+ + + // 공백 처리 + space += comment + comment = "//" (~"\n" any)* "\n" +} diff --git a/midterm/interval-analysis.ts b/midterm/interval-analysis.ts new file mode 100644 index 0000000..9cffd9b --- /dev/null +++ b/midterm/interval-analysis.ts @@ -0,0 +1,694 @@ +import TIPParser from "./parser"; +import { TIPICFGConverter, ControlFlowGraph } from "./tip-icfg-converter"; +import { + Program, + FunctionDeclaration, + Statement, + Expression, + FunctionCall, + Variable, + NumberLiteral, +} from "./types"; +import * as fs from "fs"; +import * as path from "path"; + +type Bound = number | typeof Infinity | typeof Number.NEGATIVE_INFINITY; + +interface Interval { + lo: number; // -Infinity allowed + hi: number; // +Infinity allowed +} + +const NEG_INF = Number.NEGATIVE_INFINITY; +const POS_INF = Number.POSITIVE_INFINITY; + +const Interval = { + top(): Interval { + return { lo: NEG_INF, hi: POS_INF }; + }, + bottom(): Interval { + return { lo: 1, hi: 0 }; // empty (lo>hi) + }, + isBottom(i: Interval): boolean { + return i.lo > i.hi; + }, + ofConst(n: number): Interval { + return { lo: n, hi: n }; + }, + join(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a)) return b; + if (Interval.isBottom(b)) return a; + return { lo: Math.min(a.lo, b.lo), hi: Math.max(a.hi, b.hi) }; + }, + equal(a: Interval, b: Interval): boolean { + return a.lo === b.lo && a.hi === b.hi; + }, + meet(a: Interval, b: Interval): Interval { + const lo = Math.max(a.lo, b.lo); + const hi = Math.min(a.hi, b.hi); + return { lo, hi }; + }, + add(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + return { lo: a.lo + b.lo, hi: a.hi + b.hi }; + }, + sub(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + return { lo: a.lo - b.hi, hi: a.hi - b.lo }; + }, + mul(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + const candidates = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi]; + return { lo: Math.min(...candidates), hi: Math.max(...candidates) }; + }, + div(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + // conservative: if 0 in divisor, return Top + if (b.lo <= 0 && 0 <= b.hi) return Interval.top(); + const candidates = [a.lo / b.lo, a.lo / b.hi, a.hi / b.lo, a.hi / b.hi]; + return { lo: Math.min(...candidates), hi: Math.max(...candidates) }; + }, +}; + +type Env = Map; // namespaced `func:var` + +function ns(func: string, v: string): string { + return `${func}:${v}`; +} + +function cloneEnv(e: Env): Env { + const m = new Map(); + for (const [k, v] of e.entries()) m.set(k, { lo: v.lo, hi: v.hi }); + return m; +} + +function joinEnv(a: Env, b: Env): Env { + const out = new Map(a); + for (const [k, v] of b.entries()) { + const prev = out.get(k); + out.set(k, prev ? Interval.join(prev, v) : v); + } + return out; +} + +function meetEnv(a: Env, b: Env): Env { + const out = new Map(); + const keys = new Set([...a.keys(), ...b.keys()]); + for (const k of keys) { + const av = a.get(k); + const bv = b.get(k); + if (av && bv) { + out.set(k, Interval.meet(av, bv)); + } else if (av && !bv) { + // meet with Top = av + out.set(k, av); + } else if (!av && bv) { + out.set(k, bv); + } + } + return out; +} + +function envEqual(a: Env, b: Env): boolean { + if (a.size !== b.size) return false; + for (const [k, v] of a.entries()) { + const u = b.get(k); + if (!u || !Interval.equal(v, u)) return false; + } + return true; +} + +// 임계값-기반 스냅: 주어진 값 이상인 첫 번째 임계값으로 상한을 끌어올림 +function thresholdBumpUp(x: number, thresholds: number[]): number { + for (const t of thresholds) { + if (x <= t) return t; + } + return POS_INF; +} +// 임계값-기반 스냅: 주어진 값 이하인 마지막 임계값으로 하한을 끌어내림 +function thresholdBumpDown(x: number, thresholds: number[]): number { + for (let i = thresholds.length - 1; i >= 0; i--) { + const t = thresholds[i]; + if (x >= t) return t; + } + return NEG_INF; +} +// 임계값-기반 widening: 증가/감소가 관측될 때 다음 임계값으로만 점프 +function thresholdWiden( + a: Interval, + b: Interval, + thresholds: number[] +): Interval { + // bottom 보존: widen 시 한 쪽이 bottom이면 다른 쪽을 그대로 사용 + if (Interval.isBottom(a)) return b; + if (Interval.isBottom(b)) return b; // 새로운 정보가 bottom이면 그대로 반영 + const lo = b.lo < a.lo ? thresholdBumpDown(b.lo, thresholds) : b.lo; + const hi = b.hi > a.hi ? thresholdBumpUp(b.hi, thresholds) : b.hi; + return { lo, hi }; +} +// 환경 단위 임계값-기반 widening +function thresholdWidenEnv( + oldEnv: Env, + newEnv: Env, + thresholds: number[] +): Env { + const out = new Map(); + const keys = new Set([...oldEnv.keys(), ...newEnv.keys()]); + for (const k of keys) { + const a = oldEnv.get(k); + const b = newEnv.get(k); + if (a && b) out.set(k, thresholdWiden(a, b, thresholds)); + else if (!a && b) out.set(k, b); + else if (a && !b) out.set(k, a); + } + return out; +} +// 임계값 정규화(스냅): 모든 경계를 {-inf, ..., +inf} 임계값 격자에 맞춤 +function normalizeInterval(i: Interval, thresholds: number[]): Interval { + // bottom은 보존 + if (Interval.isBottom(i)) return i; + // 유한 경계는 스냅하지 않고 그대로 유지해 정밀도 보존 + if (Number.isFinite(i.lo) && Number.isFinite(i.hi)) return i; + return { + lo: thresholdBumpDown(i.lo, thresholds), + hi: thresholdBumpUp(i.hi, thresholds), + }; +} +function normalizeEnv(env: Env, thresholds: number[]): Env { + // 환경 전체에 대해 정규화를 적용 + const out = new Map(); + for (const [k, v] of env.entries()) + out.set(k, normalizeInterval(v, thresholds)); + return out; +} + +function evalExpr(expr: Expression, env: Env, funcName: string): Interval { + switch (expr.type) { + case "NumberLiteral": + return Interval.ofConst((expr as NumberLiteral).value); + case "Variable": { + const name = (expr as Variable).name; + return env.get(ns(funcName, name)) ?? Interval.top(); + } + case "BinaryExpression": { + const l = evalExpr(expr.left, env, funcName); + const r = evalExpr(expr.right, env, funcName); + switch (expr.operator) { + case "+": + return Interval.add(l, r); + case "-": + return Interval.sub(l, r); + case "*": + return Interval.mul(l, r); + case "/": + return Interval.div(l, r); + case ">": + case "==": + // 비교식의 값은 불리언이지만 수치 구간으로 표현 불가 -> Top 반환 + return Interval.top(); + default: + return Interval.top(); + } + } + case "FunctionCall": + return Interval.top(); + case "InputExpression": + return Interval.top(); + default: + return Interval.top(); + } +} + +function refineEnvByCondition( + cond: Expression | undefined, + env: Env, + funcName: string, + truthy: boolean +): Env { + if (!cond || cond.type !== "BinaryExpression") return env; + const be = cond as any; + const out = cloneEnv(env); + + const isVar = (e: Expression): e is Variable => e.type === "Variable"; + const isNum = (e: Expression): e is NumberLiteral => + e.type === "NumberLiteral"; + + if (be.operator === ">") { + // 형태 1) x > c + if (isVar(be.left) && isNum(be.right)) { + const vName = ns(funcName, be.left.name); + const cur = out.get(vName) ?? Interval.top(); + if (truthy) { + out.set(vName, { + lo: Math.max(cur.lo, be.right.value + 1), + hi: cur.hi, + }); + } else { + out.set(vName, { lo: cur.lo, hi: Math.min(cur.hi, be.right.value) }); + } + return out; + } + // 형태 2) c > x ⇔ x < c + if (isNum(be.left) && isVar(be.right)) { + const vName = ns(funcName, be.right.name); + const cur = out.get(vName) ?? Interval.top(); + if (truthy) { + // x <= c-1 + out.set(vName, { lo: cur.lo, hi: Math.min(cur.hi, be.left.value - 1) }); + } else { + // x >= c + out.set(vName, { lo: Math.max(cur.lo, be.left.value), hi: cur.hi }); + } + return out; + } + return out; + } + + if (be.operator === "==") { + // 형태 1) x == c + if (isVar(be.left) && isNum(be.right)) { + const vName = ns(funcName, be.left.name); + const cur = out.get(vName) ?? Interval.top(); + if (truthy) { + const meet = Interval.meet(cur, Interval.ofConst(be.right.value)); + out.set(vName, meet); + } else { + // 단일 점을 제외하는 것은 구간 도메인에서 표현 불가 → 보수적으로 유지 + // 단, 현재가 정확히 [c,c]이면 모순이므로 bottom으로 정제 + if (cur.lo === be.right.value && cur.hi === be.right.value) { + out.set(vName, Interval.bottom()); + } + } + return out; + } + // 형태 2) c == x + if (isNum(be.left) && isVar(be.right)) { + const vName = ns(funcName, be.right.name); + const cur = out.get(vName) ?? Interval.top(); + if (truthy) { + const meet = Interval.meet(cur, Interval.ofConst(be.left.value)); + out.set(vName, meet); + } else { + if (cur.lo === be.left.value && cur.hi === be.left.value) { + out.set(vName, Interval.bottom()); + } + } + return out; + } + return out; + } + + return out; +} + +interface AnalysisResult { + nodeIn: Map; + nodeOut: Map; +} + +export function analyzeIntervals(program: Program): { + graph: ControlFlowGraph; + result: AnalysisResult; + trace: any[]; +} { + const icfg = new TIPICFGConverter().convertProgramUnified(program); + + // Threshold extraction + const thresholds = (() => { + const nums = new Set(); + function visitExpr(e: Expression) { + switch (e.type) { + case "NumberLiteral": + nums.add((e as NumberLiteral).value); + break; + case "BinaryExpression": + visitExpr(e.left); + visitExpr(e.right); + break; + case "FunctionCall": + visitExpr(e.callee); + { + const args: any[] = Array.isArray((e as any).arguments) + ? ((e as any).arguments as any[]).flat() + : (e as any).arguments; + for (const a of args) visitExpr(a); + } + break; + default: + break; + } + } + function visitStmt(s: Statement) { + switch (s.type) { + case "AssignmentStatement": + visitExpr((s as any).expression); + break; + case "OutputStatement": + visitExpr((s as any).expression); + break; + case "AssertStatement": + // assert의 조건식에 등장하는 상수들도 임계값으로 포함 + visitExpr((s as any).condition); + break; + case "IfStatement": + visitExpr((s as any).condition); + visitStmt((s as any).thenStatement); + if ((s as any).elseStatement) visitStmt((s as any).elseStatement); + break; + case "WhileStatement": + visitExpr((s as any).condition); + visitStmt((s as any).body); + break; + case "SequenceStatement": + for (const st of (s as any).statements) visitStmt(st); + break; + case "ReturnStatement": + visitExpr((s as any).expression); + break; + default: + break; + } + } + for (const f of program.functions) { + if (f.body) visitStmt(f.body); + if (f.returnExpression) visitExpr(f.returnExpression); + } + const arr = Array.from(nums.values()).sort((a, b) => a - b); + return [NEG_INF, ...arr, POS_INF]; + })(); + + // 함수 이름 → 선언 매핑 + const funcMap = new Map(); + for (const f of program.functions) funcMap.set(f.name, f); + + // 워크리스트 초기화 + const inMap = new Map(); + const outMap = new Map(); + + // 인접 리스트 구성 + const preds = new Map(); + const succs = new Map(); + for (const e of icfg.edges) { + const s = succs.get(e.from) || []; + s.push({ to: e.to, label: e.label }); + succs.set(e.from, s); + const p = preds.get(e.to) || []; + p.push({ from: e.from, label: e.label }); + preds.set(e.to, p); + } + + // 진입 노드들 초기화 + for (const node of icfg.nodes.values()) { + if (node.type === "entry" && node.funcName) { + const env: Env = new Map(); + const f = funcMap.get(node.funcName); + if (f) { + // parameters: string[][] → flatten + const params = f.parameters.reduce( + (acc, arr) => acc.concat(arr), + [] as string[] + ); + // 호출자가 있는 entry의 경우 파라미터는 초기화하지 않음 (call edge에서 바인딩) + // 프레드가 없는 경우(프로그램 시작점 등)만 Top으로 설정 + const hasPred = (preds.get(node.id) || []).length > 0; + if (!hasPred) { + for (const p of params) env.set(ns(f.name, p), Interval.top()); + } + if (f.localVariables) { + // 호출자가 없는 경우에만 로컬 변수를 Top으로 초기화 + if (!hasPred) { + for (const vs of f.localVariables) { + for (const v of vs) { + env.set(ns(f.name, v), Interval.top()); + } + } + } + } + env.set(ns(f.name, "@ret"), Interval.bottom()); + } + inMap.set(node.id, env); + } + } + + const worklist: number[] = Array.from(icfg.nodes.keys()); + const trace: any[] = []; + + function transfer(nodeId: number, inEnv: Env): Env { + const node = icfg.nodes.get(nodeId)!; + const func = node.funcName || ""; + const env = cloneEnv(inEnv); + + if (node.type === "after-call" && node.statement) { + const stmt = node.statement as any; + if (stmt.type === "AssignmentStatement") { + let retVal: Interval | undefined; + for (const [k, v] of env.entries()) { + if (k.endsWith(":@ret")) + retVal = retVal ? Interval.join(retVal, v) : v; + } + env.set(ns(func, stmt.variable), retVal ?? Interval.top()); + } + } else if (node.statement) { + const stmt = node.statement as Statement; + switch (stmt.type) { + case "AssignmentStatement": { + const isCall = (stmt as any).expression?.type === "FunctionCall"; + if (!isCall) { + const val = evalExpr((stmt as any).expression, env, func); + env.set(ns(func, (stmt as any).variable), val); + } + break; + } + case "AssertStatement": { + const refined = refineEnvByCondition( + (stmt as any).condition, + env, + func, + true + ); + return refined; + } + case "ReturnStatement": { + const val = evalExpr((stmt as any).expression, env, func); + env.set(ns(func, "@ret"), val); + break; + } + default: + break; + } + } + + return env; + } + + function edgeTransfer(from: number, to: number, outEnv: Env): Env { + const edgeList = succs.get(from) || []; + const label = edgeList.find((e) => e.to === to)?.label; + const fromNode = icfg.nodes.get(from)!; + if (label === "true" || label === "false") { + const condExpr = fromNode.expression; // condition node carries expression + const funcName = fromNode.funcName || ""; + const truthy = label === "true"; + return refineEnvByCondition(condExpr as any, outEnv, funcName, truthy); + } + // 라벨이 없더라도, assert 노드에서 나가는 엣지는 조건이 항상 참이므로 필터를 적용 + if ( + !label && + fromNode.statement && + (fromNode.statement as any).type === "AssertStatement" + ) { + const funcName = fromNode.funcName || ""; + const condExpr = fromNode.expression; + return refineEnvByCondition(condExpr as any, outEnv, funcName, true); + } + if (label === "call") { + // 인자 → 파라미터 바인딩 + const callerNode = icfg.nodes.get(from)!; + const func = callerNode.funcName || ""; + const stmt = callerNode.statement as any; + const callExpr: FunctionCall | undefined = + stmt?.expression?.type === "FunctionCall" + ? (stmt.expression as FunctionCall) + : stmt?.expression; + if (!callExpr || callExpr.type !== "FunctionCall") return outEnv; + + let c: Expression = callExpr.callee; + while ((c as any).type === "FunctionCall") c = (c as any).callee; + if (c.type !== "Variable") return outEnv; + const calleeName = (c as Variable).name; + const fd = funcMap.get(calleeName); + if (!fd) return outEnv; + + // 호출자 환경을 들고 가지 않고, 피호출자 전용 환경을 새로 시작 + const env: Env = new Map(); + const params = fd.parameters.reduce( + (acc, arr) => acc.concat(arr), + [] as string[] + ); + const args = Array.isArray((callExpr as any).arguments) + ? ((callExpr as any).arguments as any[]).flat() + : (callExpr as any).arguments; + for (let i = 0; i < params.length; i++) { + const p = params[i]; + const argExpr = args[i]; + const val = argExpr ? evalExpr(argExpr, outEnv, func) : Interval.top(); + env.set(ns(calleeName, p), val); + } + if (fd.localVariables) { + for (const v of fd.localVariables) { + for (const vv of v) { + env.set(ns(calleeName, vv), Interval.top()); + } + } + } + env.set(ns(calleeName, "@ret"), Interval.bottom()); + return env; + } + if (label === "return") { + // 반환 시에는 피호출자의 반환값(@ret)만 전달 + const env = new Map(); + for (const [k, v] of outEnv.entries()) { + if (k.endsWith(":@ret")) env.set(k, v); + } + return env; + } + return outEnv; + } + + while (worklist.length) { + const n = worklist.shift()!; + let inEnv: Env | undefined = inMap.get(n); + const ps = preds.get(n) || []; + let accum: Env | undefined = undefined; + for (const { from, label } of ps) { + const outEnv = outMap.get(from) || new Map(); + const afterEdge = edgeTransfer(from, n, outEnv); + accum = accum ? joinEnv(accum, afterEdge) : cloneEnv(afterEdge); + } + if (accum) inEnv = inEnv ? joinEnv(inEnv, accum) : accum; + if (!inEnv) inEnv = new Map(); + inMap.set(n, inEnv); + + const rawOut0 = transfer(n, inEnv); + const rawOut = normalizeEnv(rawOut0, thresholds); + const oldOut = outMap.get(n); + const newOut0 = oldOut + ? thresholdWidenEnv(oldOut, rawOut, thresholds) + : rawOut; + const newOut = normalizeEnv(newOut0, thresholds); + if (!oldOut || !envEqual(oldOut, newOut)) { + // trace entry (간소화: 방문/원시 out 기록 제거) + const toBound = (x: number) => + x === POS_INF ? "inf" : x === NEG_INF ? "-inf" : x; + const ser = (env: Env | undefined) => { + if (!env) return null; + const o: any = {}; + for (const [k, v] of env.entries()) + o[k] = [toBound(v.lo), toBound(v.hi)]; + return o; + }; + const nodeMeta = icfg.nodes.get(n)!; + trace.push({ + node: n, + func: nodeMeta.funcName || null, + type: nodeMeta.type, + label: nodeMeta.label, + in: ser(inEnv), + out: ser(newOut), + }); + outMap.set(n, newOut); + for (const s of succs.get(n) || []) worklist.push(s.to); + } + } + + // Narrowing phase: when widening reached a fixed point, re-run without widening + // until a (potentially) more precise fixed point is reached. + let changed = true; + while (changed) { + changed = false; + const queue: number[] = Array.from(icfg.nodes.keys()); + while (queue.length) { + const n = queue.shift()!; + + // Recompute IN with current OUTs (may-join only, no widening) + let inEnv: Env | undefined = undefined; + const ps = preds.get(n) || []; + let accum: Env | undefined = undefined; + for (const { from } of ps) { + const outEnv = outMap.get(from) || new Map(); + const afterEdge = edgeTransfer(from, n, outEnv); + accum = accum ? joinEnv(accum, afterEdge) : cloneEnv(afterEdge); + } + inEnv = accum ?? new Map(); + inMap.set(n, inEnv); + + // Standard transfer (no widen, no threshold snap) + const nextOut = transfer(n, inEnv); + const curOut = outMap.get(n); + const narrowedOut = curOut ? meetEnv(nextOut, curOut) : nextOut; + if (!curOut || !envEqual(curOut, narrowedOut)) { + // trace narrowing step + const toBound = (x: number) => + x === POS_INF ? "inf" : x === NEG_INF ? "-inf" : x; + const ser = (env: Env | undefined) => { + if (!env) return null; + const o: any = {}; + for (const [k, v] of env.entries()) + o[k] = [toBound(v.lo), toBound(v.hi)]; + return o; + }; + const nodeMeta = icfg.nodes.get(n)!; + trace.push({ + phase: "narrowing", + node: n, + func: nodeMeta.funcName || null, + type: nodeMeta.type, + label: nodeMeta.label, + in: ser(inEnv), + out: ser(narrowedOut), + }); + + outMap.set(n, narrowedOut); + changed = true; + for (const s of succs.get(n) || []) queue.push(s.to); + } + } + } + + return { graph: icfg, result: { nodeIn: inMap, nodeOut: outMap }, trace }; +} + +export function runIntervalAnalysisFromFile( + inputPath: string, + outputDir = "output" +) { + const code = fs.readFileSync(inputPath, "utf-8"); + const parser = new TIPParser(); + const parsed = parser.parse(code); + if (!parsed.success) throw new Error(parsed.error || "Parse failed"); + const { graph, result, trace } = analyzeIntervals(parsed.ast!); + + const outJson: any = {}; + for (const [nid, env] of result.nodeOut.entries()) { + const entry: any = {}; + const toBound = (x: number) => + x === POS_INF ? "inf" : x === NEG_INF ? "-inf" : x; + for (const [k, v] of env.entries()) { + entry[k] = [toBound(v.lo), toBound(v.hi)]; + } + const nodeMeta = graph.nodes.get(nid)! as any; + outJson[nid] = { + func: nodeMeta?.funcName ?? null, + type: nodeMeta?.type ?? null, + label: nodeMeta?.label ?? null, + out: entry, + }; + } + const outPath = path.join(outputDir, "intervals.json"); + fs.writeFileSync(outPath, JSON.stringify(outJson, null, 2)); + const tracePath = path.join(outputDir, "intervals_trace.json"); + fs.writeFileSync(tracePath, JSON.stringify(trace, null, 2)); + return outPath; +} + +export default analyzeIntervals; diff --git a/midterm/interval-analysis_iteration.ts b/midterm/interval-analysis_iteration.ts new file mode 100644 index 0000000..5c99974 --- /dev/null +++ b/midterm/interval-analysis_iteration.ts @@ -0,0 +1,468 @@ +import TIPParser from "./parser"; +import { TIPICFGConverter, ControlFlowGraph } from "./tip-icfg-converter"; +import { + Program, + FunctionDeclaration, + Statement, + Expression, + FunctionCall, + Variable, + NumberLiteral, +} from "./types"; +import * as fs from "fs"; +import * as path from "path"; + +type Bound = number | typeof Infinity | typeof Number.NEGATIVE_INFINITY; + +interface Interval { + lo: number; // -Infinity allowed + hi: number; // +Infinity allowed +} + +const NEG_INF = Number.NEGATIVE_INFINITY; +const POS_INF = Number.POSITIVE_INFINITY; + +const Interval = { + top(): Interval { + return { lo: NEG_INF, hi: POS_INF }; + }, + bottom(): Interval { + return { lo: 1, hi: 0 }; // empty (lo>hi) + }, + isBottom(i: Interval): boolean { + return i.lo > i.hi; + }, + ofConst(n: number): Interval { + return { lo: n, hi: n }; + }, + join(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a)) return b; + if (Interval.isBottom(b)) return a; + return { lo: Math.min(a.lo, b.lo), hi: Math.max(a.hi, b.hi) }; + }, + equal(a: Interval, b: Interval): boolean { + return a.lo === b.lo && a.hi === b.hi; + }, + add(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + return { lo: a.lo + b.lo, hi: a.hi + b.hi }; + }, + sub(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + return { lo: a.lo - b.hi, hi: a.hi - b.lo }; + }, + mul(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + const candidates = [a.lo * b.lo, a.lo * b.hi, a.hi * b.lo, a.hi * b.hi]; + return { lo: Math.min(...candidates), hi: Math.max(...candidates) }; + }, + div(a: Interval, b: Interval): Interval { + if (Interval.isBottom(a) || Interval.isBottom(b)) return Interval.bottom(); + // conservative: if 0 in divisor, return Top + if (b.lo <= 0 && 0 <= b.hi) return Interval.top(); + const candidates = [a.lo / b.lo, a.lo / b.hi, a.hi / b.lo, a.hi / b.hi]; + return { lo: Math.min(...candidates), hi: Math.max(...candidates) }; + }, + widen(a: Interval, b: Interval): Interval { + // Standard interval widening: push bounds to infinities when they diverge + if (Interval.isBottom(a)) return b; + if (Interval.isBottom(b)) return a; + const lo = b.lo < a.lo ? NEG_INF : b.lo; + const hi = b.hi > a.hi ? POS_INF : b.hi; + return { lo, hi }; + }, + narrow(a: Interval, b: Interval): Interval { + // Standard interval narrowing: tighten bounds when possible + if (Interval.isBottom(a)) return b; + if (Interval.isBottom(b)) return a; + const lo = b.lo > a.lo ? b.lo : a.lo; + const hi = b.hi < a.hi ? b.hi : a.hi; + return { lo, hi }; + }, +}; + +type Env = Map; // namespaced `func:var` + +function ns(func: string, v: string): string { + return `${func}:${v}`; +} + +function cloneEnv(e: Env): Env { + const m = new Map(); + for (const [k, v] of e.entries()) m.set(k, { lo: v.lo, hi: v.hi }); + return m; +} + +function joinEnv(a: Env, b: Env): Env { + const out = new Map(a); + for (const [k, v] of b.entries()) { + const prev = out.get(k); + out.set(k, prev ? Interval.join(prev, v) : v); + } + return out; +} + +function envEqual(a: Env, b: Env): boolean { + if (a.size !== b.size) return false; + for (const [k, v] of a.entries()) { + const u = b.get(k); + if (!u || !Interval.equal(v, u)) return false; + } + return true; +} + +function widenEnv(oldEnv: Env, newEnv: Env): Env { + const out = new Map(); + const keys = new Set([...oldEnv.keys(), ...newEnv.keys()]); + for (const k of keys) { + const a = oldEnv.get(k); + const b = newEnv.get(k); + if (a && b) out.set(k, Interval.widen(a, b)); + else if (!a && b) out.set(k, b); + else if (a && !b) out.set(k, a); + } + return out; +} + +function narrowEnv(oldEnv: Env, newEnv: Env): Env { + const out = new Map(); + const keys = new Set([...oldEnv.keys(), ...newEnv.keys()]); + for (const k of keys) { + const a = oldEnv.get(k); + const b = newEnv.get(k); + if (a && b) out.set(k, Interval.narrow(a, b)); + else if (!a && b) out.set(k, b); + else if (a && !b) out.set(k, a); + } + return out; +} + +function evalExpr(expr: Expression, env: Env, funcName: string): Interval { + switch (expr.type) { + case "NumberLiteral": + return Interval.ofConst((expr as NumberLiteral).value); + case "Variable": { + const name = (expr as Variable).name; + return env.get(ns(funcName, name)) ?? Interval.top(); + } + case "BinaryExpression": { + const l = evalExpr(expr.left, env, funcName); + const r = evalExpr(expr.right, env, funcName); + switch (expr.operator) { + case "+": + return Interval.add(l, r); + case "-": + return Interval.sub(l, r); + case "*": + return Interval.mul(l, r); + case "/": + return Interval.div(l, r); + default: + return Interval.top(); + } + } + case "FunctionCall": + // 값 자체는 알 수 없으니 Top. 실제 반환은 callee에서 @ret로 제공됨 + return Interval.top(); + case "InputExpression": + return Interval.top(); + default: + return Interval.top(); + } +} + +interface AnalysisResult { + nodeIn: Map; + nodeOut: Map; +} + +export function analyzeIntervals(program: Program): { + graph: ControlFlowGraph; + result: AnalysisResult; + trace: any[]; +} { + const icfg = new TIPICFGConverter().convertProgramUnified(program); + + // 함수 이름 → 선언 매핑 + const funcMap = new Map(); + for (const f of program.functions) funcMap.set(f.name, f); + + // 워크리스트 초기화 + const inMap = new Map(); + const outMap = new Map(); + + // 인접 리스트 구성 + const preds = new Map(); + const succs = new Map(); + for (const e of icfg.edges) { + const s = succs.get(e.from) || []; + s.push({ to: e.to, label: e.label }); + succs.set(e.from, s); + const p = preds.get(e.to) || []; + p.push({ from: e.from, label: e.label }); + preds.set(e.to, p); + } + + // 진입 노드들: 각 함수 entry의 in을 Top으로 초기화 + for (const node of icfg.nodes.values()) { + if (node.type === "entry" && node.funcName) { + const env: Env = new Map(); + const f = funcMap.get(node.funcName); + if (f) { + const params = f.parameters.reduce( + (acc, arr) => acc.concat(arr), + [] as string[] + ); + for (const p of params) env.set(ns(f.name, p), Interval.top()); + if (f.localVariables) { + for (const vs of f.localVariables) { + for (const v of vs) { + env.set(ns(f.name, v), Interval.top()); + } + } + } + env.set(ns(f.name, "@ret"), Interval.bottom()); + } + inMap.set(node.id, env); + } + } + + const worklist: number[] = Array.from(icfg.nodes.keys()); + const visitCount = new Map(); + const trace: any[] = []; + + function transfer(nodeId: number, inEnv: Env): Env { + const node = icfg.nodes.get(nodeId)!; + const func = node.funcName || ""; + const env = cloneEnv(inEnv); + + // Node-level transfer (edge-level은 아래에서 처리) + if (node.statement) { + const stmt = node.statement as Statement; + switch (stmt.type) { + case "AssignmentStatement": { + // 단순 할당만 처리 (호출은 call/after-call로 분리됨) + const isCall = (stmt as any).expression?.type === "FunctionCall"; + if (!isCall) { + const val = evalExpr((stmt as any).expression, env, func); + env.set(ns(func, (stmt as any).variable), val); + } + break; + } + case "ReturnStatement": { + // 노드가 명시적 ReturnStatement를 가진 경우: 해당 식으로 @ret 설정 + const val = evalExpr((stmt as any).expression, env, func); + env.set(ns(func, "@ret"), val); + break; + } + default: + break; + } + } else if (node.type === "after-call" && node.statement) { + const stmt = node.statement as any; + if (stmt.type === "AssignmentStatement") { + // callee 의 @ret 값을 caller 변수에 복사 + // callee 이름은 전 단계 edge에서 확인되므로, edge transfer에서 처리되어 이미 env에 섞여 있음 + // 여기서는 가장 최근 ret 값을 사용 (보수적으로 join되어 있음) + // 어떤 callee인지 특정하지 않고, env의 모든 "*: @ret"을 join하여 사용 + let retVal: Interval | undefined; + for (const [k, v] of env.entries()) { + if (k.endsWith(":@ret")) + retVal = retVal ? Interval.join(retVal, v) : v; + } + env.set(ns(func, stmt.variable), retVal ?? Interval.top()); + } + } + + return env; + } + + function edgeTransfer(from: number, to: number, outEnv: Env): Env { + const edgeList = succs.get(from) || []; + const label = edgeList.find((e) => e.to === to)?.label; + if (!label) return outEnv; + if (label === "call") { + // 인자 → 파라미터 바인딩 + const callerNode = icfg.nodes.get(from)!; + const func = callerNode.funcName || ""; + const stmt = callerNode.statement as any; + const callExpr: FunctionCall | undefined = + stmt?.expression?.type === "FunctionCall" + ? (stmt.expression as FunctionCall) + : stmt?.expression; + if (!callExpr || callExpr.type !== "FunctionCall") return outEnv; + + // callee 이름 찾기 + let c: Expression = callExpr.callee; + while ((c as any).type === "FunctionCall") c = (c as any).callee; + if (c.type !== "Variable") return outEnv; + const calleeName = (c as Variable).name; + const fd = funcMap.get(calleeName); + if (!fd) return outEnv; + + const env = cloneEnv(outEnv); + // 파라미터 초기화 (string[][] → string[]) + const params = fd.parameters.reduce( + (acc, arr) => acc.concat(arr), + [] as string[] + ); + for (let i = 0; i < params.length; i++) { + const p = params[i]; + const argExpr = callExpr.arguments[i]; + const val = argExpr ? evalExpr(argExpr, outEnv, func) : Interval.top(); + env.set(ns(calleeName, p), val); + } + // 로컬 변수는 Top으로 유지, @ret 초기화 + if (fd.localVariables) { + for (const v of fd.localVariables) { + for (const vv of v) { + env.set( + ns(calleeName, vv), + env.get(ns(calleeName, vv)) ?? Interval.top() + ); + } + } + } + env.set( + ns(calleeName, "@ret"), + env.get(ns(calleeName, "@ret")) ?? Interval.bottom() + ); + return env; + } + // return 엣지는 특별 처리 불필요 (callee 내부에서 @ret 설정됨) + return outEnv; + } + + while (worklist.length) { + const n = worklist.shift()!; + const cnt = (visitCount.get(n) || 0) + 1; + visitCount.set(n, cnt); + // in[n] = join_{p in preds[n]} edgeTransfer(p->n, out[p]) + let inEnv: Env | undefined = inMap.get(n); + const ps = preds.get(n) || []; + let accum: Env | undefined = undefined; + for (const { from, label } of ps) { + const outEnv = outMap.get(from) || new Map(); + const afterEdge = edgeTransfer(from, n, outEnv); + accum = accum ? joinEnv(accum, afterEdge) : cloneEnv(afterEdge); + } + if (accum) inEnv = inEnv ? joinEnv(inEnv, accum) : accum; + if (!inEnv) inEnv = new Map(); + inMap.set(n, inEnv); + + const rawOut = transfer(n, inEnv); + const oldOut = outMap.get(n); + const newOut = oldOut && cnt >= 2 ? widenEnv(oldOut, rawOut) : rawOut; + // newOut is not equal to oldOut -> Fixed point가 아닌경우 + if (!oldOut || !envEqual(oldOut, newOut)) { + // trace entry (갱신 시점만 기록) + const toBound = (x: number) => + x === POS_INF ? "inf" : x === NEG_INF ? "-inf" : x; + const ser = (env: Env | undefined) => { + if (!env) return null; + const o: any = {}; + for (const [k, v] of env.entries()) + o[k] = [toBound(v.lo), toBound(v.hi)]; + return o; + }; + const nodeMeta = icfg.nodes.get(n)!; + trace.push({ + node: n, + visit: cnt, + func: nodeMeta.funcName || null, + type: nodeMeta.type, + label: nodeMeta.label, + in: ser(inEnv), + rawOut: ser(rawOut), + oldOut: ser(oldOut), + newOut: ser(newOut), + widened: !!oldOut && cnt >= 2, + }); + outMap.set(n, newOut); + for (const s of succs.get(n) || []) worklist.push(s.to); + } + } + + // Narrowing phase: limited iterations to refine widened bounds + const narrowingIterations = 2; + for (let iter = 1; iter <= narrowingIterations; iter++) { + const q: number[] = Array.from(icfg.nodes.keys()); + while (q.length) { + const n = q.shift()!; + // recompute IN with current OUTs + let inEnv: Env | undefined = inMap.get(n); + const ps = preds.get(n) || []; + let accum: Env | undefined = undefined; + for (const { from } of ps) { + const outEnv = outMap.get(from) || new Map(); + const afterEdge = edgeTransfer(from, n, outEnv); + accum = accum ? joinEnv(accum, afterEdge) : cloneEnv(afterEdge); + } + if (accum) inEnv = inEnv ? joinEnv(inEnv, accum) : accum; + if (!inEnv) inEnv = new Map(); + inMap.set(n, inEnv); + + const rawOut = transfer(n, inEnv); + const oldOut = outMap.get(n) || new Map(); + const newOut = narrowEnv(oldOut, rawOut); + if (!envEqual(oldOut, newOut)) { + const toBound = (x: number) => + x === POS_INF ? "inf" : x === NEG_INF ? "-inf" : x; + const ser = (env: Env | undefined) => { + if (!env) return null; + const o: any = {}; + for (const [k, v] of env.entries()) + o[k] = [toBound(v.lo), toBound(v.hi)]; + return o; + }; + const nodeMeta = icfg.nodes.get(n)!; + trace.push({ + node: n, + visit: `narrow-${iter}`, + func: nodeMeta.funcName || null, + type: nodeMeta.type, + label: nodeMeta.label, + in: ser(inEnv), + rawOut: ser(rawOut), + oldOut: ser(oldOut), + newOut: ser(newOut), + widened: false, + narrowed: true, + }); + outMap.set(n, newOut); + for (const s of succs.get(n) || []) q.push(s.to); + } + } + } + + return { graph: icfg, result: { nodeIn: inMap, nodeOut: outMap }, trace }; +} + +export function runIntervalAnalysisFromFile( + inputPath: string, + outputDir = "output" +) { + const code = fs.readFileSync(inputPath, "utf-8"); + const parser = new TIPParser(); + const parsed = parser.parse(code); + if (!parsed.success) throw new Error(parsed.error || "Parse failed"); + const { graph, result, trace } = analyzeIntervals(parsed.ast!); + + const outJson: any = {}; + for (const [nid, env] of result.nodeOut.entries()) { + const entry: any = {}; + for (const [k, v] of env.entries()) { + const toBound = (x: number) => + x === POS_INF ? "inf" : x === NEG_INF ? "-inf" : x; + entry[k] = [toBound(v.lo), toBound(v.hi)]; + } + outJson[nid] = entry; + } + const outPath = path.join(outputDir, "intervals.json"); + fs.writeFileSync(outPath, JSON.stringify(outJson, null, 2)); + const tracePath = path.join(outputDir, "intervals_trace.json"); + fs.writeFileSync(tracePath, JSON.stringify(trace, null, 2)); + return outPath; +} + +export default analyzeIntervals; diff --git a/midterm/package-lock.json b/midterm/package-lock.json new file mode 100644 index 0000000..ef4903d --- /dev/null +++ b/midterm/package-lock.json @@ -0,0 +1,838 @@ +{ + "name": "2-week", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "2-week", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ast-flow-graph": "^1.0.18", + "ohm-js": "^17.2.1" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz", + "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ast-flow-graph": { + "version": "1.0.18", + "resolved": "https://registry.npmjs.org/ast-flow-graph/-/ast-flow-graph-1.0.18.tgz", + "integrity": "sha512-VF7YZ7xDWbO1mXAxpNa47DAc92sxHZK9AmmbEP2lNvOHlvRS4J/+rkhhMG3i91Zw6V1XHW34r+gY8BgB40pB2Q==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-table3": "^0.5.1", + "command-line-args": "^5.1.1", + "command-line-usage": "^6.0.2", + "dominators": "^1.1.2", + "escope": "^3.6.0", + "esm": "^3.2.25", + "espree": "^6.0.0", + "estraverse": "^4.2.0", + "traversals": "^1.0.15", + "yallist": "^3.0.3" + }, + "bin": { + "ast-flow-graph": "_cli.js" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-table3": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", + "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.0", + "string-width": "^2.1.1" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "colors": "^1.1.2" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.3.tgz", + "integrity": "sha512-sH5ZSPr+7UStsloltmDh7Ce5fb8XPlHyoPzTpyyMuYCtervL65+ubVZ6Q61cFtFl62UyJlc8/JwERRbAFPUqgw==", + "license": "MIT", + "dependencies": { + "array-back": "^4.0.2", + "chalk": "^2.4.2", + "table-layout": "^1.0.2", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "license": "ISC", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dominators": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/dominators/-/dominators-1.1.2.tgz", + "integrity": "sha512-WC0Sv+xQYMp18SRlpSd7VJ5kTXM6CdXe2LGTW0uI+1eTVpTqtImLEleQy/CpZ8Yf52Ft3hYwu2ErNfZxHJJ27Q==", + "license": "MIT", + "dependencies": { + "traversals": "^1.0.15" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "license": "ISC", + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha512-mz3UqCh0uPCIqsw1SSAkB/p0rOzF/M0V++vyN7JqlPtSW/VsYgQBvVvqMLmfBuyMzTpLnNqi6JmcSizs4jy19A==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "node_modules/es6-set": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.6.tgz", + "integrity": "sha512-TE3LgGLDIBX332jq3ypv6bcOpkLO0AslAQo7p2VqX/1N46YNsvIWgvjojjSEnWEGWMhr1qUbYeTSir5J6mFHOw==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "es6-iterator": "~2.0.3", + "es6-symbol": "^3.1.3", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "license": "ISC", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escope": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", + "integrity": "sha512-75IUQsusDdalQEW/G/2esa87J7raqdJF+Ca0/Xm5C3Q58Nr4yVYjZGp/P1+2xiEVgXRrA39dpRb8LcshajbqDQ==", + "license": "BSD-2-Clause", + "dependencies": { + "es6-map": "^0.1.3", + "es6-weak-map": "^2.0.1", + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "license": "ISC", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/espree": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", + "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^7.1.1", + "acorn-jsx": "^5.2.0", + "eslint-visitor-keys": "^1.1.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "license": "MIT", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "license": "ISC", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", + "license": "ISC" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ohm-js": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/ohm-js/-/ohm-js-17.2.1.tgz", + "integrity": "sha512-4cXF0G09fAYU9z61kTfkNbKK1Kz/sGEZ5NbVWHoe9Qi7VB7y+Spwk051CpUTfUENdlIr+vt8tMV4/LosTE2cDQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.1" + } + }, + "node_modules/reduce-flatten": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/reduce-flatten/-/reduce-flatten-2.0.0.tgz", + "integrity": "sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/table-layout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-1.0.2.tgz", + "integrity": "sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==", + "license": "MIT", + "dependencies": { + "array-back": "^4.0.1", + "deep-extend": "~0.6.0", + "typical": "^5.2.0", + "wordwrapjs": "^4.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-4.0.2.tgz", + "integrity": "sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/table-layout/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/traversals": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/traversals/-/traversals-1.0.15.tgz", + "integrity": "sha512-bYKOp6AEc8uaYJnYuHm2sYh7g9v3Mv2UyP7tXNSIJPJGtH1M4gjNl6FBqLV+UOl2MrZaGK42sdTcL9fG+0E0YQ==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", + "license": "ISC" + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wordwrapjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-4.0.1.tgz", + "integrity": "sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==", + "license": "MIT", + "dependencies": { + "reduce-flatten": "^2.0.0", + "typical": "^5.2.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/wordwrapjs/node_modules/typical": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-5.2.0.tgz", + "integrity": "sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/midterm/package.json b/midterm/package.json new file mode 100644 index 0000000..e8893ae --- /dev/null +++ b/midterm/package.json @@ -0,0 +1,24 @@ +{ + "name": "midterm", + "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "kyungmink", + "type": "commonjs", + "main": "index.ts", + "scripts": { + "parser": "ts-node parser.ts", + "cfg": "ts-node cfg.ts", + "normal": "ts-node normal.ts", + "all": "ts-node parser.ts && ts-node cfg.ts && ts-node normal.ts", + "tip-all": "ts-node tip-all-in-one.ts" + }, + "devDependencies": { + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + }, + "dependencies": { + "ast-flow-graph": "^1.0.18", + "ohm-js": "^17.2.1" + } +} diff --git a/midterm/parser.ts b/midterm/parser.ts new file mode 100644 index 0000000..a259741 --- /dev/null +++ b/midterm/parser.ts @@ -0,0 +1,369 @@ +import * as ohm from "ohm-js"; +import * as fs from "fs"; +import * as path from "path"; +import { + Program, + FunctionDeclaration, + Statement, + Expression, + AssignmentStatement, + OutputStatement, + SequenceStatement, + IfStatement, + WhileStatement, + ReturnStatement, + AssertStatement, + NumberLiteral, + Variable, + BinaryExpression, + FunctionCall, + InputExpression, + ParseResult, + ParseOptions, +} from "./types"; + +export class TIPParser { + private grammar: ohm.Grammar; + private semantics!: ohm.Semantics; // definite assignment assertion + + constructor() { + // 문법 파일 로드 + const grammarSource = fs.readFileSync( + path.join(__dirname, "grammar.ohm"), + "utf-8" + ); + this.grammar = ohm.grammar(grammarSource); + this.setupSemantics(); + } + + private setupSemantics() { + this.semantics = this.grammar.createSemantics(); + + this.semantics.addOperation("toAST", { + // 프로그램 + Program(functions) { + return { + type: "Program", + functions: functions.toAST(), + } as Program; + }, + + // 함수 선언 + Function( + name, + _lparen, + params, + _rparen, + _lbrace, + varDecl, + statements, + returnStmt, + _rbrace + ) { + const paramList = params.numChildren > 0 ? params.toAST() : []; + const localVars = varDecl.numChildren > 0 ? varDecl.toAST() : undefined; + const stmtList = statements.toAST(); + const bodyStmt = + stmtList.length > 0 + ? stmtList.length === 1 + ? stmtList[0] + : ({ + type: "SequenceStatement", + statements: stmtList, + } as SequenceStatement) + : ({ + type: "SequenceStatement", + statements: [], + } as SequenceStatement); + + return { + type: "FunctionDeclaration", + name: name.sourceString, + parameters: paramList, + localVariables: localVars, + body: bodyStmt, + returnExpression: returnStmt.toAST(), + } as FunctionDeclaration; + }, + ReturnStmt(_return, expr, _semi) { + // ReturnStmt는 최종 리턴 식만 반환(Statement 노드가 아님) + return expr.toAST(); + }, + + // 매개변수 목록 + Params(first, _commas, rest) { + return [ + first.sourceString, + ...rest.children.map((p: any) => p.sourceString), + ]; + }, + + // 변수 선언 + VarDecl(_var, first, _commas, rest, _semi) { + return [ + first.sourceString, + ...rest.children.map((v: any) => v.sourceString), + ]; + }, + + // 구문들 + Statement(stmt) { + return stmt.toAST(); + }, + + CallStmt(id, _lparen, args, _rparen, _semi) { + if (id.sourceString === "assert") { + return { + type: "AssertStatement", + condition: args.children[0].toAST()[0], + } as AssertStatement; + } else { + const argList = args.numChildren > 0 ? args.toAST() : []; + return { + type: "CallStatement", + expression: { + type: "FunctionCall", + callee: { + type: "Variable", + name: id.sourceString, + }, + arguments: argList, + }, + }; + } + }, + + AssignmentStmt(variable, _eq, expr, _semi) { + return { + type: "AssignmentStatement", + variable: variable.sourceString, + expression: expr.toAST(), + } as AssignmentStatement; + }, + + OutputStmt(_output, expr, _semi) { + return { + type: "OutputStatement", + expression: expr.toAST(), + } as OutputStatement; + }, + + IfStmt(_if, _lparen, condition, _rparen, thenBlock, elseClause) { + const elseStatement = + elseClause.numChildren > 0 ? elseClause.toAST() : undefined; + return { + type: "IfStatement", + condition: condition.toAST(), + thenStatement: thenBlock.toAST(), + elseStatement, + } as IfStatement; + }, + + ElseClause(_else, elseBlock) { + return elseBlock.toAST(); + }, + + WhileStmt(_while, _lparen, condition, _rparen, body) { + return { + type: "WhileStatement", + condition: condition.toAST(), + body: body.toAST(), + } as WhileStatement; + }, + + Block(_lbrace, statements, _rbrace) { + const stmts = statements.toAST(); + if (stmts.length === 0) { + return { + type: "SequenceStatement", + statements: [], + } as SequenceStatement; + } else if (stmts.length === 1) { + return stmts[0]; + } else { + return { + type: "SequenceStatement", + statements: stmts, + } as SequenceStatement; + } + }, + + BlockStatement(stmt) { + return stmt.toAST(); + }, + + // 함수 내부 조기 return은 문법상 제거됨 + + // 표현식들 + ComparisonExpr_greater(left, _op, right) { + return { + type: "BinaryExpression", + operator: ">", + left: left.toAST(), + right: right.toAST(), + } as BinaryExpression; + }, + + ComparisonExpr_equal(left, _op, right) { + return { + type: "BinaryExpression", + operator: "==", + left: left.toAST(), + right: right.toAST(), + } as BinaryExpression; + }, + + ArithExpr_add(left, _op, right) { + return { + type: "BinaryExpression", + operator: "+", + left: left.toAST(), + right: right.toAST(), + } as BinaryExpression; + }, + + ArithExpr_sub(left, _op, right) { + return { + type: "BinaryExpression", + operator: "-", + left: left.toAST(), + right: right.toAST(), + } as BinaryExpression; + }, + + MulExpr_mul(left, _op, right) { + return { + type: "BinaryExpression", + operator: "*", + left: left.toAST(), + right: right.toAST(), + } as BinaryExpression; + }, + + MulExpr_div(left, _op, right) { + return { + type: "BinaryExpression", + operator: "/", + left: left.toAST(), + right: right.toAST(), + } as BinaryExpression; + }, + + PrimaryExpr_input(_input) { + return { + type: "InputExpression", + } as InputExpression; + }, + + PrimaryExpr_paren(_lparen, expr, _rparen) { + return expr.toAST(); + }, + + PrimaryExpr_number(num) { + return num.toAST(); + }, + + PrimaryExpr_identifier(id) { + return id.toAST(); + }, + + FunctionCallOrAccess_call(callee, _lparen, args, _rparen) { + const argList = args.numChildren > 0 ? args.toAST() : []; + return { + type: "FunctionCall", + callee: callee.toAST(), + arguments: argList, + } as FunctionCall; + }, + + FunctionCallOrAccess_base(id) { + return { + type: "Variable", + name: id.sourceString, + } as Variable; + }, + + // CallStmt 에서 받은 형태는 FunctionCallOrAccess '(' Args? ')' ';' 이므로 + // CallStmt에서 이미 expr.toAST()가 함수 호출로 온다고 가정 + + Args(first, _commas, rest) { + return [first.toAST(), ...rest.children.map((arg: any) => arg.toAST())]; + }, + + // 기본 타입들 + number(_digits) { + return { + type: "NumberLiteral", + value: parseInt(this.sourceString, 10), + } as NumberLiteral; + }, + + identifier(_letter, _rest) { + return { + type: "Variable", + name: this.sourceString, + } as Variable; + }, + + // 기본 처리 + _terminal() { + return this.sourceString; + }, + + _iter(...children) { + return children.map((child) => child.toAST()); + }, + }); + } + + parse(source: string, options: ParseOptions = {}): ParseResult { + try { + const matchResult = this.grammar.match(source); + + if (matchResult.failed()) { + const error = matchResult.message; + const errorInfo = matchResult.getInterval(); + + return { + success: false, + error: `Parse error: ${error}`, + errorLocation: { + line: errorInfo.startIdx, // Ohm.js는 인덱스만 제공하므로 실제 라인/컬럼 계산 필요 + column: 0, + }, + }; + } + + const ast = this.semantics(matchResult).toAST() as Program; + + return { + success: true, + ast, + }; + } catch (error) { + return { + success: false, + error: `Unexpected error: ${ + error instanceof Error ? error.message : String(error) + }`, + }; + } + } + + // 편의 함수: 파일에서 파싱 + parseFile(filePath: string, options: ParseOptions = {}): ParseResult { + try { + const source = fs.readFileSync(filePath, "utf-8"); + return this.parse(source, options); + } catch (error) { + return { + success: false, + error: `Failed to read file: ${ + error instanceof Error ? error.message : String(error) + }`, + }; + } + } +} + +export default TIPParser; diff --git a/midterm/test/1. widening_narrowing.txt b/midterm/test/1. widening_narrowing.txt new file mode 100644 index 0000000..7e73b48 --- /dev/null +++ b/midterm/test/1. widening_narrowing.txt @@ -0,0 +1,12 @@ +main() { + var x, y; + y = 0; + x = 7; + x = x+1; + while(input) { + x = 7; + x = x+1; + y = y+1; + } + return x; +} diff --git a/midterm/test/2-1. control_sensitive.txt b/midterm/test/2-1. control_sensitive.txt new file mode 100644 index 0000000..27ac12c --- /dev/null +++ b/midterm/test/2-1. control_sensitive.txt @@ -0,0 +1,17 @@ +main() { + var x, y, z; + x = input; + y = 0; + z = 0; + while (x > 0) { + assert(x > 0); + z = z+x; + if (17 > y) { + assert(17 > y); + y = y+1; + } + x = x-1; + } + assert(1 > x); + return x; +} diff --git a/midterm/test/2-2. control_sensitive_without_assert.txt b/midterm/test/2-2. control_sensitive_without_assert.txt new file mode 100644 index 0000000..6c3e775 --- /dev/null +++ b/midterm/test/2-2. control_sensitive_without_assert.txt @@ -0,0 +1,14 @@ +main() { + var x, y, z; + x = input; + y = 0; + z = 0; + while (x > 0) { + z = z+x; + if (17 > y) { + y = y+1; + } + x = x-1; + } + return x; +} diff --git a/midterm/test/3. interprocedural.txt b/midterm/test/3. interprocedural.txt new file mode 100644 index 0000000..a9832d6 --- /dev/null +++ b/midterm/test/3. interprocedural.txt @@ -0,0 +1,11 @@ +foo(x) { + return x; +} + +main() { + var x, y; + x = foo(3); + y = foo(10); + foo(2); + return x + y; +} diff --git a/midterm/tip-all-in-one.ts b/midterm/tip-all-in-one.ts new file mode 100644 index 0000000..2be30ae --- /dev/null +++ b/midterm/tip-all-in-one.ts @@ -0,0 +1,218 @@ +import TIPParser from "./parser"; +import { TIPCFGConverter } from "./tip-cfg-converter"; +import { TIPICFGConverter } from "./tip-icfg-converter"; +import { runIntervalAnalysisFromFile } from "./interval-analysis"; +import * as fs from "fs"; +import * as path from "path"; +import { execSync } from "child_process"; + +// 색상 출력을 위한 ANSI 코드 +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", +}; + +function colorLog(color: keyof typeof colors, message: string) { + console.log(`${colors[color]}${message}${colors.reset}`); +} + +function checkGraphvizInstalled(): boolean { + try { + execSync("dot -V", { stdio: "ignore" }); + return true; + } catch { + return false; + } +} + +async function processAllTIP() { + colorLog("cyan", "🚀 === TIP 통합 처리 시작 ===\n"); + + // 출력 폴더 생성 + const outputDir = "output"; + const cfgDir = path.join(outputDir, "cfg"); + const icfgDir = path.join(outputDir, "icfg"); + + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + colorLog("blue", `📁 출력 폴더 생성: ${outputDir}/`); + } + if (!fs.existsSync(cfgDir)) { + fs.mkdirSync(cfgDir, { recursive: true }); + colorLog("blue", `📁 CFG 폴더 생성: ${cfgDir}/`); + } + if (!fs.existsSync(icfgDir)) { + fs.mkdirSync(icfgDir, { recursive: true }); + colorLog("blue", `📁 ICFG 폴더 생성: ${icfgDir}/`); + } + + // 1. tip_code.txt 파일 읽기 + const inputFile = "tip_code.txt"; + if (!fs.existsSync(inputFile)) { + colorLog("red", `❌ 오류: ${inputFile} 파일이 존재하지 않습니다.`); + colorLog( + "yellow", + "💡 tip_code.txt 파일을 생성하고 TIP 코드를 입력해주세요." + ); + return; + } + + const tipCode = fs.readFileSync(inputFile, "utf-8").trim(); + if (!tipCode) { + colorLog("red", `❌ 오류: ${inputFile} 파일이 비어있습니다.`); + return; + } + + colorLog("green", `✅ TIP 코드 읽기 완료 (${inputFile})`); + colorLog("blue", "--- TIP 코드 내용 ---"); + console.log(tipCode); + console.log(""); + + // 2. TIP 코드 파싱 (AST 생성) + colorLog("yellow", "🔍 1단계: TIP 코드 파싱 및 AST 생성..."); + const parser = new TIPParser(); + const parseResult = parser.parse(tipCode); + + if (!parseResult.success) { + colorLog("red", `❌ 파싱 실패: ${parseResult.error}`); + return; + } + + colorLog("green", "✅ AST 생성 완료"); + + // AST를 JSON 파일로 저장 + const astJson = JSON.stringify(parseResult.ast, null, 2); + const astFile = path.join(outputDir, "ast.json"); + fs.writeFileSync(astFile, astJson); + colorLog("blue", `📄 AST 저장: ${astFile}`); + + // 3. CFG 생성 + colorLog("yellow", "\n🔄 2단계: CFG 생성..."); + const cfgConverter = new TIPCFGConverter(); + const cfgs = cfgConverter.convertProgram(parseResult.ast!); + + colorLog("green", `✅ CFG 생성 완료 (${cfgs.size}개 함수)`); + + // CFG DOT 파일들 생성 + const cfgFiles: string[] = []; + for (const [funcName, cfg] of cfgs.entries()) { + const dotContent = cfg.toDot(funcName); + const dotFileName = path.join(cfgDir, `${funcName}.dot`); + fs.writeFileSync(dotFileName, dotContent); + cfgFiles.push(dotFileName); + colorLog("blue", `📄 CFG DOT 파일: ${dotFileName}`); + } + + // 4. ICFG 생성 (단일 그래프) + colorLog("yellow", "\n🔄 3단계: ICFG 생성..."); + const icfgConverter = new TIPICFGConverter(); + const unifiedICFG = icfgConverter.convertProgramUnified(parseResult.ast!); + colorLog("green", `✅ ICFG 생성 완료 (단일 그래프)`); + + // ICFG 단일 DOT 파일 생성 + const icfgFiles: string[] = []; + const icfgCombinedFile = path.join(icfgDir, `main.dot`); + fs.writeFileSync(icfgCombinedFile, unifiedICFG.toDot("ICFG")); + icfgFiles.push(icfgCombinedFile); + colorLog("blue", `📄 ICFG DOT 파일: ${icfgCombinedFile}`); + + // 4.5 Interval Analysis (Monotone Framework, widening/narrowing 미적용) + colorLog("yellow", "\n🧮 3.5단계: Interval Analysis..."); + try { + const intervalsPath = runIntervalAnalysisFromFile(inputFile, outputDir); + colorLog("green", `✅ Interval 결과 저장: ${intervalsPath}`); + } catch (e) { + colorLog( + "red", + `❌ Interval 분석 실패: ${e instanceof Error ? e.message : String(e)}` + ); + } + + // 5. Graphviz 설치 확인 및 PDF 변환 + colorLog("yellow", "\n🖼️ 4단계: PDF 변환..."); + + if (!checkGraphvizInstalled()) { + colorLog("red", "❌ Graphviz가 설치되지 않았습니다."); + colorLog("yellow", "💡 설치 방법:"); + colorLog("yellow", " macOS: brew install graphviz"); + colorLog("yellow", " Ubuntu: sudo apt-get install graphviz"); + colorLog("yellow", " Windows: https://graphviz.org/download/"); + colorLog( + "blue", + "\n📄 DOT 파일들이 생성되었습니다. Graphviz 설치 후 다음 명령어로 PDF 변환 가능:" + ); + [...cfgFiles, ...icfgFiles].forEach((file) => { + const pdfFile = file.replace(".dot", ".pdf"); + colorLog("blue", ` dot -Tpdf ${file} -o ${pdfFile}`); + }); + return; + } + + colorLog("green", "✅ Graphviz 설치 확인됨"); + + // CFG/ICFG PDF 변환 + colorLog("blue", "🔄 CFG PDF 변환 중..."); + for (const dotFile of [...cfgFiles, ...icfgFiles]) { + try { + const pdfFile = dotFile.replace(".dot", ".pdf"); + execSync(`dot -Tpdf "${dotFile}" -o "${pdfFile}"`, { stdio: "ignore" }); + colorLog("green", `✅ ${pdfFile} 생성 완료`); + } catch (error) { + colorLog("red", `❌ ${dotFile} PDF 변환 실패: ${error}`); + } + } + + // 6. 결과 요약 + colorLog("cyan", "\n🎉 === 처리 완료 ==="); + colorLog("green", `생성된 파일들:`); + + colorLog("blue", "\n📊 AST:"); + colorLog("blue", ` - ${astFile}`); + + colorLog("blue", "\n📈 CFG (cfg/ 폴더):"); + cfgFiles.forEach((file) => { + colorLog("blue", ` - ${file}`); + const pdfFile = file.replace(".dot", ".pdf"); + if (fs.existsSync(pdfFile)) { + colorLog("blue", ` - ${pdfFile}`); + } + }); + + colorLog("blue", "\n📉 ICFG (icfg/ 폴더):"); + icfgFiles.forEach((file) => { + colorLog("blue", ` - ${file}`); + const pdfFile = file.replace(".dot", ".pdf"); + if (fs.existsSync(pdfFile)) { + colorLog("blue", ` - ${pdfFile}`); + } + }); + + colorLog("cyan", "\n✨ 모든 처리가 완료되었습니다!"); +} + +// 에러 처리 +process.on("uncaughtException", (error) => { + colorLog("red", `❌ 예상치 못한 오류: ${error.message}`); + process.exit(1); +}); + +process.on("unhandledRejection", (reason) => { + colorLog("red", `❌ 처리되지 않은 Promise 거부: ${reason}`); + process.exit(1); +}); + +// 메인 실행 +if (require.main === module) { + processAllTIP().catch((error) => { + colorLog("red", `❌ 실행 오류: ${error.message}`); + process.exit(1); + }); +} + +export default processAllTIP; diff --git a/midterm/tip-cfg-converter.ts b/midterm/tip-cfg-converter.ts new file mode 100644 index 0000000..33a32be --- /dev/null +++ b/midterm/tip-cfg-converter.ts @@ -0,0 +1,403 @@ +import { TIPParser } from "./parser"; +import { + Program, + FunctionDeclaration, + Statement, + Expression, + SequenceStatement, + IfStatement, + WhileStatement, +} from "./types"; +import * as fs from "fs"; + +// CFG 노드 타입 +interface CFGNode { + id: number; + label: string; + type: "entry" | "exit" | "statement" | "condition" | "merge"; + statement?: Statement; + expression?: Expression; +} + +// CFG 엣지 타입 +interface CFGEdge { + from: number; + to: number; + label?: string; // "true", "false" 등 +} + +// CFG 클래스 +class ControlFlowGraph { + nodes: Map = new Map(); + edges: CFGEdge[] = []; + private nextId = 0; + + addNode( + label: string, + type: CFGNode["type"], + statement?: Statement, + expression?: Expression + ): CFGNode { + const node: CFGNode = { + id: this.nextId++, + label, + type, + statement, + expression, + }; + this.nodes.set(node.id, node); + return node; + } + + addEdge(from: number, to: number, label?: string) { + this.edges.push({ from, to, label }); + } + + // DOT 파일 생성 + toDot(functionName: string): string { + let dot = `digraph "${functionName}" {\n`; + dot += " node [shape=box];\n"; + + // 노드 정의 + for (const node of this.nodes.values()) { + const shape = node.type === "condition" ? "diamond" : "box"; + const color = + node.type === "entry" + ? "green" + : node.type === "exit" + ? "red" + : "lightblue"; + + dot += ` ${node.id} [label="${this.escapeLabel( + node.label + )}", shape=${shape}, fillcolor=${color}, style=filled];\n`; + } + + // 엣지 정의 + for (const edge of this.edges) { + const label = edge.label ? ` [label="${edge.label}"]` : ""; + dot += ` ${edge.from} -> ${edge.to}${label};\n`; + } + + dot += "}\n"; + return dot; + } + + private escapeLabel(label: string): string { + return label.replace(/"/g, '\\"').replace(/\n/g, "\\n"); + } +} + +// TIP AST를 CFG로 변환하는 클래스 +class TIPCFGConverter { + convertProgram(program: Program): Map { + const cfgs = new Map(); + + for (const func of program.functions) { + const cfg = this.convertFunction(func); + cfgs.set(func.name, cfg); + } + + return cfgs; + } + + convertFunction(func: FunctionDeclaration): ControlFlowGraph { + const cfg = new ControlFlowGraph(); + + // Entry 노드 + const entryNode = cfg.addNode(`Entry: ${func.name}`, "entry"); + + // 함수 본문 변환 + const { entryId, exitIds } = this.convertStatement(cfg, func.body); + + // Entry에서 함수 본문으로 연결 + cfg.addEdge(entryNode.id, entryId); + + // Return 문 처리 + const returnNode = cfg.addNode( + `return ${this.expressionToString(func.returnExpression)}`, + "statement" + ); + + // 모든 exit에서 return으로 연결 + for (const exitId of exitIds) { + cfg.addEdge(exitId, returnNode.id); + } + + // Exit 노드 + const exitNode = cfg.addNode(`Exit: ${func.name}`, "exit"); + cfg.addEdge(returnNode.id, exitNode.id); + + return cfg; + } + + convertStatement( + cfg: ControlFlowGraph, + stmt: Statement + ): { entryId: number; exitIds: number[] } { + switch (stmt.type) { + case "AssignmentStatement": + const assignNode = cfg.addNode( + `${stmt.variable} = ${this.expressionToString(stmt.expression)}`, + "statement", + stmt + ); + return { entryId: assignNode.id, exitIds: [assignNode.id] }; + + case "OutputStatement": + const outputNode = cfg.addNode( + `output ${this.expressionToString(stmt.expression)}`, + "statement", + stmt + ); + return { entryId: outputNode.id, exitIds: [outputNode.id] }; + + case "SequenceStatement": + return this.convertSequence(cfg, stmt); + + case "IfStatement": + return this.convertIf(cfg, stmt); + + case "WhileStatement": + return this.convertWhile(cfg, stmt); + + case "ReturnStatement": + const returnNode = cfg.addNode( + `return ${this.expressionToString(stmt.expression)}`, + "statement", + stmt + ); + return { entryId: returnNode.id, exitIds: [returnNode.id] }; + + case "CallStatement": + const callNode = cfg.addNode( + `${this.expressionToString(stmt.expression)}`, + "statement", + stmt + ); + return { entryId: callNode.id, exitIds: [callNode.id] }; + + case "AssertStatement": + // assert는 실행 중단을 일으키지 않으며, 분석 단계에서 제약으로 처리 + const assertNode = cfg.addNode( + `assert(${this.expressionToString(stmt.condition)})`, + "statement", + stmt, + stmt.condition + ); + return { entryId: assertNode.id, exitIds: [assertNode.id] }; + + default: + const unknownNode = cfg.addNode( + `Unknown: ${(stmt as any).type}`, + "statement", + stmt + ); + return { entryId: unknownNode.id, exitIds: [unknownNode.id] }; + } + } + + convertSequence( + cfg: ControlFlowGraph, + stmt: SequenceStatement + ): { entryId: number; exitIds: number[] } { + if (stmt.statements.length === 0) { + const emptyNode = cfg.addNode("(empty)", "statement"); + return { entryId: emptyNode.id, exitIds: [emptyNode.id] }; + } + + let currentExitIds: number[] = []; + let entryId: number | undefined; + + for (let i = 0; i < stmt.statements.length; i++) { + const { entryId: stmtEntry, exitIds: stmtExits } = this.convertStatement( + cfg, + stmt.statements[i] + ); + + if (i === 0) { + entryId = stmtEntry; + } else { + // 이전 구문의 모든 exit에서 현재 구문의 entry로 연결 + for (const exitId of currentExitIds) { + cfg.addEdge(exitId, stmtEntry); + } + } + + currentExitIds = stmtExits; + } + + return { entryId: entryId!, exitIds: currentExitIds }; + } + + convertIf( + cfg: ControlFlowGraph, + stmt: IfStatement + ): { entryId: number; exitIds: number[] } { + // 조건 노드 + const conditionNode = cfg.addNode( + this.expressionToString(stmt.condition), + "condition", + undefined, + stmt.condition + ); + + // Then 분기 + const { entryId: thenEntry, exitIds: thenExits } = this.convertStatement( + cfg, + stmt.thenStatement + ); + cfg.addEdge(conditionNode.id, thenEntry, "true"); + + let allExitIds = [...thenExits]; + + // Else 분기 (선택적) + if (stmt.elseStatement) { + // elseStatement가 배열인 경우 처리 + let elseStmt: Statement | undefined = stmt.elseStatement; + if (Array.isArray(elseStmt)) { + if (elseStmt.length === 1) { + elseStmt = elseStmt[0]; + } else if (elseStmt.length > 1) { + elseStmt = { + type: "SequenceStatement", + statements: elseStmt, + } as any; + } else { + elseStmt = undefined; + } + } + + if (elseStmt) { + const { entryId: elseEntry, exitIds: elseExits } = + this.convertStatement(cfg, elseStmt); + cfg.addEdge(conditionNode.id, elseEntry, "false"); + allExitIds.push(...elseExits); + } else { + // else가 비어있으면 조건이 false일 때 바로 다음으로 + const falseNode = cfg.addNode("(skip)", "statement"); + cfg.addEdge(conditionNode.id, falseNode.id, "false"); + allExitIds.push(falseNode.id); + } + } else { + // else가 없으면 조건이 false일 때 바로 다음으로 + // false 라벨을 명시적으로 표시하기 위해 더미 노드 생성 + const falseNode = cfg.addNode("(skip)", "statement"); + cfg.addEdge(conditionNode.id, falseNode.id, "false"); + allExitIds.push(falseNode.id); + } + + return { entryId: conditionNode.id, exitIds: allExitIds }; + } + + convertWhile( + cfg: ControlFlowGraph, + stmt: WhileStatement + ): { entryId: number; exitIds: number[] } { + // 조건 노드 + const conditionNode = cfg.addNode( + this.expressionToString(stmt.condition), + "condition", + undefined, + stmt.condition + ); + + // 루프 본문 + const { entryId: bodyEntry, exitIds: bodyExits } = this.convertStatement( + cfg, + stmt.body + ); + + // 루프 종료 노드 (false 경로를 명시적으로 표시) + const exitLoopNode = cfg.addNode("exit loop", "statement"); + + // 조건 -> 본문 (true) + cfg.addEdge(conditionNode.id, bodyEntry, "true"); + + // 조건 -> 루프 종료 (false) + cfg.addEdge(conditionNode.id, exitLoopNode.id, "false"); + + // 본문의 모든 exit -> 조건 (루프백) + for (const exitId of bodyExits) { + cfg.addEdge(exitId, conditionNode.id); + } + + return { entryId: conditionNode.id, exitIds: [exitLoopNode.id] }; + } + + expressionToString(expr: Expression): string { + switch (expr.type) { + case "NumberLiteral": + return expr.value.toString(); + case "Variable": + return expr.name; + case "BinaryExpression": + return `(${this.expressionToString(expr.left)} ${ + expr.operator + } ${this.expressionToString(expr.right)})`; + case "FunctionCall": + const args = expr.arguments + .flat() + .map((arg) => this.expressionToString(arg)) + .join(", "); + return `${this.expressionToString(expr.callee)}(${args})`; + + case "InputExpression": + return "input"; + default: + return `Unknown(${(expr as any).type})`; + } + } +} + +// 메인 함수 +async function generateTIPCFG( + tipCode: string, + outputName: string = "tip-program" +) { + console.log("=== TIP AST to CFG Converter ===\n"); + + // 1. TIP 코드 파싱 + const parser = new TIPParser(); + const parseResult = parser.parse(tipCode); + + if (!parseResult.success) { + console.error("TIP 파싱 실패:", parseResult.error); + return; + } + + console.log("✅ TIP 파싱 성공"); + + // 2. AST를 CFG로 변환 + const converter = new TIPCFGConverter(); + const cfgs = converter.convertProgram(parseResult.ast!); + + console.log(`\n✅ CFG 생성 완료 (${cfgs.size}개 함수)`); + + // 3. DOT 파일 생성 + for (const [funcName, cfg] of cfgs.entries()) { + const dotContent = cfg.toDot(funcName); + const dotFileName = `${outputName}-${funcName}.dot`; + fs.writeFileSync(dotFileName, dotContent); + console.log(`✅ DOT 파일 생성: ${dotFileName}`); + + console.log(`\n--- ${funcName} CFG 정보 ---`); + console.log(`노드 수: ${cfg.nodes.size}`); + console.log(`엣지 수: ${cfg.edges.length}`); + } + + console.log("\n=== 완료 ==="); + console.log("DOT 파일을 Graphviz로 시각화하려면:"); + console.log(`dot -Tpng ${outputName}-*.dot -o {함수명}.png`); +} + +// 테스트 실행 +if (require.main === module) { + const testTipCode = ` + iterate (n) { var f; f = 1; while (n > 0) { f = f * n; n = n - 1; } return f; } + `; + + generateTIPCFG(testTipCode, "factorial-example"); +} + +export { TIPCFGConverter, ControlFlowGraph, generateTIPCFG }; diff --git a/midterm/tip-icfg-converter.ts b/midterm/tip-icfg-converter.ts new file mode 100644 index 0000000..9ca9f88 --- /dev/null +++ b/midterm/tip-icfg-converter.ts @@ -0,0 +1,517 @@ +import { + Program, + FunctionDeclaration, + Statement, + Expression, + SequenceStatement, + IfStatement, + WhileStatement, +} from "./types"; + +// CFG 노드 타입 +interface CFGNode { + id: number; + label: string; + type: + | "entry" + | "exit" + | "statement" + | "condition" + | "merge" + | "call" + | "after-call"; + statement?: Statement; + expression?: Expression; + funcName?: string; +} + +// CFG 엣지 타입 +interface CFGEdge { + from: number; + to: number; + label?: string; // "true", "false" 등 + dotted?: boolean; +} + +// CFG 클래스 +class ControlFlowGraph { + nodes: Map = new Map(); + edges: CFGEdge[] = []; + private nextId = 0; + + addNode( + label: string, + type: CFGNode["type"], + statement?: Statement, + expression?: Expression + ): CFGNode { + const node: CFGNode = { + id: this.nextId++, + label, + type, + statement, + expression, + }; + this.nodes.set(node.id, node); + return node; + } + + addEdge(from: number, to: number, label?: string, dotted?: boolean) { + this.edges.push({ from, to, label, dotted: dotted ?? false }); + } + + // DOT 파일 생성 (함수별 서브그래프 그룹화) + toDot(functionName: string): string { + let dot = `digraph "${functionName}" {\n`; + dot += " node [shape=box];\n"; + + // 그룹화: funcName별 서브그래프 + const groups = new Map(); + const ungrouped: CFGNode[] = []; + for (const node of this.nodes.values()) { + if (node.funcName) { + const arr = groups.get(node.funcName) || []; + arr.push(node); + groups.set(node.funcName, arr); + } else { + ungrouped.push(node); + } + } + + for (const [fname, nodes] of groups.entries()) { + dot += ` subgraph "cluster_${this.escapeLabel(fname)}" {\n`; + dot += ` label=\"${this.escapeLabel(fname)}\";\n`; + for (const node of nodes) { + const shape = node.type === "condition" ? "diamond" : "box"; + const color = + node.type === "entry" + ? "green" + : node.type === "exit" + ? "red" + : "lightblue"; + dot += ` ${node.id} [label=\"${this.escapeLabel( + node.label + )}\", shape=${shape}, fillcolor=${color}, style=filled];\n`; + } + dot += " }\n"; + } + + for (const node of ungrouped) { + const shape = node.type === "condition" ? "diamond" : "box"; + const color = + node.type === "entry" + ? "green" + : node.type === "exit" + ? "red" + : "lightblue"; + dot += ` ${node.id} [label=\"${this.escapeLabel( + node.label + )}\", shape=${shape}, fillcolor=${color}, style=filled];\n`; + } + + // 엣지 정의 + for (const edge of this.edges) { + const attrs: string[] = []; + if (edge.label) attrs.push(`label=\"${edge.label}\"`); + if (edge.dotted) attrs.push("style=dashed"); + const attrStr = attrs.length ? ` [${attrs.join(", ")}]` : ""; + dot += ` ${edge.from} -> ${edge.to}${attrStr};\n`; + } + + dot += "}\n"; + return dot; + } + + private escapeLabel(label: string): string { + return label.replace(/"/g, '\\"').replace(/\n/g, "\\n"); + } +} + +// TIP AST를 ICFG로 변환하는 클래스 +class TIPICFGConverter { + private tempCounter = 0; + + generateTempVar(): string { + return `_t${this.tempCounter++}`; + } + + convertProgram(program: Program): Map { + const cfgs = new Map(); + + for (const func of program.functions) { + this.tempCounter = 0; + const cfg = this.convertFunction(func); + cfgs.set(func.name, cfg); + } + + return cfgs; + } + + // 단일 ICFG를 생성해 반환 + convertProgramUnified(program: Program): ControlFlowGraph { + const global = new ControlFlowGraph(); + const funcToLocal: Map = new Map(); + const funcEntryExit: Map = + new Map(); + const localToGlobal: Map = new Map(); // `${funcName}:${localId}` -> globalId + + // 1) 각 함수 로컬 그래프 생성 후 글로벌에 복사 + for (const func of program.functions) { + this.tempCounter = 0; + const local = this.convertFunction(func); + funcToLocal.set(func.name, local); + + let entryId: number | undefined; + let exitId: number | undefined; + + for (const node of local.nodes.values()) { + const newNode = global.addNode( + `${node.label}`, + node.type, + node.statement as any, + node.expression as any + ); + newNode.funcName = func.name; + localToGlobal.set(`${func.name}:${node.id}`, newNode.id); + if (node.type === "entry") entryId = newNode.id; + if (node.type === "exit") exitId = newNode.id; + } + + if (entryId === undefined || exitId === undefined) { + throw new Error( + `Function ${func.name} missing entry/exit in ICFG conversion` + ); + } + + funcEntryExit.set(func.name, { entry: entryId, exit: exitId }); + + for (const edge of local.edges) { + const gFrom = localToGlobal.get(`${func.name}:${edge.from}`)!; + const gTo = localToGlobal.get(`${func.name}:${edge.to}`)!; + global.addEdge(gFrom, gTo, edge.label, edge.dotted); + } + } + + // 2) interprocedural 엣지 추가 (call-to-return 확장) + const getCalleeNameFromCall = (callExpr: any): string | undefined => { + let c = callExpr && callExpr.callee; + while (c && c.type === "FunctionCall") c = c.callee; + if (c && c.type === "Variable") return c.name; + return undefined; + }; + + for (const [callerName, local] of funcToLocal) { + for (const edge of local.edges) { + if (edge.label === "call-to-return") { + const callNode = local.nodes.get(edge.from); + const afterLocalId = edge.to; + let calleeName: string | undefined; + if (callNode && callNode.statement) { + const stmt: any = callNode.statement; + if ( + stmt.type === "AssignmentStatement" && + stmt.expression && + stmt.expression.type === "FunctionCall" + ) { + calleeName = getCalleeNameFromCall(stmt.expression); + } else if ( + stmt.type === "CallStatement" && + stmt.expression && + stmt.expression.type === "FunctionCall" + ) { + calleeName = getCalleeNameFromCall(stmt.expression); + } + } + if (!calleeName) continue; + const entryExit = funcEntryExit.get(calleeName); + if (!entryExit) continue; + + const gFrom = localToGlobal.get(`${callerName}:${edge.from}`)!; + const gAfter = localToGlobal.get(`${callerName}:${afterLocalId}`)!; + const calleeEntry = entryExit.entry; + const calleeExit = entryExit.exit; + + global.addEdge(gFrom, calleeEntry, "call"); + global.addEdge(calleeExit, gAfter, "return"); + } + } + } + + return global; + } + + convertFunction(func: FunctionDeclaration): ControlFlowGraph { + const cfg = new ControlFlowGraph(); + + // Entry 노드 + const entryNode = cfg.addNode(`Entry: ${func.name}`, "entry"); + + // 함수 본문 변환 + const { entryId, exitIds } = this.convertStatement(cfg, func.body); + + // Entry에서 함수 본문으로 연결 + cfg.addEdge(entryNode.id, entryId); + + // Return 문 처리 (실제 ReturnStatement를 statement로 부착) + const returnNode = cfg.addNode( + `@ret = ${this.expressionToString(func.returnExpression)}`, + "statement", + { + type: "ReturnStatement", + expression: func.returnExpression, + } as any + ); + + // 모든 exit에서 return으로 연결 + for (const exitId of exitIds) { + cfg.addEdge(exitId, returnNode.id); + } + + // Exit 노드 + const exitNode = cfg.addNode(`Exit: ${func.name}`, "exit"); + cfg.addEdge(returnNode.id, exitNode.id); + + return cfg; + } + + convertStatement( + cfg: ControlFlowGraph, + stmt: Statement + ): { entryId: number; exitIds: number[] } { + switch (stmt.type) { + case "AssignmentStatement": + if (stmt.expression.type === "FunctionCall") { + const callNode = cfg.addNode( + `${stmt.variable} = ${this.expressionToString(stmt.expression)}`, + "call", + stmt + ); + const afterCallNode = cfg.addNode( + `${stmt.variable} = ${this.expressionToString(stmt.expression)}`, + "after-call", + stmt + ); + cfg.addEdge(callNode.id, afterCallNode.id, "call-to-return", true); + return { entryId: callNode.id, exitIds: [afterCallNode.id] }; + } else { + const assignNode = cfg.addNode( + `${stmt.variable} = ${this.expressionToString(stmt.expression)}`, + "statement", + stmt + ); + return { entryId: assignNode.id, exitIds: [assignNode.id] }; + } + + case "OutputStatement": + const outputNode = cfg.addNode( + `output ${this.expressionToString(stmt.expression)}`, + "statement", + stmt + ); + return { entryId: outputNode.id, exitIds: [outputNode.id] }; + + case "SequenceStatement": + return this.convertSequence(cfg, stmt); + + case "IfStatement": + return this.convertIf(cfg, stmt); + + case "WhileStatement": + return this.convertWhile(cfg, stmt); + + case "ReturnStatement": + const returnNode = cfg.addNode( + `@ret = ${this.expressionToString(stmt.expression)}`, + "statement", + stmt + ); + return { entryId: returnNode.id, exitIds: [returnNode.id] }; + + case "CallStatement": + const variable = this.generateTempVar(); + const callNode = cfg.addNode( + `${variable} = ${this.expressionToString(stmt.expression)}`, + "call", + stmt + ); + const afterCallNode = cfg.addNode( + `${variable} = ${this.expressionToString(stmt.expression)}`, + "after-call", + stmt + ); + cfg.addEdge(callNode.id, afterCallNode.id, "call-to-return", true); + return { entryId: callNode.id, exitIds: [afterCallNode.id] }; + + case "AssertStatement": + // assert는 단일 statement 노드로 표현 (제약은 해석기에서 적용) + const assertNode = cfg.addNode( + `assert(${this.expressionToString(stmt.condition)})`, + "statement", + stmt, + stmt.condition + ); + return { entryId: assertNode.id, exitIds: [assertNode.id] }; + + default: + const unknownNode = cfg.addNode( + `Unknown: ${(stmt as any).type}`, + "statement", + stmt + ); + return { entryId: unknownNode.id, exitIds: [unknownNode.id] }; + } + } + + convertSequence( + cfg: ControlFlowGraph, + stmt: SequenceStatement + ): { entryId: number; exitIds: number[] } { + if (stmt.statements.length === 0) { + const emptyNode = cfg.addNode("(empty)", "statement"); + return { entryId: emptyNode.id, exitIds: [emptyNode.id] }; + } + + let currentExitIds: number[] = []; + let entryId: number | undefined; + + for (let i = 0; i < stmt.statements.length; i++) { + const { entryId: stmtEntry, exitIds: stmtExits } = this.convertStatement( + cfg, + stmt.statements[i] + ); + + if (i === 0) { + entryId = stmtEntry; + } else { + // 이전 구문의 모든 exit에서 현재 구문의 entry로 연결 + for (const exitId of currentExitIds) { + cfg.addEdge(exitId, stmtEntry); + } + } + + currentExitIds = stmtExits; + } + + return { entryId: entryId!, exitIds: currentExitIds }; + } + + convertIf( + cfg: ControlFlowGraph, + stmt: IfStatement + ): { entryId: number; exitIds: number[] } { + // 조건 노드 + const conditionNode = cfg.addNode( + this.expressionToString(stmt.condition), + "condition", + undefined, + stmt.condition + ); + + // Then 분기 + const { entryId: thenEntry, exitIds: thenExits } = this.convertStatement( + cfg, + stmt.thenStatement + ); + cfg.addEdge(conditionNode.id, thenEntry, "true"); + + let allExitIds = [...thenExits]; + + // Else 분기 (선택적) + if (stmt.elseStatement) { + // elseStatement가 배열인 경우 처리 + let elseStmt: Statement | undefined = stmt.elseStatement; + if (Array.isArray(elseStmt)) { + if (elseStmt.length === 1) { + elseStmt = elseStmt[0]; + } else if (elseStmt.length > 1) { + elseStmt = { + type: "SequenceStatement", + statements: elseStmt, + } as any; + } else { + elseStmt = undefined; + } + } + + if (elseStmt) { + const { entryId: elseEntry, exitIds: elseExits } = + this.convertStatement(cfg, elseStmt); + cfg.addEdge(conditionNode.id, elseEntry, "false"); + allExitIds.push(...elseExits); + } else { + // else가 비어있으면 조건이 false일 때 바로 다음으로 + const falseNode = cfg.addNode("(skip)", "statement"); + cfg.addEdge(conditionNode.id, falseNode.id, "false"); + allExitIds.push(falseNode.id); + } + } else { + // else가 없으면 조건이 false일 때 바로 다음으로 + // false 라벨을 명시적으로 표시하기 위해 더미 노드 생성 + const falseNode = cfg.addNode("(skip)", "statement"); + cfg.addEdge(conditionNode.id, falseNode.id, "false"); + allExitIds.push(falseNode.id); + } + + return { entryId: conditionNode.id, exitIds: allExitIds }; + } + + convertWhile( + cfg: ControlFlowGraph, + stmt: WhileStatement + ): { entryId: number; exitIds: number[] } { + // 조건 노드 + const conditionNode = cfg.addNode( + this.expressionToString(stmt.condition), + "condition", + undefined, + stmt.condition + ); + + // 루프 본문 + const { entryId: bodyEntry, exitIds: bodyExits } = this.convertStatement( + cfg, + stmt.body + ); + + // 루프 종료 노드 (false 경로를 명시적으로 표시) + const exitLoopNode = cfg.addNode("exit loop", "statement"); + + // 조건 -> 본문 (true) + cfg.addEdge(conditionNode.id, bodyEntry, "true"); + + // 조건 -> 루프 종료 (false) + cfg.addEdge(conditionNode.id, exitLoopNode.id, "false"); + + // 본문의 모든 exit -> 조건 (루프백) + for (const exitId of bodyExits) { + cfg.addEdge(exitId, conditionNode.id); + } + + return { entryId: conditionNode.id, exitIds: [exitLoopNode.id] }; + } + + expressionToString(expr: Expression): string { + switch (expr.type) { + case "NumberLiteral": + return expr.value.toString(); + case "Variable": + return expr.name; + case "BinaryExpression": + return `(${this.expressionToString(expr.left)} ${ + expr.operator + } ${this.expressionToString(expr.right)})`; + case "FunctionCall": + const args = expr.arguments + .flat() + .map((arg) => this.expressionToString(arg)) + .join(", "); + return `${this.expressionToString(expr.callee)}(${args})`; + case "InputExpression": + return "input"; + default: + return `Unknown(${(expr as any).type})`; + } + } +} + +export { TIPICFGConverter, ControlFlowGraph }; diff --git a/midterm/tip_code.txt b/midterm/tip_code.txt new file mode 100644 index 0000000..6c3e775 --- /dev/null +++ b/midterm/tip_code.txt @@ -0,0 +1,14 @@ +main() { + var x, y, z; + x = input; + y = 0; + z = 0; + while (x > 0) { + z = z+x; + if (17 > y) { + y = y+1; + } + x = x-1; + } + return x; +} diff --git a/midterm/tsconfig.json b/midterm/tsconfig.json new file mode 100644 index 0000000..af877b7 --- /dev/null +++ b/midterm/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"], + "ts-node": { + "esm": false + } +} diff --git a/midterm/types.ts b/midterm/types.ts new file mode 100644 index 0000000..b81e294 --- /dev/null +++ b/midterm/types.ts @@ -0,0 +1,143 @@ +// TIP 언어의 AST 노드 타입 정의 + +export interface ASTNode { + type: string; + location?: { + line: number; + column: number; + }; +} + +// 프로그램: 함수들의 배열 +export interface Program extends ASTNode { + type: "Program"; + functions: FunctionDeclaration[]; +} + +// 함수 선언 +export interface FunctionDeclaration extends ASTNode { + type: "FunctionDeclaration"; + name: string; + parameters: string[][]; + localVariables?: string[][]; // var x, ..., x; (optional) + body: Statement; + returnExpression: Expression; +} + +// 구문들 (Statement) +export type Statement = + | AssignmentStatement + | OutputStatement + | SequenceStatement + | IfStatement + | WhileStatement + | ReturnStatement + | CallStatement + | AssertStatement; + +export interface AssignmentStatement extends ASTNode { + type: "AssignmentStatement"; + variable: string; + expression: Expression; +} + +export interface OutputStatement extends ASTNode { + type: "OutputStatement"; + expression: Expression; +} + +export interface SequenceStatement extends ASTNode { + type: "SequenceStatement"; + statements: Statement[]; +} + +export interface IfStatement extends ASTNode { + type: "IfStatement"; + condition: Expression; + thenStatement: Statement; + elseStatement?: Statement; // optional +} + +export interface WhileStatement extends ASTNode { + type: "WhileStatement"; + condition: Expression; + body: Statement; +} + +export interface ReturnStatement extends ASTNode { + type: "ReturnStatement"; + expression: Expression; +} + +export interface CallStatement extends ASTNode { + type: "CallStatement"; + expression: FunctionCall; +} + +export interface AssertStatement extends ASTNode { + type: "AssertStatement"; + condition: Expression; // boolean expression expected +} + +// 표현식들 (Expression) +export type Expression = + | NumberLiteral + | Variable + | BinaryExpression + | UnaryExpression + | FunctionCall + | NullLiteral + | InputExpression; + +export interface NumberLiteral extends ASTNode { + type: "NumberLiteral"; + value: number; +} + +export interface Variable extends ASTNode { + type: "Variable"; + name: string; +} + +export interface BinaryExpression extends ASTNode { + type: "BinaryExpression"; + operator: "+" | "-" | "*" | "/" | ">" | "=="; + left: Expression; + right: Expression; +} + +export interface UnaryExpression extends ASTNode { + type: "UnaryExpression"; + operator: "*" | "&"; + operand: Expression; +} + +export interface FunctionCall extends ASTNode { + type: "FunctionCall"; + callee: Expression; + arguments: Expression[]; +} + +export interface NullLiteral extends ASTNode { + type: "NullLiteral"; +} + +export interface InputExpression extends ASTNode { + type: "InputExpression"; +} + +// 파서 옵션 +export interface ParseOptions { + includeLocation?: boolean; +} + +// 파서 결과 +export interface ParseResult { + success: boolean; + ast?: Program; + error?: string; + errorLocation?: { + line: number; + column: number; + }; +}