1use dioxus::prelude::*;
23#[cfg(all(feature = "split", target_arch = "wasm32"))]
24use dioxus::wasm_split;
25use std::cell::RefCell;
26use logicaffeine_compile::{
27 compile_for_ui, compile_for_proof, compile_theorem_for_ui, generate_rust_code,
28 extract_math_rust, extract_logic_rust,
29 interpret_for_ui_baseline, interpret_streaming_with_vfs, CompileResult, ProofCompileResult,
30 TheoremCompileResult, SolvedGrid,
31 interpreter::InterpreterResult,
32};
33use logicaffeine_proof::{
34 BackwardChainer, DerivationTree, ProofExpr,
35 hints::{suggest_hint, SuggestedTactic},
36};
37use crate::ui::components::editor::LiveEditor;
38use crate::ui::components::logic_output::{LogicOutput, OutputFormat};
39use crate::ui::components::ast_tree::AstTree;
40use crate::ui::components::socratic_guide::{SocraticGuide, GuideMode, get_success_message, get_context_hint};
41use crate::ui::components::main_nav::{MainNav, ActivePage};
42use crate::ui::components::symbol_dictionary::SymbolDictionary;
43use crate::ui::seo::{JsonLdMultiple, PageHead, organization_schema, software_application_schema, breadcrumb_schema, BreadcrumbItem, pages as seo_pages};
44use crate::ui::components::mode_toggle::ModeToggle;
45use crate::ui::components::file_browser::FileBrowser;
46use crate::ui::components::repl_output::ReplOutput;
47use crate::ui::components::context_view::{ContextView, ContextEntry, EntryKind};
48use crate::ui::components::code_editor::{CodeEditor, CodeView, Language};
49use crate::ui::components::proof_panel::{ProofPanel, ProofStatus, Tactic};
50use crate::ui::components::debug_drawer::{DebugDrawer, IC_BUG};
51use crate::ui::state::{StudioMode, FileNode, ReplLine};
52use crate::ui::responsive::{MOBILE_BASE_STYLES, MOBILE_TAB_BAR_STYLES};
53use logicaffeine_kernel::interface::Repl;
54use crate::ui::examples::seed_examples;
55#[cfg(target_arch = "wasm32")]
56use logicaffeine_system::fs::{get_platform_vfs_with_fallback, WebVfs};
57use logicaffeine_system::fs::{get_platform_vfs, Vfs, DirEntry, VfsResult};
58#[cfg(target_arch = "wasm32")]
59use wasm_bindgen::JsValue;
60use std::rc::Rc;
61
62pub fn parse_math_statements(code: &str) -> Vec<String> {
69 let mut statements = Vec::new();
70 let lines: Vec<&str> = code.lines().collect();
71 let mut i = 0;
72
73 while i < lines.len() {
74 let line = lines[i];
75 let trimmed = line.trim();
76
77 if trimmed.is_empty() || trimmed.starts_with("--") {
79 i += 1;
80 continue;
81 }
82
83 if trimmed.starts_with("## To ") {
85 let mut block = String::new();
86 block.push_str(trimmed);
87 i += 1;
88
89 while i < lines.len() {
91 let next_line = lines[i];
92 let next_trimmed = next_line.trim();
93
94 if next_trimmed.is_empty() {
96 i += 1;
97 continue;
98 }
99
100 if next_trimmed.starts_with("--") {
102 i += 1;
103 continue;
104 }
105
106 let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
109
110 let is_continuation = next_trimmed.starts_with("Consider ")
112 || next_trimmed.starts_with("When ")
113 || next_trimmed.starts_with("Yield ");
114
115 if is_indented || is_continuation {
116 block.push(' ');
117 block.push_str(next_trimmed);
118 i += 1;
119 } else {
120 break;
122 }
123 }
124
125 statements.push(block);
126 continue;
127 }
128
129 if trimmed.starts_with("## Theorem:") {
132 let mut block = String::new();
133 block.push_str(trimmed);
134 i += 1;
135
136 while i < lines.len() {
138 let next_line = lines[i];
139 let next_trimmed = next_line.trim();
140
141 if next_trimmed.is_empty() {
143 i += 1;
144 continue;
145 }
146
147 if next_trimmed.starts_with("--") {
149 i += 1;
150 continue;
151 }
152
153 let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
155
156 let is_theorem_part = next_trimmed.starts_with("Statement:")
158 || next_trimmed.starts_with("Proof:");
159
160 if is_indented || is_theorem_part {
161 block.push('\n');
162 block.push_str(next_line);
163 i += 1;
164
165 if next_trimmed.starts_with("Proof:") && next_trimmed.ends_with('.') {
167 break;
168 }
169 } else {
170 break;
172 }
173 }
174
175 statements.push(block);
176 continue;
177 }
178
179 if (trimmed.starts_with("A ") || trimmed.starts_with("An ")) && trimmed.contains(" is either") {
181 if trimmed.ends_with('.') && !trimmed.trim_end_matches('.').ends_with(':') {
183 statements.push(trimmed.to_string());
184 i += 1;
185 continue;
186 }
187
188 let mut block = String::new();
190 block.push_str(trimmed);
191 i += 1;
192
193 while i < lines.len() {
195 let next_line = lines[i];
196 let next_trimmed = next_line.trim();
197
198 if next_trimmed.is_empty() {
200 i += 1;
201 continue;
202 }
203
204 if next_trimmed.starts_with("--") {
206 i += 1;
207 continue;
208 }
209
210 let is_indented = next_line.starts_with(' ') || next_line.starts_with('\t');
212
213 let looks_like_variant = next_trimmed.starts_with("a ")
215 || next_trimmed.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
216
217 if is_indented || (looks_like_variant && !next_trimmed.starts_with("A ") && !next_trimmed.starts_with("An ")) {
218 if !block.ends_with(':') {
220 block.push_str(" or ");
221 } else {
222 block.push(' ');
223 }
224 block.push_str(next_trimmed.trim_end_matches('.'));
225 i += 1;
226 } else {
227 break;
228 }
229 }
230
231 if !block.ends_with('.') {
233 block.push('.');
234 }
235 statements.push(block);
236 continue;
237 }
238
239 let mut current_stmt = String::new();
241 while i < lines.len() {
242 let line = lines[i];
243 let trimmed = line.trim();
244
245 if trimmed.is_empty() || trimmed.starts_with("--") {
246 i += 1;
247 continue;
248 }
249
250 if !current_stmt.is_empty() {
251 current_stmt.push(' ');
252 }
253 current_stmt.push_str(trimmed);
254
255 i += 1;
256
257 if trimmed.ends_with('.') {
259 break;
260 }
261 }
262
263 if !current_stmt.is_empty() {
264 statements.push(current_stmt);
265 }
266 }
267
268 statements
269}
270
271async fn load_dir_recursive<V: Vfs>(vfs: &V, path: &str, parent: &mut FileNode) -> VfsResult<()> {
273 let entries = vfs.list_dir(path).await?;
274
275 for entry in entries {
276 let full_path = if path == "/" {
277 format!("/{}", entry.name)
278 } else {
279 format!("{}/{}", path, entry.name)
280 };
281
282 if entry.is_directory {
283 let mut dir_node = FileNode::directory(entry.name.clone(), full_path.clone());
284 let _ = Box::pin(load_dir_recursive(vfs, &full_path, &mut dir_node)).await;
286 parent.children.push(dir_node);
287 } else {
288 parent.children.push(FileNode::file(entry.name, full_path));
289 }
290 }
291
292 parent.children.sort_by(|a, b| {
294 match (a.is_directory, b.is_directory) {
295 (true, false) => std::cmp::Ordering::Less,
296 (false, true) => std::cmp::Ordering::Greater,
297 _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
298 }
299 });
300
301 Ok(())
302}
303
304fn count_files(node: &FileNode) -> usize {
306 let mut count = node.children.len();
307 for child in &node.children {
308 if child.is_directory {
309 count += count_files(child);
310 }
311 }
312 count
313}
314
315fn format_derivation_html(tree: &DerivationTree) -> String {
317 fn format_node(tree: &DerivationTree, depth: usize) -> String {
318 let indent = " ".repeat(depth);
319 let rule_class = "rule";
320 let conclusion_class = "conclusion";
321
322 let mut result = String::new();
323
324 result.push_str(&format!(
326 "{}<span class=\"{}\">{:?}:</span> <span class=\"{}\">{}</span>\n",
327 indent,
328 rule_class,
329 tree.rule,
330 conclusion_class,
331 tree.conclusion
332 ));
333
334 if !tree.premises.is_empty() {
336 for premise in &tree.premises {
337 result.push_str(&format_node(premise, depth + 1));
338 }
339 }
340
341 result
342 }
343
344 let mut output = String::new();
345 output.push_str("<div style=\"font-family: monospace; white-space: pre-wrap;\">\n");
346 output.push_str(&format_node(tree, 0));
347 output.push_str("</div>");
348 output
349}
350
351fn esc(s: &str) -> String {
353 s.replace('&', "&").replace('<', "<").replace('>', ">")
354}
355
356fn format_grid_html(grid: &SolvedGrid) -> String {
360 let cell = "border:1px solid var(--studio-border);padding:4px 10px;text-align:center;";
361 let head = "border:1px solid var(--studio-border);padding:4px 10px;text-align:center;color:var(--studio-accent);font-weight:600;";
362 let mut s = String::new();
363 s.push_str("<div style=\"margin-bottom:16px;\">");
364 s.push_str("<div style=\"font-family:monospace;font-size:12px;color:var(--studio-text-secondary);margin-bottom:6px;\">Solved grid — every filled cell certified, no Z3:</div>");
365 s.push_str("<table style=\"border-collapse:collapse;font-family:monospace;font-size:13px;color:var(--studio-text);\">");
366 s.push_str("<tr>");
367 s.push_str(&format!("<th style=\"{head}\">{}</th>", esc(&grid.row_label)));
368 for col in &grid.columns {
369 s.push_str(&format!("<th style=\"{head}\">{}</th>", esc(&col.label)));
370 }
371 s.push_str("</tr>");
372 for (ri, row) in grid.rows.iter().enumerate() {
373 s.push_str("<tr>");
374 s.push_str(&format!("<td style=\"{cell}font-weight:600;\">{}</td>", esc(row)));
375 for col in &grid.columns {
376 match col.cells.get(ri).and_then(|c| c.as_ref()) {
377 Some(v) => s.push_str(&format!("<td style=\"{cell}\">{}</td>", esc(v))),
378 None => s.push_str(&format!("<td style=\"{cell}color:var(--studio-text-muted);\">·</td>")),
379 }
380 }
381 s.push_str("</tr>");
382 }
383 s.push_str("</table></div>");
384 s
385}
386
387fn format_answer_html(answers: &[String]) -> String {
389 let body = if answers.is_empty() {
390 "<span style=\"color:var(--studio-text-muted);\">no individual satisfies the question</span>".to_string()
391 } else {
392 format!(
393 "<span style=\"color:#34d399;font-weight:600;\">{}</span>",
394 esc(&answers.join(", "))
395 )
396 };
397 format!(
398 "<div style=\"font-family:monospace;font-size:14px;margin-bottom:12px;\"><span style=\"color:var(--studio-text-secondary);\">Answer: </span>{body}</div>"
399 )
400}
401
402fn theorem_proof_html(result: &TheoremCompileResult) -> String {
405 let mut html = String::new();
406 if let Some(grid) = &result.grid {
407 html.push_str(&format_grid_html(grid));
408 }
409 if let Some(answer) = &result.answer {
410 html.push_str(&format_answer_html(answer));
411 } else if let Some(derivation) = &result.derivation {
412 html.push_str(&format_derivation_html(derivation));
413 }
414 html
415}
416
417fn theorem_proof_hint(result: &TheoremCompileResult) -> String {
419 if let Some(answer) = &result.answer {
420 if answer.is_empty() {
421 "No answer found.".to_string()
422 } else {
423 format!("Answer: {}", answer.join(", "))
424 }
425 } else if result.verified {
426 format!("Theorem '{}' proved!", result.name)
427 } else {
428 format!("Theorem '{}' — grid solved.", result.name)
429 }
430}
431
432#[derive(Clone, Copy, PartialEq, Eq, Default)]
434enum CodeOutputMode {
435 #[default]
436 Interpret,
437 Rust,
438}
439
440#[derive(Clone, Copy, PartialEq, Eq, Default)]
443enum LogicView {
444 #[default]
445 Logic,
446 Rust,
447}
448
449const STUDIO_STYLE: &str = r#"
451/* ============================================ */
452/* STUDIO PAGE - Design Tokens */
453/* ============================================ */
454:root {
455 --studio-bg: #0f1419;
456 --studio-panel-bg: #12161c;
457 --studio-elevated: #1a1f27;
458 --studio-border: rgba(255, 255, 255, 0.08);
459 --studio-border-hover: rgba(255, 255, 255, 0.15);
460 --studio-text: #e8eaed;
461 --studio-text-secondary: #9ca3af;
462 --studio-text-muted: #6b7280;
463 --studio-accent: #667eea;
464 /* Heights for positioning fixed sidebar below header */
465 --nav-height: 97px;
466 --toolbar-height: 49px;
467 --header-height: calc(var(--nav-height) + var(--toolbar-height));
468}
469
470@media (max-width: 980px) {
471 :root {
472 --nav-height: 89px;
473 }
474}
475
476@media (max-width: 768px) {
477 :root {
478 --nav-height: 81px;
479 --toolbar-height: 90px;
480 }
481}
482
483@media (max-width: 640px) {
484 :root {
485 --nav-height: 65px;
486 }
487}
488
489/* ============================================ */
490/* STUDIO PAGE - Desktop Layout */
491/* ============================================ */
492.studio-container {
493 display: flex;
494 flex-direction: column;
495 height: 100vh;
496 height: 100dvh;
497 background: var(--studio-bg);
498 color: var(--studio-text);
499 overflow: hidden;
500}
501
502/* Toolbar with mode toggle */
503.studio-toolbar {
504 display: flex;
505 align-items: center;
506 justify-content: space-between;
507 height: var(--toolbar-height);
508 padding: 0 16px;
509 background: var(--studio-panel-bg);
510 border-bottom: 1px solid var(--studio-border);
511 gap: 12px;
512 flex-shrink: 0;
513}
514
515.studio-toolbar-left {
516 display: flex;
517 align-items: center;
518 gap: 12px;
519}
520
521.studio-toolbar-center {
522 flex: 1;
523 display: flex;
524 justify-content: center;
525}
526
527.studio-toolbar-right {
528 display: flex;
529 align-items: center;
530 gap: 8px;
531}
532
533.sidebar-toggle-btn {
534 padding: 8px 12px;
535 background: rgba(255, 255, 255, 0.04);
536 border: 1px solid rgba(255, 255, 255, 0.08);
537 border-radius: 6px;
538 color: rgba(255, 255, 255, 0.7);
539 font-size: 14px;
540 cursor: pointer;
541 transition: all 0.15s ease;
542}
543
544.sidebar-toggle-btn:hover {
545 background: rgba(255, 255, 255, 0.08);
546 color: rgba(255, 255, 255, 0.9);
547}
548
549/* Main content area with optional sidebar */
550.studio-content {
551 flex: 1;
552 display: flex;
553 overflow: hidden;
554 position: relative;
555}
556
557/* Overlay to close sidebar when clicking outside (mobile) */
558.sidebar-overlay {
559 display: none;
560}
561
562@media (max-width: 768px) {
563 .sidebar-overlay {
564 display: block;
565 position: fixed;
566 top: calc(var(--header-height) + 10px);
567 left: 0;
568 right: 0;
569 bottom: 0;
570 background: rgba(0, 0, 0, 0.5);
571 z-index: 99;
572 }
573}
574
575/* Sidebar wrapper for controlled width */
576.studio-sidebar {
577 display: flex;
578 flex-shrink: 0;
579 overflow: hidden;
580}
581
582@media (max-width: 768px) {
583 .studio-sidebar {
584 position: fixed;
585 left: 0;
586 top: calc(var(--header-height) + 10px);
587 bottom: 0;
588 z-index: 100;
589 width: 280px !important;
590 min-width: 280px !important;
591 max-width: 280px !important;
592 box-shadow: 4px 0 20px rgba(0, 0, 0, 0.3);
593 background: #12161c;
594 }
595
596 /* Text color overrides for Safari compatibility */
597 .studio-sidebar .file-tree-item {
598 color: rgba(255, 255, 255, 0.9) !important;
599 -webkit-text-fill-color: rgba(255, 255, 255, 0.9);
600 }
601
602 .studio-sidebar .file-tree-item.selected {
603 color: #00d4ff !important;
604 -webkit-text-fill-color: #00d4ff;
605 }
606
607 .studio-sidebar .file-tree-item .name {
608 color: inherit;
609 -webkit-text-fill-color: inherit;
610 }
611}
612
613/* Desktop: 3-column panel layout */
614.studio-main {
615 flex: 1;
616 display: flex;
617 overflow: hidden;
618 gap: 1px;
619 background: var(--studio-border);
620}
621
622.studio-panel {
623 background: var(--studio-panel-bg);
624 display: flex;
625 flex-direction: column;
626 overflow: hidden;
627 min-width: 200px;
628}
629
630.studio-panel .panel-header {
631 padding: 0 20px;
632 height: 52px;
633 background: rgba(255, 255, 255, 0.02);
634 border-bottom: 1px solid var(--studio-border);
635 font-size: 16px;
636 font-weight: 600;
637 letter-spacing: 0.3px;
638 color: var(--studio-text);
639 display: flex;
640 justify-content: space-between;
641 align-items: center;
642 flex-shrink: 0;
643}
644
645.studio-panel .panel-content {
646 flex: 1;
647 min-height: 0;
648 overflow: auto;
649 -webkit-overflow-scrolling: touch;
650}
651
652/* Panel Resizers (desktop only) */
653.panel-resizer {
654 width: 4px;
655 background: var(--studio-border);
656 cursor: col-resize;
657 transition: background 0.2s ease;
658 flex-shrink: 0;
659}
660
661.panel-resizer:hover,
662.panel-resizer.active {
663 background: var(--studio-accent);
664}
665
666/* Format Toggle (Unicode/LaTeX) */
667.format-toggle {
668 display: flex;
669 gap: 4px;
670 background: rgba(255, 255, 255, 0.04);
671 border: 1px solid var(--studio-border);
672 border-radius: 6px;
673 padding: 2px;
674}
675
676.format-btn {
677 padding: 4px 10px;
678 border: none;
679 background: transparent;
680 color: var(--studio-text-muted);
681 font-size: 12px;
682 border-radius: 4px;
683 cursor: pointer;
684 transition: all 0.15s ease;
685 line-height: 1;
686}
687
688.format-btn:hover {
689 color: var(--studio-text);
690 background: rgba(255, 255, 255, 0.04);
691}
692
693.format-btn.active {
694 background: rgba(255, 255, 255, 0.08);
695 color: var(--studio-text);
696}
697
698/* Guide Bar - above panels */
699.studio-guide {
700 background: var(--studio-panel-bg);
701 border-bottom: 1px solid var(--studio-border);
702 flex-shrink: 0;
703}
704
705/* Execute button for Code mode */
706.execute-btn {
707 padding: 8px 14px;
708 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
709 border: none;
710 border-radius: 8px;
711 color: white;
712 font-size: 13px;
713 font-weight: 500;
714 cursor: pointer;
715 transition: all 0.15s ease;
716}
717
718/* Desktop: show short labels */
719.execute-btn .mobile-label {
720 display: none;
721}
722
723.execute-btn .desktop-label {
724 display: inline;
725}
726
727.execute-btn:hover {
728 transform: translateY(-1px);
729 box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
730}
731
732.execute-btn:active {
733 transform: translateY(0);
734}
735
736/* Mobile execute buttons */
737@media (max-width: 768px) {
738 .execute-btn {
739 padding: 6px 10px;
740 font-size: 12px;
741 white-space: nowrap;
742 }
743}
744
745@media (max-width: 480px) {
746 .execute-btn {
747 padding: 5px 8px;
748 font-size: 11px;
749 }
750}
751
752/* Output mode toggle for Code mode */
753.output-mode-toggle {
754 display: flex;
755 gap: 4px;
756 background: rgba(255, 255, 255, 0.04);
757 border: 1px solid var(--studio-border);
758 border-radius: 6px;
759 padding: 2px;
760}
761
762.output-mode-btn {
763 padding: 4px 10px;
764 border: none;
765 background: transparent;
766 color: var(--studio-text-muted);
767 font-size: 12px;
768 border-radius: 4px;
769 cursor: pointer;
770 transition: all 0.15s ease;
771 line-height: 1;
772}
773
774.output-mode-btn:hover {
775 color: var(--studio-text);
776 background: rgba(255, 255, 255, 0.04);
777}
778
779.output-mode-btn.active {
780 background: rgba(255, 255, 255, 0.08);
781 color: var(--studio-text);
782}
783
784/* Interpreter output display */
785.interpreter-output {
786 padding: 16px;
787 font-family: 'SF Mono', 'Fira Code', monospace;
788 font-size: 14px;
789 line-height: 1.6;
790}
791
792.interpreter-line {
793 margin-bottom: 4px;
794 color: #e8eaed;
795}
796
797.interpreter-error {
798 color: #e06c75;
799 padding: 12px;
800 background: rgba(224, 108, 117, 0.1);
801 border-radius: 6px;
802 margin-top: 8px;
803}
804
805.interpreter-empty {
806 color: rgba(255, 255, 255, 0.4);
807 text-align: center;
808 padding: 40px 20px;
809}
810
811/* ============================================ */
812/* STUDIO PAGE - Mobile Overrides */
813/* ============================================ */
814
815/* Mode label: hidden on desktop, shown on mobile */
816.mode-label {
817 display: none;
818}
819
820@media (max-width: 768px) {
821 .studio-toolbar {
822 flex-wrap: wrap;
823 height: auto;
824 padding: 10px 12px;
825 gap: 10px;
826 position: relative;
827 z-index: 101;
828 }
829
830 /* First row: file toggle, mode selector */
831 .studio-toolbar-left {
832 flex-shrink: 0;
833 order: 1;
834 }
835
836 .studio-toolbar-center {
837 flex: 1;
838 align-items: center;
839 min-width: 0;
840 order: 2;
841 }
842
843 /* Second row: action buttons - full width */
844 .studio-toolbar-right {
845 flex-basis: 100%;
846 justify-content: center;
847 gap: 12px;
848 order: 3;
849 padding-top: 8px;
850 border-top: 1px solid rgba(255, 255, 255, 0.06);
851 }
852
853 /* Show mode toggle with "Mode:" label */
854 .mode-label {
855 display: block;
856 font-size: 12px;
857 font-weight: 500;
858 color: rgba(255, 255, 255, 0.5);
859 margin-right: 6px;
860 white-space: nowrap;
861 }
862
863 /* Full text on mobile buttons */
864 .execute-btn {
865 padding: 10px 16px;
866 font-size: 14px;
867 }
868
869 .execute-btn .mobile-label {
870 display: inline;
871 }
872
873 .execute-btn .desktop-label {
874 display: none;
875 }
876
877 /* Hide mobile tab bar - panels stack instead */
878 .mobile-tabs {
879 display: none !important;
880 }
881
882 /* Hide desktop resizers */
883 .panel-resizer {
884 display: none;
885 }
886
887 /* Stacked vertical panel layout */
888 .studio-main {
889 flex-direction: column;
890 gap: 0;
891 background: var(--studio-bg);
892 }
893
894 /* Both panels visible, stacked vertically */
895 .studio-panel {
896 min-width: unset;
897 min-height: 0;
898 width: 100% !important;
899 }
900
901 .studio-panel.mobile-expanded {
902 flex: var(--panel-flex, 1);
903 overflow: hidden;
904 }
905
906 .studio-panel.mobile-collapsed {
907 flex: 0 0 auto;
908 }
909
910 .studio-panel.mobile-collapsed .panel-content {
911 display: none;
912 }
913
914 /* Show panel headers with collapse affordance */
915 .studio-panel .panel-header {
916 display: flex !important;
917 cursor: pointer;
918 padding: 0 14px;
919 height: 44px;
920 font-size: 14px;
921 }
922
923 /* Collapse chevron indicator */
924 .studio-panel .panel-header::after {
925 content: '\25BC';
926 font-size: 10px;
927 color: rgba(255, 255, 255, 0.4);
928 margin-left: 8px;
929 transition: transform 0.15s ease;
930 }
931
932 .studio-panel.mobile-collapsed .panel-header::after {
933 content: '\25B6';
934 }
935
936 /* Divider between stacked panels */
937 .studio-panel + .studio-panel {
938 border-top: 1px solid var(--studio-border);
939 }
940
941 /* Hide Panel 3 (Console/Tree/Context) on mobile */
942 .studio-main > aside {
943 display: none !important;
944 }
945
946 /* Mobile-sized format toggle */
947 .format-toggle {
948 gap: 4px;
949 padding: 2px;
950 border-radius: 6px;
951 }
952
953 .format-btn {
954 padding: 6px 10px;
955 font-size: 12px;
956 border-radius: 4px;
957 min-height: 32px;
958 min-width: 32px;
959 display: flex;
960 align-items: center;
961 justify-content: center;
962 }
963
964 /* Footer constraints */
965 .studio-footer {
966 max-height: 30vh;
967 overflow: auto;
968 }
969
970 .mobile-panel-resizer {
971 display: flex;
972 align-items: center;
973 justify-content: center;
974 height: 16px;
975 cursor: row-resize;
976 flex-shrink: 0;
977 touch-action: none;
978 -webkit-tap-highlight-color: transparent;
979 z-index: 2;
980 position: relative;
981 }
982
983 .mobile-panel-resizer::after {
984 content: "";
985 position: absolute;
986 left: 50%;
987 top: 50%;
988 transform: translate(-50%, -50%);
989 width: 48px;
990 height: 4px;
991 border-radius: 2px;
992 background: rgba(255, 255, 255, 0.15);
993 transition: background 0.15s ease;
994 }
995
996 .mobile-panel-resizer:hover::after,
997 .mobile-panel-resizer.active::after {
998 background: rgba(102, 126, 234, 0.6);
999 }
1000}
1001
1002/* Extra small screens */
1003@media (max-width: 480px) {
1004 .format-btn {
1005 padding: 4px 8px;
1006 font-size: 11px;
1007 }
1008}
1009
1010/* Desktop: hide mobile resizer */
1011.mobile-panel-resizer {
1012 display: none;
1013}
1014
1015.studio-container.mobile-resizing {
1016 user-select: none;
1017 -webkit-user-select: none;
1018}
1019
1020/* Landscape mobile */
1021@media (max-height: 500px) and (orientation: landscape) {
1022 .studio-footer {
1023 max-height: 25vh;
1024 }
1025}
1026"#;
1027
1028#[derive(Clone, Copy, PartialEq, Default)]
1030enum MobileTab {
1031 #[default]
1032 Panel1,
1033 Panel2,
1034 Panel3,
1035}
1036
1037fn kg_svg(kg: &logicaffeine_compile::codegen_sva::hw_pipeline::KgSummary) -> String {
1041 use std::fmt::Write as _;
1042 let n = kg.nodes.len();
1043 if n == 0 {
1044 return String::new();
1045 }
1046 let (w, h) = (340.0_f64, 300.0_f64);
1047 let (cx, cy) = (w / 2.0, h / 2.0);
1048 let ring = (w.min(h) / 2.0 - 50.0).max(34.0);
1049 let node_r = 15.0_f64;
1050 let pos: Vec<(f64, f64)> = (0..n)
1051 .map(|i| {
1052 let ang = (i as f64) / (n as f64) * std::f64::consts::TAU - std::f64::consts::FRAC_PI_2;
1053 (cx + ring * ang.cos(), cy + ring * ang.sin())
1054 })
1055 .collect();
1056 let color = |role: &str| match role {
1057 "input" => "#60a5fa",
1058 "output" => "#4ade80",
1059 "clock" => "#fbbf24",
1060 _ => "#a78bfa",
1061 };
1062 let esc = |s: &str| s.replace('&', "&").replace('<', "<").replace('>', ">");
1063
1064 let mut svg = String::new();
1065 let _ = write!(
1066 svg,
1067 r##"<svg viewBox="0 0 {w} {h}" width="100%" style="max-height:300px" xmlns="http://www.w3.org/2000/svg">"##
1068 );
1069 svg.push_str(
1070 r##"<defs><marker id="kgar" markerWidth="9" markerHeight="9" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="rgba(255,255,255,0.45)"/></marker></defs>"##,
1071 );
1072 svg.push_str(
1073 r##"<style>
1074.kge{stroke:rgba(167,139,250,0.55);stroke-width:1.5;fill:none;stroke-dasharray:5 4;animation:kgflow .9s linear infinite}
1075.kgn{stroke:rgba(255,255,255,0.9);stroke-width:1.5;animation:kgpulse 2.6s ease-in-out infinite}
1076.kgr{fill:rgba(255,255,255,0.5);font:9px ui-monospace,monospace;text-anchor:middle}
1077.kgl{fill:#e5e7eb;font:11px ui-sans-serif,system-ui;text-anchor:middle;font-weight:600}
1078@keyframes kgflow{to{stroke-dashoffset:-18}}
1079@keyframes kgpulse{0%,100%{opacity:1}50%{opacity:.72}}
1080</style>"##,
1081 );
1082 for l in &kg.links {
1083 let (x1, y1) = pos[l.from];
1084 let (x2, y2) = pos[l.to];
1085 let (dx, dy) = (x2 - x1, y2 - y1);
1086 let len = (dx * dx + dy * dy).sqrt().max(1.0);
1087 let (ux, uy) = (dx / len, dy / len);
1088 let (sx, sy) = (x1 + ux * node_r, y1 + uy * node_r);
1089 let (ex, ey) = (x2 - ux * (node_r + 6.0), y2 - uy * (node_r + 6.0));
1090 let _ = write!(
1091 svg,
1092 r##"<line class="kge" x1="{sx:.1}" y1="{sy:.1}" x2="{ex:.1}" y2="{ey:.1}" marker-end="url(#kgar)"/>"##
1093 );
1094 let (mx, my) = ((sx + ex) / 2.0, (sy + ey) / 2.0 - 2.0);
1095 let _ = write!(svg, r##"<text class="kgr" x="{mx:.1}" y="{my:.1}">{}</text>"##, esc(&l.relation));
1096 }
1097 for (i, node) in kg.nodes.iter().enumerate() {
1098 let (x, y) = pos[i];
1099 let _ = write!(
1100 svg,
1101 r##"<circle class="kgn" cx="{x:.1}" cy="{y:.1}" r="{node_r}" fill="{}"/>"##,
1102 color(&node.role)
1103 );
1104 let label = if node.width > 1 {
1105 format!("{}[{}]", node.name, node.width)
1106 } else {
1107 node.name.clone()
1108 };
1109 let _ = write!(svg, r##"<text class="kgl" x="{x:.1}" y="{:.1}">{}</text>"##, y + 28.0, esc(&label));
1110 }
1111 svg.push_str("</svg>");
1112 svg
1113}
1114
1115fn waveform_svg(wf: &logicaffeine_compile::codegen_sva::hw_pipeline::Waveform) -> String {
1119 use std::fmt::Write as _;
1120 let (ts, n) = (wf.timesteps, wf.signals.len());
1121 if ts == 0 || n == 0 {
1122 return String::new();
1123 }
1124 let (gutter, col_w, row_h, top) = (72.0_f64, 46.0_f64, 34.0_f64, 18.0_f64);
1125 let span = (ts as f64) * col_w;
1126 let (w, h) = (gutter + span + 10.0, top + (n as f64) * row_h + 6.0);
1127 let dur = (0.45 * ts as f64).max(1.2);
1128 let esc = |s: &str| s.replace('&', "&").replace('<', "<").replace('>', ">");
1129
1130 let mut s = String::new();
1131 let _ = write!(s, r##"<svg viewBox="0 0 {w:.0} {h:.0}" width="100%" xmlns="http://www.w3.org/2000/svg">"##);
1132 let _ = write!(
1133 s,
1134 r##"<style>
1135.wfg{{stroke:rgba(255,255,255,0.07);stroke-width:1}}
1136.wft{{fill:none;stroke:#22d3ee;stroke-width:2;stroke-linejoin:round}}
1137.wfb{{fill:rgba(96,165,250,0.12);stroke:#60a5fa;stroke-width:1.5}}
1138.wfv{{fill:#dbeafe;font:11px ui-monospace,monospace;text-anchor:middle}}
1139.wfn{{fill:#e5e7eb;font:11px ui-monospace,monospace;text-anchor:end}}
1140.wftl{{fill:rgba(255,255,255,0.4);font:9px ui-monospace,monospace;text-anchor:middle}}
1141.wfph{{stroke:#f472b6;stroke-width:1.5;opacity:.8;animation:wfsweep {dur:.1}s linear infinite}}
1142@keyframes wfsweep{{from{{transform:translateX(0)}}to{{transform:translateX({span:.0}px)}}}}
1143</style>"##
1144 );
1145 for t in 0..ts {
1146 let x = gutter + (t as f64) * col_w;
1147 let _ = write!(s, r##"<line class="wfg" x1="{x:.1}" y1="{top:.1}" x2="{x:.1}" y2="{:.1}"/>"##, h - 4.0);
1148 let _ = write!(s, r##"<text class="wftl" x="{:.1}" y="12">t{t}</text>"##, x + col_w / 2.0);
1149 }
1150 let xe = gutter + span;
1151 let _ = write!(s, r##"<line class="wfg" x1="{xe:.1}" y1="{top:.1}" x2="{xe:.1}" y2="{:.1}"/>"##, h - 4.0);
1152
1153 for (ri, sig) in wf.signals.iter().enumerate() {
1154 let rtop = top + (ri as f64) * row_h;
1155 let (hi, lo) = (rtop + 6.0, rtop + row_h - 12.0);
1156 let mid = (hi + lo) / 2.0;
1157 let label = if sig.width > 1 { format!("{}[{}]", sig.name, sig.width) } else { sig.name.clone() };
1158 let _ = write!(s, r##"<text class="wfn" x="{:.1}" y="{:.1}">{}</text>"##, gutter - 8.0, mid + 4.0, esc(&label));
1159 if sig.width == 1 {
1160 let mut pts = String::new();
1161 for t in 0..ts {
1162 let x0 = gutter + (t as f64) * col_w;
1163 let lvl = match sig.values.get(t as usize) {
1164 Some(Some(v)) => {
1165 if *v != 0 {
1166 hi
1167 } else {
1168 lo
1169 }
1170 }
1171 _ => mid,
1172 };
1173 let _ = write!(pts, "{x0:.1},{lvl:.1} {:.1},{lvl:.1} ", x0 + col_w);
1174 }
1175 let _ = write!(s, r##"<polyline class="wft" points="{}"/>"##, pts.trim());
1176 } else {
1177 for t in 0..ts {
1178 if let Some(Some(v)) = sig.values.get(t as usize) {
1179 let x0 = gutter + (t as f64) * col_w;
1180 let _ = write!(
1181 s,
1182 r##"<rect class="wfb" x="{:.1}" y="{hi:.1}" width="{:.1}" height="{:.1}" rx="3"/>"##,
1183 x0 + 2.0,
1184 col_w - 4.0,
1185 lo - hi
1186 );
1187 let _ = write!(s, r##"<text class="wfv" x="{:.1}" y="{:.1}">{v}</text>"##, x0 + col_w / 2.0, mid + 4.0);
1188 }
1189 }
1190 }
1191 }
1192 let _ = write!(s, r##"<line class="wfph" x1="{gutter:.1}" y1="{top:.1}" x2="{gutter:.1}" y2="{:.1}"/>"##, h - 4.0);
1193 s.push_str("</svg>");
1194 s
1195}
1196
1197fn wf_val(sig: &logicaffeine_compile::codegen_sva::hw_pipeline::WaveSignal, t: usize) -> u64 {
1198 sig.values.get(t).and_then(|v| *v).unwrap_or(0)
1199}
1200
1201#[derive(Clone, Copy, Debug, PartialEq)]
1204struct TrafficPhase {
1205 ns: u64,
1206 ew: u64,
1207 nsl: u64,
1208 ewl: u64,
1209 ped: u64,
1210 start: f64,
1211 dur: f64,
1212}
1213
1214impl TrafficPhase {
1215 fn val(&self, pfx: &str) -> u64 {
1216 match pfx {
1217 "ns" => self.ns,
1218 "ew" => self.ew,
1219 "nsl" => self.nsl,
1220 "ewl" => self.ewl,
1221 "ped" => self.ped,
1222 _ => 0,
1223 }
1224 }
1225}
1226
1227struct TrafficPlan {
1230 phases: Vec<TrafficPhase>,
1231 firsts: Vec<usize>,
1233 total: f64,
1234 conflict: Option<usize>,
1235}
1236
1237#[derive(Clone, Copy, Debug, PartialEq)]
1240enum NsDrive {
1241 Hidden,
1242 Through,
1243 CrashStop,
1244}
1245
1246fn traffic_plan(wf: &logicaffeine_compile::codegen_sva::hw_pipeline::Waveform) -> Option<TrafficPlan> {
1248 use logicaffeine_compile::codegen_sva::hw_pipeline::WaveSignal;
1249 let get = |name: &str| wf.signals.iter().find(|s| s.name.eq_ignore_ascii_case(name));
1250 let ns = get("ns")?;
1251 let ew = get("ew")?;
1252 let nsl = get("nsl");
1253 let ewl = get("ewl");
1254 let ped = get("ped");
1255 let n = (wf.timesteps as usize).max(1);
1256 let sv = |sig: &WaveSignal, t: usize| wf_val(sig, t);
1257 let svo = |sig: Option<&WaveSignal>, t: usize| sig.map_or(0u64, |s| wf_val(s, t));
1258 let state = |t: usize| (sv(ns, t), sv(ew, t), svo(nsl, t), svo(ewl, t), svo(ped, t));
1259
1260 let mut firsts: Vec<usize> = Vec::new();
1261 let mut prev: Option<(u64, u64, u64, u64, u64)> = None;
1262 for t in 0..n {
1263 let k = state(t);
1264 if Some(k) != prev {
1265 firsts.push(t);
1266 prev = Some(k);
1267 }
1268 }
1269 let states: Vec<(u64, u64, u64, u64, u64)> = firsts.iter().map(|&t| state(t)).collect();
1270 let nsc = states.len();
1271 let conflict = (0..nsc).find(|&i| {
1272 let (a, b, c, d, e) = states[i];
1273 (a != 0 && b != 0) || (e == 1 && (a != 0 || b != 0 || c != 0 || d != 0))
1274 });
1275 let mut phases = Vec::with_capacity(nsc);
1276 let mut acc = 0.0f64;
1277 for i in 0..nsc {
1278 let (a, b, c, d, e) = states[i];
1279 let dur = if Some(i) == conflict {
1280 3.6
1281 } else if e == 1 {
1282 7.0
1283 } else if a == 2 || b == 2 || c == 2 || d == 2 {
1284 1.3
1285 } else if a == 1 || b == 1 || c == 1 || d == 1 {
1286 2.6
1287 } else {
1288 1.0
1289 };
1290 phases.push(TrafficPhase { ns: a, ew: b, nsl: c, ewl: d, ped: e, start: acc, dur });
1291 acc += dur;
1292 }
1293 Some(TrafficPlan { phases, firsts, total: acc.max(1.0), conflict })
1294}
1295
1296fn ns_drive(plan: &TrafficPlan, i: usize) -> NsDrive {
1297 if plan.phases[i].ns != 1 {
1298 NsDrive::Hidden
1299 } else if plan.conflict == Some(i) {
1300 NsDrive::CrashStop
1301 } else {
1302 NsDrive::Through
1303 }
1304}
1305
1306fn countdown_seq(dur: f64) -> Vec<u32> {
1309 let secs = dur.ceil() as i32;
1310 (0..secs).map(|j| (secs - j).clamp(0, 9) as u32).collect()
1311}
1312
1313fn traffic_svg(wf: &logicaffeine_compile::codegen_sva::hw_pipeline::Waveform) -> Option<String> {
1320 use std::fmt::Write as _;
1321 use logicaffeine_compile::codegen_sva::hw_pipeline::WaveSignal;
1322 let get = |name: &str| wf.signals.iter().find(|s| s.name.eq_ignore_ascii_case(name));
1323 let ns = get("ns")?;
1324 let ew = get("ew")?;
1325 let nsl = get("nsl");
1326 let ewl = get("ewl");
1327 let ped = get("ped");
1328 let sv = |sig: &WaveSignal, t: usize| wf_val(sig, t);
1329
1330 let plan = traffic_plan(wf)?;
1332 let scenes = &plan.firsts;
1333 let nsc = plan.phases.len();
1334 let conflict_scene = plan.conflict;
1335 let total = plan.total;
1336 let starts: Vec<f64> = plan.phases.iter().map(|ph| ph.start).collect();
1337 let durs: Vec<f64> = plan.phases.iter().map(|ph| ph.dur).collect();
1338 let p = |sec: f64| (sec / total * 100.0).clamp(0.0, 100.0);
1339
1340 let mut vheads: Vec<(&str, &WaveSignal, f64, f64, &str, f64, f64)> = vec![
1342 ("ns", ns, 18.0, 16.0, "NS", 100.0, 17.0),
1343 ("ew", ew, 18.0, 278.0, "EW", 100.0, 279.0),
1344 ];
1345 if let Some(s) = nsl {
1346 vheads.push(("nsl", s, 184.0, 16.0, "NS \u{21B0}", 266.0, 17.0));
1347 }
1348 if let Some(s) = ewl {
1349 vheads.push(("ewl", s, 184.0, 278.0, "EW \u{21B0}", 266.0, 279.0));
1350 }
1351
1352 let seven_seg = |x: f64, y: f64, d: u32, color: &str| -> String {
1354 let segs: &str = match d {
1355 0 => "abcdef",
1356 1 => "bc",
1357 2 => "abged",
1358 3 => "abgcd",
1359 4 => "fgbc",
1360 5 => "afgcd",
1361 6 => "afgecd",
1362 7 => "abc",
1363 8 => "abcdefg",
1364 9 => "abcdfg",
1365 _ => "",
1366 };
1367 let (w, h, t) = (13.0f64, 22.0f64, 2.6f64);
1368 let half = h / 2.0;
1369 let mut g = String::new();
1370 for c in segs.chars() {
1371 let (rx, ry, rw, rh) = match c {
1372 'a' => (x + t, y, w - 2.0 * t, t),
1373 'b' => (x + w - t, y + t, t, half - 1.5 * t),
1374 'c' => (x + w - t, y + half + 0.5 * t, t, half - 1.5 * t),
1375 'd' => (x + t, y + h - t, w - 2.0 * t, t),
1376 'e' => (x, y + half + 0.5 * t, t, half - 1.5 * t),
1377 'f' => (x, y + t, t, half - 1.5 * t),
1378 'g' => (x + t, y + half - 0.5 * t, w - 2.0 * t, t),
1379 _ => (0.0, 0.0, 0.0, 0.0),
1380 };
1381 let _ = write!(g, r##"<rect x="{rx:.1}" y="{ry:.1}" width="{rw:.1}" height="{rh:.1}" rx="1" fill="{color}"/>"##);
1382 }
1383 g
1384 };
1385 let walk_icon = |cx: f64, cy: f64, color: &str| -> String {
1387 let mut g = String::new();
1388 let _ = write!(g, r##"<g stroke="{color}" stroke-width="2.6" stroke-linecap="round" fill="none">"##);
1389 let _ = write!(g, r##"<circle cx="{cx:.1}" cy="{:.1}" r="2.7" fill="{color}" stroke="none"/>"##, cy - 9.0);
1390 let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{cx:.1}" y2="{:.1}"/>"##, cy - 6.0, cy + 1.0);
1391 let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy - 4.0, cx - 4.0, cy - 1.0);
1392 let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy - 4.0, cx + 4.0, cy - 2.0);
1393 let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy + 1.0, cx - 4.0, cy + 8.0);
1394 let _ = write!(g, r##"<line x1="{cx:.1}" y1="{:.1}" x2="{:.1}" y2="{:.1}"/>"##, cy + 1.0, cx + 4.0, cy + 7.0);
1395 g.push_str("</g>");
1396 g
1397 };
1398 let hand_icon = |cx: f64, cy: f64| -> String {
1400 let mut g = String::from(r##"<g fill="#f59e0b">"##);
1401 let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="14" height="13" rx="4.5"/>"##, cx - 7.0, cy - 5.0);
1402 let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="8" rx="1.5"/>"##, cx - 7.0, cy - 11.0);
1403 let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="9" rx="1.5"/>"##, cx - 3.5, cy - 12.0);
1404 let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="9" rx="1.5"/>"##, cx + 0.5, cy - 12.0);
1405 let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="3" height="8" rx="1.5"/>"##, cx + 4.0, cy - 11.0);
1406 let _ = write!(g, r##"<rect x="{:.1}" y="{:.1}" width="5" height="3" rx="1.5"/>"##, cx - 10.5, cy - 1.0);
1407 g.push_str("</g>");
1408 g
1409 };
1410
1411 let mut style = String::from("<style>");
1412 for (pfx, sig, _, _, _, _, _) in &vheads {
1414 for (suffix, want) in [("R", 0u64), ("Y", 2), ("G", 1)] {
1415 let _ = write!(style, "@keyframes {pfx}{suffix}{{");
1416 for i in 0..nsc {
1417 let op = if sv(sig, scenes[i]) == want { "1" } else { "0.12" };
1418 let _ = write!(style, "{:.2}%{{opacity:{op}}}", p(starts[i]));
1419 }
1420 let last = if sv(sig, scenes[nsc - 1]) == want { "1" } else { "0.12" };
1421 let _ = write!(style, "100%{{opacity:{last}}}}}");
1422 }
1423 }
1424 if let Some(pp) = ped {
1426 for (name, want) in [("pedWALK", 1u64), ("pedSTOP", 0)] {
1427 let _ = write!(style, "@keyframes {name}{{");
1428 for i in 0..nsc {
1429 let op = if sv(pp, scenes[i]) == want { "1" } else { "0" };
1430 let _ = write!(style, "{:.2}%{{opacity:{op}}}", p(starts[i]));
1431 }
1432 let last = if sv(pp, scenes[nsc - 1]) == want { "1" } else { "0" };
1433 let _ = write!(style, "100%{{opacity:{last}}}}}");
1434 }
1435 }
1436 {
1443 let _ = write!(style, "@keyframes nsplat_op{{");
1444 for i in 0..nsc {
1445 let o = if ns_drive(&plan, i) == NsDrive::Hidden { "0" } else { "1" };
1446 let _ = write!(style, "{:.2}%{{opacity:{o}}}", p(starts[i]));
1447 }
1448 let lo = if ns_drive(&plan, nsc - 1) == NsDrive::Hidden { "0" } else { "1" };
1449 let _ = write!(style, "100%{{opacity:{lo}}}}}");
1450 let _ = write!(style, "@keyframes nsplat_mv{{");
1451 for i in 0..nsc {
1452 let t0 = starts[i];
1453 match ns_drive(&plan, i) {
1454 NsDrive::Through => {
1455 let _ = write!(style, "{:.2}%{{transform:translateY(0px)}}", p(t0));
1456 let _ = write!(style, "{:.2}%{{transform:translateY(360px)}}", p(t0 + durs[i]));
1457 }
1458 NsDrive::CrashStop => {
1459 let _ = write!(style, "{:.2}%{{transform:translateY(0px)}}", p(t0));
1460 let _ = write!(style, "{:.2}%{{transform:translateY(240px)}}", p(t0 + 1.8));
1461 let _ = write!(style, "{:.2}%{{transform:translateY(240px)}}", p(t0 + durs[i]));
1462 }
1463 NsDrive::Hidden => {
1464 let _ = write!(style, "{:.2}%{{transform:translateY(0px)}}", p(t0));
1465 }
1466 }
1467 }
1468 let lty = match ns_drive(&plan, nsc - 1) {
1469 NsDrive::Through => "360px",
1470 NsDrive::CrashStop => "240px",
1471 NsDrive::Hidden => "0px",
1472 };
1473 let _ = write!(style, "100%{{transform:translateY({lty})}}}}");
1474 }
1475 {
1476 let _ = write!(style, "@keyframes ewplat_op{{");
1477 for i in 0..nsc {
1478 let o = if sv(ew, scenes[i]) == 1 { "1" } else { "0" };
1479 let _ = write!(style, "{:.2}%{{opacity:{o}}}", p(starts[i]));
1480 }
1481 let lo = if sv(ew, scenes[nsc - 1]) == 1 { "1" } else { "0" };
1482 let _ = write!(style, "100%{{opacity:{lo}}}}}");
1483 let _ = write!(style, "@keyframes ewplat_mv{{");
1484 for i in 0..nsc {
1485 let t0 = starts[i];
1486 if sv(ew, scenes[i]) == 1 {
1487 let _ = write!(style, "{:.2}%{{transform:translateX(0px)}}", p(t0));
1488 let _ = write!(style, "{:.2}%{{transform:translateX(360px)}}", p(t0 + durs[i]));
1489 } else {
1490 let _ = write!(style, "{:.2}%{{transform:translateX(0px)}}", p(t0));
1491 }
1492 }
1493 let lty = if sv(ew, scenes[nsc - 1]) == 1 { "360px" } else { "0px" };
1494 let _ = write!(style, "100%{{transform:translateX({lty})}}}}");
1495 }
1496 if ped.is_some() && conflict_scene.is_none() {
1498 let pp = ped.unwrap();
1499 let mut off = 0.0f64;
1500 let _ = write!(style, "@keyframes pedcross{{");
1501 for i in 0..nsc {
1502 let vis = if sv(pp, scenes[i]) == 1 { "1" } else { "0" };
1503 let _ = write!(style, "{:.2}%{{transform:translateX({off:.1}px);opacity:{vis}}}", p(starts[i]));
1504 if sv(pp, scenes[i]) == 1 {
1505 off += 11.0 * durs[i];
1506 }
1507 }
1508 let _ = write!(style, "100%{{transform:translateX({off:.1}px);opacity:0}}}}");
1509 }
1510 if let Some(csi) = conflict_scene {
1512 let c0 = starts[csi];
1513 let impact = c0 + 1.8;
1514 let _ = write!(style, "@keyframes doomwalk{{0%{{opacity:0;transform:translateX(0px)}}{:.2}%{{opacity:0;transform:translateX(0px)}}{:.2}%{{opacity:1;transform:translateX(0px)}}{:.2}%{{opacity:1;transform:translateX(30px)}}{:.2}%{{opacity:0}}100%{{opacity:0}}}}", p(c0), p(c0 + 0.2), p(impact), p(impact + 0.25));
1515 let _ = write!(style, "@keyframes impact{{0%{{opacity:0}}{:.2}%{{opacity:0}}{:.2}%{{opacity:1}}100%{{opacity:0.95}}}}", p(impact), p(impact + 0.2));
1516 }
1517 let nsl_g = nsl.and_then(|sig| (0..nsc).find(|&i| sv(sig, scenes[i]) == 1));
1520 let ewl_g = ewl.and_then(|sig| (0..nsc).find(|&i| sv(sig, scenes[i]) == 1));
1521 if let Some(i) = nsl_g {
1522 let (w0, wm, w1) = (p(starts[i]), p(starts[i] + durs[i] * 0.5), p(starts[i] + durs[i]));
1523 let we = (w1 + 1.0).min(100.0);
1524 let _ = write!(style, "@keyframes nslturn{{0%{{opacity:0;transform:translate(0px,-210px)}}{w0:.2}%{{opacity:1;transform:translate(0px,-210px)}}{wm:.2}%{{opacity:1;transform:translate(0px,0px)}}{w1:.2}%{{opacity:1;transform:translate(175px,0px)}}{we:.2}%{{opacity:0;transform:translate(175px,0px)}}100%{{opacity:0}}}}");
1525 }
1526 if let Some(i) = ewl_g {
1527 let (w0, wm, w1) = (p(starts[i]), p(starts[i] + durs[i] * 0.5), p(starts[i] + durs[i]));
1528 let we = (w1 + 1.0).min(100.0);
1529 let _ = write!(style, "@keyframes ewlturn{{0%{{opacity:0;transform:translate(-210px,0px)}}{w0:.2}%{{opacity:1;transform:translate(-210px,0px)}}{wm:.2}%{{opacity:1;transform:translate(0px,0px)}}{w1:.2}%{{opacity:1;transform:translate(0px,175px)}}{we:.2}%{{opacity:0;transform:translate(0px,175px)}}100%{{opacity:0}}}}");
1530 }
1531 let mut cds: Vec<(f64, f64, &str, usize)> = Vec::new();
1534 if let Some(pp) = ped {
1535 for i in 0..nsc {
1536 if sv(pp, scenes[i]) == 1 {
1537 cds.push((286.0, 68.0, "#fbbf24", i));
1538 }
1539 }
1540 }
1541 for (_pfx, sig, _hx, _hy, _lab, cdx, cdy) in &vheads {
1542 for i in 0..nsc {
1543 let v = sv(sig, scenes[i]);
1544 if v == 1 || v == 2 {
1545 cds.push((*cdx, *cdy, if v == 1 { "#22c55e" } else { "#fbbf24" }, i));
1546 }
1547 }
1548 }
1549 let mut cd_body = String::new();
1550 let mut cdc = 0usize;
1551 for (x, y, color, i) in &cds {
1552 for (k, val) in countdown_seq(durs[*i]).into_iter().enumerate() {
1553 let t0 = starts[*i] + k as f64;
1554 let t1 = (t0 + 1.0).min(starts[*i] + durs[*i]);
1555 let name = format!("cd{cdc}");
1556 cdc += 1;
1557 let _ = write!(style, "@keyframes {name}{{0%{{opacity:0}}{:.2}%{{opacity:0}}{:.2}%{{opacity:1}}{:.2}%{{opacity:0}}100%{{opacity:0}}}}", p(t0), p(t0), p(t1));
1558 let _ = write!(cd_body, r##"<g class="st" style="animation-name:{name}">{}</g>"##, seven_seg(*x, *y, val, *color));
1559 }
1560 }
1561 let _ = write!(style, ".st{{animation-duration:{total:.1}s;animation-timing-function:step-end;animation-iteration-count:infinite}}");
1562 let _ = write!(style, ".fl{{animation-duration:{total:.1}s;animation-timing-function:linear;animation-iteration-count:infinite}}");
1563 style.push_str("</style>");
1564
1565 let mut s = String::new();
1566 let _ = write!(s, r##"<svg viewBox="0 0 320 320" width="100%" style="max-height:320px" xmlns="http://www.w3.org/2000/svg">"##);
1567 s.push_str(&style);
1568 s.push_str(r##"<rect x="0" y="128" width="320" height="64" fill="#26262b"/><rect x="128" y="0" width="64" height="320" fill="#26262b"/>"##);
1570 s.push_str(r##"<line x1="0" y1="160" x2="320" y2="160" stroke="#6b5d2e" stroke-width="2" stroke-dasharray="11 9"/><line x1="160" y1="0" x2="160" y2="320" stroke="#6b5d2e" stroke-width="2" stroke-dasharray="11 9"/>"##);
1571 if ped.is_some() {
1573 for i in 0..6 {
1574 let x = 133.0 + i as f64 * 10.0;
1575 let _ = write!(s, r##"<rect x="{x:.0}" y="202" width="5" height="22" fill="rgba(255,255,255,0.22)"/>"##);
1576 }
1577 }
1578 s.push_str(r##"<g class="st" style="animation-name:nsplat_op"><g class="fl" style="animation-name:nsplat_mv">"##);
1582 for k in 0..3 {
1583 let y = -30 - k * 52;
1584 let _ = write!(s, r##"<rect x="139" y="{y}" width="18" height="30" rx="3" fill="#38bdf8"/><rect x="143" y="{}" width="10" height="4" rx="1" fill="rgba(0,0,0,0.35)"/>"##, y + 6);
1585 }
1586 s.push_str("</g></g>");
1587 s.push_str(r##"<g class="st" style="animation-name:ewplat_op"><g class="fl" style="animation-name:ewplat_mv">"##);
1588 for k in 0..3 {
1589 let x = -30 - k * 52;
1590 let _ = write!(s, r##"<rect x="{x}" y="163" width="30" height="18" rx="3" fill="#f472b6"/><rect x="{}" y="167" width="4" height="10" rx="1" fill="rgba(0,0,0,0.35)"/>"##, x + 6);
1591 }
1592 s.push_str("</g></g>");
1593 if ped.is_some() && conflict_scene.is_none() {
1595 let _ = write!(s, r##"<g class="fl" style="animation-name:pedcross">{}</g>"##, walk_icon(120.0, 209.0, "#fde047"));
1596 }
1597 if nsl_g.is_some() {
1599 s.push_str(r##"<g class="fl" style="animation-name:nslturn"><rect x="140" y="142" width="16" height="16" rx="3" fill="#38bdf8"/><rect x="143" y="145" width="10" height="4" rx="1" fill="rgba(0,0,0,0.35)"/></g>"##);
1600 }
1601 if ewl_g.is_some() {
1602 s.push_str(r##"<g class="fl" style="animation-name:ewlturn"><rect x="140" y="142" width="16" height="16" rx="3" fill="#f472b6"/><rect x="143" y="145" width="10" height="4" rx="1" fill="rgba(0,0,0,0.35)"/></g>"##);
1603 }
1604 if conflict_scene.is_some() && ped.is_some() {
1606 let _ = write!(s, r##"<g class="fl" style="animation-name:doomwalk">{}</g>"##, walk_icon(120.0, 209.0, "#fde047"));
1607 }
1608 for (pfx, _sig, x, y, label, cdx, cdy) in &vheads {
1610 let (rx, yx, gx) = (x + 16.0, x + 39.0, x + 62.0);
1611 let cy = y + 14.0;
1612 let lab_y = if *y < 100.0 { y - 4.0 } else { y + 42.0 };
1613 let _ = write!(s, r##"<rect x="{x:.0}" y="{y:.0}" width="78" height="28" rx="7" fill="#141418" stroke="rgba(255,255,255,0.15)"/>"##);
1614 let _ = write!(s, r##"<circle class="st" style="animation-name:{pfx}R" cx="{rx:.0}" cy="{cy:.0}" r="9" fill="#ef4444"/><circle class="st" style="animation-name:{pfx}Y" cx="{yx:.0}" cy="{cy:.0}" r="9" fill="#fbbf24"/><circle class="st" style="animation-name:{pfx}G" cx="{gx:.0}" cy="{cy:.0}" r="9" fill="#22c55e"/>"##);
1615 let _ = write!(s, r##"<text x="{:.0}" y="{lab_y:.0}" fill="rgba(255,255,255,0.6)" text-anchor="middle" font-size="11" font-family="ui-monospace,monospace">{label}</text>"##, x + 39.0);
1616 s.push_str(&seven_seg(*cdx, *cdy, 8, "rgba(255,255,255,0.06)"));
1617 }
1618 if ped.is_some() {
1620 s.push_str(r##"<rect x="238" y="54" width="72" height="52" rx="6" fill="#141418" stroke="rgba(255,255,255,0.15)"/>"##);
1621 let _ = write!(s, r##"<g class="st" style="animation-name:pedWALK">{}</g>"##, walk_icon(258.0, 80.0, "#34d399"));
1622 let _ = write!(s, r##"<g class="st" style="animation-name:pedSTOP">{}</g>"##, hand_icon(258.0, 80.0));
1623 s.push_str(&seven_seg(286.0, 68.0, 8, "rgba(255,255,255,0.06)"));
1624 s.push_str(r##"<text x="274" y="100" fill="rgba(255,255,255,0.55)" text-anchor="middle" font-size="9" font-family="ui-monospace,monospace">PED</text>"##);
1625 }
1626 s.push_str(&cd_body);
1628 if conflict_scene.is_some() {
1630 let mut g = String::from(r##"<g class="st" style="animation-name:impact">"##);
1631 g.push_str(r##"<circle cx="150" cy="207" r="15" fill="rgba(251,191,36,0.6)"/>"##);
1632 g.push_str(r##"<path d="M150,190 L155,202 L168,207 L155,212 L150,224 L145,212 L132,207 L145,202 Z" fill="#fbbf24"/>"##);
1633 if ped.is_some() {
1634 g.push_str(r##"<ellipse cx="174" cy="216" rx="12" ry="4.5" fill="#ef4444"/><circle cx="160" cy="214" r="4" fill="#ef4444"/>"##);
1635 g.push_str(r##"<text x="160" y="116" fill="#fca5a5" text-anchor="middle" font-size="13" font-weight="700" font-family="ui-sans-serif">⚠ CAR HITS PEDESTRIAN</text>"##);
1636 } else {
1637 g.push_str(r##"<text x="160" y="116" fill="#fca5a5" text-anchor="middle" font-size="14" font-weight="700" font-family="ui-sans-serif">⚠ CONFLICT</text>"##);
1638 }
1639 g.push_str("</g>");
1640 s.push_str(&g);
1641 }
1642 s.push_str("</svg>");
1643 Some(s)
1644}
1645
1646#[cfg(test)]
1647mod traffic_viz_tests {
1648 use super::{countdown_seq, ns_drive, traffic_plan, traffic_svg, NsDrive};
1649 use logicaffeine_compile::codegen_sva::hw_pipeline::{Waveform, WaveSignal};
1650
1651 fn sig(name: &str, vals: &[u64]) -> WaveSignal {
1652 WaveSignal {
1653 name: name.to_string(),
1654 width: 2,
1655 values: vals.iter().map(|&v| Some(v)).collect(),
1656 }
1657 }
1658 fn wf(signals: Vec<WaveSignal>, n: u32) -> Waveform {
1659 Waveform { timesteps: n, signals }
1660 }
1661
1662 fn safe_trace() -> Waveform {
1664 wf(
1665 vec![
1666 sig("ns", &[1, 1, 2, 0, 0, 0]),
1667 sig("ew", &[0, 0, 0, 0, 0, 0]),
1668 sig("ped", &[0, 0, 0, 0, 1, 1]),
1669 ],
1670 6,
1671 )
1672 }
1673
1674 #[test]
1675 fn collapses_consecutive_identical_states_into_phases() {
1676 let plan = traffic_plan(&safe_trace()).unwrap();
1677 assert_eq!(plan.phases.len(), 4, "ns-green, ns-yellow, all-red, walk");
1678 assert_eq!(plan.firsts, vec![0, 2, 3, 4]);
1679 assert_eq!(plan.phases[0].ns, 1);
1680 assert_eq!(plan.phases[1].ns, 2);
1681 assert_eq!(plan.phases[3].ped, 1);
1682 }
1683
1684 #[test]
1685 fn phase_durations_are_human_paced_and_cumulative() {
1686 let plan = traffic_plan(&safe_trace()).unwrap();
1687 assert_eq!(plan.phases[0].dur, 2.6, "green");
1688 assert_eq!(plan.phases[1].dur, 1.3, "yellow");
1689 assert_eq!(plan.phases[2].dur, 1.0, "all-red");
1690 assert_eq!(plan.phases[3].dur, 7.0, "walk");
1691 assert_eq!(plan.phases[0].start, 0.0);
1692 assert_eq!(plan.phases[1].start, 2.6);
1693 assert!((plan.phases[3].start - (2.6 + 1.3 + 1.0)).abs() < 1e-9);
1694 assert!((plan.total - (2.6 + 1.3 + 1.0 + 7.0)).abs() < 1e-9);
1695 }
1696
1697 #[test]
1698 fn exclusive_movements_have_no_conflict() {
1699 assert_eq!(traffic_plan(&safe_trace()).unwrap().conflict, None);
1700 }
1701
1702 #[test]
1703 fn pedestrian_walking_into_green_traffic_is_a_conflict() {
1704 let plan = traffic_plan(&wf(
1705 vec![sig("ns", &[1, 1]), sig("ew", &[0, 0]), sig("ped", &[1, 1])],
1706 2,
1707 ))
1708 .unwrap();
1709 assert_eq!(plan.phases.len(), 1);
1710 assert_eq!(plan.conflict, Some(0));
1711 assert_eq!(plan.phases[0].dur, 3.6, "the crash phase lingers");
1712 }
1713
1714 #[test]
1715 fn ns_cars_are_hidden_unless_ns_through_is_green() {
1716 let plan = traffic_plan(&safe_trace()).unwrap();
1718 assert_eq!(ns_drive(&plan, 0), NsDrive::Through, "ns green → drive through");
1719 assert_eq!(ns_drive(&plan, 1), NsDrive::Hidden, "ns yellow → hidden");
1720 assert_eq!(ns_drive(&plan, 2), NsDrive::Hidden, "all-red → hidden");
1721 assert_eq!(ns_drive(&plan, 3), NsDrive::Hidden, "pedestrian walk → hidden");
1722 }
1723
1724 #[test]
1725 fn the_crash_car_stops_at_the_crosswalk_not_drives_through() {
1726 let plan = traffic_plan(&wf(
1727 vec![sig("ns", &[1, 1]), sig("ew", &[0, 0]), sig("ped", &[1, 1])],
1728 2,
1729 ))
1730 .unwrap();
1731 assert_eq!(ns_drive(&plan, 0), NsDrive::CrashStop);
1732 }
1733
1734 #[test]
1735 fn countdown_counts_down_whole_seconds() {
1736 assert_eq!(countdown_seq(7.0), vec![7, 6, 5, 4, 3, 2, 1]);
1737 assert_eq!(countdown_seq(2.6), vec![3, 2, 1]);
1738 assert_eq!(countdown_seq(1.3), vec![2, 1]);
1739 assert_eq!(countdown_seq(1.0), vec![1]);
1740 }
1741
1742 #[test]
1743 fn every_animation_name_used_in_the_svg_has_a_matching_keyframes_block() {
1744 let svg = traffic_svg(&wf(
1747 vec![
1748 sig("ns", &[1, 1, 0, 0, 0, 0]),
1749 sig("ew", &[0, 0, 0, 1, 1, 0]),
1750 sig("nsl", &[0, 0, 0, 0, 0, 1]),
1751 sig("ewl", &[0, 0, 0, 0, 0, 0]),
1752 sig("ped", &[0, 0, 0, 0, 0, 0]),
1753 ],
1754 6,
1755 ))
1756 .expect("renders an intersection");
1757 let mut rest = svg.as_str();
1758 while let Some(pos) = rest.find("animation-name:") {
1759 rest = &rest[pos + "animation-name:".len()..];
1760 let end = rest
1761 .find(|c: char| !(c.is_ascii_alphanumeric() || c == '_'))
1762 .unwrap_or(rest.len());
1763 let name = &rest[..end];
1764 assert!(
1765 svg.contains(&format!("@keyframes {name}{{"))
1766 || svg.contains(&format!("@keyframes {name} {{")),
1767 "animation-name {name} has no @keyframes definition"
1768 );
1769 }
1770 }
1771
1772 #[test]
1773 fn non_intersection_traces_do_not_render() {
1774 assert!(traffic_svg(&wf(vec![sig("a", &[0, 1]), sig("b", &[1, 0])], 2)).is_none());
1776 }
1777}
1778
1779#[cfg(target_arch = "wasm32")]
1782fn sync_studio_url(vfs_path: &str) {
1783 crate::ui::router::replace_bar_url(&crate::ui::router::studio_file_url(vfs_path));
1784}
1785
1786#[component(lazy)]
1787pub fn Studio(file: Option<String>) -> Element {
1788 let mut mode = use_signal(|| StudioMode::Logic);
1790
1791 let mut sidebar_open = use_signal(|| true);
1793 let mut file_tree = use_signal(FileNode::root); let mut current_file = use_signal(|| None::<String>);
1795 let mut vfs_error = use_signal(|| None::<String>); let mut vfs_is_fallback = use_signal(|| false); let mut input = use_signal(String::new);
1800 let mut result = use_signal(|| CompileResult {
1801 logic: None,
1802 simple_logic: None,
1803 kripke_logic: None,
1804 ast: None,
1805 readings: Vec::new(),
1806 simple_readings: Vec::new(),
1807 kripke_readings: Vec::new(),
1808 tokens: Vec::new(),
1809 error: None,
1810 });
1811 let mut format = use_signal(|| OutputFormat::SimpleFOL);
1812
1813 let mut proof_text = use_signal(String::new);
1815 let mut proof_status = use_signal(|| ProofStatus::Idle);
1816 let mut proof_hint = use_signal(|| None::<String>);
1817 let mut current_proof_expr = use_signal(|| None::<ProofExpr>);
1819 let mut knowledge_base = use_signal(Vec::<ProofExpr>::new);
1821 let mut logic_output_mode = use_signal(|| LogicView::Logic);
1823 let mut generated_logic_rust = use_signal(String::new);
1824
1825 let mut code_input = use_signal(String::new);
1827 let mut code_output_mode = use_signal(|| CodeOutputMode::Interpret);
1828 let mut interpreter_result = use_signal(|| InterpreterResult {
1829 lines: vec![],
1830 error: None,
1831 });
1832 let mut generated_rust = use_signal(String::new);
1833 let mut debugging = use_signal(|| false);
1835
1836 let mut math_input = use_signal(String::new);
1838 let mut math_repl = use_signal(Repl::new);
1839 let mut math_output = use_signal(Vec::<ReplLine>::new);
1840 let mut math_output_mode = use_signal(|| CodeOutputMode::Interpret);
1842 let mut generated_math_rust = use_signal(String::new);
1843
1844 let mut hw_input = use_signal(String::new);
1846 let mut hw_output_mode = use_signal(|| CodeOutputMode::Interpret);
1848 let mut hw_sva = use_signal(String::new);
1849 let mut hw_psl = use_signal(String::new);
1850 let mut hw_signals = use_signal(Vec::<String>::new);
1851 let mut hw_proof = use_signal(String::new);
1853 let mut hw_proof_ok = use_signal(|| None::<bool>);
1854 let mut hw_counterexample = use_signal(Vec::<(String, String)>::new);
1856 let mut hw_kg = use_signal(logicaffeine_compile::codegen_sva::hw_pipeline::KgSummary::default);
1858 let mut generated_hw_rust = use_signal(String::new);
1859 let mut hw_error = use_signal(|| None::<String>);
1860
1861 let mut busy = use_signal(|| false);
1864
1865 let mut sidebar_width = use_signal(|| 240.0f64);
1867 let mut left_width = use_signal(|| 35.0f64);
1868 let mut right_width = use_signal(|| 25.0f64);
1869 let mut resizing = use_signal(|| None::<&'static str>);
1870
1871 let mut active_tab = use_signal(|| MobileTab::Panel1);
1873
1874 let mut editor_expanded = use_signal(|| true);
1876 let mut output_expanded = use_signal(|| true);
1877
1878 let mut touch_start_x = use_signal(|| 0.0f64);
1880 let mut touch_start_y = use_signal(|| 0.0f64);
1881
1882 let mut mobile_split = use_signal(|| 50.0f64);
1884
1885 let mut vfs_initialized = use_signal(|| false);
1887
1888 #[cfg(target_arch = "wasm32")]
1891 let mut vfs_handle = use_signal(|| None::<WebVfs>);
1892
1893 #[cfg(target_arch = "wasm32")]
1895 use_effect(move || {
1896 if *vfs_initialized.read() {
1897 return;
1898 }
1899 vfs_initialized.set(true);
1900
1901 let file = file.clone();
1902 spawn(async move {
1903 match get_platform_vfs_with_fallback().await {
1905 Ok(vfs) => {
1906 vfs_handle.set(Some(vfs.clone()));
1909 if vfs.is_fallback() {
1910 vfs_is_fallback.set(true);
1911 }
1912
1913 if let Err(e) = seed_examples(&vfs).await {
1915 vfs_error.set(Some(format!("Failed to seed examples: {:?}", e)));
1916 }
1917
1918 let mut root = FileNode::root();
1920 match load_dir_recursive(&vfs, "/", &mut root).await {
1921 Ok(()) => {
1922 if !root.children.is_empty() {
1923 file_tree.set(root);
1924 } else {
1925 vfs_error.set(Some("VFS returned empty tree - no files found".to_string()));
1926 }
1927 }
1928 Err(e) => {
1929 vfs_error.set(Some(format!("Failed to load file tree: {:?}", e)));
1930 }
1931 }
1932
1933 let file_to_load = file
1935 .map(|f| {
1936 if f.starts_with('/') {
1938 f
1939 } else {
1940 format!("/{}", f)
1941 }
1942 })
1943 .unwrap_or_else(|| "/examples/logic/prover-demo.logic".to_string());
1944
1945 if let Ok(content) = vfs.read_to_string(&file_to_load).await {
1947 current_file.set(Some(file_to_load.clone()));
1948 sync_studio_url(&file_to_load);
1949
1950 let ext = file_to_load.rsplit('.').next().unwrap_or("").to_lowercase();
1951 let is_math_dir = file_to_load.contains("/math/") || file_to_load.contains("/examples/math");
1952
1953 if is_math_dir || ext == "math" || ext == "vernac" {
1954 mode.set(StudioMode::Math);
1956 math_input.set(content);
1957 } else if ext == "logos" {
1958 mode.set(StudioMode::Code);
1960 code_input.set(content.clone());
1961 let interp_result = interpret_for_ui_baseline(&content).await;
1962 interpreter_result.set(interp_result);
1963 } else {
1964 mode.set(StudioMode::Logic);
1966 input.set(content.clone());
1967
1968 if content.contains("## Theorem:") {
1969 let theorem_result = compile_theorem_for_ui(&content);
1971 if theorem_result.error.is_none() {
1972 result.set(CompileResult {
1973 logic: theorem_result.goal_string.clone(),
1974 simple_logic: theorem_result.goal_string.clone(),
1975 kripke_logic: None,
1976 ast: None,
1977 readings: Vec::new(),
1978 simple_readings: Vec::new(),
1979 kripke_readings: Vec::new(),
1980 tokens: Vec::new(),
1981 error: None,
1982 });
1983
1984 knowledge_base.write().clear();
1985 for premise in &theorem_result.premises {
1986 knowledge_base.write().push(premise.clone());
1987 }
1988
1989 if let Some(goal) = theorem_result.goal.clone() {
1990 current_proof_expr.set(Some(goal));
1991 }
1992
1993 let html = theorem_proof_html(&theorem_result);
1994 if !html.is_empty() {
1995 proof_text.set(html);
1996 proof_status.set(if theorem_result.verified {
1997 ProofStatus::Success
1998 } else {
1999 ProofStatus::Idle
2000 });
2001 proof_hint.set(Some(theorem_proof_hint(&theorem_result)));
2002 } else {
2003 proof_status.set(ProofStatus::Idle);
2004 proof_hint.set(Some(format!(
2005 "Theorem '{}' ready. {} premise(s) loaded.",
2006 theorem_result.name,
2007 knowledge_base.read().len()
2008 )));
2009 }
2010 }
2011 } else {
2012 let sentences: Vec<&str> = content
2014 .lines()
2015 .filter(|line| {
2016 let trimmed = line.trim();
2017 !trimmed.is_empty()
2018 && !trimmed.starts_with('#')
2019 && !trimmed.starts_with("--")
2020 })
2021 .collect();
2022
2023 if !sentences.is_empty() {
2024 let all_text = sentences.join("\n");
2025 let compiled = compile_for_ui(&all_text);
2026 result.set(compiled);
2027
2028 let first_sentence = sentences[0];
2029 let proof_result = compile_for_proof(first_sentence);
2030 if let Some(expr) = proof_result.proof_expr {
2031 current_proof_expr.set(Some(expr));
2032 }
2033 }
2034 }
2035 }
2036 }
2037 }
2038 Err(e) => {
2039 vfs_error.set(Some(format!("VFS INIT FAILED: {:?}", e)));
2040 }
2041 }
2042 });
2043 });
2044
2045 #[cfg(not(target_arch = "wasm32"))]
2047 use_effect(move || {
2048 if *vfs_initialized.read() {
2049 return;
2050 }
2051 vfs_initialized.set(true);
2052 vfs_error.set(Some("VFS not available on native".to_string()));
2053 });
2054
2055 #[cfg(target_arch = "wasm32")]
2057 {
2058 let mut sidebar_init_done = use_signal(|| false);
2059 use_effect(move || {
2060 if *sidebar_init_done.read() {
2061 return;
2062 }
2063 sidebar_init_done.set(true);
2064
2065 if let Some(window) = web_sys::window() {
2066 let width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(1024.0);
2067 if width <= 768.0 {
2068 sidebar_open.set(false);
2069 }
2070 }
2071 });
2072 }
2073
2074 let handle_logic_input = move |new_value: String| {
2079 input.set(new_value);
2080 };
2081
2082 let handle_code_run = move |_| {
2084 let code = code_input.read().clone();
2085 active_tab.set(MobileTab::Panel2);
2087 code_output_mode.set(CodeOutputMode::Interpret);
2088 interpreter_result.set(InterpreterResult {
2090 lines: vec![],
2091 error: None,
2092 });
2093 spawn(async move {
2094 let callback = Rc::new(RefCell::new(move |line: String| {
2096 interpreter_result.write().lines.push(line);
2098 }));
2099
2100 #[cfg(target_arch = "wasm32")]
2104 let vfs = vfs_handle
2105 .read()
2106 .clone()
2107 .map(|w| std::sync::Arc::new(w) as std::sync::Arc<dyn Vfs>);
2108 #[cfg(not(target_arch = "wasm32"))]
2109 let vfs: Option<std::sync::Arc<dyn Vfs>> = None;
2110 let result = interpret_streaming_with_vfs(&code, callback, vfs).await;
2111 interpreter_result.set(result);
2113 });
2114 };
2115
2116 let handle_code_compile = move |_| {
2119 let code = code_input.read().clone();
2120 active_tab.set(MobileTab::Panel2);
2122 busy.set(true);
2123 spawn(async move {
2124 #[cfg(target_arch = "wasm32")]
2126 gloo_timers::future::TimeoutFuture::new(0).await;
2127 match generate_rust_code(&code) {
2128 Ok(rust_code) => {
2129 generated_rust.set(rust_code);
2130 code_output_mode.set(CodeOutputMode::Rust);
2131 }
2132 Err(e) => {
2133 interpreter_result.set(InterpreterResult {
2134 lines: vec![],
2135 error: Some(format!("Compile error: {:?}", e)),
2136 });
2137 code_output_mode.set(CodeOutputMode::Interpret);
2138 }
2139 }
2140 busy.set(false);
2141 });
2142 };
2143
2144 let handle_math_execute = move |_| {
2149 active_tab.set(MobileTab::Panel2);
2150 math_output_mode.set(CodeOutputMode::Interpret);
2151 let code = math_input.read().clone();
2152 busy.set(true);
2153 spawn(async move {
2154 #[cfg(target_arch = "wasm32")]
2155 gloo_timers::future::TimeoutFuture::new(0).await;
2156
2157 let statements = parse_math_statements(&code);
2158 let mut repl = Repl::new();
2159 let mut lines = Vec::new();
2160 for stmt in statements {
2161 match repl.execute(&stmt) {
2162 Ok(output) => lines.push(ReplLine::success(stmt, output)),
2163 Err(e) => lines.push(ReplLine::error(stmt, e.to_string())),
2164 }
2165 }
2166 math_repl.set(repl);
2167 math_output.set(lines);
2168 busy.set(false);
2169 });
2170 };
2171
2172 let handle_math_compile = move |_| {
2175 active_tab.set(MobileTab::Panel2);
2176 let code = math_input.read().clone();
2177 busy.set(true);
2178 spawn(async move {
2179 #[cfg(target_arch = "wasm32")]
2180 gloo_timers::future::TimeoutFuture::new(0).await;
2181
2182 let statements = parse_math_statements(&code);
2183 let mut repl = Repl::new();
2184 for stmt in statements {
2185 let _ = repl.execute(&stmt);
2186 }
2187 let rust = match extract_math_rust(repl.context()) {
2188 Ok(rust) => rust,
2189 Err(e) => format!("// extraction error: {e}"),
2190 };
2191 math_repl.set(repl);
2192 generated_math_rust.set(rust);
2193 math_output_mode.set(CodeOutputMode::Rust);
2194 busy.set(false);
2195 });
2196 };
2197
2198 let handle_logic_execute = move |_| {
2202 active_tab.set(MobileTab::Panel2);
2203 logic_output_mode.set(LogicView::Logic);
2204 let new_value = input.read().clone();
2205
2206 if new_value.contains("## Theorem:") {
2207 let theorem_result = compile_theorem_for_ui(&new_value);
2208
2209 if let Some(err) = theorem_result.error {
2210 result.set(CompileResult {
2211 logic: None,
2212 simple_logic: None,
2213 kripke_logic: None,
2214 ast: None,
2215 readings: Vec::new(),
2216 simple_readings: Vec::new(),
2217 kripke_readings: Vec::new(),
2218 tokens: Vec::new(),
2219 error: Some(err.clone()),
2220 });
2221 proof_status.set(ProofStatus::Failed(err));
2222 current_proof_expr.set(None);
2223 knowledge_base.write().clear();
2224 } else {
2225 result.set(CompileResult {
2226 logic: theorem_result.goal_string.clone(),
2227 simple_logic: theorem_result.goal_string.clone(),
2228 kripke_logic: None,
2229 ast: None,
2230 readings: Vec::new(),
2231 simple_readings: Vec::new(),
2232 kripke_readings: Vec::new(),
2233 tokens: Vec::new(),
2234 error: None,
2235 });
2236
2237 knowledge_base.write().clear();
2238 for premise in &theorem_result.premises {
2239 knowledge_base.write().push(premise.clone());
2240 }
2241
2242 if let Some(goal) = theorem_result.goal.clone() {
2243 current_proof_expr.set(Some(goal));
2244 }
2245
2246 let html = theorem_proof_html(&theorem_result);
2247 if !html.is_empty() {
2248 proof_text.set(html);
2249 proof_status.set(if theorem_result.verified {
2250 ProofStatus::Success
2251 } else {
2252 ProofStatus::Idle
2253 });
2254 proof_hint.set(Some(theorem_proof_hint(&theorem_result)));
2255 } else {
2256 proof_status.set(ProofStatus::Idle);
2257 proof_hint.set(Some(format!(
2258 "Theorem '{}' ready. {} premise(s) loaded. Click Auto to prove.",
2259 theorem_result.name,
2260 knowledge_base.read().len()
2261 )));
2262 proof_text.set(String::new());
2263 }
2264 }
2265 } else {
2266 let sentences: Vec<&str> = new_value
2267 .lines()
2268 .filter(|line| {
2269 let trimmed = line.trim();
2270 !trimmed.is_empty()
2271 && !trimmed.starts_with('#')
2272 && !trimmed.starts_with("--")
2273 })
2274 .collect();
2275
2276 if !sentences.is_empty() {
2277 let all_text = sentences.join("\n");
2278 let compiled = compile_for_ui(&all_text);
2279 result.set(compiled);
2280
2281 knowledge_base.write().clear();
2282
2283 let first_sentence = sentences[0];
2284 let proof_result = compile_for_proof(first_sentence);
2285 if let Some(expr) = proof_result.proof_expr {
2286 current_proof_expr.set(Some(expr));
2287 proof_status.set(ProofStatus::Idle);
2288 proof_hint.set(Some("Enter premises or click Auto to prove.".to_string()));
2289 } else if let Some(err) = proof_result.error {
2290 current_proof_expr.set(None);
2291 proof_status.set(ProofStatus::Failed(err));
2292 }
2293 } else {
2294 result.set(CompileResult {
2295 logic: None,
2296 simple_logic: None,
2297 kripke_logic: None,
2298 ast: None,
2299 readings: Vec::new(),
2300 simple_readings: Vec::new(),
2301 kripke_readings: Vec::new(),
2302 tokens: Vec::new(),
2303 error: None,
2304 });
2305 current_proof_expr.set(None);
2306 knowledge_base.write().clear();
2307 proof_text.set(String::new());
2308 proof_status.set(ProofStatus::Idle);
2309 }
2310 }
2311 };
2312
2313 let handle_logic_compile = move |_| {
2316 active_tab.set(MobileTab::Panel2);
2317 let text = input.read().clone();
2318 busy.set(true);
2319 spawn(async move {
2320 #[cfg(target_arch = "wasm32")]
2321 gloo_timers::future::TimeoutFuture::new(0).await;
2322
2323 let rust = match extract_logic_rust(&text) {
2324 Ok(rust) => rust,
2325 Err(e) => format!("// extraction error: {e}"),
2326 };
2327 generated_logic_rust.set(rust);
2328 logic_output_mode.set(LogicView::Rust);
2329 busy.set(false);
2330 });
2331 };
2332
2333 let handle_tactic = {
2335 let result_signal = result.clone();
2336 move |tactic: Tactic| {
2337 let maybe_goal = current_proof_expr.read().clone();
2339 let kb = knowledge_base.read().clone();
2340 let logic_str = result_signal.read().simple_logic.clone();
2341
2342 match maybe_goal {
2343 Some(goal) => {
2344 proof_status.set(ProofStatus::Proving);
2345
2346 match tactic {
2347 Tactic::Auto => {
2348 let mut engine = BackwardChainer::new();
2350
2351 for axiom in &kb {
2353 engine.add_axiom(axiom.clone());
2354 }
2355
2356 match engine.prove(goal.clone()) {
2358 Ok(derivation) => {
2359 let tree_display = format_derivation_html(&derivation);
2361 proof_text.set(tree_display);
2362 proof_status.set(ProofStatus::Success);
2363 proof_hint.set(Some("Proof found! The derivation tree shows the inference steps.".to_string()));
2364 }
2365 Err(e) => {
2366 let hint = suggest_hint(&goal, &kb, &[]);
2368 proof_text.set(format!(
2369 "<span class=\"rule\">Auto-prove failed</span>\n\nGoal: {}\n\nError: {}",
2370 logic_str.as_deref().unwrap_or("(no expression)"),
2371 e
2372 ));
2373 proof_status.set(ProofStatus::Failed(format!("{}", e)));
2374 proof_hint.set(Some(hint.text));
2375 }
2376 }
2377 }
2378 Tactic::ModusPonens => {
2379 let hint = suggest_hint(&goal, &kb, &[]);
2380 proof_text.set(format!(
2381 "<span class=\"rule\">Modus Ponens</span>\n\nFrom P\u{2192}Q and P, derive Q\n\nCurrent goal: {}\n\nKnowledge base has {} axiom(s)",
2382 logic_str.as_deref().unwrap_or("(none)"),
2383 kb.len()
2384 ));
2385 proof_status.set(ProofStatus::Idle);
2386 proof_hint.set(Some(hint.text));
2387 }
2388 Tactic::UniversalInst => {
2389 let hint = suggest_hint(&goal, &kb, &[]);
2390 proof_text.set(format!(
2391 "<span class=\"rule\">\u{2200} Elimination</span>\n\nFrom \u{2200}x.P(x), derive P(c)\n\nCurrent goal: {}",
2392 logic_str.as_deref().unwrap_or("(none)")
2393 ));
2394 proof_status.set(ProofStatus::Idle);
2395 proof_hint.set(Some(hint.text));
2396 }
2397 Tactic::ExistentialIntro => {
2398 let hint = suggest_hint(&goal, &kb, &[]);
2399 proof_text.set(format!(
2400 "<span class=\"rule\">\u{2203} Introduction</span>\n\nFrom P(c), derive \u{2203}x.P(x)\n\nCurrent goal: {}",
2401 logic_str.as_deref().unwrap_or("(none)")
2402 ));
2403 proof_status.set(ProofStatus::Idle);
2404 proof_hint.set(Some(hint.text));
2405 }
2406 Tactic::Induction => {
2407 let hint = suggest_hint(&goal, &kb, &[]);
2408 proof_text.set(format!(
2409 "<span class=\"rule\">Induction</span>\n\nBase case + Inductive step\n\nCurrent goal: {}",
2410 logic_str.as_deref().unwrap_or("(none)")
2411 ));
2412 proof_status.set(ProofStatus::Idle);
2413 proof_hint.set(Some(hint.text));
2414 }
2415 Tactic::Rewrite => {
2416 let hint = suggest_hint(&goal, &kb, &[]);
2417 proof_text.set(format!(
2418 "<span class=\"rule\">Rewrite</span>\n\nUse equality to substitute\n\nCurrent goal: {}",
2419 logic_str.as_deref().unwrap_or("(none)")
2420 ));
2421 proof_status.set(ProofStatus::Idle);
2422 proof_hint.set(Some(hint.text));
2423 }
2424 }
2425 }
2426 None => {
2427 proof_status.set(ProofStatus::Failed("No logic expression to prove. Enter a sentence first.".to_string()));
2428 proof_text.set(String::new());
2429 proof_hint.set(Some("Enter an English sentence above to generate a logical formula.".to_string()));
2430 }
2431 }
2432 }
2433 };
2434
2435 let (definitions, inductives) = {
2437 let repl_guard = math_repl.read();
2438 let ctx = repl_guard.context();
2439 let mut defs = Vec::new();
2440 let mut inds = Vec::new();
2441
2442 for (name, ty, body) in ctx.iter_definitions() {
2443 defs.push(ContextEntry {
2444 name: name.to_string(),
2445 ty: format!("{}", ty),
2446 body: Some(format!("{}", body)),
2447 kind: EntryKind::Definition,
2448 });
2449 }
2450
2451 for (name, ty) in ctx.iter_inductives() {
2452 inds.push(ContextEntry {
2453 name: name.to_string(),
2454 ty: format!("{}", ty),
2455 body: None,
2456 kind: EntryKind::Inductive,
2457 });
2458 }
2459
2460 (defs, inds)
2461 };
2462
2463 fn hw_property_from_synth(
2470 synth: &logicaffeine_compile::codegen_sva::fol_to_sva::SynthesizedSva,
2471 ) -> logicaffeine_compile::codegen_sva::SvaProperty {
2472 use logicaffeine_compile::codegen_sva::{SvaAssertionKind, SvaProperty, sanitize_property_name};
2473 let kind = match synth.kind.as_str() {
2474 "cover" => SvaAssertionKind::Cover,
2475 "assume" => SvaAssertionKind::Assume,
2476 _ => SvaAssertionKind::Assert,
2477 };
2478 let name = if synth.signals.is_empty() {
2479 "p_property".to_string()
2480 } else {
2481 sanitize_property_name(&synth.signals.join("_"))
2482 };
2483 SvaProperty { name, clock: "clk".to_string(), body: synth.body.clone(), kind }
2484 }
2485
2486 #[allow(clippy::too_many_arguments)]
2489 fn load_hardware_spec(
2490 content: &str,
2491 mut hw_sva: Signal<String>,
2492 mut hw_psl: Signal<String>,
2493 mut hw_signals: Signal<Vec<String>>,
2494 mut hw_proof: Signal<String>,
2495 mut hw_proof_ok: Signal<Option<bool>>,
2496 mut hw_counterexample: Signal<Vec<(String, String)>>,
2497 mut hw_kg: Signal<logicaffeine_compile::codegen_sva::hw_pipeline::KgSummary>,
2498 mut hw_error: Signal<Option<String>>,
2499 ) {
2500 if content.trim().is_empty() {
2501 hw_sva.set(String::new());
2502 hw_psl.set(String::new());
2503 hw_signals.write().clear();
2504 hw_proof.set(String::new());
2505 hw_proof_ok.set(None);
2506 hw_counterexample.write().clear();
2507 hw_kg.set(Default::default());
2508 hw_error.set(None);
2509 return;
2510 }
2511 if let Some(pspec) = crate::ui::pages::pigeonhole_viz::parse_pigeonhole_spec(content) {
2514 hw_sva.set(String::new());
2515 hw_psl.set(String::new());
2516 hw_signals.write().clear();
2517 hw_counterexample.write().clear();
2518 hw_kg.set(Default::default());
2519 hw_error.set(None);
2520 hw_proof.set(format!(
2521 "\u{2717} PHP({0}) \u{2014} {0} pigeons can't fit {1} holes. Certified UNSAT by our prover (maximum matching + symmetry breaking), no Z3.",
2522 pspec.pigeons,
2523 pspec.holes()
2524 ));
2525 hw_proof_ok.set(Some(false));
2526 return;
2527 }
2528 if content.contains("registers:") || content.contains("Registers:") {
2532 hw_sva.set(String::new());
2533 hw_psl.set(String::new());
2534 hw_signals.write().clear();
2535 hw_proof.set(String::new());
2536 hw_proof_ok.set(None);
2537 hw_counterexample.write().clear();
2538 hw_kg.set(Default::default());
2539 hw_error.set(None);
2540 return;
2541 }
2542 if content.contains("module") && content.contains("endmodule") {
2545 use logicaffeine_compile::codegen_sva::rtl::parse_transition_system;
2546 use logicaffeine_proof::bmc::{BmcOutcome, InductionOutcome};
2547 hw_psl.set(String::new());
2548 hw_signals.write().clear();
2549 hw_kg.set(Default::default());
2550 let to_ce = |trace: Vec<(String, bool)>| -> Vec<(String, String)> {
2551 trace
2552 .into_iter()
2553 .map(|(n, v)| (n, if v { "1" } else { "0" }.to_string()))
2554 .collect()
2555 };
2556 match parse_transition_system(content) {
2557 Ok(ts) => {
2558 hw_sva.set(format!(
2559 "// RTL transition system \u{2014} {} register(s)\n// bounded model checking + k-induction (no Z3)",
2560 ts.registers.len()
2561 ));
2562 match ts.prove_invariant(4) {
2563 InductionOutcome::Proven => {
2564 hw_proof.set("\u{2713} Always holds \u{2014} proven for every reachable state (k-induction, no Z3)".to_string());
2565 hw_proof_ok.set(Some(true));
2566 match ts.witness_trace(18) {
2569 Some(trace) => hw_counterexample.set(to_ce(trace)),
2570 None => hw_counterexample.write().clear(),
2571 }
2572 }
2573 InductionOutcome::CounterexampleAt { k, trace } => {
2574 hw_proof.set(format!("\u{2717} Can break at step {k} \u{2014} counterexample below"));
2575 hw_proof_ok.set(Some(false));
2576 hw_counterexample.set(to_ce(trace));
2577 }
2578 InductionOutcome::NotInductive => match ts.bmc(28) {
2579 BmcOutcome::CounterexampleAt { k, trace } => {
2580 hw_proof.set(format!("\u{2717} Can break at step {k} \u{2014} counterexample below"));
2581 hw_proof_ok.set(Some(false));
2582 hw_counterexample.set(to_ce(trace));
2583 }
2584 BmcOutcome::NoneWithin(n) => {
2585 hw_proof.set(format!("No failure found within {n} steps \u{2014} couldn't prove it always holds at this depth"));
2586 hw_proof_ok.set(None);
2587 hw_counterexample.write().clear();
2588 }
2589 BmcOutcome::Unsupported => {
2590 hw_proof.set("This property isn't expressible yet \u{2014} try a simpler one".to_string());
2591 hw_proof_ok.set(None);
2592 hw_counterexample.write().clear();
2593 }
2594 },
2595 InductionOutcome::Unsupported => {
2596 hw_proof.set("This property isn't expressible yet \u{2014} try a simpler one".to_string());
2597 hw_proof_ok.set(None);
2598 hw_counterexample.write().clear();
2599 }
2600 }
2601 hw_error.set(None);
2602 }
2603 Err(e) => {
2604 hw_sva.set(String::new());
2605 hw_proof.set(String::new());
2606 hw_proof_ok.set(None);
2607 hw_counterexample.write().clear();
2608 hw_error.set(Some(format!("RTL parse error: {}", e.message)));
2609 }
2610 }
2611 return;
2612 }
2613 if content.to_lowercase().contains("conflict") {
2616 use logicaffeine_compile::codegen_sva::signal_design::design_from_spec;
2617 hw_psl.set(String::new());
2618 hw_kg.set(Default::default());
2619 hw_counterexample.write().clear();
2620 match design_from_spec(content) {
2621 Ok((intersection, plan)) => {
2622 let mut out = String::new();
2623 for (p, group) in plan.groups().iter().enumerate() {
2624 out.push_str(&format!(
2625 "Phase {}: {}\n",
2626 p + 1,
2627 intersection.names(group).join(", ")
2628 ));
2629 }
2630 use logicaffeine_compile::codegen_sva::controller_gen::generate_controller;
2633 use logicaffeine_compile::codegen_sva::rtl::parse_transition_system;
2634 use logicaffeine_proof::bmc::InductionOutcome;
2635 let verilog = generate_controller(&intersection, &plan);
2636 let controller_proven = matches!(
2637 parse_transition_system(&verilog).map(|ts| ts.prove_invariant(4)),
2638 Ok(InductionOutcome::Proven)
2639 );
2640 out.push_str("\n\n// Generated controller (provably conflict-free):\n");
2641 out.push_str(&verilog);
2642 hw_sva.set(out.trim_end().to_string());
2643 hw_signals.set(intersection.movements.clone());
2644 let minimal = if !plan.minimal_certified {
2645 String::new()
2646 } else if plan.num_phases <= 1 {
2647 " \u{2014} a single phase suffices".to_string()
2648 } else {
2649 format!(" \u{2014} provably minimal ({}-phase impossible)", plan.num_phases - 1)
2650 };
2651 let controller_note = if controller_proven {
2652 " \u{00b7} generated controller PROVEN conflict-free (k-induction)"
2653 } else {
2654 ""
2655 };
2656 hw_proof.set(format!(
2657 "\u{2713} Conflict-free {}-phase plan synthesized{}{} (certified by our SAT solver, no Z3)",
2658 plan.num_phases, minimal, controller_note
2659 ));
2660 hw_proof_ok.set(Some(true));
2661 hw_error.set(None);
2662 }
2663 Err(e) => {
2664 hw_sva.set(String::new());
2665 hw_signals.write().clear();
2666 hw_proof.set(String::new());
2667 hw_proof_ok.set(None);
2668 hw_error.set(Some(e));
2669 }
2670 }
2671 return;
2672 }
2673 match logicaffeine_compile::codegen_sva::fol_to_sva::synthesize_sva_from_spec(content, "clk") {
2674 Ok(synth) => {
2675 let prop = hw_property_from_synth(&synth);
2676 hw_psl.set(logicaffeine_compile::codegen_sva::emit_psl_property(&prop));
2677 hw_signals.set(synth.signals.clone());
2678 use logicaffeine_compile::codegen_sva::hw_pipeline::{
2680 check_spec_vacuity, prove_spec_sva_equivalence, VacuityReport,
2681 };
2682 let vacuity_note = match check_spec_vacuity(content, 8) {
2684 Ok(VacuityReport::Vacuous) => {
2685 " \u{00b7} \u{26A0} never triggers (vacuous)".to_string()
2686 }
2687 Ok(VacuityReport::NonVacuous) => {
2688 " \u{00b7} the trigger can actually fire".to_string()
2689 }
2690 _ => String::new(),
2691 };
2692 match prove_spec_sva_equivalence(content, &synth.body, 8) {
2693 Ok(r) if r.equivalent => {
2694 hw_proof.set(format!(
2695 "\u{2713} Matches the spec \u{2014} certified, kernel-checked, no Z3{}",
2696 vacuity_note
2697 ));
2698 hw_proof_ok.set(Some(true));
2699 hw_counterexample.write().clear();
2700 }
2701 Ok(r) => {
2702 let ce_vec = r.counterexample.unwrap_or_default();
2703 let ce = ce_vec
2704 .iter()
2705 .map(|(k, v)| format!("{}={}", k, v))
2706 .collect::<Vec<_>>()
2707 .join(", ");
2708 hw_proof.set(format!(
2709 "\u{2717} Not equivalent to the spec \u{2014} counterexample: {}{}",
2710 ce, vacuity_note
2711 ));
2712 hw_proof_ok.set(Some(false));
2713 hw_counterexample.set(ce_vec);
2714 }
2715 Err(e) => {
2716 hw_proof.set(format!("Proof unavailable: {}{}", e, vacuity_note));
2717 hw_proof_ok.set(None);
2718 hw_counterexample.write().clear();
2719 }
2720 }
2721 hw_sva.set(synth.sva_text);
2722 hw_kg.set(
2724 logicaffeine_compile::codegen_sva::hw_pipeline::kg_summary(content)
2725 .unwrap_or_default(),
2726 );
2727 hw_error.set(None);
2728 }
2729 Err(e) => {
2730 hw_sva.set(String::new());
2731 hw_psl.set(String::new());
2732 hw_signals.write().clear();
2733 hw_proof.set(String::new());
2734 hw_proof_ok.set(None);
2735 hw_counterexample.write().clear();
2736 hw_kg.set(Default::default());
2737 hw_error.set(Some(e));
2738 }
2739 }
2740 }
2741
2742 let handle_hardware_execute = move |_| {
2743 let spec = hw_input.read().clone();
2744 load_hardware_spec(&spec, hw_sva, hw_psl, hw_signals, hw_proof, hw_proof_ok, hw_counterexample, hw_kg, hw_error);
2745 hw_output_mode.set(CodeOutputMode::Interpret);
2746 };
2747
2748 let handle_hardware_compile = move |_| {
2749 let spec = hw_input.read().clone();
2750 if spec.trim().is_empty() {
2751 generated_hw_rust.set(String::new());
2752 return;
2753 }
2754 match logicaffeine_compile::codegen_sva::fol_to_sva::synthesize_sva_from_spec(&spec, "clk") {
2755 Ok(synth) => {
2756 let prop = hw_property_from_synth(&synth);
2757 generated_hw_rust.set(logicaffeine_compile::codegen_sva::emit_rust_monitor(&prop));
2758 hw_error.set(None);
2759 hw_output_mode.set(CodeOutputMode::Rust);
2760 }
2761 Err(e) => {
2762 generated_hw_rust.set(String::new());
2763 hw_error.set(Some(e));
2764 }
2765 }
2766 };
2767
2768 let current_result = result.read();
2770 let guide_mode = if *mode.read() == StudioMode::Logic {
2771 if let Some(err) = ¤t_result.error {
2772 GuideMode::Error(err.clone())
2773 } else if current_result.logic.is_some() {
2774 let msg = get_success_message(current_result.readings.len());
2775 if let Some(hint) = get_context_hint(&input.read()) {
2776 GuideMode::Info(format!("{} {}", msg, hint))
2777 } else {
2778 GuideMode::Success(msg)
2779 }
2780 } else {
2781 GuideMode::Idle
2782 }
2783 } else {
2784 GuideMode::Idle
2785 };
2786
2787 let hw_is_regalloc = matches!(*mode.read(), StudioMode::Hardware)
2792 && crate::ui::pages::register_alloc_viz::is_register_alloc_spec(&hw_input.read());
2793 let hw_is_pigeonhole = matches!(*mode.read(), StudioMode::Hardware)
2797 && crate::ui::pages::pigeonhole_viz::is_pigeonhole_spec(&hw_input.read());
2798
2799 let show_panel3 = match *mode.read() {
2800 StudioMode::Logic => current_result.ast.is_some(),
2801 StudioMode::Code => interpreter_result.read().error.is_some(),
2802 StudioMode::Math => !definitions.is_empty() || !inductives.is_empty(),
2803 StudioMode::Hardware => {
2804 hw_is_regalloc
2805 || hw_is_pigeonhole
2806 || !hw_signals.read().is_empty()
2807 || !hw_counterexample.read().is_empty()
2808 || !hw_kg.read().nodes.is_empty()
2809 }
2810 };
2811
2812 let sidebar_w = *sidebar_width.read();
2813 let left_w = *left_width.read();
2814 let right_w = if show_panel3 { *right_width.read() } else { 0.0 };
2815 let center_w = 100.0 - left_w - right_w;
2816
2817 let handle_mouse_move = move |evt: MouseEvent| {
2819 if let Some(which) = *resizing.read() {
2820 let window = web_sys::window().unwrap();
2821 let width = window.inner_width().unwrap().as_f64().unwrap();
2822 let coords = evt.data().client_coordinates();
2823 let x: f64 = coords.x;
2824 let pct: f64 = (x / width) * 100.0;
2825
2826 match which {
2827 "sidebar" => {
2828 let new_sidebar: f64 = x.clamp(150.0, 400.0);
2829 sidebar_width.set(new_sidebar);
2830 }
2831 "left" => {
2832 let new_left: f64 = pct.clamp(15.0, 60.0);
2833 left_width.set(new_left);
2834 }
2835 "right" => {
2836 let new_right: f64 = (100.0 - pct).clamp(15.0, 40.0);
2837 right_width.set(new_right);
2838 }
2839 "mobile" => {
2840 let document = window.document().unwrap();
2841 if let Ok(Some(el)) = document.query_selector(".studio-main") {
2842 let rect = el.get_bounding_client_rect();
2843 let y: f64 = coords.y;
2844 let relative_y = y - rect.top();
2845 let pct = (relative_y / rect.height()) * 100.0;
2846 mobile_split.set(pct.clamp(15.0, 85.0));
2847 }
2848 }
2849 _ => {}
2850 }
2851 }
2852 };
2853
2854 let handle_mouse_up = move |_: MouseEvent| {
2855 resizing.set(None);
2856 };
2857
2858 let handle_touch_start = move |evt: TouchEvent| {
2860 let touches = evt.data().touches();
2861 if let Some(touch) = touches.first() {
2862 let coords = touch.client_coordinates();
2863 touch_start_x.set(coords.x);
2864 touch_start_y.set(coords.y);
2865 }
2866 };
2867
2868 let handle_touch_end = move |evt: TouchEvent| {
2869 if *resizing.read() == Some("mobile") {
2870 resizing.set(None);
2871 return;
2872 }
2873 let changed = evt.data().touches_changed();
2874 if let Some(touch) = changed.first() {
2875 let coords = touch.client_coordinates();
2876 let end_x = coords.x;
2877 let end_y = coords.y;
2878 let dx = end_x - *touch_start_x.read();
2879 let dy = end_y - *touch_start_y.read();
2880
2881 if dx.abs() > dy.abs() && dx.abs() > 50.0 {
2882 let current = *active_tab.read();
2883 if dx < 0.0 {
2884 match current {
2885 MobileTab::Panel1 => active_tab.set(MobileTab::Panel2),
2886 MobileTab::Panel2 => active_tab.set(MobileTab::Panel3),
2887 MobileTab::Panel3 => {}
2888 }
2889 } else {
2890 match current {
2891 MobileTab::Panel1 => {}
2892 MobileTab::Panel2 => active_tab.set(MobileTab::Panel1),
2893 MobileTab::Panel3 => active_tab.set(MobileTab::Panel2),
2894 }
2895 }
2896 }
2897 }
2898 };
2899
2900 let handle_touch_move = move |evt: TouchEvent| {
2901 if *resizing.read() == Some("mobile") {
2902 evt.prevent_default();
2903 let touches = evt.data().touches();
2904 if let Some(touch) = touches.first() {
2905 let window = web_sys::window().unwrap();
2906 let document = window.document().unwrap();
2907 if let Ok(Some(el)) = document.query_selector(".studio-main") {
2908 let rect = el.get_bounding_client_rect();
2909 let coords = touch.client_coordinates();
2910 let y: f64 = coords.y;
2911 let relative_y = y - rect.top();
2912 let pct = (relative_y / rect.height()) * 100.0;
2913 mobile_split.set(pct.clamp(15.0, 85.0));
2914 }
2915 }
2916 }
2917 };
2918
2919 let current_format = *format.read();
2920 let current_tab = *active_tab.read();
2921 let current_mode = *mode.read();
2922
2923 let editor_exp = *editor_expanded.read();
2925 let output_exp = *output_expanded.read();
2926 let panel1_class = if editor_exp { "studio-panel mobile-expanded" } else { "studio-panel mobile-collapsed" };
2927 let panel2_class = if output_exp { "studio-panel mobile-expanded" } else { "studio-panel mobile-collapsed" };
2928 let panel3_class = "studio-panel";
2929
2930 let mobile_pct = *mobile_split.read();
2932 let panel1_flex = mobile_pct / 50.0;
2933 let panel2_flex = (100.0 - mobile_pct) / 50.0;
2934 let both_expanded = editor_exp && output_exp;
2935
2936 let panel1_style = if both_expanded {
2937 format!("width: {left_w}%; --panel-flex: {panel1_flex};")
2938 } else {
2939 format!("width: {left_w}%;")
2940 };
2941 let panel2_style = if both_expanded {
2942 format!("width: {center_w}%; --panel-flex: {panel2_flex};")
2943 } else {
2944 format!("width: {center_w}%;")
2945 };
2946
2947 let container_class = if *resizing.read() == Some("mobile") {
2948 "studio-container mobile-resizing"
2949 } else {
2950 "studio-container"
2951 };
2952
2953 let current_code_output_mode = *code_output_mode.read();
2955 let current_math_output_mode = *math_output_mode.read();
2956 let current_logic_output_mode = *logic_output_mode.read();
2957 let current_hw_output_mode = *hw_output_mode.read();
2958
2959 let (tab1_icon, tab1_label, tab2_icon, tab2_label, tab3_icon, tab3_label) = match current_mode {
2961 StudioMode::Logic => ("\u{270F}", "Input", "\u{2200}", "Logic", "\u{1F333}", "Tree"),
2962 StudioMode::Code => ("\u{03BB}", "Editor", "\u{276F}", "Output", "\u{1F4CB}", "Console"),
2963 StudioMode::Math => ("\u{2200}", "Editor", "\u{276F}", "Output", "\u{1F4CB}", "Context"),
2964 StudioMode::Hardware => ("\u{270F}", "Spec", "\u{22A8}", "SVA", "\u{25A6}", "Graph"),
2965 };
2966
2967 rsx! {
2968 PageHead {
2969 title: seo_pages::STUDIO.title,
2970 description: seo_pages::STUDIO.description,
2971 canonical_path: seo_pages::STUDIO.canonical_path,
2972 }
2973 style { "{MOBILE_BASE_STYLES}" }
2974 style { "{MOBILE_TAB_BAR_STYLES}" }
2975 style { "{STUDIO_STYLE}" }
2976 JsonLdMultiple { schemas: vec![
2977 organization_schema(),
2978 software_application_schema(),
2979 breadcrumb_schema(&[
2980 BreadcrumbItem { name: "Home", path: "/" },
2981 BreadcrumbItem { name: "Studio", path: "/studio" },
2982 ]),
2983 ] }
2984
2985 div {
2986 class: "{container_class}",
2987 onmousemove: handle_mouse_move,
2988 onmouseup: handle_mouse_up,
2989 onmouseleave: handle_mouse_up,
2990 ontouchstart: handle_touch_start,
2991 ontouchend: handle_touch_end,
2992 ontouchmove: handle_touch_move,
2993
2994 MainNav { active: ActivePage::Studio, subtitle: Some("Your logic workspace") }
2995
2996 div { class: "studio-toolbar",
2998 div { class: "studio-toolbar-left",
2999 button {
3000 class: "sidebar-toggle-btn",
3001 onclick: move |_| {
3002 let current = *sidebar_open.read();
3003 sidebar_open.set(!current);
3004 },
3005 title: "Toggle file browser",
3006 if *sidebar_open.read() { "\u{2630}" } else { "\u{1F4C1}" }
3007 }
3008 }
3009 div { class: "studio-toolbar-center",
3010 span { class: "mode-label", "Mode:" }
3011 ModeToggle {
3012 mode: current_mode,
3013 on_change: move |new_mode| {
3014 mode.set(new_mode);
3015 active_tab.set(MobileTab::Panel1);
3016 },
3017 }
3018 }
3019 div { class: "studio-toolbar-right",
3020 if busy() {
3021 span {
3022 class: "execute-btn",
3023 style: "opacity: 0.75; cursor: default; pointer-events: none;",
3024 "\u{23F3} Working\u{2026}"
3025 }
3026 }
3027 if current_mode == StudioMode::Code {
3028 button {
3029 class: "execute-btn",
3030 onclick: handle_code_run,
3031 span { class: "desktop-label", "\u{25B6} Run" }
3032 span { class: "mobile-label", "\u{25B6} Run" }
3033 }
3034 button {
3035 class: "execute-btn compile-btn",
3036 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3037 onclick: handle_code_compile,
3038 span { class: "desktop-label", "\u{1F980} Compile" }
3039 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3040 }
3041 button {
3042 class: "execute-btn",
3043 style: "background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%); display:inline-flex; align-items:center; gap:6px;",
3044 onclick: move |_| debugging.set(true),
3045 span { style: "display:inline-flex;", dangerous_inner_html: IC_BUG }
3046 span { class: "desktop-label", "Debug" }
3047 span { class: "mobile-label", "Debug" }
3048 }
3049 }
3050 if current_mode == StudioMode::Math {
3051 button {
3052 class: "execute-btn",
3053 onclick: handle_math_execute,
3054 span { class: "desktop-label", "\u{25B6} Execute" }
3055 span { class: "mobile-label", "\u{25B6} Execute" }
3056 }
3057 button {
3058 class: "execute-btn compile-btn",
3059 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3060 onclick: handle_math_compile,
3061 span { class: "desktop-label", "\u{1F980} Compile" }
3062 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3063 }
3064 }
3065 if current_mode == StudioMode::Logic {
3066 button {
3067 class: "execute-btn",
3068 onclick: handle_logic_execute,
3069 span { class: "desktop-label", "\u{25B6} Execute" }
3070 span { class: "mobile-label", "\u{25B6} Execute" }
3071 }
3072 button {
3073 class: "execute-btn compile-btn",
3074 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3075 onclick: handle_logic_compile,
3076 span { class: "desktop-label", "\u{1F980} Compile" }
3077 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3078 }
3079 }
3080 if current_mode == StudioMode::Hardware {
3081 button {
3082 class: "execute-btn",
3083 onclick: handle_hardware_execute,
3084 span { class: "desktop-label", "\u{25B6} Execute" }
3085 span { class: "mobile-label", "\u{25B6} Execute" }
3086 }
3087 button {
3088 class: "execute-btn compile-btn",
3089 style: "background: linear-gradient(135deg, #56b6c2 0%, #61afef 100%);",
3090 onclick: handle_hardware_compile,
3091 span { class: "desktop-label", "\u{1F980} Compile" }
3092 span { class: "mobile-label", "\u{1F980} Compile to Rust" }
3093 }
3094 }
3095 }
3096 }
3097
3098 nav { class: "mobile-tabs",
3100 button {
3101 class: if current_tab == MobileTab::Panel1 { "mobile-tab active" } else { "mobile-tab" },
3102 onclick: move |_| active_tab.set(MobileTab::Panel1),
3103 span { class: "mobile-tab-icon", "{tab1_icon}" }
3104 span { class: "mobile-tab-label", "{tab1_label}" }
3105 }
3106 button {
3107 class: if current_tab == MobileTab::Panel2 { "mobile-tab active" } else { "mobile-tab" },
3108 onclick: move |_| active_tab.set(MobileTab::Panel2),
3109 span { class: "mobile-tab-icon", "{tab2_icon}" }
3110 span { class: "mobile-tab-label", "{tab2_label}" }
3111 }
3112 button {
3113 class: if current_tab == MobileTab::Panel3 { "mobile-tab active" } else { "mobile-tab" },
3114 onclick: move |_| active_tab.set(MobileTab::Panel3),
3115 span { class: "mobile-tab-icon", "{tab3_icon}" }
3116 span { class: "mobile-tab-label", "{tab3_label}" }
3117 }
3118 div { style: "margin-left: auto;",
3120 ModeToggle {
3121 mode: current_mode,
3122 on_change: move |new_mode| {
3123 mode.set(new_mode);
3124 active_tab.set(MobileTab::Panel1);
3125 },
3126 }
3127 }
3128 }
3129
3130 if current_mode == StudioMode::Logic {
3132 div { class: "studio-guide",
3133 SocraticGuide {
3134 mode: guide_mode.clone(),
3135 on_hint_request: None,
3136 }
3137 }
3138 }
3139
3140 div { class: "studio-content",
3142 if *sidebar_open.read() {
3145 div {
3146 class: "sidebar-overlay",
3147 onclick: move |_| sidebar_open.set(false),
3148 }
3149 }
3150
3151 if *sidebar_open.read() {
3152 div {
3153 class: "studio-sidebar",
3154 style: "width: {sidebar_w}px; flex-shrink: 0;",
3155
3156 if let Some(err) = vfs_error.read().as_ref() {
3158 div {
3159 style: "background: #ff4444; color: white; padding: 6px 12px; font-size: 11px; border-radius: 0;",
3160 "VFS Error: {err}"
3161 }
3162 }
3163
3164 FileBrowser {
3165 show_private_mode: *vfs_is_fallback.read(),
3166 tree: file_tree.read().clone(),
3167 selected_path: current_file.read().clone(),
3168 on_select: EventHandler::new(move |path: String| {
3169 #[cfg(target_arch = "wasm32")]
3171 {
3172 let window = web_sys::window().unwrap();
3173 let width = window.inner_width().unwrap().as_f64().unwrap_or(1024.0);
3174 if width <= 768.0 {
3175 sidebar_open.set(false);
3176 }
3177 }
3178 current_file.set(Some(path.clone()));
3179
3180 #[cfg(target_arch = "wasm32")]
3182 sync_studio_url(&path);
3183
3184 #[cfg(target_arch = "wasm32")]
3186 {
3187 let path_clone = path.clone();
3188 spawn(async move {
3189 let cached = vfs_handle.peek().clone();
3194 let vfs_result = match cached {
3195 Some(vfs) => Ok(vfs),
3196 None => get_platform_vfs_with_fallback().await.map(|vfs| {
3197 vfs_handle.set(Some(vfs.clone()));
3198 vfs
3199 }),
3200 };
3201 match vfs_result {
3202 Ok(vfs) => {
3203 match vfs.read_to_string(&path_clone).await {
3204 Ok(content) => {
3205 let ext = path_clone.rsplit('.').next().unwrap_or("").to_lowercase();
3208 let is_math_dir = path_clone.contains("/math/") || path_clone.contains("/examples/math");
3209 let is_hardware_dir = path_clone.contains("/hardware/") || path_clone.contains("/examples/hardware");
3210
3211 if is_math_dir || ext == "math" || ext == "vernac" {
3213 mode.set(StudioMode::Math);
3215 active_tab.set(MobileTab::Panel2);
3216 math_input.set(content);
3217 } else if ext == "logic" {
3218 mode.set(StudioMode::Logic);
3220 active_tab.set(MobileTab::Panel2);
3221
3222 input.set(content.clone());
3224
3225 if content.contains("## Theorem:") {
3227 let theorem_result = compile_theorem_for_ui(&content);
3229
3230 if let Some(err) = theorem_result.error {
3231 result.set(CompileResult {
3233 logic: None,
3234 simple_logic: None,
3235 kripke_logic: None,
3236 ast: None,
3237 readings: Vec::new(),
3238 simple_readings: Vec::new(),
3239 kripke_readings: Vec::new(),
3240 tokens: Vec::new(),
3241 error: Some(err.clone()),
3242 });
3243 proof_status.set(ProofStatus::Failed(err));
3244 current_proof_expr.set(None);
3245 knowledge_base.write().clear();
3246 } else {
3247 result.set(CompileResult {
3249 logic: theorem_result.goal_string.clone(),
3250 simple_logic: theorem_result.goal_string.clone(),
3251 kripke_logic: None,
3252 ast: None,
3253 readings: Vec::new(),
3254 simple_readings: Vec::new(),
3255 kripke_readings: Vec::new(),
3256 tokens: Vec::new(),
3257 error: None,
3258 });
3259
3260 knowledge_base.write().clear();
3262 for premise in &theorem_result.premises {
3263 knowledge_base.write().push(premise.clone());
3264 }
3265
3266 if let Some(goal) = theorem_result.goal.clone() {
3268 current_proof_expr.set(Some(goal));
3269 }
3270
3271 let html = theorem_proof_html(&theorem_result);
3273 if !html.is_empty() {
3274 proof_text.set(html);
3275 proof_status.set(if theorem_result.verified {
3276 ProofStatus::Success
3277 } else {
3278 ProofStatus::Idle
3279 });
3280 proof_hint.set(Some(theorem_proof_hint(&theorem_result)));
3281 } else {
3282 proof_status.set(ProofStatus::Idle);
3283 proof_hint.set(Some(format!(
3284 "Theorem '{}' ready. {} premise(s) loaded.",
3285 theorem_result.name,
3286 knowledge_base.read().len()
3287 )));
3288 proof_text.set(String::new());
3289 }
3290 }
3291 } else {
3292 let sentences: Vec<&str> = content
3294 .lines()
3295 .filter(|line| {
3296 let trimmed = line.trim();
3297 !trimmed.is_empty()
3298 && !trimmed.starts_with('#')
3299 && !trimmed.starts_with("--")
3300 })
3301 .collect();
3302
3303 if !sentences.is_empty() {
3304 let all_text = sentences.join("\n");
3306 let compiled = compile_for_ui(&all_text);
3307 result.set(compiled);
3308
3309 let first_sentence = sentences[0];
3311 let proof_result = compile_for_proof(first_sentence);
3312 if let Some(expr) = proof_result.proof_expr {
3313 current_proof_expr.set(Some(expr));
3314 }
3315 }
3316 }
3317 } else if ext == "logos" {
3318 mode.set(StudioMode::Code);
3320 active_tab.set(MobileTab::Panel2);
3321
3322 code_input.set(content.clone());
3323 let interp_result = interpret_for_ui_baseline(&content).await;
3325 interpreter_result.set(interp_result);
3326 } else if is_hardware_dir || ext == "hw" {
3327 mode.set(StudioMode::Hardware);
3329 active_tab.set(MobileTab::Panel2);
3330 hw_input.set(content.clone());
3331 load_hardware_spec(
3332 &content,
3333 hw_sva, hw_psl, hw_signals, hw_proof, hw_proof_ok, hw_counterexample, hw_kg, hw_error,
3334 );
3335 } else {
3336 active_tab.set(MobileTab::Panel2);
3338 let current_mode = *mode.read();
3339 match current_mode {
3340 StudioMode::Logic => {
3341 input.set(content.clone());
3342 let compiled = compile_for_ui(&content);
3343 result.set(compiled);
3344 }
3345 StudioMode::Code => {
3346 code_input.set(content.clone());
3347 let interp_result = interpret_for_ui_baseline(&content).await;
3348 interpreter_result.set(interp_result);
3349 }
3350 StudioMode::Math => math_input.set(content),
3351 StudioMode::Hardware => {
3352 hw_input.set(content.clone());
3353 load_hardware_spec(
3354 &content,
3355 hw_sva, hw_psl, hw_signals, hw_proof, hw_proof_ok, hw_counterexample, hw_kg, hw_error,
3356 );
3357 }
3358 }
3359 }
3360 }
3361 Err(e) => {
3362 vfs_error.set(Some(format!("Failed to read file: {:?}", e)));
3363 }
3364 }
3365 }
3366 Err(e) => {
3367 vfs_error.set(Some(format!("VFS INIT FAILED: {:?}", e)));
3368 }
3369 }
3370 });
3371 }
3372 }),
3373 on_toggle_dir: EventHandler::new(move |path: String| {
3374 if let Some(node) = file_tree.write().find_mut(&path) {
3375 node.toggle_expanded();
3376 }
3377 }),
3378 on_new_file: EventHandler::new(move |_: ()| {
3379 }),
3381 }
3382 }
3383
3384 div {
3386 class: if resizing.read().is_some() { "panel-resizer active" } else { "panel-resizer" },
3387 onmousedown: move |_| resizing.set(Some("sidebar")),
3388 }
3389 }
3390
3391 main { class: "studio-main",
3393 section {
3395 class: "{panel1_class}",
3396 style: "{panel1_style}",
3397
3398 match current_mode {
3399 StudioMode::Logic => rsx! {
3400 div {
3401 class: "panel-header",
3402 onclick: move |_| {
3403 let v = *editor_expanded.read();
3404 editor_expanded.set(!v);
3405 },
3406 span { "English Input" }
3407 }
3408 div { class: "panel-content",
3409 LiveEditor {
3410 value: input.read().clone(),
3411 on_change: handle_logic_input,
3412 placeholder: Some("Type an English sentence...".to_string()),
3413 }
3414 ProofPanel {
3416 proof_text: proof_text.read().clone(),
3417 status: proof_status.read().clone(),
3418 hint: proof_hint.read().clone(),
3419 on_tactic: handle_tactic.clone(),
3420 }
3421 }
3422 },
3423 StudioMode::Code => rsx! {
3424 div {
3425 class: "panel-header",
3426 onclick: move |_| {
3427 let v = *editor_expanded.read();
3428 editor_expanded.set(!v);
3429 },
3430 span { "Code Editor" }
3431 }
3432 div { class: "panel-content",
3433 CodeEditor {
3434 value: code_input.read().clone(),
3435 on_change: move |v| code_input.set(v),
3436 language: Language::Logos,
3437 placeholder: "-- Imperative LOGOS code\n\n## Main\n\nLet x be 1.\nLet y be 2.\nShow x + y.".to_string(),
3438 }
3439 }
3440 },
3441 StudioMode::Math => rsx! {
3442 div {
3443 class: "panel-header",
3444 onclick: move |_| {
3445 let v = *editor_expanded.read();
3446 editor_expanded.set(!v);
3447 },
3448 span { "Theorem Editor" }
3449 }
3450 div { class: "panel-content",
3451 CodeEditor {
3452 value: math_input.read().clone(),
3453 on_change: move |v| math_input.set(v),
3454 language: Language::Vernacular,
3455 placeholder: "-- Define natural numbers\nInductive Nat := Zero : Nat | Succ : Nat -> Nat.\n\nDefinition one : Nat := Succ Zero.\n\nCheck one.".to_string(),
3456 }
3457 }
3458 },
3459 StudioMode::Hardware => rsx! {
3460 div {
3461 class: "panel-header",
3462 onclick: move |_| {
3463 let v = *editor_expanded.read();
3464 editor_expanded.set(!v);
3465 },
3466 span { "Hardware Spec" }
3467 }
3468 div { class: "panel-content",
3469 LiveEditor {
3470 value: hw_input.read().clone(),
3471 on_change: move |v: String| hw_input.set(v),
3472 placeholder: Some("Describe hardware in English (\"Always, if request is high, then acknowledge is high.\"), design a signal plan (\"NS-left conflicts with the EW crossing.\"), or paste Verilog (module \u{2026} endmodule) to model-check.".to_string()),
3473 }
3474 }
3475 },
3476 }
3477 }
3478
3479 if both_expanded {
3481 div {
3482 class: if *resizing.read() == Some("mobile") {
3483 "mobile-panel-resizer active"
3484 } else {
3485 "mobile-panel-resizer"
3486 },
3487 onmousedown: move |e| {
3488 e.prevent_default();
3489 resizing.set(Some("mobile"));
3490 },
3491 ontouchstart: move |e| {
3492 e.prevent_default();
3493 resizing.set(Some("mobile"));
3494 },
3495 }
3496 }
3497
3498 div {
3500 class: if resizing.read().is_some() { "panel-resizer active" } else { "panel-resizer" },
3501 onmousedown: move |_| resizing.set(Some("left")),
3502 }
3503
3504 section {
3506 class: "{panel2_class}",
3507 style: "{panel2_style}",
3508
3509 match current_mode {
3510 StudioMode::Logic => rsx! {
3511 div {
3512 class: "panel-header",
3513 onclick: move |_| {
3514 let v = *output_expanded.read();
3515 output_expanded.set(!v);
3516 },
3517 span { "Logic Output" }
3518 div {
3519 class: "output-mode-toggle",
3520 onclick: move |evt| evt.stop_propagation(),
3521 button {
3522 class: if current_logic_output_mode == LogicView::Logic { "output-mode-btn active" } else { "output-mode-btn" },
3523 onclick: move |_| logic_output_mode.set(LogicView::Logic),
3524 "Logic"
3525 }
3526 button {
3527 class: if current_logic_output_mode == LogicView::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3528 onclick: move |_| logic_output_mode.set(LogicView::Rust),
3529 "Rust"
3530 }
3531 }
3532 if current_logic_output_mode == LogicView::Logic {
3533 div {
3534 class: "format-toggle",
3535 onclick: move |evt| evt.stop_propagation(),
3536 button {
3537 class: if current_format == OutputFormat::SimpleFOL { "format-btn active" } else { "format-btn" },
3538 onclick: move |_| format.set(OutputFormat::SimpleFOL),
3539 "Simple"
3540 }
3541 button {
3542 class: if current_format == OutputFormat::Unicode { "format-btn active" } else { "format-btn" },
3543 onclick: move |_| format.set(OutputFormat::Unicode),
3544 "Full"
3545 }
3546 button {
3547 class: if current_format == OutputFormat::LaTeX { "format-btn active" } else { "format-btn" },
3548 onclick: move |_| format.set(OutputFormat::LaTeX),
3549 "LaTeX"
3550 }
3551 button {
3552 class: if current_format == OutputFormat::Kripke { "format-btn active" } else { "format-btn" },
3553 onclick: move |_| format.set(OutputFormat::Kripke),
3554 "Deep"
3555 }
3556 }
3557 }
3558 }
3559 div { class: "panel-content",
3560 if current_logic_output_mode == LogicView::Logic {
3561 {
3562 rsx! {
3563 LogicOutput {
3564 logic: current_result.logic.clone(),
3565 simple_logic: current_result.simple_logic.clone(),
3566 kripke_logic: current_result.kripke_logic.clone(),
3567 readings: current_result.readings.clone(),
3568 simple_readings: current_result.simple_readings.clone(),
3569 kripke_readings: current_result.kripke_readings.clone(),
3570 error: current_result.error.clone(),
3571 format: current_format,
3572 }
3573 if let Some(ref logic) = current_result.logic {
3574 SymbolDictionary {
3575 logic: logic.clone(),
3576 collapsed: false,
3577 inline: false,
3578 }
3579 }
3580 }
3581 }
3582 } else {
3583 {
3584 let rust_code = generated_logic_rust.read().clone();
3585 rsx! {
3586 if rust_code.is_empty() {
3587 div { class: "interpreter-empty",
3588 "Compile to see generated Rust"
3589 }
3590 } else {
3591 CodeView {
3592 code: rust_code,
3593 language: Language::Rust,
3594 }
3595 }
3596 }
3597 }
3598 }
3599 }
3600 },
3601 StudioMode::Code => rsx! {
3602 div {
3603 class: "panel-header",
3604 onclick: move |_| {
3605 let v = *output_expanded.read();
3606 output_expanded.set(!v);
3607 },
3608 span { "Output" }
3609 div {
3610 class: "output-mode-toggle",
3611 onclick: move |evt| evt.stop_propagation(),
3612 button {
3613 class: if current_code_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
3614 onclick: move |_| code_output_mode.set(CodeOutputMode::Interpret),
3615 "Output"
3616 }
3617 button {
3618 class: if current_code_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3619 onclick: move |_| code_output_mode.set(CodeOutputMode::Rust),
3620 "Rust"
3621 }
3622 }
3623 }
3624 div { class: "panel-content",
3625 if current_code_output_mode == CodeOutputMode::Interpret {
3626 {
3627 let result = interpreter_result.read();
3628 rsx! {
3629 div { class: "interpreter-output",
3630 if result.lines.is_empty() && result.error.is_none() {
3631 div { class: "interpreter-empty",
3632 "Run your code to see output"
3633 }
3634 } else {
3635 for line in result.lines.iter() {
3636 div { class: "interpreter-line", "{line}" }
3637 }
3638 if let Some(ref err) = result.error {
3639 div { class: "interpreter-error", "{err}" }
3640 }
3641 }
3642 }
3643 }
3644 }
3645 } else {
3646 {
3647 let rust_code = generated_rust.read().clone();
3648 rsx! {
3649 if rust_code.is_empty() {
3650 div { class: "interpreter-empty",
3651 "Compile your code to see generated Rust"
3652 }
3653 } else {
3654 CodeView {
3655 code: rust_code,
3656 language: Language::Rust,
3657 }
3658 }
3659 }
3660 }
3661 }
3662 }
3663 },
3664 StudioMode::Math => rsx! {
3665 div {
3666 class: "panel-header",
3667 onclick: move |_| {
3668 let v = *output_expanded.read();
3669 output_expanded.set(!v);
3670 },
3671 span { "Output" }
3672 div {
3673 class: "output-mode-toggle",
3674 onclick: move |evt| evt.stop_propagation(),
3675 button {
3676 class: if current_math_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
3677 onclick: move |_| math_output_mode.set(CodeOutputMode::Interpret),
3678 "Output"
3679 }
3680 button {
3681 class: if current_math_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3682 onclick: move |_| math_output_mode.set(CodeOutputMode::Rust),
3683 "Rust"
3684 }
3685 }
3686 }
3687 div { class: "panel-content",
3688 if current_math_output_mode == CodeOutputMode::Interpret {
3689 ReplOutput {
3690 lines: math_output.read().clone(),
3691 on_clear: move |_| {
3692 math_output.write().clear();
3693 math_repl.set(Repl::new()); },
3695 }
3696 } else {
3697 {
3698 let rust_code = generated_math_rust.read().clone();
3699 rsx! {
3700 if rust_code.is_empty() {
3701 div { class: "interpreter-empty",
3702 "Compile to see generated Rust"
3703 }
3704 } else {
3705 CodeView {
3706 code: rust_code,
3707 language: Language::Rust,
3708 }
3709 }
3710 }
3711 }
3712 }
3713 }
3714 },
3715 StudioMode::Hardware => rsx! {
3716 div {
3717 class: "panel-header",
3718 onclick: move |_| {
3719 let v = *output_expanded.read();
3720 output_expanded.set(!v);
3721 },
3722 span { if hw_is_regalloc { "Allocation" } else if hw_is_pigeonhole { "Pigeonhole" } else { "SVA" } }
3723 if !hw_is_regalloc && !hw_is_pigeonhole {
3724 div {
3725 class: "output-mode-toggle",
3726 onclick: move |evt| evt.stop_propagation(),
3727 button {
3728 class: if current_hw_output_mode == CodeOutputMode::Interpret { "output-mode-btn active" } else { "output-mode-btn" },
3729 onclick: move |_| hw_output_mode.set(CodeOutputMode::Interpret),
3730 "SVA"
3731 }
3732 button {
3733 class: if current_hw_output_mode == CodeOutputMode::Rust { "output-mode-btn active" } else { "output-mode-btn" },
3734 onclick: move |_| hw_output_mode.set(CodeOutputMode::Rust),
3735 "Rust"
3736 }
3737 }
3738 }
3739 }
3740 div { class: "panel-content",
3741 if hw_is_regalloc {
3742 {
3743 let report = crate::ui::pages::register_alloc_viz::parse_register_spec(&hw_input.read())
3744 .map(|s| crate::ui::pages::register_alloc_viz::allocation_report(&s))
3745 .unwrap_or_default();
3746 rsx! {
3747 div { class: "interpreter-output",
3748 div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "Certified register allocation" }
3749 pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0;", "{report}" }
3750 }
3751 }
3752 }
3753 } else if hw_is_pigeonhole {
3754 {
3755 let report = crate::ui::pages::pigeonhole_viz::parse_pigeonhole_spec(&hw_input.read())
3756 .map(|s| crate::ui::pages::pigeonhole_viz::report(&s))
3757 .unwrap_or_default();
3758 rsx! {
3759 div { class: "interpreter-output",
3760 div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "Certified pigeonhole refutation" }
3761 pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0;", "{report}" }
3762 }
3763 }
3764 }
3765 } else if current_hw_output_mode == CodeOutputMode::Interpret {
3766 {
3767 let sva = hw_sva.read().clone();
3768 let psl = hw_psl.read().clone();
3769 let proof = hw_proof.read().clone();
3770 let proof_ok = *hw_proof_ok.read();
3771 let err = hw_error.read().clone();
3772 rsx! {
3773 div { class: "interpreter-output",
3774 if let Some(e) = err {
3775 div { class: "interpreter-error", "{e}" }
3776 } else if sva.is_empty() {
3777 div { class: "interpreter-empty",
3778 "Execute to analyze \u{2014} synthesize an assertion, design a signal plan, or model-check Verilog"
3779 }
3780 } else {
3781 div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "SystemVerilog Assertion" }
3782 pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0 0 12px;", "{sva}" }
3783 if !proof.is_empty() {
3784 div {
3785 style: {
3786 let (border, bg, fg) = match proof_ok {
3787 Some(true) => ("rgba(74,222,128,0.4)", "rgba(74,222,128,0.08)", "#4ade80"),
3788 Some(false) => ("rgba(248,113,113,0.4)", "rgba(248,113,113,0.08)", "#f87171"),
3789 None => ("rgba(255,255,255,0.12)", "rgba(255,255,255,0.03)", "#d0d0d0"),
3790 };
3791 format!("font-size: 13px; line-height: 1.5; color: {fg}; background: {bg}; border: 1px solid {border}; border-radius: 8px; padding: 10px; margin: 0 0 12px;")
3792 },
3793 "{proof}"
3794 }
3795 }
3796 if !psl.is_empty() {
3797 div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin: 4px 0;", "PSL" }
3798 pre { style: "font-family: ui-monospace, monospace; font-size: 13px; white-space: pre-wrap; word-break: break-word; color: #e5e7eb; background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 10px; margin: 0;", "{psl}" }
3799 }
3800 }
3801 }
3802 }
3803 }
3804 } else {
3805 {
3806 let rust_code = generated_hw_rust.read().clone();
3807 rsx! {
3808 if rust_code.is_empty() {
3809 div { class: "interpreter-empty",
3810 "Compile to see the Rust runtime monitor"
3811 }
3812 } else {
3813 CodeView {
3814 code: rust_code,
3815 language: Language::Rust,
3816 }
3817 }
3818 }
3819 }
3820 }
3821 }
3822 },
3823 }
3824 }
3825
3826 if show_panel3 {
3828 div {
3829 class: if resizing.read().is_some() { "panel-resizer active" } else { "panel-resizer" },
3830 onmousedown: move |_| resizing.set(Some("right")),
3831 }
3832
3833 aside {
3834 class: "{panel3_class}",
3835 style: "width: {right_w}%;",
3836
3837 match current_mode {
3838 StudioMode::Logic => rsx! {
3839 div { class: "panel-header",
3840 span { "Syntax Tree" }
3841 }
3842 div { class: "panel-content",
3843 AstTree {
3844 ast: current_result.ast.clone(),
3845 }
3846 }
3847 },
3848 StudioMode::Code => rsx! {
3849 div { class: "panel-header",
3850 span { "Console" }
3851 }
3852 div { class: "panel-content",
3853 div { class: "interpreter-output",
3854 {
3855 let result = interpreter_result.read();
3856 rsx! {
3857 if let Some(ref err) = result.error {
3858 div { class: "interpreter-error", "{err}" }
3859 }
3860 }
3861 }
3862 }
3863 }
3864 },
3865 StudioMode::Math => rsx! {
3866 div { class: "panel-header",
3867 span { "Context" }
3868 }
3869 div { class: "panel-content",
3870 ContextView {
3871 definitions: definitions.clone(),
3872 inductives: inductives.clone(),
3873 }
3874 }
3875 },
3876 StudioMode::Hardware => {
3877 let ce = hw_counterexample.read().clone();
3878 let sigs = hw_signals.read().clone();
3879 if let Some(pspec) = crate::ui::pages::pigeonhole_viz::parse_pigeonhole_spec(&hw_input.read()) {
3880 let (svg, verdict) = crate::ui::pages::pigeonhole_viz::render(&pspec);
3884 rsx! {
3885 div { class: "panel-header", span { "Pigeonhole" } }
3886 div { class: "panel-content",
3887 div { style: "padding: 8px; overflow-x: auto;",
3888 div { dangerous_inner_html: "{svg}" }
3889 div {
3890 style: "margin-top:4px;font-size:11px;line-height:1.4;color:rgba(255,255,255,0.45);",
3891 "Each pigeon flies toward a hole; with one more pigeon than holes, one is always left out. Maximum bipartite matching proves it impossible in polynomial time (the re-verified Hall witness), and our prover emits a certified symmetry-breaking refutation — while every resolution-based solver (Kissat, CaDiCaL, Z3) provably needs exponentially many steps (Haken 1985)."
3892 }
3893 div {
3894 style: "margin-top:8px;padding:8px;border-radius:6px;font-size:12px;line-height:1.45;background:rgba(224,108,117,0.15);color:#f3c0c4;",
3895 "{verdict}"
3896 }
3897 }
3898 }
3899 }
3900 } else if let Some(spec) = crate::ui::pages::register_alloc_viz::parse_register_spec(&hw_input.read()) {
3901 let (svg, verdict) = crate::ui::pages::register_alloc_viz::render(&spec);
3904 let ok = verdict.starts_with('\u{2713}');
3905 rsx! {
3906 div { class: "panel-header", span { "Register Allocation" } }
3907 div { class: "panel-content",
3908 div { style: "padding: 8px; overflow-x: auto;",
3909 div { dangerous_inner_html: "{svg}" }
3910 div {
3911 style: "margin-top:4px;font-size:11px;line-height:1.4;color:rgba(255,255,255,0.45);",
3912 "The sweep line is the program counter; each register lane lights up while it holds a value and goes dark when it's freed. The pressure histogram counts how many values are live at each instruction — bars above the dashed budget line turn red, the exact points where spilling is forced and a value drops to the MEM lane. The interference graph is what allocation really colours: an edge joins two values whose lifetimes overlap, nodes are coloured by their register, and a red clique is a set that mutually conflicts and provably cannot share."
3913 }
3914 div {
3915 style: if ok {
3916 "margin-top:8px;padding:8px;border-radius:6px;font-size:12px;line-height:1.45;background:rgba(152,195,121,0.15);color:#cdebc5;"
3917 } else {
3918 "margin-top:8px;padding:8px;border-radius:6px;font-size:12px;line-height:1.45;background:rgba(224,108,117,0.15);color:#f3c0c4;"
3919 },
3920 "{verdict}"
3921 }
3922 }
3923 }
3924 }
3925 } else if !ce.is_empty() {
3926 let wf = logicaffeine_compile::codegen_sva::hw_pipeline::counterexample_waveform(&ce);
3928 let traffic = traffic_svg(&wf);
3929 let osc = waveform_svg(&wf);
3930 let violated = *hw_proof_ok.read() == Some(false);
3931 let header = match (traffic.is_some(), violated) {
3932 (true, true) => "Intersection \u{2014} Conflict Found",
3933 (true, false) => "Intersection \u{2014} Proven Safe (witness run)",
3934 (false, true) => "Counterexample Waveform",
3935 (false, false) => "Witness Waveform",
3936 };
3937 rsx! {
3938 div { class: "panel-header", span { "{header}" } }
3939 div { class: "panel-content",
3940 div { style: "padding: 8px; overflow-x: auto;",
3941 if let Some(t) = traffic.as_ref() {
3942 div { dangerous_inner_html: "{t}" }
3943 }
3944 div { dangerous_inner_html: "{osc}" }
3945 }
3946 }
3947 }
3948 } else {
3949 let svg = kg_svg(&hw_kg.read());
3950 rsx! {
3951 div { class: "panel-header", span { "Knowledge Graph" } }
3952 div { class: "panel-content",
3953 if !svg.is_empty() {
3954 div { style: "padding: 6px;",
3955 div { dangerous_inner_html: "{svg}" }
3956 div { style: "display:flex; gap:12px; flex-wrap:wrap; justify-content:center; margin-top:4px; font-size:10px;",
3957 span { style: "color:#60a5fa;", "● input" }
3958 span { style: "color:#4ade80;", "● output" }
3959 span { style: "color:#a78bfa;", "● internal" }
3960 span { style: "color:#fbbf24;", "● clock" }
3961 }
3962 }
3963 } else if !sigs.is_empty() {
3964 div { style: "padding: 8px;",
3965 div { style: "font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: rgba(255,255,255,0.45); margin-bottom: 6px;", "Signals" }
3966 for s in sigs.iter() {
3967 div { style: "font-family: ui-monospace, monospace; font-size: 13px; color: #e5e7eb; padding: 2px 0;", "{s}" }
3968 }
3969 }
3970 } else {
3971 div { class: "interpreter-empty",
3972 "Execute to extract the hardware knowledge graph"
3973 }
3974 }
3975 }
3976 }
3977 }
3978 },
3979 }
3980 }
3981 }
3982 }
3983 }
3984
3985 if debugging() && current_mode == StudioMode::Code {
3987 DebugDrawer {
3988 source: code_input.read().clone(),
3989 on_close: move |_| debugging.set(false),
3990 }
3991 }
3992 }
3993 }
3994}