boolean-algebra-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cli/main.py ADDED
@@ -0,0 +1,411 @@
1
+ """
2
+ cli/main.py — boolcalc command-line interface.
3
+
4
+ Usage:
5
+ boolcalc [OPTIONS] EXPRESSION one-shot mode
6
+ boolcalc interactive REPL mode
7
+
8
+ Examples:
9
+ boolcalc "A+B"
10
+ boolcalc "A.(B+C)" --format json
11
+ boolcalc "A.!A" --satisfiable
12
+ boolcalc "A+B.C" --synthesize --metrics
13
+ echo "A^B" | boolcalc --format minimal
14
+ """
15
+ from __future__ import annotations
16
+ import json
17
+ import shlex
18
+ import sys
19
+ from enum import Enum
20
+ from typing import Optional
21
+
22
+ import typer
23
+ from rich.console import Console
24
+ from rich.table import Table
25
+ from rich import box
26
+
27
+ from core.evaluator import evaluate
28
+ from core.synthesizer import synthesize
29
+
30
+ app = typer.Typer(
31
+ name="boolcalc",
32
+ help="Boolean algebra engine — evaluate expressions and synthesize minimal forms.",
33
+ add_completion=False,
34
+ )
35
+ console = Console()
36
+ err_console = Console(stderr=True)
37
+
38
+
39
+ class Format(str, Enum):
40
+ table = "table"
41
+ json = "json"
42
+ csv = "csv"
43
+ minimal = "minimal"
44
+
45
+
46
+ def _read_stdin() -> str | None:
47
+ if not sys.stdin.isatty():
48
+ return sys.stdin.read().strip()
49
+ return None
50
+
51
+
52
+ @app.command()
53
+ def main(
54
+ expression: Optional[str] = typer.Argument(
55
+ None,
56
+ help="Boolean expression. Variables: A–Z. Operators: ! . ^ +"
57
+ ),
58
+ format: Format = typer.Option(
59
+ Format.table,
60
+ "--format", "-f",
61
+ help="Output format.",
62
+ ),
63
+ output: Optional[str] = typer.Option(
64
+ None,
65
+ "--output", "-o",
66
+ help="Write output to file instead of stdout.",
67
+ ),
68
+ synthesize_flag: bool = typer.Option(
69
+ False,
70
+ "--synthesize", "-s",
71
+ help="Print the minimal equivalent expression.",
72
+ ),
73
+ satisfiable: bool = typer.Option(
74
+ False,
75
+ "--satisfiable",
76
+ help="Exit 0 if satisfiable, 1 if not. No truth table printed.",
77
+ ),
78
+ tautology: bool = typer.Option(
79
+ False,
80
+ "--tautology",
81
+ help="Exit 0 if tautology, 1 if not. No truth table printed.",
82
+ ),
83
+ minterms: bool = typer.Option(
84
+ False,
85
+ "--minterms",
86
+ help="Print minterm indices (rows where output = 1).",
87
+ ),
88
+ maxterms: bool = typer.Option(
89
+ False,
90
+ "--maxterms",
91
+ help="Print maxterm indices (rows where output = 0).",
92
+ ),
93
+ metrics: bool = typer.Option(
94
+ False,
95
+ "--metrics",
96
+ help="Print performance metrics (time and memory).",
97
+ ),
98
+ interactive: bool = typer.Option(
99
+ False,
100
+ "--interactive", "-i",
101
+ help="Launch interactive REPL mode.",
102
+ ),
103
+ ):
104
+ # Launch REPL if requested
105
+ if interactive:
106
+ _repl()
107
+ return
108
+
109
+ # Accept expression from stdin if not passed as argument
110
+ expr = expression or _read_stdin()
111
+ if not expr:
112
+ err_console.print("[red]Error:[/red] No expression provided. Pass as argument or via stdin.")
113
+ raise typer.Exit(1)
114
+
115
+ # Evaluate
116
+ try:
117
+ table, eval_metrics = evaluate(expr)
118
+ except ValueError as e:
119
+ err_console.print(f"[red]Error:[/red] {e}")
120
+ raise typer.Exit(1)
121
+
122
+ # --- Query flags (no truth table output) ---
123
+ if satisfiable:
124
+ raise typer.Exit(0 if table.satisfiable else 1)
125
+
126
+ if tautology:
127
+ raise typer.Exit(0 if table.tautology else 1)
128
+
129
+ # --- Synthesize ---
130
+ synth_expr = None
131
+ synth_metrics = None
132
+ if synthesize_flag:
133
+ synth_expr, synth_metrics = synthesize(table)
134
+
135
+ # --- Build output ---
136
+ out = _build_output(table, eval_metrics, synth_expr, synth_metrics,
137
+ format, minterms, maxterms, metrics)
138
+
139
+ # --- Write or print ---
140
+ if output:
141
+ with open(output, 'w') as f:
142
+ f.write(out if isinstance(out, str) else '\n'.join(out))
143
+ console.print(f"[green]Written to {output}[/green]")
144
+ else:
145
+ if format == Format.table:
146
+ _print_rich_table(table, eval_metrics, synth_expr, synth_metrics,
147
+ minterms, maxterms, metrics)
148
+ else:
149
+ print(out)
150
+
151
+
152
+ def _build_output(table, eval_metrics, synth_expr, synth_metrics,
153
+ format, show_minterms, show_maxterms, show_metrics) -> str:
154
+ if format == Format.json:
155
+ data = {
156
+ "expression": table.expression,
157
+ "variables": table.variables,
158
+ "rows": [
159
+ {**row.inputs, "output": row.output}
160
+ for row in table.rows
161
+ ],
162
+ "satisfiable": table.satisfiable,
163
+ "tautology": table.tautology,
164
+ "minterms": table.minterms,
165
+ "maxterms": table.maxterms,
166
+ }
167
+ if synth_expr is not None:
168
+ data["minimal_expression"] = synth_expr
169
+ if show_metrics:
170
+ data["metrics"] = {
171
+ "eval_time_ms": eval_metrics.eval_time_ms,
172
+ "peak_memory_bytes": eval_metrics.peak_memory_bytes,
173
+ "rows_evaluated": eval_metrics.rows_evaluated,
174
+ }
175
+ if synth_metrics:
176
+ data["metrics"]["synth_time_ms"] = synth_metrics.synth_time_ms
177
+ data["metrics"]["synth_peak_memory_bytes"] = synth_metrics.peak_memory_bytes
178
+ data["metrics"]["prime_implicant_count"] = synth_metrics.prime_implicant_count
179
+ return json.dumps(data, indent=2)
180
+
181
+ if format == Format.csv:
182
+ lines = [",".join(table.variables + ["output"])]
183
+ for row in table.rows:
184
+ lines.append(",".join(str(row.inputs[v]) for v in table.variables) + f",{row.output}")
185
+ return "\n".join(lines)
186
+
187
+ if format == Format.minimal:
188
+ return "\n".join(str(row.output) for row in table.rows)
189
+
190
+ return ""
191
+
192
+
193
+ def _print_rich_table(table, eval_metrics, synth_expr, synth_metrics,
194
+ show_minterms, show_maxterms, show_metrics):
195
+ t = Table(box=box.SIMPLE_HEAVY, show_header=True, header_style="bold cyan")
196
+
197
+ for var in table.variables:
198
+ t.add_column(var, justify="center", style="dim")
199
+ t.add_column(table.expression, justify="center", style="bold")
200
+
201
+ for row in table.rows:
202
+ values = [str(row.inputs[v]) for v in table.variables]
203
+ output = "[green]1[/green]" if row.output == 1 else "[red]0[/red]"
204
+ t.add_row(*values, output)
205
+
206
+ console.print(t)
207
+ console.print(f" Variables : [cyan]{', '.join(table.variables)}[/cyan]")
208
+ console.print(f" Rows : {eval_metrics.rows_evaluated}")
209
+ console.print(f" Satisfiable: {'[green]Yes[/green]' if table.satisfiable else '[red]No[/red]'}")
210
+ console.print(f" Tautology : {'[green]Yes[/green]' if table.tautology else '[red]No[/red]'}")
211
+
212
+ if show_minterms:
213
+ console.print(f" Minterms : {table.minterms}")
214
+ if show_maxterms:
215
+ console.print(f" Maxterms : {table.maxterms}")
216
+ if synth_expr is not None:
217
+ console.print(f" Minimal : [bold yellow]{synth_expr}[/bold yellow]")
218
+ if show_metrics:
219
+ console.print(f"\n [dim]Eval : {eval_metrics.eval_time_ms} ms | {eval_metrics.peak_memory_bytes} bytes[/dim]")
220
+ if synth_metrics:
221
+ console.print(f" [dim]Synth : {synth_metrics.synth_time_ms} ms | {synth_metrics.peak_memory_bytes} bytes | {synth_metrics.prime_implicant_count} prime implicants[/dim]")
222
+
223
+
224
+ REPL_BANNER = """
225
+ [bold cyan]╔══════════════════════════════════════════════════════╗
226
+ ║ Boolean Algebra Engine — boolcalc ║
227
+ ╚══════════════════════════════════════════════════════╝[/bold cyan]
228
+
229
+ [white]A boolean algebra engine that evaluates expressions against
230
+ truth tables and synthesizes minimal forms using Quine-McCluskey.[/white]
231
+
232
+ [bold]What it does:[/bold]
233
+ [green]Forward[/green] expression → truth table
234
+ [green]Inverse[/green] truth table → minimal expression
235
+
236
+ [bold]Operators:[/bold]
237
+ [yellow]![/yellow] NOT (highest precedence)
238
+ [yellow].[/yellow] AND
239
+ [yellow]^[/yellow] XOR
240
+ [yellow]+[/yellow] OR (lowest precedence)
241
+
242
+ Variables must be uppercase letters [cyan]A–Z[/cyan].
243
+ Parentheses override precedence as usual.
244
+ """
245
+
246
+ REPL_HELP = """
247
+ [bold cyan]── Commands ──────────────────────────────────────────────[/bold cyan]
248
+
249
+ [yellow]<expression>[/yellow] evaluate and show truth table
250
+ [yellow]<expression> -s[/yellow] also show minimal expression
251
+ [yellow]<expression> --metrics[/yellow] show timing and memory usage
252
+ [yellow]<expression> --minterms[/yellow] show minterm indices
253
+ [yellow]<expression> --maxterms[/yellow] show maxterm indices
254
+ [yellow]<expression> --satisfiable[/yellow] check if satisfiable
255
+ [yellow]<expression> --tautology[/yellow] check if tautology
256
+
257
+ [bold cyan]── Output formats ────────────────────────────────────────[/bold cyan]
258
+
259
+ [yellow]<expression> --format table[/yellow] rich table (default)
260
+ [yellow]<expression> --format json[/yellow] JSON — good for scripting
261
+ [yellow]<expression> --format csv[/yellow] CSV
262
+ [yellow]<expression> --format minimal[/yellow] output column only
263
+
264
+ [bold cyan]── Examples ──────────────────────────────────────────────[/bold cyan]
265
+
266
+ [dim]boolcalc>[/dim] [green]A+B[/green]
267
+ [dim]boolcalc>[/dim] [green]A.(B+C) -s --metrics[/green]
268
+ [dim]boolcalc>[/dim] [green]!(A.B) --format json[/green]
269
+ [dim]boolcalc>[/dim] [green]A.B+!A.C+B.C -s[/green] [dim]← consensus theorem[/dim]
270
+ [dim]boolcalc>[/dim] [green]A.!A --satisfiable[/green] [dim]← contradiction check[/dim]
271
+
272
+ [bold cyan]── Session ───────────────────────────────────────────────[/bold cyan]
273
+
274
+ [yellow]help[/yellow] show this manual
275
+ [yellow]exit[/yellow] / [yellow]quit[/yellow] / [yellow]Ctrl+C[/yellow] exit
276
+ """
277
+
278
+ def _repl():
279
+ console.print(REPL_BANNER)
280
+ console.print(REPL_HELP)
281
+ while True:
282
+ try:
283
+ line = console.input("[bold cyan]boolcalc>[/bold cyan] ").strip()
284
+ except (KeyboardInterrupt, EOFError):
285
+ console.print("\n[dim]bye[/dim]")
286
+ break
287
+
288
+ if not line:
289
+ continue
290
+ if line.lower() in ("exit", "quit", "q"):
291
+ console.print("[dim]bye[/dim]")
292
+ break
293
+ if line.lower() in ("help", "?", "h"):
294
+ console.print(REPL_HELP)
295
+ continue
296
+
297
+ # Parse the line as if it were CLI args
298
+ try:
299
+ parts = shlex.split(line)
300
+ # First token is the expression, rest are flags
301
+ app(parts, standalone_mode=False)
302
+ except SystemExit:
303
+ pass
304
+ except Exception as e:
305
+ err_console.print(f"[red]Error:[/red] {e}")
306
+
307
+ console.print()
308
+
309
+
310
+ def _make_provider(provider_name: str, api_key: Optional[str], model: Optional[str], base_url: Optional[str]):
311
+ from nl.nl import AnthropicProvider, OpenAIProvider, OllamaProvider, OpenAICompatProvider
312
+ if provider_name == "anthropic":
313
+ return AnthropicProvider(api_key=api_key, model=model or "claude-sonnet-4-6")
314
+ if provider_name == "openai":
315
+ return OpenAIProvider(api_key=api_key, model=model or "gpt-4o")
316
+ if provider_name == "ollama":
317
+ return OllamaProvider(model=model or "llama3", base_url=base_url or "http://localhost:11434")
318
+ if provider_name == "compat":
319
+ if not base_url or not model:
320
+ err_console.print("[red]Error:[/red] --base-url and --model required for compat provider")
321
+ raise typer.Exit(1)
322
+ return OpenAICompatProvider(api_key=api_key or "", base_url=base_url, model=model)
323
+ err_console.print(f"[red]Error:[/red] Unknown provider '{provider_name}'. Choose: anthropic, openai, ollama, compat")
324
+ raise typer.Exit(1)
325
+
326
+
327
+ @app.command("ask")
328
+ def nl_ask(
329
+ sentence: str = typer.Argument(..., help="Plain English logical statement."),
330
+ provider: str = typer.Option("anthropic", "--provider", "-p", help="LLM provider: anthropic, openai, ollama, compat"),
331
+ api_key: Optional[str] = typer.Option(None, "--api-key", help="API key for the provider."),
332
+ model: Optional[str] = typer.Option(None, "--model", help="Model ID override."),
333
+ base_url: Optional[str] = typer.Option(None, "--base-url", help="Base URL for compat provider."),
334
+ format: Format = typer.Option(Format.table, "--format", "-f"),
335
+ ):
336
+ """Convert a plain English rule into a verified boolean result."""
337
+ try:
338
+ from nl.nl import ask
339
+ prov = _make_provider(provider, api_key, model, base_url)
340
+ result = ask(sentence, provider=prov)
341
+ except ImportError as e:
342
+ err_console.print(f"[red]Error:[/red] {e}")
343
+ raise typer.Exit(1)
344
+ except Exception as e:
345
+ err_console.print(f"[red]Error:[/red] {e}")
346
+ raise typer.Exit(1)
347
+
348
+ if format == Format.json:
349
+ print(json.dumps({
350
+ "input": result.input_sentence,
351
+ "expression": result.expression,
352
+ "variables": result.variables,
353
+ "minimal": result.minimal,
354
+ "satisfiable": result.satisfiable,
355
+ "tautology": result.tautology,
356
+ "contradiction": result.contradiction,
357
+ "minterms": result.minterms,
358
+ "explanation": result.explanation,
359
+ }, indent=2))
360
+ return
361
+
362
+ console.print(f"\n [dim]Input :[/dim] {result.input_sentence}")
363
+ console.print(f" [dim]Parsed as:[/dim] [cyan]{result.expression}[/cyan]")
364
+ console.print(f"\n [bold]Variables:[/bold]")
365
+ for var, meaning in result.variables.items():
366
+ console.print(f" [yellow]{var}[/yellow] = {meaning}")
367
+
368
+ t = Table(box=box.SIMPLE_HEAVY, show_header=True, header_style="bold cyan")
369
+ for var in sorted(result.variables.keys()):
370
+ t.add_column(var, justify="center", style="dim")
371
+ t.add_column(result.expression, justify="center", style="bold")
372
+ for row in result.rows:
373
+ values = [str(row[v]) for v in sorted(result.variables.keys())]
374
+ output = "[green]1[/green]" if row["output"] == 1 else "[red]0[/red]"
375
+ t.add_row(*values, output)
376
+ console.print(t)
377
+
378
+ console.print(f" Minimal : [bold yellow]{result.minimal}[/bold yellow]")
379
+ console.print(f" Satisfiable: {'[green]Yes[/green]' if result.satisfiable else '[red]No[/red]'}")
380
+ console.print(f" Tautology : {'[green]Yes[/green]' if result.tautology else '[red]No[/red]'}")
381
+ console.print(f"\n [bold]Explanation:[/bold]\n {result.explanation}\n")
382
+
383
+
384
+ @app.command("check-rules")
385
+ def nl_check_rules(
386
+ rules: list[str] = typer.Argument(..., help="Plain English rules to check."),
387
+ provider: str = typer.Option("anthropic", "--provider", "-p", help="LLM provider: anthropic, openai, ollama, compat"),
388
+ api_key: Optional[str] = typer.Option(None, "--api-key", help="API key for the provider."),
389
+ model: Optional[str] = typer.Option(None, "--model", help="Model ID override."),
390
+ base_url: Optional[str] = typer.Option(None, "--base-url", help="Base URL for compat provider."),
391
+ ):
392
+ """Check a list of plain English rules for contradictions and conflicts."""
393
+ try:
394
+ from nl.nl import check_rules
395
+ prov = _make_provider(provider, api_key, model, base_url)
396
+ result = check_rules(rules, provider=prov)
397
+ except ImportError as e:
398
+ err_console.print(f"[red]Error:[/red] {e}")
399
+ raise typer.Exit(1)
400
+ except Exception as e:
401
+ err_console.print(f"[red]Error:[/red] {e}")
402
+ raise typer.Exit(1)
403
+
404
+ print(json.dumps(result, indent=2))
405
+
406
+
407
+ if __name__ == "__main__":
408
+ if len(sys.argv) == 1 and sys.stdin.isatty():
409
+ _repl()
410
+ else:
411
+ app()
core/__init__.py ADDED
File without changes
core/evaluator.py ADDED
@@ -0,0 +1,86 @@
1
+ """
2
+ core/evaluator.py — truth table generation.
3
+
4
+ Public API:
5
+ evaluate(expression) → (TruthTable, PerformanceMetrics)
6
+
7
+ Raises ValueError for invalid expressions (delegates to parser.validate).
8
+ Each row in the truth table is evaluated independently — this is the
9
+ sequential baseline that CUDA will later parallelise (one thread per row).
10
+ """
11
+ from __future__ import annotations
12
+ import time
13
+ import tracemalloc
14
+ from .models import TruthTable, TruthTableRow, PerformanceMetrics
15
+ from .parser import get_variables, validate, infix_to_prefix
16
+
17
+
18
+ def _evaluate_prefix(prefix: str, variable_values: dict[str, int]) -> int:
19
+ """Evaluate a prefix expression for a single row of variable assignments."""
20
+ stack = []
21
+ for c in reversed(prefix):
22
+ if c.isupper():
23
+ stack.append(variable_values[c])
24
+ elif c == '!':
25
+ stack.append(1 - stack.pop())
26
+ else:
27
+ a, b = stack.pop(), stack.pop()
28
+ if c == '.':
29
+ stack.append(a & b)
30
+ elif c == '+':
31
+ stack.append(a | b)
32
+ elif c == '^':
33
+ stack.append(a ^ b)
34
+ return stack[0]
35
+
36
+
37
+ def evaluate(expression: str) -> tuple[TruthTable, PerformanceMetrics]:
38
+ """
39
+ Evaluate a boolean expression and return its full truth table.
40
+
41
+ Args:
42
+ expression: Infix boolean expression string. Variables must be
43
+ uppercase letters. Operators: ! . ^ +
44
+
45
+ Returns:
46
+ (TruthTable, PerformanceMetrics) — truth table and timing/memory data.
47
+
48
+ Raises:
49
+ ValueError: If the expression fails validation.
50
+
51
+ Example:
52
+ table, metrics = evaluate('A.(B+C)')
53
+ print(table.minterms) # [5, 6, 7]
54
+ print(metrics.eval_time_ms) # 0.21
55
+ """
56
+ error = validate(expression)
57
+ if error:
58
+ raise ValueError(error)
59
+
60
+ variables = get_variables(expression)
61
+ prefix = infix_to_prefix(expression)
62
+ n = len(variables)
63
+
64
+ tracemalloc.start()
65
+ t_start = time.perf_counter()
66
+
67
+ rows = []
68
+ for i in range(2 ** n):
69
+ values = {
70
+ var: (i >> (n - 1 - j)) & 1
71
+ for j, var in enumerate(variables)
72
+ }
73
+ output = _evaluate_prefix(prefix, values)
74
+ rows.append(TruthTableRow(inputs=values, output=output))
75
+
76
+ eval_time_ms = (time.perf_counter() - t_start) * 1000
77
+ _, peak = tracemalloc.get_traced_memory()
78
+ tracemalloc.stop()
79
+
80
+ table = TruthTable(expression=expression, variables=variables, rows=rows)
81
+ metrics = PerformanceMetrics(
82
+ eval_time_ms=round(eval_time_ms, 4),
83
+ peak_memory_bytes=peak,
84
+ rows_evaluated=2 ** n,
85
+ )
86
+ return table, metrics
core/models.py ADDED
@@ -0,0 +1,96 @@
1
+ """
2
+ core/models.py — data structures returned by the engine.
3
+
4
+ All public functions in evaluator.py and synthesizer.py return these types.
5
+ Nothing in this file does computation — it is pure data.
6
+ """
7
+ from __future__ import annotations
8
+ from dataclasses import dataclass, field
9
+
10
+
11
+ @dataclass
12
+ class PerformanceMetrics:
13
+ """
14
+ Timing and memory measurements for a single engine operation.
15
+
16
+ Captured automatically by evaluate() and synthesize(). Use these as
17
+ your baseline before applying CUDA parallelism or expression caching —
18
+ the numbers make the speedup concrete and measurable.
19
+
20
+ Attributes:
21
+ eval_time_ms: Time to evaluate the expression (ms).
22
+ synth_time_ms: Time to synthesize the minimal form (ms). None if not run.
23
+ peak_memory_bytes: Peak memory allocated during the operation.
24
+ rows_evaluated: Number of truth table rows (2^n). This is the unit
25
+ of work that CUDA will parallelise — each row is
26
+ independent and maps directly to a GPU thread.
27
+ prime_implicant_count: Number of prime implicants found during synthesis.
28
+ Indicates Quine-McCluskey complexity — a good target
29
+ for caching repeated sub-expressions.
30
+ """
31
+ eval_time_ms: float = 0.0
32
+ synth_time_ms: float | None = None
33
+ peak_memory_bytes: int = 0
34
+ rows_evaluated: int = 0
35
+ prime_implicant_count: int | None = None
36
+
37
+
38
+ @dataclass
39
+ class TruthTableRow:
40
+ """A single row in a truth table — one combination of inputs and its output."""
41
+ inputs: dict[str, int]
42
+ output: int
43
+
44
+
45
+ @dataclass
46
+ class TruthTable:
47
+ """
48
+ A fully evaluated truth table for a boolean expression.
49
+
50
+ Attributes:
51
+ expression: The original infix expression string.
52
+ variables: Sorted list of variable names found in the expression.
53
+ rows: All 2^n rows, ordered from all-0 inputs to all-1 inputs.
54
+ """
55
+ expression: str
56
+ variables: list[str]
57
+ rows: list[TruthTableRow]
58
+
59
+ @property
60
+ def satisfiable(self) -> bool:
61
+ """True if at least one row has output = 1."""
62
+ return any(row.output == 1 for row in self.rows)
63
+
64
+ @property
65
+ def tautology(self) -> bool:
66
+ """True if every row has output = 1."""
67
+ return all(row.output == 1 for row in self.rows)
68
+
69
+ @property
70
+ def minterms(self) -> list[int]:
71
+ """Row indices where output = 1."""
72
+ return [i for i, row in enumerate(self.rows) if row.output == 1]
73
+
74
+ @property
75
+ def maxterms(self) -> list[int]:
76
+ """Row indices where output = 0."""
77
+ return [i for i, row in enumerate(self.rows) if row.output == 0]
78
+
79
+
80
+ @dataclass
81
+ class EvaluationResult:
82
+ """
83
+ Combined result of evaluate() + synthesize() in a single object.
84
+
85
+ Used by higher layers (CLI, API, MCP) that want everything in one place.
86
+ For direct engine use, call evaluate() and synthesize() separately.
87
+ """
88
+ truth_table: TruthTable
89
+ minimal_expression: str | None = None
90
+ error: str | None = None
91
+ metrics: PerformanceMetrics | None = None
92
+
93
+ @property
94
+ def ok(self) -> bool:
95
+ """True when no error occurred."""
96
+ return self.error is None
core/parser.py ADDED
@@ -0,0 +1,73 @@
1
+ """
2
+ core/parser.py — expression validation and infix-to-prefix conversion.
3
+
4
+ Three public functions:
5
+ get_variables(expression) → sorted list of unique variable names
6
+ validate(expression) → None if valid, error string if not
7
+ infix_to_prefix(expression) → prefix (Polish notation) string
8
+
9
+ Operator precedence: ! (NOT, 4) > . (AND, 3) > ^ (XOR, 2) > + (OR, 1).
10
+ Variables must be uppercase A–Z. Parentheses override precedence.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ PRECEDENCE = {'!': 4, '.': 3, '^': 2, '+': 1}
15
+ OPERATORS = set(PRECEDENCE)
16
+
17
+
18
+ def get_variables(expression: str) -> list[str]:
19
+ """Return sorted, deduplicated list of uppercase variable names in expression."""
20
+ return sorted(set(c for c in expression if c.isupper()))
21
+
22
+
23
+ def validate(expression: str) -> str | None:
24
+ """Return None if expression is valid, or an error message string if not."""
25
+ if not expression:
26
+ return "Expression cannot be empty"
27
+ if ' ' in expression:
28
+ return "Expression must not contain spaces"
29
+ depth = 0
30
+ for i, c in enumerate(expression):
31
+ if c == '(':
32
+ depth += 1
33
+ elif c == ')':
34
+ depth -= 1
35
+ if depth < 0:
36
+ return f"Unmatched closing parenthesis at position {i}"
37
+ elif not (c.isupper() or c in OPERATORS or c in '()'):
38
+ return f"Unexpected character '{c}' at position {i}"
39
+ if depth != 0:
40
+ return "Unmatched opening parenthesis"
41
+ return None
42
+
43
+
44
+ def infix_to_prefix(expression: str) -> str:
45
+ """Convert an infix boolean expression to prefix (Polish) notation."""
46
+ result = []
47
+ stack = []
48
+
49
+ chars = list(reversed(expression))
50
+ for i, c in enumerate(chars):
51
+ if c == '(':
52
+ chars[i] = ')'
53
+ elif c == ')':
54
+ chars[i] = '('
55
+
56
+ for c in chars:
57
+ if c.isupper():
58
+ result.append(c)
59
+ elif c in PRECEDENCE:
60
+ while stack and stack[-1] in PRECEDENCE and PRECEDENCE[stack[-1]] >= PRECEDENCE[c]:
61
+ result.append(stack.pop())
62
+ stack.append(c)
63
+ elif c == '(':
64
+ stack.append(c)
65
+ elif c == ')':
66
+ while stack and stack[-1] != '(':
67
+ result.append(stack.pop())
68
+ stack.pop()
69
+
70
+ while stack:
71
+ result.append(stack.pop())
72
+
73
+ return ''.join(reversed(result))