19 |
--------------------------------------------------------------------------------
/src/lib/shadcn-ui/components/ui/textarea/index.ts:
--------------------------------------------------------------------------------
1 | import Root from "./textarea.svelte";
2 |
3 | type FormTextareaEvent = T & {
4 | currentTarget: EventTarget & HTMLTextAreaElement;
5 | };
6 |
7 | type TextareaEvents = {
8 | blur: FormTextareaEvent;
9 | change: FormTextareaEvent;
10 | click: FormTextareaEvent;
11 | focus: FormTextareaEvent;
12 | keydown: FormTextareaEvent;
13 | keypress: FormTextareaEvent;
14 | keyup: FormTextareaEvent;
15 | mouseover: FormTextareaEvent;
16 | mouseenter: FormTextareaEvent;
17 | mouseleave: FormTextareaEvent;
18 | paste: FormTextareaEvent;
19 | input: FormTextareaEvent;
20 | };
21 |
22 | export {
23 | Root,
24 | //
25 | Root as Textarea,
26 | type TextareaEvents,
27 | type FormTextareaEvent,
28 | };
29 |
--------------------------------------------------------------------------------
/src/lib/shadcn-ui/components/ui/textarea/textarea.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
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 |