Skip to content

Commit 58b45f6

Browse files
hyperpolymathclaude
andcommitted
feat: migrate TS files to ReScript
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6b19496 commit 58b45f6

10 files changed

Lines changed: 639 additions & 476 deletions

File tree

packages/jtv-analyzer/src/Main.res

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// Julia the Viper - Code Analyzer for Python/JavaScript/Ruby
3+
// Analyzes legacy code for termination guarantees, purity, and complexity
4+
5+
/** Supported analysis target languages */
6+
type language = [#python | #javascript | #ruby]
7+
8+
/** Severity levels for analysis warnings */
9+
type severity = [#error | #warning | #info]
10+
11+
/** Types of arithmetic data operations */
12+
type dataOperationType = [#add | #subtract | #multiply | #divide | #other]
13+
14+
/** Types of control flow nodes */
15+
type controlFlowType = [#"if" | #"while" | #"for" | #function_call | #"return"]
16+
17+
/** Types of suggestions the analyzer can produce */
18+
type suggestionType = [#extract_to_jtv | #mark_pure | #bound_loop]
19+
20+
/** Complexity metrics for a single function */
21+
type complexityAnalysis = {
22+
cyclomaticComplexity: int,
23+
cognitiveComplexity: int,
24+
loops: int,
25+
conditionals: int,
26+
recursion: bool,
27+
}
28+
29+
/** A single data operation found in the source */
30+
type dataOperation = {
31+
@as("type") operationType: dataOperationType,
32+
line: int,
33+
safe: bool,
34+
}
35+
36+
/** A single control flow node found in the source */
37+
type controlFlowNode = {
38+
@as("type") nodeType: controlFlowType,
39+
line: int,
40+
terminating: bool,
41+
}
42+
43+
/** Analysis result for a single function */
44+
type functionAnalysis = {
45+
name: string,
46+
line: int,
47+
mutable isPure: bool,
48+
mutable terminates: bool,
49+
complexity: complexityAnalysis,
50+
dataOperations: array<dataOperation>,
51+
controlFlow: array<controlFlowNode>,
52+
}
53+
54+
/** A warning produced during analysis */
55+
type warning = {
56+
severity: severity,
57+
message: string,
58+
line: int,
59+
suggestion: option<string>,
60+
}
61+
62+
/** A suggestion for improving the analyzed code */
63+
type suggestion = {
64+
@as("type") suggestionType: suggestionType,
65+
message: string,
66+
line: int,
67+
codeSnippet: string,
68+
jtvEquivalent: option<string>,
69+
}
70+
71+
/** Complete analysis result for a file */
72+
type analysisResult = {
73+
file: string,
74+
language: language,
75+
mutable functions: array<functionAnalysis>,
76+
mutable warnings: array<warning>,
77+
mutable suggestions: array<suggestion>,
78+
}
79+
80+
// -- FFI bindings for Deno APIs --
81+
82+
@module("Deno") @val
83+
external readTextFile: string => Js.Promise2.t<string> = "readTextFile"
84+
85+
@module("Deno") @val
86+
external denoArgs: array<string> = "args"
87+
88+
@module("Deno") @val
89+
external denoExit: int => unit = "exit"
90+
91+
// -- Helper functions --
92+
93+
/** Check if a line is a function declaration for the given language */
94+
let isFunctionDeclaration = (line: string, lang: language): bool => {
95+
switch lang {
96+
| #python => Js.String2.startsWith(line, "def ")
97+
| #javascript =>
98+
Js.String2.includes(line, "function ") || Js.String2.includes(line, "=> ")
99+
| #ruby => Js.String2.startsWith(line, "def ")
100+
}
101+
}
102+
103+
/** Extract the function name from a declaration line */
104+
let extractFunctionName = (line: string): string => {
105+
let re = %re("/(?:def|function)\s+(\w+)/")
106+
switch Js.Re.exec_(re, line) {
107+
| Some(result) =>
108+
switch Js.Nullable.toOption(Js.Re.captures(result)[1]) {
109+
| Some(name) => name
110+
| None => "anonymous"
111+
}
112+
| None => "anonymous"
113+
}
114+
}
115+
116+
/** Convert a line to a JtV equivalent suggestion */
117+
let convertToJtv = (line: string): string => {
118+
if Js.String2.includes(line, "*") {
119+
"// Use loop with addition in JtV: for i in 0..n { sum = sum + x }"
120+
} else if Js.String2.includes(line, "/") {
121+
"// Division not directly supported - use rational type or iterative subtraction"
122+
} else {
123+
line
124+
->Js.String2.replaceByRe(%re("/\*/g"), "+")
125+
->Js.String2.replaceByRe(%re("/\//g"), "+")
126+
}
127+
}
128+
129+
/** Create a fresh complexity analysis with default values */
130+
let makeComplexity = (): complexityAnalysis => {
131+
cyclomaticComplexity: 1,
132+
cognitiveComplexity: 0,
133+
loops: 0,
134+
conditionals: 0,
135+
recursion: false,
136+
}
137+
138+
/** Analyze source code and produce an analysis result */
139+
let analyzeCode = (code: string, filePath: string, lang: language): analysisResult => {
140+
let result: analysisResult = {
141+
file: filePath,
142+
language: lang,
143+
functions: [],
144+
warnings: [],
145+
suggestions: [],
146+
}
147+
148+
let lines = Js.String2.split(code, "\n")
149+
let currentFunction = ref(None)
150+
let braceDepth = ref(0)
151+
152+
lines->Belt.Array.forEachWithIndex((i, rawLine) => {
153+
let line = Js.String2.trim(rawLine)
154+
let lineNum = i + 1
155+
156+
// Detect function declarations
157+
if isFunctionDeclaration(line, lang) {
158+
switch currentFunction.contents {
159+
| Some(func) =>
160+
result.functions = Belt.Array.concat(result.functions, [func])
161+
| None => ()
162+
}
163+
164+
let funcName = extractFunctionName(line)
165+
currentFunction :=
166+
Some({
167+
name: funcName,
168+
line: lineNum,
169+
isPure: true,
170+
terminates: true,
171+
complexity: makeComplexity(),
172+
dataOperations: [],
173+
controlFlow: [],
174+
})
175+
}
176+
177+
switch currentFunction.contents {
178+
| None => ()
179+
| Some(func) => {
180+
// Analyze data operations
181+
if Js.String2.includes(line, "+") && !Js.String2.includes(line, "++") {
182+
let op: dataOperation = {operationType: #add, line: lineNum, safe: true}
183+
ignore(
184+
Js.Array2.push(func.dataOperations, op),
185+
)
186+
} else if Js.String2.includes(line, "*") || Js.String2.includes(line, "/") {
187+
let opType = if Js.String2.includes(line, "*") {
188+
#multiply
189+
} else {
190+
#divide
191+
}
192+
let op: dataOperation = {operationType: opType, line: lineNum, safe: false}
193+
ignore(Js.Array2.push(func.dataOperations, op))
194+
195+
let sug: suggestion = {
196+
suggestionType: #extract_to_jtv,
197+
message: "Consider extracting pure computation to JtV for guaranteed termination",
198+
line: lineNum,
199+
codeSnippet: line,
200+
jtvEquivalent: Some(convertToJtv(line)),
201+
}
202+
result.suggestions = Belt.Array.concat(result.suggestions, [sug])
203+
}
204+
205+
// Detect control flow - conditionals
206+
if Js.String2.includes(line, "if") || Js.String2.includes(line, "else") {
207+
let cfNode: controlFlowNode = {nodeType: #"if", line: lineNum, terminating: true}
208+
ignore(Js.Array2.push(func.controlFlow, cfNode))
209+
}
210+
211+
// Detect control flow - loops
212+
if Js.String2.includes(line, "while") || Js.String2.includes(line, "for") {
213+
func.terminates = false
214+
let loopType = if Js.String2.includes(line, "while") {
215+
#"while"
216+
} else {
217+
#"for"
218+
}
219+
let cfNode: controlFlowNode = {nodeType: loopType, line: lineNum, terminating: false}
220+
ignore(Js.Array2.push(func.controlFlow, cfNode))
221+
222+
let warn: warning = {
223+
severity: #warning,
224+
message: "Unbounded loop detected - termination not guaranteed",
225+
line: lineNum,
226+
suggestion: Some("Add loop bound or extract to JtV with range-based iteration"),
227+
}
228+
result.warnings = Belt.Array.concat(result.warnings, [warn])
229+
}
230+
231+
// Detect side effects
232+
if (
233+
Js.String2.includes(line, "print") ||
234+
Js.String2.includes(line, "console.log") ||
235+
Js.String2.includes(line, "puts") ||
236+
Js.String2.includes(line, "write") ||
237+
Js.String2.includes(line, "fetch") ||
238+
Js.String2.includes(line, "axios")
239+
) {
240+
func.isPure = false
241+
242+
let warn: warning = {
243+
severity: #info,
244+
message: "Function has side effects (I/O operations)",
245+
line: lineNum,
246+
suggestion: Some("Consider separating pure computation from side effects"),
247+
}
248+
result.warnings = Belt.Array.concat(result.warnings, [warn])
249+
}
250+
251+
// Detect recursion
252+
if Js.String2.includes(line, func.name ++ "(") {
253+
func.terminates = false
254+
255+
let warn: warning = {
256+
severity: #warning,
257+
message: "Recursive function - termination not guaranteed",
258+
line: lineNum,
259+
suggestion: Some("Convert to iterative form or prove termination"),
260+
}
261+
result.warnings = Belt.Array.concat(result.warnings, [warn])
262+
}
263+
264+
// Track brace depth for function boundaries
265+
let openBraces =
266+
Js.String2.match_(line, %re("/{/g"))
267+
->Belt.Option.mapWithDefault(0, Js.Array2.length)
268+
let closeBraces =
269+
Js.String2.match_(line, %re("/}/g"))
270+
->Belt.Option.mapWithDefault(0, Js.Array2.length)
271+
braceDepth := braceDepth.contents + openBraces - closeBraces
272+
273+
if braceDepth.contents == 0 {
274+
result.functions = Belt.Array.concat(result.functions, [func])
275+
currentFunction := None
276+
}
277+
}
278+
}
279+
})
280+
281+
// Add final function if still in progress
282+
switch currentFunction.contents {
283+
| Some(func) =>
284+
result.functions = Belt.Array.concat(result.functions, [func])
285+
| None => ()
286+
}
287+
288+
// Generate overall suggestions
289+
let pureFunctions = result.functions->Belt.Array.keep(f => f.isPure && f.terminates)
290+
if Belt.Array.length(pureFunctions) > 0 {
291+
let names = pureFunctions->Belt.Array.map(f => f.name)->Js.Array2.joinWith(", ")
292+
let count = Belt.Array.length(pureFunctions)->Belt.Int.toString
293+
let sug: suggestion = {
294+
suggestionType: #extract_to_jtv,
295+
message: count ++ " pure, terminating functions found - excellent candidates for JtV extraction",
296+
line: 0,
297+
codeSnippet: names,
298+
jtvEquivalent: None,
299+
}
300+
result.suggestions = Belt.Array.concat(result.suggestions, [sug])
301+
}
302+
303+
let highComplexity =
304+
result.functions->Belt.Array.keep(f => f.complexity.cyclomaticComplexity > 10)
305+
if Belt.Array.length(highComplexity) > 0 {
306+
let count = Belt.Array.length(highComplexity)->Belt.Int.toString
307+
let warn: warning = {
308+
severity: #warning,
309+
message: count ++ " functions with high complexity - consider refactoring",
310+
line: 0,
311+
suggestion: None,
312+
}
313+
result.warnings = Belt.Array.concat(result.warnings, [warn])
314+
}
315+
316+
result
317+
}
318+
319+
/** Print a human-readable analysis report to the console */
320+
let printReport = (result: analysisResult): unit => {
321+
Js.log("\n=== JtV Analysis Report: " ++ result.file ++ " ===\n")
322+
Js.log("Language: " ++ (result.language :> string))
323+
Js.log("Functions analyzed: " ++ Belt.Int.toString(Belt.Array.length(result.functions)) ++ "\n")
324+
325+
let pure = result.functions->Belt.Array.keep(f => f.isPure)->Belt.Array.length
326+
let terminating = result.functions->Belt.Array.keep(f => f.terminates)->Belt.Array.length
327+
let total = Belt.Array.length(result.functions)
328+
329+
Js.log(
330+
"Pure functions: " ++ Belt.Int.toString(pure) ++ "/" ++ Belt.Int.toString(total),
331+
)
332+
Js.log(
333+
"Terminating functions: " ++ Belt.Int.toString(terminating) ++ "/" ++ Belt.Int.toString(total) ++ "\n",
334+
)
335+
336+
if Belt.Array.length(result.warnings) > 0 {
337+
Js.log("Warnings (" ++ Belt.Int.toString(Belt.Array.length(result.warnings)) ++ "):")
338+
result.warnings->Belt.Array.forEach(w => {
339+
let icon = switch w.severity {
340+
| #error => "[ERROR]"
341+
| #warning => "[WARN]"
342+
| #info => "[INFO]"
343+
}
344+
Js.log(" " ++ icon ++ " Line " ++ Belt.Int.toString(w.line) ++ ": " ++ w.message)
345+
switch w.suggestion {
346+
| Some(s) => Js.log(" -> " ++ s)
347+
| None => ()
348+
}
349+
})
350+
Js.log("")
351+
}
352+
353+
if Belt.Array.length(result.suggestions) > 0 {
354+
Js.log("Suggestions (" ++ Belt.Int.toString(Belt.Array.length(result.suggestions)) ++ "):")
355+
result.suggestions->Belt.Array.forEach(s => {
356+
Js.log(" [TIP] Line " ++ Belt.Int.toString(s.line) ++ ": " ++ s.message)
357+
switch s.jtvEquivalent {
358+
| Some(equiv) => Js.log(" JtV: " ++ equiv)
359+
| None => ()
360+
}
361+
})
362+
Js.log("")
363+
}
364+
365+
Js.log("Function Details:")
366+
result.functions->Belt.Array.forEach(func => {
367+
let pureIcon = if func.isPure { "Y" } else { "N" }
368+
let termIcon = if func.terminates { "Y" } else { "N" }
369+
Js.log("\n " ++ func.name ++ " (line " ++ Belt.Int.toString(func.line) ++ ")")
370+
Js.log(" Pure: " ++ pureIcon ++ " | Terminates: " ++ termIcon)
371+
Js.log(
372+
" Complexity: " ++ Belt.Int.toString(func.complexity.cyclomaticComplexity) ++ " (cyclomatic)",
373+
)
374+
Js.log(
375+
" Loops: " ++
376+
Belt.Int.toString(func.complexity.loops) ++
377+
" | Conditionals: " ++
378+
Belt.Int.toString(func.complexity.conditionals),
379+
)
380+
})
381+
382+
Js.log("\n=== End Report ===\n")
383+
}
384+
385+
/** Analyze a file by reading it from disk (async, requires Deno) */
386+
let analyzeFile = (filePath: string, lang: language): Js.Promise2.t<analysisResult> => {
387+
readTextFile(filePath)->Js.Promise2.then(code => {
388+
Js.Promise2.resolve(analyzeCode(code, filePath, lang))
389+
})
390+
}

0 commit comments

Comments
 (0)