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.
- api/__init__.py +0 -0
- api/routes.py +386 -0
- boolean_algebra_engine-0.1.0.dist-info/METADATA +213 -0
- boolean_algebra_engine-0.1.0.dist-info/RECORD +19 -0
- boolean_algebra_engine-0.1.0.dist-info/WHEEL +5 -0
- boolean_algebra_engine-0.1.0.dist-info/entry_points.txt +2 -0
- boolean_algebra_engine-0.1.0.dist-info/licenses/LICENSE +674 -0
- boolean_algebra_engine-0.1.0.dist-info/top_level.txt +5 -0
- cli/__init__.py +0 -0
- cli/main.py +411 -0
- core/__init__.py +0 -0
- core/evaluator.py +86 -0
- core/models.py +96 -0
- core/parser.py +73 -0
- core/synthesizer.py +167 -0
- mcp_server/__init__.py +0 -0
- mcp_server/server.py +247 -0
- nl/__init__.py +0 -0
- nl/nl.py +449 -0
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))
|