├── .gitignore ├── .npmrc ├── README.md ├── components.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── src ├── app.css ├── app.d.ts ├── app.html ├── lib │ ├── components │ │ ├── generic │ │ │ ├── ComputeButtonGroup.svelte │ │ │ ├── DarkMode.svelte │ │ │ ├── Logo.svelte │ │ │ ├── Navbar.svelte │ │ │ └── ShareLink.svelte │ │ ├── render │ │ │ ├── slr-automaton │ │ │ │ ├── AutomatonBoard.svelte │ │ │ │ ├── AutomatonEdge.svelte │ │ │ │ └── AutomatonNode.svelte │ │ │ └── tree │ │ │ │ ├── ParseTreeBoard.svelte │ │ │ │ └── TreeNode.svelte │ │ └── tools │ │ │ ├── GrammarInput.svelte │ │ │ ├── GrammarToolLayout.svelte │ │ │ ├── ParseInput.svelte │ │ │ └── tables │ │ │ ├── AutomatonStepsTable.svelte │ │ │ ├── FirstFollowTable.svelte │ │ │ ├── LL1Table.svelte │ │ │ ├── ParseTraceTable.svelte │ │ │ └── SLRTable.svelte │ ├── shadcn-ui │ │ ├── components │ │ │ └── ui │ │ │ │ ├── button │ │ │ │ ├── button.svelte │ │ │ │ └── index.ts │ │ │ │ ├── checkbox │ │ │ │ ├── checkbox.svelte │ │ │ │ └── index.ts │ │ │ │ ├── dialog │ │ │ │ ├── dialog-content.svelte │ │ │ │ ├── dialog-description.svelte │ │ │ │ ├── dialog-footer.svelte │ │ │ │ ├── dialog-header.svelte │ │ │ │ ├── dialog-overlay.svelte │ │ │ │ ├── dialog-title.svelte │ │ │ │ └── index.ts │ │ │ │ ├── input │ │ │ │ ├── index.ts │ │ │ │ └── input.svelte │ │ │ │ ├── label │ │ │ │ ├── index.ts │ │ │ │ └── label.svelte │ │ │ │ ├── select │ │ │ │ ├── index.ts │ │ │ │ ├── select-content.svelte │ │ │ │ ├── select-group-heading.svelte │ │ │ │ ├── select-item.svelte │ │ │ │ ├── select-scroll-down-button.svelte │ │ │ │ ├── select-scroll-up-button.svelte │ │ │ │ ├── select-separator.svelte │ │ │ │ └── select-trigger.svelte │ │ │ │ ├── separator │ │ │ │ ├── index.ts │ │ │ │ └── separator.svelte │ │ │ │ ├── sheet │ │ │ │ ├── index.ts │ │ │ │ ├── sheet-content.svelte │ │ │ │ ├── sheet-description.svelte │ │ │ │ ├── sheet-footer.svelte │ │ │ │ ├── sheet-header.svelte │ │ │ │ ├── sheet-overlay.svelte │ │ │ │ └── sheet-title.svelte │ │ │ │ ├── table │ │ │ │ ├── index.ts │ │ │ │ ├── table-body.svelte │ │ │ │ ├── table-caption.svelte │ │ │ │ ├── table-cell.svelte │ │ │ │ ├── table-footer.svelte │ │ │ │ ├── table-head.svelte │ │ │ │ ├── table-header.svelte │ │ │ │ ├── table-row.svelte │ │ │ │ └── table.svelte │ │ │ │ └── textarea │ │ │ │ ├── index.ts │ │ │ │ └── textarea.svelte │ │ └── utils.ts │ ├── types │ │ ├── first-follow.ts │ │ ├── grammar.ts │ │ ├── ll1.ts │ │ ├── parse.ts │ │ ├── slr.ts │ │ └── tree.ts │ └── utils │ │ ├── automatonGenerator.ts │ │ ├── cnf │ │ └── transform.ts │ │ ├── edgeUtils.ts │ │ ├── first-follow │ │ ├── first.ts │ │ └── follow.ts │ │ ├── grammar │ │ ├── parse.ts │ │ └── pretty_print.ts │ │ ├── ll1 │ │ ├── left-factorization.ts │ │ ├── left-recursion.ts │ │ ├── parse.ts │ │ └── table.ts │ │ ├── sets.ts │ │ ├── sharing.ts │ │ ├── slr │ │ ├── closure.ts │ │ ├── states.ts │ │ └── table.ts │ │ ├── treeGenerator.ts │ │ └── utils.ts └── routes │ ├── +error.svelte │ ├── +layout.svelte │ ├── +layout.ts │ ├── +page.svelte │ ├── cnf │ └── +page.svelte │ ├── first-follow │ └── +page.svelte │ ├── left-fact │ └── +page.svelte │ ├── left-rec │ └── +page.svelte │ ├── ll1-table │ └── +page.svelte │ └── slr-table │ └── +page.svelte ├── static ├── favicon-dark.svg └── favicon-light.svg ├── svelte.config.js ├── tailwind.config.ts ├── tsconfig.json └── vite.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | .netlify 7 | .wrangler 8 | /.svelte-kit 9 | /build 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Env 16 | .env 17 | .env.* 18 | !.env.example 19 | !.env.test 20 | 21 | # Vite 22 | vite.config.js.timestamp-* 23 | vite.config.ts.timestamp-* 24 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LFC Playground 🚀 2 | 3 | Questo sito è stato creato per offrire agli studenti alle prese con il corso di Linguaggi Formali e Compilatori, un ambiente 4 | per controllare la correttezza di alcuni esercizi (soprattutto quelli "meccanici") e sperimentare con grammatiche, tabelle di parsing, regex, automi e altro 5 | 6 | ## Contribuire 🤝 7 | Ogni tipo di contributo è ben accetto, se avete suggerimenti, correzioni o nuove funzionalità da proporre, sentitevi liberi di aprire una [issue](https://github.com/Isax03/lfc-playground/issues) o una [pull request](https://github.com/Isax03/lfc-playground/pulls). 8 | 9 | ## Features disponibili e future ✨ 10 | - [x] Calcolo di FIRST e FOLLOW per una grammatica 11 | - [x] Costruzione di una tabella di parsing LL(1) per una grammatica 12 | - [x] Parsing di una stringa con una tabella di parsing LL(1) e visualizzazione dell'albero di parsing 13 | - [x] Rimozione di ricorsione a sinistra da una grammatica 14 | - [x] Fattorizzazione di una grammatica 15 | - [x] Conversione di una grammatica in forma normale di Chomsky 16 | - [x] Costuzione della tabella di parsing SLR(1) per una grammatica 17 | - [x] Costuzione dell'automa caratteristico SLR(1) per una grammatica 18 | - [x] Parsing di una stringa con algoritmo shift/reduce SLR(1) e visualizzazione dell'albero di parsing 19 | - [ ] Costuzione di automi a stati finiti (NFA, DFA, Min-DFA) da una regex 20 | 21 | #### TODO: 22 | - Fix visualizzazione albero di parsing -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://next.shadcn-svelte.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.ts", 6 | "css": "src\\app.css", 7 | "baseColor": "neutral" 8 | }, 9 | "aliases": { 10 | "components": "$lib/shadcn-ui/components", 11 | "utils": "$lib/shadcn-ui/utils", 12 | "ui": "$lib/shadcn-ui/components/ui", 13 | "hooks": "$lib/shadcn-ui/hooks" 14 | }, 15 | "typescript": true, 16 | "registry": "https://next.shadcn-svelte.com/registry" 17 | } 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lfc-playground", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite dev", 8 | "build": "vite build", 9 | "preview": "vite preview", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 12 | }, 13 | "devDependencies": { 14 | "@sveltejs/adapter-vercel": "^5.7.2", 15 | "@sveltejs/kit": "^2.21.1", 16 | "@sveltejs/vite-plugin-svelte": "^5.0.3", 17 | "@tailwindcss/container-queries": "^0.1.1", 18 | "@tailwindcss/forms": "^0.5.10", 19 | "@tailwindcss/postcss": "^4.1.7", 20 | "@tailwindcss/typography": "^0.5.16", 21 | "@types/dagre": "^0.7.52", 22 | "bits-ui": "1.5.3", 23 | "clsx": "^2.1.1", 24 | "lucide-svelte": "^0.511.0", 25 | "svelte": "^5.32.1", 26 | "svelte-check": "^4.2.1", 27 | "tailwind-merge": "^3.3.0", 28 | "tailwind-variants": "^1.0.0", 29 | "tailwindcss": "^4.1.7", 30 | "tailwindcss-animate": "^1.0.7", 31 | "typescript": "^5.8.3", 32 | "vite": "^6.3.5" 33 | }, 34 | "dependencies": { 35 | "@vercel/analytics": "^1.5.0", 36 | "@xyflow/svelte": "0.1.30", 37 | "dagre": "^0.8.5", 38 | "mode-watcher": "^1.0.7" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | 3 | @config '../tailwind.config.ts'; 4 | 5 | /* 6 | The default border color has changed to `currentColor` in Tailwind CSS v4, 7 | so we've added these compatibility styles to make sure everything still 8 | looks the same as it did with Tailwind CSS v3. 9 | 10 | If we ever want to remove these styles, we need to add an explicit border 11 | color utility to any element that depends on these defaults. 12 | */ 13 | @layer base { 14 | *, 15 | ::after, 16 | ::before, 17 | ::backdrop, 18 | ::file-selector-button { 19 | border-color: var(--color-gray-200, currentColor); 20 | } 21 | } 22 | 23 | @layer base { 24 | :root { 25 | --background: 0 0% 100%; 26 | --foreground: 0 0% 3.9%; 27 | --muted: 0 0% 96.1%; 28 | --muted-foreground: 0 0% 45.1%; 29 | --popover: 0 0% 100%; 30 | --popover-foreground: 0 0% 3.9%; 31 | --card: 0 0% 100%; 32 | --card-foreground: 0 0% 3.9%; 33 | --border: 0 0% 89.8%; 34 | --input: 0 0% 89.8%; 35 | --primary: 0 0% 9%; 36 | --primary-foreground: 0 0% 98%; 37 | --secondary: 0 0% 96.1%; 38 | --secondary-foreground: 0 0% 9%; 39 | --accent: 0 0% 96.1%; 40 | --accent-foreground: 0 0% 9%; 41 | --destructive: 0 72.2% 50.6%; 42 | --destructive-foreground: 0 0% 98%; 43 | --ring: 0 0% 3.9%; 44 | --radius: 0.5rem; 45 | --sidebar-background: 0 0% 98%; 46 | --sidebar-foreground: 240 5.3% 26.1%; 47 | --sidebar-primary: 240 5.9% 10%; 48 | --sidebar-primary-foreground: 0 0% 98%; 49 | --sidebar-accent: 240 4.8% 95.9%; 50 | --sidebar-accent-foreground: 240 5.9% 10%; 51 | --sidebar-border: 220 13% 91%; 52 | --sidebar-ring: 217.2 91.2% 59.8%; 53 | } 54 | 55 | .dark { 56 | --background: 0 0% 3.9%; 57 | --foreground: 0 0% 98%; 58 | --muted: 0 0% 14.9%; 59 | --muted-foreground: 0 0% 63.9%; 60 | --popover: 0 0% 3.9%; 61 | --popover-foreground: 0 0% 98%; 62 | --card: 0 0% 3.9%; 63 | --card-foreground: 0 0% 98%; 64 | --border: 0 0% 14.9%; 65 | --input: 0 0% 14.9%; 66 | --primary: 0 0% 98%; 67 | --primary-foreground: 0 0% 9%; 68 | --secondary: 0 0% 14.9%; 69 | --secondary-foreground: 0 0% 98%; 70 | --accent: 0 0% 14.9%; 71 | --accent-foreground: 0 0% 98%; 72 | --destructive: 0 62.8% 30.6%; 73 | --destructive-foreground: 0 0% 98%; 74 | --ring: 0 0% 83.1%; 75 | --sidebar-background: 240 5.9% 10%; 76 | --sidebar-foreground: 240 4.8% 95.9%; 77 | --sidebar-primary: 224.3 76.3% 48%; 78 | --sidebar-primary-foreground: 0 0% 100%; 79 | --sidebar-accent: 240 3.7% 15.9%; 80 | --sidebar-accent-foreground: 240 4.8% 95.9%; 81 | --sidebar-border: 240 3.7% 15.9%; 82 | --sidebar-ring: 217.2 91.2% 59.8%; 83 | } 84 | } 85 | 86 | @layer base { 87 | * { 88 | @apply border-border; 89 | } 90 | body { 91 | @apply bg-background text-foreground; 92 | } 93 | 94 | /* width */ 95 | ::-webkit-scrollbar { 96 | width: 8px; 97 | height: 5px; 98 | } 99 | 100 | /* Track */ 101 | ::-webkit-scrollbar-track { 102 | @apply bg-inherit rounded-lg; 103 | } 104 | 105 | /* Handle */ 106 | ::-webkit-scrollbar-thumb { 107 | @apply bg-muted-foreground/30 rounded-lg; 108 | } 109 | 110 | /* Handle on hover */ 111 | ::-webkit-scrollbar-thumb:hover { 112 | @apply bg-muted-foreground; 113 | } 114 | } -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://svelte.dev/docs/kit/types#app.d.ts 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %sveltekit.head% 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/generic/ComputeButtonGroup.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 |
14 | 15 | {#if showShare} 16 | 17 | {/if} 18 |
19 | 24 |
25 | -------------------------------------------------------------------------------- /src/lib/components/generic/DarkMode.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /src/lib/components/generic/Navbar.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 113 | -------------------------------------------------------------------------------- /src/lib/components/generic/ShareLink.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | 33 | 34 | 35 | Share 36 | 37 | 38 | 39 | Share Grammar 40 | 41 | Review your grammar and copy the shareable link 42 | 43 | 44 |
45 |
46 | 47 | 23 | -------------------------------------------------------------------------------- /src/lib/shadcn-ui/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/types/first-follow.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the set of terminals that can appear first 3 | * in a derivation from a given symbol 4 | */ 5 | export type FirstSet = Set; 6 | export type FirstSets = Map; 7 | 8 | /** 9 | * Represents the set of terminals that can follow 10 | * a given non-terminal in any derivation 11 | */ 12 | export type FollowSet = Set; 13 | export type FollowSets = Map; -------------------------------------------------------------------------------- /src/lib/types/grammar.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents a formal grammar 3 | * defined as a 4-tuple (N, T, S, P) 4 | */ 5 | export type Grammar = { 6 | N: Set; // Set of non-terminals 7 | T: Set; // Set of terminals 8 | S: string; // Start symbol 9 | P: ProductionRule; // Map of productions 10 | }; 11 | 12 | /** 13 | * Maps non-terminal symbols to their production rules 14 | */ 15 | export type ProductionRule = Map; 16 | 17 | /** 18 | * Represents a single production rule as an array of symbols 19 | */ 20 | export type Production = string[]; -------------------------------------------------------------------------------- /src/lib/types/ll1.ts: -------------------------------------------------------------------------------- 1 | import { type ProductionRule } from "./grammar"; 2 | 3 | /** 4 | * Represents an LL(1) parsing table where each cell contains 5 | * the production rule to use for a given non-terminal and lookahead 6 | */ 7 | export type LL1Table = Record>; -------------------------------------------------------------------------------- /src/lib/types/parse.ts: -------------------------------------------------------------------------------- 1 | import type { TreeNode } from "./tree"; 2 | 3 | export interface ParseStep { 4 | stack: string[]; 5 | input: string[]; 6 | production?: string; 7 | } 8 | 9 | export interface ParseResult { 10 | tree: TreeNode | null; 11 | trace: ParseStep[]; 12 | success: boolean; 13 | error?: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/types/slr.ts: -------------------------------------------------------------------------------- 1 | import type { ProductionRule } from "./grammar"; 2 | 3 | /** 4 | * Represents a move to make in an SLR parsing table 5 | */ 6 | export type SLRMove = 7 | | "accept" 8 | | "error" 9 | | { action: "shift"; state: number } 10 | | { action: "reduce"; rule: ProductionRule } 11 | | { action: "conflict"; moves: SLRMove[] } 12 | | number; // for goto 13 | 14 | /** 15 | * Represents an SLR parsing table where each cell contains 16 | * the move to make for a given state and lookahead 17 | */ 18 | export type SLRTable = Record>; 19 | 20 | export type Closure = { 21 | kernel: ProductionRule; 22 | body: ProductionRule; 23 | }; 24 | 25 | export type StatesAutomaton = { 26 | states: Record; // state -> closure 27 | transitions: Record>; // state -> symbol -> next state 28 | }; 29 | 30 | export type AutomatonStep = { 31 | stateId: string; 32 | symbol?: string; // The symbol being processed (undefined for initial state) 33 | fromStateId?: string; // The source state (undefined for initial state) 34 | closure?: Closure; // The full closure containing both kernel and body 35 | kernel?: ProductionRule; 36 | isExistingState?: boolean; 37 | }; 38 | 39 | export type ReducingLabel = { 40 | rule: { 41 | head: string; 42 | body: string[]; 43 | }; 44 | label: string; 45 | }; 46 | 47 | export type AutomatonBuildResult = { 48 | automaton: StatesAutomaton; 49 | steps: AutomatonStep[]; 50 | reducingLabels: ReducingLabel[]; 51 | }; 52 | -------------------------------------------------------------------------------- /src/lib/types/tree.ts: -------------------------------------------------------------------------------- 1 | export interface TreeNode { 2 | id: string; 3 | symbol: string; 4 | children: TreeNode[]; 5 | x?: number; 6 | y?: number; 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/utils/automatonGenerator.ts: -------------------------------------------------------------------------------- 1 | import type { ReducingLabel, StatesAutomaton } from "$lib/types/slr"; 2 | import { type Edge, MarkerType, type Node } from "@xyflow/svelte"; 3 | import dagre from "dagre"; 4 | 5 | function hasMarkerAtEnd(productions: Map): boolean { 6 | for (const [, prods] of productions.entries()) { 7 | for (const prod of prods) { 8 | if (prod[prod.length - 1] === "·") { 9 | return true; 10 | } 11 | } 12 | } 13 | return false; 14 | } 15 | 16 | export function generateAutomatonLayout( 17 | automaton: StatesAutomaton, 18 | reducingLabels: ReducingLabel[] 19 | ): { nodes: Node[]; edges: Edge[] } { 20 | const nodes: Node[] = []; 21 | const edges: Edge[] = []; 22 | 23 | // Create a new dagre graph 24 | const g = new dagre.graphlib.Graph(); 25 | g.setGraph({ rankdir: "LR", nodesep: 70, ranksep: 70 }); 26 | g.setDefaultEdgeLabel(() => ({})); 27 | 28 | // Add nodes 29 | const nodeWidth = 200; 30 | const nodeHeight = 100; 31 | 32 | Object.entries(automaton.states).forEach(([stateId, closure]) => { 33 | // Controlla se è uno stato di reduce 34 | const isReduce = 35 | hasMarkerAtEnd(closure.kernel) || hasMarkerAtEnd(closure.body); 36 | 37 | // Controlla se è uno stato di accept - il marker è alla fine e c'è S' nella kernel 38 | const isAccept = Array.from(closure.kernel.entries()).some( 39 | ([nt, prods]) => 40 | nt === "S'" && prods.some((p) => p[p.length - 1] === "·") 41 | ); 42 | 43 | const node = { 44 | id: stateId, 45 | type: "stateNode", 46 | data: { 47 | id: stateId, 48 | kernel: closure.kernel, 49 | closure: closure.body, 50 | isAccept, 51 | isReduce: !isAccept && isReduce, // se è accept non è reduce 52 | reducingLabels, // aggiungiamo reducingLabels ai dati del nodo 53 | }, 54 | position: { x: 0, y: 0 }, 55 | }; 56 | 57 | nodes.push(node); 58 | g.setNode(stateId, { width: nodeWidth, height: nodeHeight }); 59 | }); 60 | 61 | // Add edges 62 | Object.entries(automaton.transitions).forEach( 63 | ([fromState, transitions]) => { 64 | Object.entries(transitions).forEach(([symbol, toState]) => { 65 | const edgeId = `${fromState}-${symbol}-${toState}`; 66 | edges.push({ 67 | id: edgeId, 68 | source: fromState, 69 | target: toState, 70 | label: symbol, 71 | type: "floating", 72 | data: { shape: "bezier" }, 73 | markerEnd: { type: MarkerType.Arrow }, 74 | }); 75 | g.setEdge(fromState, toState); 76 | }); 77 | } 78 | ); 79 | 80 | // Calculate layout 81 | dagre.layout(g); 82 | 83 | // Apply layout to nodes 84 | nodes.forEach((node) => { 85 | const nodeWithPosition = g.node(node.id); 86 | node.position = { 87 | x: nodeWithPosition.x - nodeWidth / 2, 88 | y: nodeWithPosition.y - nodeHeight / 2, 89 | }; 90 | }); 91 | 92 | return { nodes, edges }; 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/utils/cnf/transform.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar, Production } from "$lib/types/grammar"; 2 | import { areProductionsEqual } from "../utils"; 3 | 4 | export function transformToCnf(grammar: Grammar): Grammar { 5 | let result = { ...grammar }; 6 | result = removeEmpties(result); 7 | result = removeSingles(result); 8 | result = convertGrammar(result); 9 | result = removeUnreachable(result); 10 | 11 | // Remove empty productions 12 | for (const [nonTerminal, productions] of result.P) { 13 | const newProductions = productions.filter((production) => production.length > 0); 14 | if (newProductions.length === 0) { 15 | result.P.delete(nonTerminal); 16 | } else { 17 | result.P.set(nonTerminal, newProductions); 18 | } 19 | } 20 | 21 | return result; 22 | } 23 | 24 | function removeEmpties(grammar: Grammar): Grammar { 25 | let result = { ...grammar }; 26 | let hasEmpty = true; 27 | 28 | while (hasEmpty) { 29 | hasEmpty = false; 30 | for (const [nonTerminal, productions] of result.P) { 31 | for (let i = 0; i < productions.length; i++) { 32 | if (productions[i].length === 1 && productions[i][0] === 'ε') { 33 | productions.splice(i, 1); 34 | i--; 35 | result = removeEmpty(result, Array.from(result.P.keys()), nonTerminal); 36 | hasEmpty = true; 37 | } 38 | } 39 | } 40 | } 41 | return result; 42 | } 43 | 44 | function removeEmpty(grammar: Grammar, keys: string[], term: string): Grammar { 45 | const result = { ...grammar }; 46 | 47 | for (const key of keys) { 48 | const productions = result.P.get(key) || []; 49 | for (const prod of productions) { 50 | if (prod.includes(term)) { 51 | const newGenerations: Production[] = [[]]; 52 | 53 | for (const symbol of prod) { 54 | if (symbol !== term) { 55 | newGenerations.forEach(gen => gen.push(symbol)); 56 | } else { 57 | const currentLength = newGenerations.length; 58 | for (let i = 0; i < currentLength; i++) { 59 | const newGen = [...newGenerations[i]]; 60 | newGenerations[i].push(symbol); 61 | newGenerations.push(newGen); 62 | } 63 | } 64 | } 65 | 66 | for (const newGen of newGenerations) { 67 | if (newGen.length === 0) { 68 | newGen.push('ε'); 69 | } 70 | addNewGeneration(result, key, newGen); 71 | } 72 | } 73 | } 74 | } 75 | return result; 76 | } 77 | 78 | function removeSingles(grammar: Grammar): Grammar { 79 | const result = { ...grammar }; 80 | let hasSingle = true; 81 | 82 | while (hasSingle) { 83 | hasSingle = false; 84 | for (const [nonTerminal, productions] of result.P) { 85 | for (let i = 0; i < productions.length; i++) { 86 | const prod = productions[i]; 87 | if (prod.length === 1 && result.P.has(prod[0])) { 88 | const key = prod[0]; 89 | productions.splice(i, 1); 90 | const keyProductions = result.P.get(key) || []; 91 | keyProductions.forEach(keyProd => { 92 | addNewGeneration(result, nonTerminal, keyProd); 93 | }); 94 | hasSingle = true; 95 | i--; 96 | } 97 | } 98 | } 99 | } 100 | return result; 101 | } 102 | 103 | function convertGrammar(grammar: Grammar): Grammar { 104 | const result = { ...grammar }; 105 | const singles: Map = new Map(); 106 | const multis: Map = new Map(); 107 | let helperIndex = 0; 108 | 109 | // Handle single terminals 110 | for (const [nonTerminal, productions] of result.P) { 111 | if (productions.length === 1 && productions[0].length === 1) { 112 | const term = productions[0][0]; 113 | if (term !== 'ε' && !result.P.has(term)) { 114 | singles.set(term, nonTerminal); 115 | } 116 | } 117 | } 118 | 119 | // Handle multiple symbols 120 | for (const [nonTerminal, productions] of result.P) { 121 | if (productions.length === 1) { 122 | multis.set(productions[0].join(' '), nonTerminal); 123 | } 124 | } 125 | 126 | // Convert productions 127 | for (const [nonTerminal, productions] of result.P) { 128 | for (let i = 0; i < productions.length; i++) { 129 | const prod = productions[i]; 130 | 131 | if (prod.length === 2) { 132 | for (let j = 0; j < 2; j++) { 133 | if (!result.P.has(prod[j]) && !singles.has(prod[j])) { 134 | let key = getHelperKey(helperIndex++); 135 | while (result.P.has(key)) { 136 | key = getHelperKey(helperIndex++); 137 | } 138 | result.P.set(key, [[prod[j]]]); 139 | result.N.add(key); 140 | singles.set(prod[j], key); 141 | } 142 | if (singles.has(prod[j])) { 143 | prod[j] = singles.get(prod[j])!; 144 | } 145 | } 146 | } else if (prod.length > 2) { 147 | // Handle longer productions 148 | const last = prod.length - 1; 149 | if (!result.P.has(prod[last]) && !singles.has(prod[last])) { 150 | const key = getHelperKey(helperIndex++); 151 | result.P.set(key, [[prod[last]]]); 152 | result.N.add(key); 153 | singles.set(prod[last], key); 154 | } 155 | 156 | const lastSymbol = singles.has(prod[last]) ? singles.get(prod[last])! : prod[last]; 157 | const prefix = prod.slice(0, last).join(' '); 158 | 159 | if (!multis.has(prefix)) { 160 | const key = getHelperKey(helperIndex++); 161 | result.P.set(key, [prod.slice(0, last)]); 162 | result.N.add(key); 163 | multis.set(prefix, key); 164 | } 165 | 166 | productions[i] = [multis.get(prefix)!, lastSymbol]; 167 | } 168 | } 169 | } 170 | return result; 171 | } 172 | 173 | function removeUnreachable(grammar: Grammar): Grammar { 174 | const result = { ...grammar }; 175 | const reachable = new Set([result.S]); 176 | const queue = [result.S]; 177 | let front = 0; 178 | 179 | while (front < queue.length) { 180 | const current = queue[front]; 181 | const productions = result.P.get(current) || []; 182 | 183 | for (const prod of productions) { 184 | for (const symbol of prod) { 185 | if (result.P.has(symbol) && !reachable.has(symbol)) { 186 | queue.push(symbol); 187 | reachable.add(symbol); 188 | } 189 | } 190 | } 191 | front++; 192 | } 193 | 194 | // Remove unreachable productions 195 | for (const [nonTerminal] of result.P) { 196 | if (!reachable.has(nonTerminal)) { 197 | result.P.delete(nonTerminal); 198 | result.N.delete(nonTerminal); 199 | } 200 | } 201 | 202 | return result; 203 | } 204 | 205 | // Utility functions 206 | function getHelperKey(index: number): string { 207 | return 'A' + toSubscript(index); 208 | } 209 | 210 | function toSubscript(num: number): string { 211 | const subscriptDigits = ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉']; 212 | return num.toString() 213 | .split('') 214 | .map(d => subscriptDigits[parseInt(d)]) 215 | .join(''); 216 | } 217 | 218 | function addNewGeneration(grammar: Grammar, key: string, newGeneration: Production): void { 219 | const productions = grammar.P.get(key) || []; 220 | if (!productions.some(prod => areProductionsEqual(prod, newGeneration))) { 221 | if (!grammar.P.has(key)) { 222 | grammar.P.set(key, []); 223 | } 224 | grammar.P.get(key)!.push(newGeneration); 225 | } 226 | } -------------------------------------------------------------------------------- /src/lib/utils/edgeUtils.ts: -------------------------------------------------------------------------------- 1 | import { Position, type InternalNode } from "@xyflow/svelte"; 2 | 3 | interface HandleCoordinates { 4 | x: number; 5 | y: number; 6 | position: Position; 7 | id: string; 8 | } 9 | 10 | function getHandleCoordinates(node: InternalNode): HandleCoordinates[] { 11 | const handles: HandleCoordinates[] = []; 12 | const handleBounds = node.internals.handleBounds?.source || []; 13 | 14 | handleBounds.forEach((handle) => { 15 | if (!handle.width || !handle.height) return; 16 | 17 | let offsetX = handle.width / 2; 18 | let offsetY = handle.height / 2; 19 | 20 | switch (handle.position) { 21 | case Position.Left: 22 | offsetX = 0; 23 | break; 24 | case Position.Right: 25 | offsetX = handle.width; 26 | break; 27 | case Position.Top: 28 | offsetY = 0; 29 | break; 30 | case Position.Bottom: 31 | offsetY = handle.height; 32 | break; 33 | } 34 | 35 | const x = node.internals.positionAbsolute.x + handle.x + offsetX; 36 | const y = node.internals.positionAbsolute.y + handle.y + offsetY; 37 | 38 | handles.push({ 39 | x, 40 | y, 41 | position: handle.position, 42 | id: handle.id ?? "", 43 | }); 44 | }); 45 | 46 | return handles; 47 | } 48 | 49 | function getClosestHandles( 50 | nodeA: InternalNode, 51 | nodeB: InternalNode 52 | ): [HandleCoordinates, HandleCoordinates] { 53 | const handlesA = getHandleCoordinates(nodeA); 54 | const handlesB = getHandleCoordinates(nodeB); 55 | 56 | let minDistance = Infinity; 57 | let closestHandleA: HandleCoordinates = handlesA[0]; 58 | let closestHandleB: HandleCoordinates = handlesB[0]; 59 | 60 | handlesA.forEach((handleA) => { 61 | handlesB.forEach((handleB) => { 62 | const distance = Math.sqrt( 63 | Math.pow(handleA.x - handleB.x, 2) + 64 | Math.pow(handleA.y - handleB.y, 2) 65 | ); 66 | 67 | if (distance < minDistance) { 68 | minDistance = distance; 69 | closestHandleA = handleA; 70 | closestHandleB = handleB; 71 | } 72 | }); 73 | }); 74 | 75 | return [closestHandleA, closestHandleB]; 76 | } 77 | 78 | function getParams( 79 | nodeA: InternalNode, 80 | nodeB: InternalNode 81 | ): [number, number, Position, string] { 82 | const [handleA] = getClosestHandles(nodeA, nodeB); 83 | return [handleA.x, handleA.y, handleA.position, handleA.id]; 84 | } 85 | 86 | // returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge 87 | export function getEdgeParams(source: InternalNode, target: InternalNode) { 88 | const [sx, sy, sourcePos, sourceHandle] = getParams(source, target); 89 | const [tx, ty, targetPos, targetHandle] = getParams(target, source); 90 | 91 | return { 92 | sx, 93 | sy, 94 | tx, 95 | ty, 96 | sourcePos, 97 | targetPos, 98 | sourceHandle, 99 | targetHandle, 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /src/lib/utils/first-follow/first.ts: -------------------------------------------------------------------------------- 1 | import type { FirstSet, FirstSets } from "$lib/types/first-follow"; 2 | import type { Grammar } from "$lib/types/grammar"; 3 | 4 | export function computeFirstForSymbol( 5 | symbol: string, 6 | grammar: Grammar, 7 | firstSets: FirstSets, 8 | processing: Set = new Set() 9 | ): FirstSet { 10 | // Se il simbolo è un terminale o epsilon, il first è il simbolo stesso 11 | if (grammar.T.has(symbol) || symbol === 'ε') { 12 | return new Set([symbol]); 13 | } 14 | 15 | // If we've already computed First for this symbol and it's not being processed, return it 16 | if (firstSets.has(symbol) && !processing.has(symbol)) { 17 | return firstSets.get(symbol)!; 18 | } 19 | 20 | // If we're already processing this symbol, return empty set to avoid infinite recursion 21 | if (processing.has(symbol)) { 22 | return new Set(); 23 | } 24 | 25 | // Add symbol to processing set to track recursive calls 26 | processing.add(symbol); 27 | 28 | // Initialize First set for this symbol 29 | let firstSet = new Set(); 30 | 31 | // Get productions for this symbol and compute First sets 32 | const productions = grammar.P.get(symbol); 33 | if (productions) { 34 | for (const production of productions) { 35 | const sequenceFirst = computeFirstForSequence(production, grammar, firstSets, processing); 36 | firstSet = new Set([...firstSet, ...sequenceFirst]); 37 | } 38 | } 39 | 40 | // Rimuoviamo il simbolo dall'insieme dei simboli in elaborazione 41 | processing.delete(symbol); 42 | 43 | // Memorizziamo il risultato 44 | firstSets.set(symbol, firstSet); 45 | 46 | return firstSet; 47 | } 48 | 49 | export function computeFirstForSequence( 50 | sequence: string[], 51 | grammar: Grammar, 52 | firstSets: FirstSets, 53 | processing: Set = new Set() 54 | ): FirstSet { 55 | let firstSet = new Set(); 56 | let allNullable = true; 57 | 58 | for (let i = 0; i < sequence.length; i++) { 59 | const symbol = sequence[i]; 60 | const firstOfSymbol = computeFirstForSymbol(symbol, grammar, firstSets, processing); 61 | 62 | // Add all symbols except epsilon 63 | const nonEpsilonFirst = new Set([...firstOfSymbol].filter(s => s !== 'ε')); 64 | firstSet = new Set([...firstSet, ...nonEpsilonFirst]); 65 | 66 | // Stop if this symbol cannot derive epsilon 67 | if (!firstOfSymbol.has('ε')) { 68 | allNullable = false; 69 | break; 70 | } 71 | 72 | // Se il simbolo è annullabile, continuiamo a considerare il prossimo simbolo nella sequenza 73 | } 74 | 75 | // If all symbols can derive epsilon, add epsilon to the result 76 | if (allNullable && sequence.length > 0) { 77 | firstSet.add('ε'); 78 | } 79 | 80 | return firstSet; 81 | } 82 | 83 | export function computeFirstSets(grammar: Grammar): FirstSets { 84 | const firstSets = new Map>(); 85 | 86 | // Inizializziamo i FIRST sets per tutti i non-terminali 87 | for (const nonTerminal of grammar.N) { 88 | firstSets.set(nonTerminal, new Set()); 89 | } 90 | 91 | // Iteriamo finché non ci sono più cambiamenti 92 | let changed = true; 93 | while (changed) { 94 | changed = false; 95 | 96 | for (const nonTerminal of grammar.N) { 97 | const oldSize = firstSets.get(nonTerminal)!.size; 98 | const productions = grammar.P.get(nonTerminal); 99 | 100 | if (productions) { 101 | for (const production of productions) { 102 | const sequenceFirst = computeFirstForSequence(production, grammar, firstSets); 103 | const currentFirst = firstSets.get(nonTerminal)!; 104 | sequenceFirst.forEach(symbol => currentFirst.add(symbol)); 105 | } 106 | } 107 | 108 | // Verifichiamo se il FIRST set è cambiato 109 | if (firstSets.get(nonTerminal)!.size !== oldSize) { 110 | changed = true; 111 | } 112 | } 113 | } 114 | 115 | return firstSets; 116 | } -------------------------------------------------------------------------------- /src/lib/utils/first-follow/follow.ts: -------------------------------------------------------------------------------- 1 | import type { FirstSets, FollowSet, FollowSets } from "$lib/types/first-follow"; 2 | import type { Grammar } from "$lib/types/grammar"; 3 | import { computeFirstForSequence } from "./first"; 4 | 5 | export function computeFollow(grammar: Grammar, firstSets: FirstSets): FollowSets { 6 | let followSets = new Map(); 7 | 8 | // Initialize follow sets for all non-terminals 9 | for (const nonTerminal of grammar.N) { 10 | followSets.set(nonTerminal, new Set()); 11 | } 12 | followSets.set(grammar.S, new Set(['$'])); 13 | 14 | // Map to track dependencies between follow sets 15 | const followDependencies = new Map>(); 16 | for (const nonTerminal of grammar.N) { 17 | followDependencies.set(nonTerminal, new Set()); 18 | } 19 | 20 | let changed: boolean; 21 | do { 22 | changed = false; 23 | for (const [nonTerminal, productions] of grammar.P) { 24 | for (const production of productions) { 25 | for (let i = 0; i < production.length; i++) { 26 | const A = production[i]; 27 | if (grammar.N.has(A)) { 28 | const beta = production.slice(i + 1); 29 | const followA = followSets.get(A)!; 30 | 31 | if (beta.length > 0) { 32 | const firstBeta = computeFirstForSequence(beta, grammar, firstSets); 33 | for (const symbol of firstBeta) { 34 | if (symbol !== 'ε' && !followA.has(symbol)) { 35 | followA.add(symbol); 36 | changed = true; 37 | } 38 | } 39 | if (firstBeta.has('ε')) { 40 | followDependencies.get(A)!.add(nonTerminal); 41 | } 42 | } else { 43 | followDependencies.get(A)!.add(nonTerminal); 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | // Propagation of updates based on dependencies 51 | for (const [A, dependencies] of followDependencies) { 52 | const followA = followSets.get(A)!; 53 | for (const B of dependencies) { 54 | const followB = followSets.get(B)!; 55 | for (const symbol of followB) { 56 | if (!followA.has(symbol)) { 57 | followA.add(symbol); 58 | changed = true; 59 | } 60 | } 61 | } 62 | } 63 | } while (changed); 64 | 65 | return followSets; 66 | } -------------------------------------------------------------------------------- /src/lib/utils/grammar/parse.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar, Production } from "$lib/types/grammar"; 2 | import { areProductionsEqual } from "../utils"; 3 | 4 | /** 5 | * Parses a context-free grammar from a string representation. 6 | * Expected format: 7 | * - Each line represents a production rule 8 | * - Left and right sides separated by -> 9 | * - Multiple productions for same non-terminal separated by | 10 | * - Symbols separated by whitespace 11 | * - 'epsilon' or 'ε' represents empty string 12 | * 13 | * Example: 14 | * S -> A B | C 15 | * A -> a A | epsilon 16 | * B -> b 17 | */ 18 | export function parseGrammar(input: string, startSymbol?: string): Grammar { 19 | const lines = input.split("\n").filter((line) => line.trim() !== ""); 20 | const N = new Set(); 21 | const T = new Set(); 22 | const P = new Map(); 23 | const symbolsInProductions = new Set(); 24 | 25 | // Helper function to merge productions for the same non-terminal 26 | const mergeProductions = (nonTerminal: string, newProductions: Production[]) => { 27 | const existingProductions = P.get(nonTerminal) || []; 28 | const mergedProductions = [...existingProductions]; 29 | 30 | newProductions.forEach((newProd) => { 31 | if (!mergedProductions.some(p => areProductionsEqual(p, newProd))) { 32 | mergedProductions.push(newProd); 33 | } 34 | }); 35 | 36 | P.set(nonTerminal, mergedProductions); 37 | }; 38 | 39 | // Parse the grammar 40 | for (const line of lines) { 41 | if (!line.includes("->")) { 42 | throw new Error( 43 | `Syntax error: Missing '->' in production: ${line}` 44 | ); 45 | } 46 | 47 | const [nonTerminal, productions] = line 48 | .split("->") 49 | .map((part) => part.trim()); 50 | if (!nonTerminal || !productions) { 51 | throw new Error(`Syntax error: Invalid production: ${line}`); 52 | } 53 | 54 | // Add non-terminal to the set of non-terminals 55 | N.add(nonTerminal); 56 | 57 | // Parse each production 58 | const productionRules = productions 59 | .split("|") 60 | .map((prod) => prod.trim()); 61 | const newProductions: Production[] = []; 62 | 63 | for (const prod of productionRules) { 64 | if (prod === "") { 65 | // Handle empty production (interpret as ε) 66 | console.warn( 67 | `Warning: Empty production for non-terminal '${nonTerminal}' interpreted as 'ε'.` 68 | ); 69 | newProductions.push(["ε"]); 70 | } else { 71 | // Split on one or more spaces 72 | const symbols = prod 73 | .split(/\s+/) 74 | .filter((symbol) => symbol !== "") 75 | .map(symbol => symbol === "epsilon" ? "ε" : symbol); 76 | newProductions.push(symbols); 77 | } 78 | } 79 | 80 | // Merge productions for the same non-terminal 81 | mergeProductions(nonTerminal, newProductions); 82 | 83 | // Add symbols to terminals and track symbols in production bodies 84 | newProductions.forEach((prod) => { 85 | prod.forEach((symbol) => { 86 | if (symbol !== "ε") { 87 | symbolsInProductions.add(symbol); 88 | } 89 | }); 90 | }); 91 | } 92 | 93 | // Determine terminals (symbols in production bodies that are not non-terminals) 94 | for (const symbol of symbolsInProductions) { 95 | if (!N.has(symbol)) { 96 | T.add(symbol); 97 | } 98 | } 99 | 100 | // Determine the start symbol 101 | let S: string; 102 | if (startSymbol) { 103 | // Use explicitly provided start symbol 104 | if (!N.has(startSymbol)) { 105 | throw new Error( 106 | `Start symbol '${startSymbol}' is not a non-terminal.` 107 | ); 108 | } 109 | S = startSymbol; 110 | } else { 111 | // Use the non-terminal of the first production rule 112 | S = Array.from(P.keys())[0]; 113 | } 114 | 115 | return { N, T, S, P }; 116 | } -------------------------------------------------------------------------------- /src/lib/utils/grammar/pretty_print.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar } from "$lib/types/grammar"; 2 | 3 | // Function to print the grammar in a readable format 4 | export function stringifyGrammar(grammar: Grammar): string { 5 | return `${Array.from(grammar.P.entries()).map(([driver, productions]) => { 6 | const productionsStr = productions.map(prod => prod.join(' ')).join(' | '); 7 | return `${driver} -> ${productionsStr}`; 8 | }).join('\n').trimEnd()}`; 9 | } -------------------------------------------------------------------------------- /src/lib/utils/ll1/left-factorization.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar, Production } from "$lib/types/grammar"; 2 | import { areProductionsEqual } from "../utils"; 3 | 4 | 5 | /** 6 | * Performs left factorization on a grammar to eliminate common prefixes. 7 | * For productions like: 8 | * A → αβ1 | αβ2 9 | * Creates: 10 | * A → αA' 11 | * A' → β1 | β2 12 | * 13 | * Continues until no common prefixes remain. 14 | */ 15 | export function factorizeToLeft(grammar: Grammar): Grammar { 16 | const newGrammar: Grammar = { 17 | N: new Set(grammar.N), 18 | T: new Set(grammar.T), 19 | S: grammar.S, 20 | P: new Map(grammar.P) 21 | }; 22 | 23 | const nonTerminals = Array.from(newGrammar.N); 24 | 25 | for (const nonTerminal of nonTerminals) { 26 | let helperName = `${nonTerminal}'`; 27 | let hasPrefix = true; 28 | 29 | while (hasPrefix) { 30 | hasPrefix = false; 31 | let longest: string[] = []; 32 | const productions = newGrammar.P.get(nonTerminal) || []; 33 | 34 | // Find the longest common prefix 35 | for (let i = 0; i < productions.length; i++) { 36 | for (let j = i + 1; j < productions.length; j++) { 37 | const prod1 = productions[i]; 38 | const prod2 = productions[j]; 39 | 40 | let commonLength = 0; 41 | while (commonLength < prod1.length && 42 | commonLength < prod2.length && 43 | prod1[commonLength] === prod2[commonLength]) { 44 | commonLength++; 45 | } 46 | 47 | if (commonLength > 0) { 48 | hasPrefix = true; 49 | if (commonLength > longest.length) { 50 | longest = prod1.slice(0, commonLength); 51 | } 52 | } 53 | } 54 | } 55 | 56 | if (hasPrefix) { 57 | // Ensure unique helper name 58 | while (newGrammar.P.has(helperName)) { 59 | helperName += "'"; 60 | } 61 | 62 | // Add new non-terminal to the grammar 63 | newGrammar.N.add(helperName); 64 | const newProductions: Production[] = []; 65 | const remainingProductions: Production[] = []; 66 | 67 | // Split productions 68 | for (const prod of productions) { 69 | if (prod.length >= longest.length && 70 | areProductionsEqual(prod.slice(0, longest.length), longest)) { 71 | if (prod.length === longest.length) { 72 | newProductions.push(['ε']); 73 | } else { 74 | newProductions.push(prod.slice(longest.length)); 75 | } 76 | } else { 77 | remainingProductions.push(prod); 78 | } 79 | } 80 | 81 | // Update grammar with new productions 82 | newGrammar.P.set(helperName, newProductions); 83 | newGrammar.P.set( 84 | nonTerminal, 85 | [[...longest, helperName], ...remainingProductions] 86 | ); 87 | } 88 | } 89 | } 90 | 91 | return newGrammar; 92 | } -------------------------------------------------------------------------------- /src/lib/utils/ll1/left-recursion.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar, Production } from "$lib/types/grammar"; 2 | 3 | /** 4 | * Eliminates left recursion from a grammar using the standard algorithm: 5 | * 1. First eliminates indirect left recursion by substituting productions 6 | * 2. Then eliminates direct left recursion by introducing new non-terminals 7 | * 8 | * For a production A → Aα | β, creates: 9 | * A → βA' 10 | * A' → αA' | ε 11 | */ 12 | export function eliminateLeftRecursion(grammar: Grammar): Grammar { 13 | const newGrammar: Grammar = { 14 | N: new Set(grammar.N), 15 | T: new Set(grammar.T), 16 | S: grammar.S, 17 | P: new Map(grammar.P) 18 | }; 19 | 20 | const nonTerminals = Array.from(grammar.N); 21 | 22 | // For each non-terminal Ai 23 | for (let i = 0; i < nonTerminals.length; i++) { 24 | // For each previous non-terminal Aj 25 | for (let j = 0; j < i; j++) { 26 | const extended: Production[] = []; 27 | const currentProductions = newGrammar.P.get(nonTerminals[i]) || []; 28 | const previousProductions = newGrammar.P.get(nonTerminals[j]) || []; 29 | 30 | // Replace Ai → Ajγ with Ai → δγ for all productions δ of Aj 31 | for (const production of currentProductions) { 32 | if (production.length > 0 && production[0] === nonTerminals[j]) { 33 | // Substitute with each production of Aj 34 | for (const prevProduction of previousProductions) { 35 | extended.push([...prevProduction, ...production.slice(1)]); 36 | } 37 | } else { 38 | extended.push([...production]); 39 | } 40 | } 41 | newGrammar.P.set(nonTerminals[i], extended); 42 | } 43 | 44 | // Check for direct left recursion 45 | let hasDirectRecursion = false; 46 | const currentProductions = newGrammar.P.get(nonTerminals[i]) || []; 47 | 48 | for (const production of currentProductions) { 49 | if (production.length > 0 && production[0] === nonTerminals[i]) { 50 | hasDirectRecursion = true; 51 | break; 52 | } 53 | } 54 | 55 | // Eliminate direct left recursion if found 56 | if (hasDirectRecursion) { 57 | let helperName = nonTerminals[i] + "'"; 58 | while (newGrammar.N.has(helperName)) { 59 | helperName += "'"; 60 | } 61 | 62 | const newProductions: Production[] = []; 63 | const helperProductions: Production[] = []; 64 | 65 | // Split productions and create new ones 66 | for (const production of currentProductions) { 67 | if (production.length > 0) { 68 | if (production[0] === nonTerminals[i]) { 69 | // If it starts with Ai, add to helper rule 70 | helperProductions.push([...production.slice(1), helperName]); 71 | } else { 72 | // If it doesn't start with Ai, append helper and keep 73 | if (production.length === 1 && production[0] === 'ε') { 74 | newProductions.push([helperName]); 75 | } else { 76 | newProductions.push([...production, helperName]); 77 | } 78 | } 79 | } 80 | } 81 | 82 | // Add epsilon production to helper 83 | helperProductions.push(['ε']); 84 | 85 | // Update grammar 86 | newGrammar.N.add(helperName); 87 | newGrammar.P.set(nonTerminals[i], newProductions); 88 | newGrammar.P.set(helperName, helperProductions); 89 | } 90 | } 91 | 92 | // Remove empty productions 93 | for (const [nonTerminal, productions] of newGrammar.P) { 94 | const newProductions = productions.filter((production) => production.length > 0); 95 | if (newProductions.length === 0) { 96 | newGrammar.P.delete(nonTerminal); 97 | } else { 98 | newGrammar.P.set(nonTerminal, newProductions); 99 | } 100 | } 101 | 102 | return newGrammar; 103 | } 104 | -------------------------------------------------------------------------------- /src/lib/utils/ll1/parse.ts: -------------------------------------------------------------------------------- 1 | import type { TreeNode } from "$lib/types/tree"; 2 | import type { Grammar } from "$lib/types/grammar"; 3 | import type { LL1Table } from "$lib/types/ll1"; 4 | import type { ParseResult, ParseStep } from "$lib/types/parse"; 5 | 6 | export function ll1Parsing(input: string, table: LL1Table, grammar: Grammar): ParseResult { 7 | const stack: [string, TreeNode][] = []; 8 | const trace: ParseStep[] = []; 9 | const root: TreeNode = { 10 | id: "0", 11 | symbol: grammar.S, 12 | children: [], 13 | }; 14 | 15 | stack.push([grammar.S, root]); 16 | let tokens = input.trim().split(/\s+/); 17 | tokens.push("$"); 18 | let currentPos = 0; 19 | let nodeCounter = 1; 20 | 21 | // Add initial state to trace 22 | trace.push({ 23 | stack: [grammar.S], 24 | input: [...tokens], 25 | }); 26 | 27 | try { 28 | while (stack.length > 0) { 29 | const [symbol, parentNode] = stack.pop()!; 30 | const currentToken = tokens[currentPos]; 31 | 32 | if (stack.length === 0 && symbol !== "$") { 33 | stack.push(["$", root]); 34 | } 35 | 36 | if (symbol === currentToken) { 37 | if (currentToken !== "$") { 38 | if (grammar.T.has(symbol)) { 39 | parentNode.symbol = currentToken; 40 | } 41 | currentPos++; 42 | } 43 | trace.push({ 44 | stack: stack.map(([s]) => s), 45 | input: tokens.slice(currentPos), 46 | production: `match ${symbol}` 47 | }); 48 | } else if (symbol in table && currentToken in table[symbol]) { 49 | const production = table[symbol][currentToken]; 50 | 51 | if (!production || production.size === 0) { 52 | throw new Error(`Invalid token "${currentToken}" found while parsing "${symbol}"`); 53 | } 54 | 55 | const symbols = production.get(symbol)?.[0] || []; 56 | const productionStr = `${symbol} → ${symbols.join(" ")}`; 57 | 58 | if (symbols.length === 1 && symbols[0] === "ε") { 59 | const epsilonNode: TreeNode = { 60 | id: `${nodeCounter++}`, 61 | symbol: "ε", 62 | children: [], 63 | }; 64 | parentNode.children.push(epsilonNode); 65 | } else { 66 | const newNodes: TreeNode[] = symbols.map(sym => ({ 67 | id: `${nodeCounter++}`, 68 | symbol: sym, 69 | children: [], 70 | })); 71 | 72 | parentNode.children.push(...newNodes); 73 | 74 | for (let i = symbols.length - 1; i >= 0; i--) { 75 | stack.push([symbols[i], newNodes[i]]); 76 | } 77 | } 78 | 79 | trace.push({ 80 | stack: stack.map(([s]) => s), 81 | input: tokens.slice(currentPos), 82 | production: productionStr 83 | }); 84 | } else { 85 | throw new Error( 86 | `Unable to parse: no production rule for ${symbol} with token "${currentToken}"` 87 | ); 88 | } 89 | } 90 | 91 | if (currentPos < tokens.length - 1) { 92 | throw new Error(`Invalid input: unexpected tokens "${tokens.slice(currentPos, -1).join(" ")}"`); 93 | } 94 | 95 | return { tree: root, trace, success: true }; 96 | } catch (error: any) { 97 | return { 98 | tree: null, 99 | trace, 100 | success: false, 101 | error: error.message 102 | }; 103 | } 104 | } -------------------------------------------------------------------------------- /src/lib/utils/ll1/table.ts: -------------------------------------------------------------------------------- 1 | import type { Production, Grammar } from "$lib/types/grammar"; 2 | import type { FirstSets, FollowSets } from "$lib/types/first-follow"; 3 | import type { LL1Table } from "$lib/types/ll1"; 4 | import { computeFirstForSequence } from "../first-follow/first"; 5 | import { areProductionsEqual } from "../utils"; 6 | 7 | export function computeLL1Table( 8 | grammar: Grammar, 9 | firstSets: FirstSets, 10 | followSets: FollowSets 11 | ): {table: LL1Table, notLL1: boolean} { 12 | let notLL1 = false; 13 | const table: LL1Table = {}; 14 | 15 | // Initialize the table 16 | for (const nonTerminal of grammar.N) { 17 | table[nonTerminal] = {}; 18 | for (const terminal of grammar.T) { 19 | table[nonTerminal][terminal] = new Map(); 20 | table[nonTerminal][terminal].set(nonTerminal, []); 21 | } 22 | table[nonTerminal]["$"] = new Map(); 23 | table[nonTerminal]["$"].set(nonTerminal, []); 24 | } 25 | 26 | // Process each production rule 27 | for (const [driver, productions] of grammar.P) { 28 | for (const production of productions) { 29 | const firstOfSeq = computeFirstForSequence( 30 | production, 31 | grammar, 32 | firstSets 33 | ); 34 | for (const b of firstOfSeq.difference(new Set(["ε"]))) { 35 | if (table[driver][b].get(driver)!!.length > 0) { 36 | notLL1 = true; 37 | } 38 | 39 | if (!table[driver][b]?.get(driver)!!.some(p => areProductionsEqual(p, production))) { 40 | table[driver][b]!!.get(driver)!!.push(production); 41 | } 42 | } 43 | 44 | if (firstOfSeq.has("ε")) { 45 | for (const b of followSets.get(driver)!!) { 46 | if (table[driver][b].get(driver)!!.length > 0) { 47 | notLL1 = true; 48 | } 49 | if (!table[driver][b]?.get(driver)!!.some(p => areProductionsEqual(p, production))) { 50 | table[driver][b]!!.get(driver)!!.push(production); 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | return {table, notLL1}; 58 | } -------------------------------------------------------------------------------- /src/lib/utils/sets.ts: -------------------------------------------------------------------------------- 1 | export function sortSetElements(set: Set): string[] { 2 | const result: string[] = []; 3 | const special = new Set(['ε', '$']); 4 | 5 | // First add all non-special symbols in alphabetical order 6 | [...set] 7 | .filter(s => !special.has(s)) 8 | .sort() 9 | .forEach(s => result.push(s)); 10 | 11 | // Then add epsilon if present 12 | if (set.has('ε')) { 13 | result.push('ε'); 14 | } 15 | 16 | // Finally add dollar if present 17 | if (set.has('$')) { 18 | result.push('$'); 19 | } 20 | 21 | return result; 22 | } 23 | -------------------------------------------------------------------------------- /src/lib/utils/sharing.ts: -------------------------------------------------------------------------------- 1 | export function encodeGrammar(grammar: string): string { 2 | return btoa(encodeURIComponent(grammar)); 3 | } 4 | 5 | export function decodeGrammar(encoded: string): string { 6 | try { 7 | return decodeURIComponent(atob(encoded)); 8 | } catch { 9 | return ''; 10 | } 11 | } 12 | 13 | export function createShareableLink(path: string, grammar: string): string { 14 | const encoded = encodeGrammar(grammar); 15 | return `${window.location.origin}${path}?grammar=${encoded}`; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/utils/slr/closure.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar, ProductionRule } from "$lib/types/grammar"; 2 | import type { Closure } from "$lib/types/slr"; 3 | 4 | function markProduction(prod: string[]): string[] { 5 | // Special case per epsilon produzioni 6 | if (prod.length === 1 && prod[0] === "ε") { 7 | return ["·"]; 8 | } 9 | return ["·", ...prod]; 10 | } 11 | 12 | export function closure(grammar: Grammar, items: ProductionRule): Closure { 13 | const result: Closure = { 14 | kernel: new Map(items), 15 | body: new Map(), 16 | }; 17 | 18 | const unmarked = new Map(items); 19 | 20 | while (unmarked.size > 0) { 21 | const [[head, productions]] = unmarked.entries(); 22 | unmarked.delete(head); 23 | 24 | for (const body of productions) { 25 | const markerIndex = body.findIndex((symbol) => symbol === "·"); 26 | if (markerIndex === body.length - 1) continue; 27 | 28 | const nextSymbol = body[markerIndex + 1]; 29 | 30 | if (grammar.N.has(nextSymbol)) { 31 | const nextProductions = grammar.P.get(nextSymbol); 32 | if (!nextProductions) continue; 33 | 34 | const markedProductions = nextProductions.map(markProduction); 35 | 36 | // Get existing productions from both unmarked and body 37 | const existingUnmarked = unmarked.get(nextSymbol) || []; 38 | const existingBody = result.body.get(nextSymbol) || []; 39 | const allExisting = [...existingUnmarked, ...existingBody]; 40 | 41 | // Filter out only truly new productions 42 | const newProductions = markedProductions.filter(newProd => { 43 | return !allExisting.some(existingProd => 44 | newProd.length === existingProd.length && 45 | newProd.every((symbol, i) => symbol === existingProd[i]) 46 | ); 47 | }); 48 | 49 | if (newProductions.length > 0) { 50 | // Add to body 51 | result.body.set(nextSymbol, [ 52 | ...existingBody, 53 | ...newProductions 54 | ]); 55 | 56 | // Merge with existing unmarked productions 57 | unmarked.set(nextSymbol, [ 58 | ...existingUnmarked, 59 | ...newProductions 60 | ]); 61 | } 62 | } 63 | } 64 | } 65 | 66 | return result; 67 | } -------------------------------------------------------------------------------- /src/lib/utils/slr/states.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar, Production, ProductionRule } from "$lib/types/grammar"; 2 | import type { 3 | AutomatonBuildResult, 4 | AutomatonStep, 5 | Closure, 6 | ReducingLabel, 7 | StatesAutomaton, 8 | } from "../../types/slr"; 9 | import { closure } from "./closure"; 10 | 11 | function moveMarker(production: Production): Production | null { 12 | // Non muovere il marker per produzioni epsilon (che hanno solo il marker) 13 | if (production.length === 1 && production[0] === "·") return null; 14 | 15 | const markerIndex = production.findIndex((symbol) => symbol === "·"); 16 | if (markerIndex === production.length - 1) return null; 17 | 18 | const result = [...production]; 19 | result[markerIndex] = result[markerIndex + 1]; 20 | result[markerIndex + 1] = "·"; 21 | return result; 22 | } 23 | 24 | function computeKernel(state: Closure, symbol: string): ProductionRule { 25 | const kernel = new Map(); 26 | 27 | // Check kernel items 28 | for (const [head, productions] of state.kernel.entries()) { 29 | for (const prod of productions) { 30 | const markerIndex = prod.findIndex((s) => s === "·"); 31 | if ( 32 | markerIndex < prod.length - 1 && 33 | prod[markerIndex + 1] === symbol 34 | ) { 35 | const moved = moveMarker(prod); 36 | if (moved) { 37 | if (!kernel.has(head)) kernel.set(head, []); 38 | kernel.get(head)!.push(moved); 39 | } 40 | } 41 | } 42 | } 43 | 44 | // Check body items 45 | for (const [head, productions] of state.body.entries()) { 46 | for (const prod of productions) { 47 | const markerIndex = prod.findIndex((s) => s === "·"); 48 | if ( 49 | markerIndex < prod.length - 1 && 50 | prod[markerIndex + 1] === symbol 51 | ) { 52 | const moved = moveMarker(prod); 53 | if (moved) { 54 | if (!kernel.has(head)) kernel.set(head, []); 55 | kernel.get(head)!.push(moved); 56 | } 57 | } 58 | } 59 | } 60 | 61 | return kernel; 62 | } 63 | 64 | function kernelsEqual(k1: ProductionRule, k2: ProductionRule): boolean { 65 | if (k1.size !== k2.size) return false; 66 | 67 | for (const [head, prods1] of k1.entries()) { 68 | const prods2 = k2.get(head); 69 | if (!prods2) return false; 70 | if (prods1.length !== prods2.length) return false; 71 | if ( 72 | !prods1.every((p1) => 73 | prods2.some( 74 | (p2) => 75 | p1.length === p2.length && 76 | p1.every((s, i) => s === p2[i]) 77 | ) 78 | ) 79 | ) 80 | return false; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | function findReducingItems( 87 | closure: Closure 88 | ): { head: string; body: string[] }[] { 89 | const reducingItems: { head: string; body: string[] }[] = []; 90 | 91 | function processProductions(head: string, productions: Production[]) { 92 | for (const prod of productions) { 93 | // Salta SOLO l'item di accept (S' -> S•) 94 | if ( 95 | head === "S'" && 96 | prod.length === 2 && 97 | prod[0] === "S" && 98 | prod[1] === "·" 99 | ) { 100 | continue; 101 | } 102 | 103 | // Processa normalmente tutti gli altri items 104 | if (prod.length === 1 && prod[0] === "·") { 105 | reducingItems.push({ head, body: ["ε"] }); 106 | } else if (prod[prod.length - 1] === "·") { 107 | const body = prod.filter((s) => s !== "·"); 108 | reducingItems.push({ head, body }); 109 | } 110 | } 111 | } 112 | 113 | // Process both kernel and body items 114 | for (const [head, productions] of closure.kernel.entries()) { 115 | processProductions(head, productions); 116 | } 117 | for (const [head, productions] of closure.body.entries()) { 118 | processProductions(head, productions); 119 | } 120 | 121 | return reducingItems; 122 | } 123 | 124 | function isRuleEqual( 125 | r1: { head: string; body: string[] }, 126 | r2: { head: string; body: string[] } 127 | ): boolean { 128 | return ( 129 | r1.head === r2.head && 130 | r1.body.length === r2.body.length && 131 | r1.body.every((s, i) => s === r2.body[i]) 132 | ); 133 | } 134 | 135 | export function buildSlrAutomaton(grammar: Grammar): AutomatonBuildResult { 136 | const automaton: StatesAutomaton = { 137 | states: {}, 138 | transitions: {}, 139 | }; 140 | 141 | const steps: AutomatonStep[] = []; 142 | const reducingLabels: ReducingLabel[] = []; 143 | let reduceCounter = 1; 144 | 145 | // Create initial state with S' → ·S 146 | const initialKernel = new Map([ 147 | [grammar.S, [["·", ...grammar.P.get(grammar.S)![0]]]], 148 | ]); 149 | const initialState = closure(grammar, initialKernel); 150 | automaton.states["0"] = initialState; 151 | 152 | // Check for reducing items in initial state 153 | const initialReducingItems = findReducingItems(initialState); 154 | for (const item of initialReducingItems) { 155 | if (!reducingLabels.some((rl) => isRuleEqual(rl.rule, item))) { 156 | reducingLabels.push({ 157 | rule: item, 158 | label: `r${reduceCounter++}`, 159 | }); 160 | } 161 | } 162 | 163 | // Record initial step 164 | steps.push({ 165 | stateId: "0", 166 | closure: initialState, 167 | }); 168 | 169 | const unmarked = new Set(["0"]); 170 | let stateCounter = 1; 171 | 172 | while (unmarked.size > 0) { 173 | const stateId = unmarked.values().next().value; 174 | unmarked.delete(stateId!!); 175 | const state = automaton.states[stateId!!]; 176 | 177 | automaton.transitions[stateId!!] = {}; 178 | 179 | // Get all symbols after markers 180 | const symbols = new Set(); 181 | for (const [, prods] of state.kernel.entries()) { 182 | for (const prod of prods) { 183 | const markerIndex = prod.findIndex((s) => s === "·"); 184 | if (markerIndex < prod.length - 1) { 185 | symbols.add(prod[markerIndex + 1]); 186 | } 187 | } 188 | } 189 | for (const [, prods] of state.body.entries()) { 190 | for (const prod of prods) { 191 | const markerIndex = prod.findIndex((s) => s === "·"); 192 | if (markerIndex < prod.length - 1) { 193 | symbols.add(prod[markerIndex + 1]); 194 | } 195 | } 196 | } 197 | 198 | console.log("State", stateId, "Symbols", symbols); 199 | 200 | for (const symbol of symbols) { 201 | const targetKernel = computeKernel(state, symbol); 202 | 203 | let targetStateId = Object.entries(automaton.states).find(([, s]) => 204 | kernelsEqual(s.kernel, targetKernel) 205 | )?.[0]; 206 | 207 | if (!targetStateId) { 208 | targetStateId = stateCounter.toString(); 209 | stateCounter++; 210 | 211 | console.log("New state", targetStateId, targetKernel); 212 | 213 | const targetClosure = closure(grammar, targetKernel); 214 | 215 | console.log("New closure", targetClosure); 216 | 217 | automaton.states[targetStateId] = targetClosure; 218 | unmarked.add(targetStateId); 219 | 220 | // Check for new reducing items 221 | const reducingItems = findReducingItems(targetClosure); 222 | for (const item of reducingItems) { 223 | if ( 224 | !reducingLabels.some((rl) => isRuleEqual(rl.rule, item)) 225 | ) { 226 | reducingLabels.push({ 227 | rule: item, 228 | label: `r${reduceCounter++}`, 229 | }); 230 | } 231 | } 232 | 233 | // Record step for new state 234 | steps.push({ 235 | stateId: targetStateId, 236 | symbol: symbol, 237 | fromStateId: stateId!!, 238 | closure: targetClosure, 239 | }); 240 | } else { 241 | // Record step even when we find an existing state 242 | steps.push({ 243 | stateId: targetStateId, 244 | symbol: symbol, 245 | fromStateId: stateId!!, 246 | kernel: targetKernel, // Instead of full closure, we just show the kernel 247 | isExistingState: true, 248 | }); 249 | } 250 | 251 | automaton.transitions[stateId!!][symbol] = targetStateId; 252 | } 253 | } 254 | 255 | return { automaton, steps, reducingLabels }; 256 | } 257 | -------------------------------------------------------------------------------- /src/lib/utils/slr/table.ts: -------------------------------------------------------------------------------- 1 | import type { Grammar } from "$lib/types/grammar"; 2 | import type { FollowSets } from "$lib/types/first-follow"; 3 | import type { ReducingLabel, SLRMove, SLRTable, StatesAutomaton } from "$lib/types/slr"; 4 | import { isRuleEqual } from "../utils"; 5 | 6 | export function computeSlrTable( 7 | automaton: StatesAutomaton, 8 | reducingLabels: ReducingLabel[], 9 | grammar: Grammar, 10 | followSets: FollowSets 11 | ): { table: SLRTable; hasConflicts: boolean } { 12 | const table: SLRTable = {}; 13 | let hasConflicts = false; 14 | 15 | // Initialize table cells 16 | for (const stateId of Object.keys(automaton.states)) { 17 | table[stateId] = {}; 18 | // Initialize terminals + $ 19 | for (const terminal of grammar.T) { 20 | table[stateId][terminal] = "error"; 21 | } 22 | table[stateId]["$"] = "error"; 23 | // Initialize non-terminals 24 | for (const nonTerminal of grammar.N) { 25 | table[stateId][nonTerminal] = "error"; 26 | } 27 | } 28 | 29 | // Process transitions for shifts and gotos 30 | for (const [stateId, transitions] of Object.entries(automaton.transitions)) { 31 | for (const [symbol, targetState] of Object.entries(transitions)) { 32 | if (grammar.T.has(symbol)) { 33 | // Shift for terminals 34 | const move: SLRMove = { action: "shift", state: parseInt(targetState) }; 35 | if (table[stateId][symbol] !== "error") { 36 | // Conflict detected 37 | hasConflicts = true; 38 | table[stateId][symbol] = mergeMove(table[stateId][symbol], move); 39 | } else { 40 | table[stateId][symbol] = move; 41 | } 42 | } else { 43 | // Goto for non-terminals 44 | table[stateId][symbol] = parseInt(targetState); 45 | } 46 | } 47 | } 48 | 49 | // Process reducing items 50 | for (const [stateId, state] of Object.entries(automaton.states)) { 51 | // Check for accepting state 52 | const hasAcceptItem = Array.from(state.kernel.entries()).some( 53 | ([nt, prods]) => nt === "S'" && prods.some(p => p[p.length - 1] === "·") 54 | ); 55 | 56 | // Find reducing items in this state 57 | const reducingItems = findReducingItems(state); 58 | 59 | for (const item of reducingItems) { 60 | const label = reducingLabels.find(rl => isRuleEqual(rl.rule, item)); 61 | if (!label) continue; 62 | 63 | const follow = followSets.get(item.head); 64 | if (!follow) continue; 65 | 66 | const reduceMove: SLRMove = { 67 | action: "reduce", 68 | rule: new Map([[item.head, [item.body]]]) 69 | }; 70 | 71 | for (const symbol of follow) { 72 | if (hasAcceptItem && symbol === "$") { 73 | // Se siamo nello stato di accettazione e il simbolo è $, 74 | // dobbiamo creare un conflitto con accept 75 | table[stateId][symbol] = mergeMove("accept", reduceMove); 76 | } else if (table[stateId][symbol] !== "error") { 77 | hasConflicts = true; 78 | table[stateId][symbol] = mergeMove(table[stateId][symbol], reduceMove); 79 | } else { 80 | table[stateId][symbol] = reduceMove; 81 | } 82 | } 83 | } 84 | } 85 | 86 | return { table, hasConflicts }; 87 | } 88 | 89 | // Nel mergeMove, modifichiamo per gestire correttamente accept con reduce: 90 | function mergeMove(existing: SLRMove, newMove: SLRMove): SLRMove { 91 | // Se uno dei due è error, ritorna l'altro 92 | if (existing === "error") return newMove; 93 | if (newMove === "error") return existing; 94 | 95 | // Se già abbiamo un conflitto, aggiungiamo il nuovo move 96 | if (typeof existing === "object" && "action" in existing && existing.action === "conflict") { 97 | return { 98 | action: "conflict", 99 | moves: [...existing.moves, newMove].sort((a, b) => { 100 | if (a === "accept") return -1; 101 | if (b === "accept") return 1; 102 | if (typeof a === "number" || typeof b === "number") return 0; 103 | if (typeof a === "string") return 1; 104 | if (typeof b === "string") return -1; 105 | if (a.action === "shift" && b.action === "reduce") return -1; 106 | if (a.action === "reduce" && b.action === "shift") return 1; 107 | return 0; 108 | }) 109 | }; 110 | } 111 | 112 | // Altrimenti creiamo un nuovo conflitto 113 | return { 114 | action: "conflict", 115 | moves: [existing, newMove].sort((a, b) => { 116 | if (a === "accept") return -1; 117 | if (b === "accept") return 1; 118 | if (typeof a === "number" || typeof b === "number") return 0; 119 | if (a.action === "shift" && b.action === "reduce") return -1; 120 | if (a.action === "reduce" && b.action === "shift") return 1; 121 | return 0; 122 | }) 123 | }; 124 | } 125 | 126 | function findReducingItems(state: { kernel: Map, body: Map }) { 127 | const items: { head: string; body: string[] }[] = []; 128 | 129 | function processProductions(head: string, productions: string[][]) { 130 | for (const prod of productions) { 131 | if (prod.length === 1 && prod[0] === "·") { 132 | // Epsilon case 133 | items.push({ head, body: ["ε"] }); 134 | } else if (prod[prod.length - 1] === "·") { 135 | // Normal reduction 136 | items.push({ head, body: prod.filter(s => s !== "·") }); 137 | } 138 | } 139 | } 140 | 141 | for (const [head, prods] of state.kernel.entries()) { 142 | processProductions(head, prods); 143 | } 144 | for (const [head, prods] of state.body.entries()) { 145 | processProductions(head, prods); 146 | } 147 | 148 | return items; 149 | } 150 | -------------------------------------------------------------------------------- /src/lib/utils/treeGenerator.ts: -------------------------------------------------------------------------------- 1 | import type { TreeNode } from "$lib/types/tree"; 2 | import type { Node, Edge } from "@xyflow/svelte"; 3 | 4 | export function generateTreeLayout(root: TreeNode | null, levelHeight: number = 100, nodeWidth: number = 100): TreeNode { 5 | if (!root) { 6 | return { 7 | id: 'none', 8 | symbol: '', 9 | children: [], 10 | x: 0, 11 | y: 0 12 | }; 13 | } 14 | 15 | let currentX = 0; 16 | 17 | function calculatePositions(node: TreeNode, level: number): number { 18 | if (node.children.length === 0) { 19 | node.x = currentX; 20 | node.y = level * levelHeight; 21 | currentX += nodeWidth; 22 | return node.x; 23 | } 24 | 25 | const childrenXs = node.children.map(child => calculatePositions(child, level + 1)); 26 | node.x = (childrenXs[0] + childrenXs[childrenXs.length - 1]) / 2; 27 | node.y = level * levelHeight; 28 | return node.x; 29 | } 30 | 31 | calculatePositions(root, 0); 32 | return root; 33 | } 34 | 35 | export function convertToFlowNodes(tree: TreeNode): { nodes: Node[], edges: Edge[] } { 36 | const nodes: Node[] = []; 37 | const edges: Edge[] = []; 38 | let idCounter = 0; 39 | 40 | function traverse(node: TreeNode, parentId?: string) { 41 | const currentId = `node_${idCounter++}`; 42 | nodes.push({ 43 | id: currentId, 44 | type: 'simple', 45 | data: { label: node.symbol }, 46 | position: { x: node.x!, y: node.y! }, 47 | }); 48 | 49 | if (parentId) { 50 | edges.push({ 51 | id: `edge_${parentId}_${currentId}`, 52 | type: 'straight', 53 | source: parentId, 54 | target: currentId, 55 | }); 56 | } 57 | 58 | node.children.forEach(child => traverse(child, currentId)); 59 | } 60 | 61 | traverse(tree); 62 | return { nodes, edges }; 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Production } from "$lib/types/grammar"; 2 | import type { ReducingLabel } from "$lib/types/slr"; 3 | 4 | export function areProductionsEqual(p1: Production, p2: Production): boolean { 5 | return ( 6 | p1.length === p2.length && 7 | p1.every((symbol, index) => symbol === p2[index]) 8 | ); 9 | } 10 | 11 | export interface Rule { 12 | head: string; 13 | body: string[]; 14 | } 15 | 16 | export function isRuleEqual(p1: Rule, p2: Rule): boolean { 17 | if (p1.head !== p2.head) return false; 18 | 19 | // Se uno dei due è epsilon, devono essere entrambi epsilon 20 | const isP1Epsilon = p1.body.length === 1 && p1.body[0] === 'ε'; 21 | const isP2Epsilon = p2.body.length === 1 && p2.body[0] === 'ε'; 22 | 23 | if (isP1Epsilon || isP2Epsilon) { 24 | return isP1Epsilon && isP2Epsilon; 25 | } 26 | 27 | return p1.body.length === p2.body.length && 28 | p1.body.every((s, i) => s === p2.body[i]); 29 | } 30 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | Error {page.status} - LFC Playground 22 | 23 | 24 |
25 |

Oops! 🤔

26 | 27 |

28 | {#if page.status === 404} 29 | Looks like you got lost in the grammar maze!
30 | This page doesn't exist, but don't worry - it happens to the best parsers. 🚀 31 | {:else} 32 | Houston, we have a problem! 33 | Something went wrong, but our finite state automata are working on it. 🛠️ 34 | {/if} 35 |

36 | 37 |

38 | Error {page.status} 39 |

40 | 41 |
42 | 46 | 50 |
51 |
52 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 |
12 | 13 |
14 |
15 | {@render children?.()} 16 |
17 |
18 |
19 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import { dev } from "$app/environment"; 2 | import { injectAnalytics } from "@vercel/analytics/sveltekit"; 3 | 4 | injectAnalytics({ mode: dev ? 'development' : 'production' }); -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | 43 | LFC Playground 44 | 45 | 46 |
49 |

Welcome to LFC Playground

50 |

51 | A collection of tools for Formal Languages and Compilators course 52 |

53 | 54 | 70 |
71 | -------------------------------------------------------------------------------- /src/routes/cnf/+page.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 | 55 | CNF - LFC Playground 56 | 57 | 58 | 59 | 60 | {#snippet InputSection()} 61 | 66 | {/snippet} 67 | 68 | {#snippet OutputSection()} 69 | {#if showOutput && transformedGrammar} 70 |
71 |
72 | Resulting Grammar 73 |
74 |
75 |

78 | {transformedGrammar} 79 |

80 |
81 |
82 | {/if} 83 | {/snippet} 84 | 85 | 91 | -------------------------------------------------------------------------------- /src/routes/first-follow/+page.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 | 60 | First & Follow - LFC Playground 61 | 62 | 63 | 64 | 65 | {#snippet InputSection()} 66 | 0} 70 | /> 71 | {/snippet} 72 | 73 | {#snippet OutputSection()} 74 | 75 | {/snippet} 76 | 77 | 83 | -------------------------------------------------------------------------------- /src/routes/left-fact/+page.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 | 55 | Left Factoring - LFC Playground 56 | 57 | 58 | 59 | 60 | {#snippet InputSection()} 61 | 66 | {/snippet} 67 | 68 | {#snippet OutputSection()} 69 | {#if showOutput && transformedGrammar} 70 |
71 |
72 | Resulting Grammar 73 |
74 |
75 |

78 | {transformedGrammar} 79 |

80 |
81 |
82 | {/if} 83 | {/snippet} 84 | 85 | 91 | -------------------------------------------------------------------------------- /src/routes/left-rec/+page.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 | 55 | Left Recursion - LFC Playground 56 | 57 | 58 | 59 | 60 | {#snippet InputSection()} 61 | 66 | {/snippet} 67 | 68 | {#snippet OutputSection()} 69 | {#if showOutput && transformedGrammar} 70 |
71 |
72 | Resulting Grammar 73 |
74 |
75 |

78 | {transformedGrammar} 79 |

80 |
81 |
82 | {/if} 83 | {/snippet} 84 | 85 | 91 | -------------------------------------------------------------------------------- /src/routes/ll1-table/+page.svelte: -------------------------------------------------------------------------------- 1 | 95 | 96 | 97 | LL(1) Parsing Table - LFC Playground 98 | 99 | 100 | 101 | 102 | {#snippet InputSection()} 103 | 0} 107 | /> 108 | {#if ll1Result.notLL1} 109 |

110 | This grammar is not LL(1)
Multiple productions found for some 111 | entries 112 |

113 | {/if} 114 | {/snippet} 115 | 116 | {#snippet OutputSection()} 117 |
118 | 119 | 120 |
121 | {/snippet} 122 | 123 |
124 | 130 | 131 | {#if firstSets.size > 0 && !ll1Result.notLL1} 132 |
133 |
134 | 140 |
141 | 142 | 145 |
146 |
147 | 148 | {#if showParseTree} 149 |
150 |
151 | 152 |
153 | {#if showParseTrace} 154 |
155 | 156 |
157 | {/if} 158 |
159 | {/if} 160 |
161 | {/if} 162 |
163 | -------------------------------------------------------------------------------- /src/routes/slr-table/+page.svelte: -------------------------------------------------------------------------------- 1 | 126 | 127 | 128 | SLR Parsing Table - LFC Playground 129 | 130 | 131 | 132 | 133 | {#snippet InputSection()} 134 | 0} 138 | > 139 |
140 |
141 | 145 | 152 | 162 |
163 |
164 |
165 | {/snippet} 166 | 167 | {#snippet OutputSection()} 168 |
169 | {#if showSteps} 170 | 171 | {/if} 172 | {#if showAutomaton && automaton !== null && !showSteps} 173 |
176 | 177 |
178 | {/if} 179 | 180 | 181 |
182 | {/snippet} 183 | 184 |
185 | 191 | 192 |
193 | {#if automaton !== null} 194 | {#if showAutomaton && showSteps} 195 |
198 | 199 |
200 | {/if} 201 | 202 | {#if showFirstFollow || (showTable && slrResult)} 203 |
204 | {#if showFirstFollow} 205 | 206 | {/if} 207 | 208 | {#if showTable && slrResult} 209 | 215 | {/if} 216 |
217 | {/if} 218 | {/if} 219 |
220 | 221 | 244 |
245 | -------------------------------------------------------------------------------- /static/favicon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 12 | 17 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /static/favicon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-vercel"; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://svelte.dev/docs/kit/integrations 7 | // for more information about preprocessors 8 | preprocess: vitePreprocess(), 9 | 10 | kit: { 11 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. 12 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 13 | // See https://svelte.dev/docs/kit/adapters for more information about adapters. 14 | adapter: adapter() 15 | } 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { fontFamily } from "tailwindcss/defaultTheme"; 2 | import type { Config } from "tailwindcss"; 3 | import tailwindcssAnimate from "tailwindcss-animate"; 4 | 5 | const config: Config = { 6 | darkMode: ["class"], 7 | content: ["./src/**/*.{html,js,svelte,ts}"], 8 | safelist: ["dark"], 9 | theme: { 10 | container: { 11 | center: true, 12 | padding: "2rem", 13 | screens: { 14 | "2xl": "1400px" 15 | } 16 | }, 17 | extend: { 18 | colors: { 19 | border: "hsl(var(--border) / )", 20 | input: "hsl(var(--input) / )", 21 | ring: "hsl(var(--ring) / )", 22 | background: "hsl(var(--background) / )", 23 | foreground: "hsl(var(--foreground) / )", 24 | primary: { 25 | DEFAULT: "hsl(var(--primary) / )", 26 | foreground: "hsl(var(--primary-foreground) / )" 27 | }, 28 | secondary: { 29 | DEFAULT: "hsl(var(--secondary) / )", 30 | foreground: "hsl(var(--secondary-foreground) / )" 31 | }, 32 | destructive: { 33 | DEFAULT: "hsl(var(--destructive) / )", 34 | foreground: "hsl(var(--destructive-foreground) / )" 35 | }, 36 | muted: { 37 | DEFAULT: "hsl(var(--muted) / )", 38 | foreground: "hsl(var(--muted-foreground) / )" 39 | }, 40 | accent: { 41 | DEFAULT: "hsl(var(--accent) / )", 42 | foreground: "hsl(var(--accent-foreground) / )" 43 | }, 44 | popover: { 45 | DEFAULT: "hsl(var(--popover) / )", 46 | foreground: "hsl(var(--popover-foreground) / )" 47 | }, 48 | card: { 49 | DEFAULT: "hsl(var(--card) / )", 50 | foreground: "hsl(var(--card-foreground) / )" 51 | }, 52 | sidebar: { 53 | DEFAULT: "hsl(var(--sidebar-background))", 54 | foreground: "hsl(var(--sidebar-foreground))", 55 | primary: "hsl(var(--sidebar-primary))", 56 | "primary-foreground": "hsl(var(--sidebar-primary-foreground))", 57 | accent: "hsl(var(--sidebar-accent))", 58 | "accent-foreground": "hsl(var(--sidebar-accent-foreground))", 59 | border: "hsl(var(--sidebar-border))", 60 | ring: "hsl(var(--sidebar-ring))", 61 | }, 62 | }, 63 | borderRadius: { 64 | xl: "calc(var(--radius) + 4px)", 65 | lg: "var(--radius)", 66 | md: "calc(var(--radius) - 2px)", 67 | sm: "calc(var(--radius) - 4px)" 68 | }, 69 | fontFamily: { 70 | sans: [...fontFamily.sans] 71 | }, 72 | keyframes: { 73 | "accordion-down": { 74 | from: { height: "0" }, 75 | to: { height: "var(--bits-accordion-content-height)" }, 76 | }, 77 | "accordion-up": { 78 | from: { height: "var(--bits-accordion-content-height)" }, 79 | to: { height: "0" }, 80 | }, 81 | "caret-blink": { 82 | "0%,70%,100%": { opacity: "1" }, 83 | "20%,50%": { opacity: "0" }, 84 | }, 85 | }, 86 | animation: { 87 | "accordion-down": "accordion-down 0.2s ease-out", 88 | "accordion-up": "accordion-up 0.2s ease-out", 89 | "caret-blink": "caret-blink 1.25s ease-out infinite", 90 | }, 91 | }, 92 | }, 93 | plugins: [tailwindcssAnimate], 94 | }; 95 | 96 | export default config; 97 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()] 6 | }); 7 | --------------------------------------------------------------------------------