diff --git a/app/src/app/api/llm/generate/route.ts b/app/src/app/api/llm/generate/route.ts index 11021b8..24c2065 100644 --- a/app/src/app/api/llm/generate/route.ts +++ b/app/src/app/api/llm/generate/route.ts @@ -4,6 +4,12 @@ import { LLMFactory, ProviderType } from '@/lib/llm/factory'; import { constructPrompt } from '@/lib/llm/context'; import { checkRateLimit, rateLimitHeaders, getClientId, LLM_RATE_LIMIT } from '@/lib/rateLimit'; +// Helper to extract code block +function extractCodeBlock(text: string): string { + const match = text.match(/```(?:verilog|systemverilog)?\n([\s\S]*?)\n```/); + return match ? match[1] : text; +} + export async function POST(req: NextRequest) { try { // Rate limiting check @@ -27,51 +33,25 @@ export async function POST(req: NextRequest) { userQuery, projectFiles = [], systemOptions = {}, - stream = false + stream = false, + mode = 'standard' // 'standard' | 'iterative' } = body; const llmProvider = LLMFactory.createProvider(provider as ProviderType, { defaultModel: model }); - const fullPrompt = constructPrompt(userQuery, projectFiles, { systemOptions }); - - const requestPayload = { - systemPrompt: fullPrompt.split('USER REQUEST:')[0], // Split strictly for API that support 'system' role - userPrompt: `USER REQUEST:\n${userQuery}`, // Or keep full prompt if needed, but providers split system/user - // Actually, constructPrompt combines them. Providers expect separate system/user often. - // Let's adjust usage: - // constructPrompt returns a big string. - // But LLMRequest interface has systemPrompt and userPrompt. - // We should probably rely on constructPrompt for the "System" part primarily, - // but generic providers might work better if we separate them. - // - // Valid Strategy: - // 1. extract system part from constructPrompt logic (or call getSystemPrompt directly) - // 2. build user part with context and query - }; - - // Refined prompt handling + // Base Prompt Construction const systemPromptText = constructPrompt('', [], { systemOptions }).split('USER REQUEST:')[0].trim(); - // Re-construct the user logic part (Context + Examples + Query) - // We can cheat: constructPrompt(userQuery, projectFiles) contains everything. - // If we pass everything as "User Prompt" it works for most models, but setting "System Prompt" is better. - - // Let's do: - // System = getSystemPrompt() - // User = Context + Examples + Query - - // We need to import helper functions again properly or just use constructPrompt's logic. - // For now, let's use the constructPrompt result as the USER prompt (and empty system default? no, system is strong). - // Better: let's update constructPrompt in future to support separation. - // Current workaround: + let currentPrompt = constructPrompt(userQuery, projectFiles, { systemOptions }).replace(systemPromptText, '').trim(); + // Initial Generation const llmRequest = { - systemPrompt: systemPromptText, // The Persona + Rues - userPrompt: fullPrompt.replace(systemPromptText, '').trim(), // The Context + Examples + Query + systemPrompt: systemPromptText, + userPrompt: currentPrompt, maxTokens: 4096 }; + // If streaming, we just return the stream (Iterative not supported in streaming yet) if (stream) { - // Simple streaming response setup const encoder = new TextEncoder(); const customStream = new ReadableStream({ async start(controller) { @@ -85,11 +65,75 @@ export async function POST(req: NextRequest) { return new NextResponse(customStream, { headers: { 'Content-Type': 'text/plain; charset=utf-8' } }); - } else { - const response = await llmProvider.generate(llmRequest); - return NextResponse.json(response); } + // Standard Generation + let response = await llmProvider.generate(llmRequest); + + // Iterative Refinement Loop + if (mode === 'iterative') { + const { parse } = await import('@/lib/verilog/parser'); + const { cstToAst } = await import('@/lib/verilog/visitor'); + const { validateModule } = await import('@/lib/verilog/validator'); + + let iterations = 0; + const maxIterations = 3; + let isValid = false; + + while (iterations < maxIterations && !isValid) { + try { + const code = extractCodeBlock(response.content); + const parseResult = parse(code); + + if (parseResult.errors.length > 0) { + // Syntax errors + const errorMsg = parseResult.errors.map(e => `Line ${e.line}: ${e.message}`).join('\n'); + const refinementPrompt = ` +The generated code had syntax errors: +${errorMsg} + +Please fix the syntax and regenerate the code. +`; + llmRequest.userPrompt += `\n\nAssistant: ${response.content}\n\nUser: ${refinementPrompt}`; + response = await llmProvider.generate(llmRequest); + iterations++; + continue; + } + + // CST -> AST -> Validate + const ast = cstToAst(parseResult.cst); + const validation = validateModule(ast); + + if (validation.isValid) { + isValid = true; + break; + } + + // Logic errors + const errors = validation.errors.map(e => `- Line ${e.line}: ${e.message}`).join('\n'); + const refinementPrompt = ` +The previous generation had the following lint errors: +${errors} + +Please fix these errors and regenerate the full module code. +`; + llmRequest.userPrompt += `\n\nAssistant: ${response.content}\n\nUser: ${refinementPrompt}`; + + console.log(`[Iterative] Retrying... (${iterations + 1}) Errors: ${validation.errors.length}`); + + response = await llmProvider.generate(llmRequest); + iterations++; + + } catch (err: any) { + console.error('Refinement loop error:', err); + break; // Exit loop on unexpected error to return what we have + } + } + } + + + return NextResponse.json(response); + } catch (error: any) { console.error('LLM API Error:', error); return NextResponse.json({ error: error.message }, { status: 500 }); diff --git a/app/src/app/api/llm/testbench/route.ts b/app/src/app/api/llm/testbench/route.ts new file mode 100644 index 0000000..925b812 --- /dev/null +++ b/app/src/app/api/llm/testbench/route.ts @@ -0,0 +1,69 @@ + +import { NextRequest, NextResponse } from 'next/server'; +import { LLMFactory } from '@/lib/llm/factory'; +import { constructPrompt } from '@/lib/llm/context'; +import { checkRateLimit, rateLimitHeaders, getClientId, LLM_RATE_LIMIT } from '@/lib/rateLimit'; + +export async function POST(req: NextRequest) { + try { + // Rate limiting check + const clientId = getClientId(req); + const rateLimitResult = checkRateLimit(`llm:generate:${clientId}`, LLM_RATE_LIMIT); + + if (!rateLimitResult.success) { + return NextResponse.json( + { error: 'Rate limit exceeded. Please try again later.' }, + { + status: 429, + headers: rateLimitHeaders(rateLimitResult, LLM_RATE_LIMIT) + } + ); + } + + const body = await req.json(); + const { code, moduleName, provider = 'anthropic', model } = body; + + const llmProvider = LLMFactory.createProvider(provider, { defaultModel: model }); + + const prompt = ` +You are an expert Verilog Verification Engineer. +Your task is to write a comprehensive, self-checking testbench for the following Verilog module. + +MODULE CODE: +\`\`\`verilog +${code} +\`\`\` + +REQUIREMENTS: +1. The testbench module name should be \`${moduleName}_tb\`. +2. Instantiate the DUT (Device Under Test). +3. Generate a clock (if the module has one). +4. Apply a proper reset sequence. +5. Provide test vectors to cover major functionality. +6. Use \`$display\` to log results and \`$finish\` to end simulation. +7. Check outputs using \`if (out !== expected) $error(...)\`. +8. Include \`initial begin $dumpfile("waveform.vcd"); $dumpvars(0, ${moduleName}_tb); end\` for VCD generation. +9. Output ONLY the Verilog code for the testbench, wrapped in a code block. + +GENERATE TESTBENCH: +`; + + const llmRequest = { + systemPrompt: 'You are an expert Verilog verification engineer.', + userPrompt: prompt, + maxTokens: 4096 + }; + + const response = await llmProvider.generate(llmRequest); + + // Extract code block + const match = response.content.match(/```(?:verilog|systemverilog)?\n([\s\S]*?)\n```/); + const tbCode = match ? match[1] : response.content; + + return NextResponse.json({ code: tbCode }); + + } catch (error: any) { + console.error('Testbench Gen Error:', error); + return NextResponse.json({ error: error.message }, { status: 500 }); + } +} diff --git a/app/src/app/api/verification/prove/route.ts b/app/src/app/api/verification/prove/route.ts new file mode 100644 index 0000000..3adb1c5 --- /dev/null +++ b/app/src/app/api/verification/prove/route.ts @@ -0,0 +1,26 @@ + +import { NextRequest, NextResponse } from 'next/server'; +import { runProof } from '@/lib/simulation/runner'; +import { z } from 'zod'; + +const schema = z.object({ + code: z.string(), +}); + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { code } = schema.parse(body); + + // Rate limiting would go here + + const result = await runProof(code); + + return NextResponse.json(result); + } catch (error: any) { + return NextResponse.json( + { error: error.message || 'Verification failed' }, + { status: 500 } + ); + } +} diff --git a/app/src/app/editor/page.tsx b/app/src/app/editor/page.tsx index 8db5459..c9bfdfe 100644 --- a/app/src/app/editor/page.tsx +++ b/app/src/app/editor/page.tsx @@ -8,6 +8,7 @@ import FileTree from '@/components/layout/FileTree'; import EditorTabs from '@/components/layout/EditorTabs'; import Toolbar from '@/components/layout/Toolbar'; import { useEditorStore } from '@/lib/store/editor'; +import { useFSMStore } from '@/lib/fsm/store'; import { VCDData } from '@/lib/waveform/vcdParser'; // Dynamic imports for heavy components (code splitting) @@ -40,6 +41,7 @@ function EditorLoading({ label }: { label: string }) { export default function IDEPage() { const { files, activeFileId } = useEditorStore(); + const { fsm, setActiveState } = useFSMStore(); const activeFile = files.find(f => f.id === activeFileId); const [vcdData, setVcdData] = useState(null); @@ -50,6 +52,43 @@ export default function IDEPage() { setShowWaveform(true); }; + const handleCursorChange = (time: number | null) => { + if (time === null || !vcdData) { + setActiveState(null); + return; + } + + // Find state signal (heuristic: contains 'state' but not 'next') + const stateSignalName = Array.from(vcdData.signals.keys()).find(k => + k.toLowerCase().includes('state') && !k.toLowerCase().includes('next') + ); + + if (stateSignalName) { + const changes = vcdData.changes.get(stateSignalName); + if (changes) { + let value = ''; + for (const change of changes) { + if (change.time > time) break; + value = change.value; + } + + if (value) { + // Logic: Map VCD value to FSM state + // value is e.g. "10" (binary) + const normalizedSignalVal = parseInt(value, 2); + + // Simple heuristic: Assume binary encoding corresponds to states array index + if (!isNaN(normalizedSignalVal) && normalizedSignalVal < fsm.states.length) { + const activeState = fsm.states[normalizedSignalVal]; + if (activeState) { + setActiveState(activeState.id, time); + } + } + } + } + } + }; + return (
{/* File Tree Sidebar */} @@ -132,7 +171,12 @@ export default function IDEPage() { }> - +
diff --git a/app/src/components/fsm/StateNode.tsx b/app/src/components/fsm/StateNode.tsx index 2c0ee0e..de670b8 100644 --- a/app/src/components/fsm/StateNode.tsx +++ b/app/src/components/fsm/StateNode.tsx @@ -5,25 +5,37 @@ import { Play } from 'lucide-react'; interface StateNodeData { label: string; isInitial?: boolean; + isActive?: boolean; // For simulation highlighting onLabelChange?: (id: string, newLabel: string) => void; outputs?: Array<{ signal: string; value: string }>; } export const StateNode = memo(({ data, id, selected }: NodeProps) => { + const isActive = data.isActive; + return (
+ {/* Active state indicator (simulation) */} + {isActive && ( +
+
+
+ )} + {/* Initial state animated indicator */} - {data.isInitial && ( + {data.isInitial && !isActive && (
)} diff --git a/app/src/components/layout/Toolbar.tsx b/app/src/components/layout/Toolbar.tsx index a36c9f3..158837d 100644 --- a/app/src/components/layout/Toolbar.tsx +++ b/app/src/components/layout/Toolbar.tsx @@ -21,6 +21,58 @@ const Toolbar: React.FC = ({ onSimulationComplete }) => { const [output, setOutput] = useState(null); const [error, setError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); + const [isGeneratingTB, setIsGeneratingTB] = useState(false); + + // Generate Testbench + const handleGenerateTB = async () => { + if (!activeFile) return; + + setIsGeneratingTB(true); + setError(null); + setOutput(null); + + try { + const moduleNameMatch = activeFile.content.match(/module\s+(\w+)/); + const moduleName = moduleNameMatch ? moduleNameMatch[1] : 'unknown'; + + const response = await fetch('/api/llm/testbench', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: activeFile.content, + moduleName + }) + }); + + const result = await response.json(); + + if (result.error) { + setError(result.error); + } else if (result.code) { + // Create new file + const tbFilename = `${activeFile.name.replace(/\.[^/.]+$/, "")}_tb.v`; + const { openFile, setActiveFile } = useEditorStore.getState(); + + // Add file to store + openFile({ + id: tbFilename, // Simple ID for now + name: tbFilename, + content: result.code, + type: 'verilog' + }); + + // Switch to it + // setActiveFile(tbFilename); // openFile already sets specific file active? Check store. + // Store says: openFile sets activeFileId to newFile.id. So we don't need setActiveFile explicit call if openFile does it. + + setOutput(`Generated ${tbFilename}`); + } + } catch (err: any) { + setError(err.message || 'Failed to generate testbench'); + } finally { + setIsGeneratingTB(false); + } + }; // Save file function const handleSave = useCallback(async () => { @@ -61,6 +113,7 @@ const Toolbar: React.FC = ({ onSimulationComplete }) => { return () => window.removeEventListener('keydown', handleKeyDown); }, [handleSave]); + const handleRunSimulation = async () => { if (!activeFile) { setError('No file selected'); @@ -97,6 +150,36 @@ const Toolbar: React.FC = ({ onSimulationComplete }) => { } }; + const [isVerifying, setIsVerifying] = useState(false); + + // Verify (Formal) + const handleVerify = async () => { + if (!activeFile) return; + setIsVerifying(true); + setError(null); + setOutput(null); + + try { + const response = await fetch('/api/verification/prove', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + code: activeFile.content + }) + }); + const result = await response.json(); + if (result.success) { + setOutput('Verification PASSED'); + } else { + setError(result.errors.join('\n') || 'Verification FAILED'); + } + } catch (err: any) { + setError(err.message || 'Verification failed'); + } finally { + setIsVerifying(false); + } + }; + return (
{/* Project Selector */} @@ -157,6 +240,48 @@ const Toolbar: React.FC = ({ onSimulationComplete }) => { )} + {/* Generate TB Button */} + + + {/* Verify Button (Formal) */} + + {/* Lint Button */}