toful_parser.py — Input Normalisation
The toful_parser module implements a two-pass transformation pipeline
for user-supplied mathematical expressions:
Pass 1 —
normalise_for_eval()transforms raw input into valid Python that can be safely passed toeval().Pass 2 —
normalise_for_display()produces Unicode-rich output for rendering in the UI.
Both passes are pure functions with no side-effects and return a
NormalisationResult object that includes the
transformed string and a human-readable list of changes made.
Data Classes
Public API
- toful_parser.normalise_for_eval(expr)[source]
Transform a raw user-supplied expression string into valid Python that can be safely passed to
eval()in the ToFUL safe context.The pipeline runs in a fixed order to avoid conflicts between rules. Each step returns the modified string and a human-readable description of changes made, which are accumulated in
NormalisationResult.changes.- Parameters:
expr (str) – Raw input from the user, e.g.
"0.3 * 0.7^x if x >= 0 else 0".- Returns:
.result— eval-ready Python expression string.changes— list of human-readable descriptions of what changed.was_changed— True if any transformation was applied- Return type:
Examples
>>> r = normalise_for_eval("0.3 * 0.7^x if x >= 0 else 0") >>> r.result '0.3 * 0.7**x if x >= 0 else 0'
>>> r = normalise_for_eval("e^(-2*x) if x ≥ 0 else 0") >>> r.result 'exp(-2*x) if x >= 0 else 0'
>>> r = normalise_for_eval("2x if 0 ≤ x ≤ 1 else 0") >>> r.result '2*x if 0 <= x <= 1 else 0'
- toful_parser.normalise_for_display(expr)[source]
Transform an expression string (either raw user input or eval-normalised output) into a Unicode-rich form suitable for rendering in the UI.
This is purely cosmetic — the result must never be passed to
eval().- Parameters:
expr (str) – Expression string to prettify.
- Returns:
.result— display-ready Unicode string.changes— list of transformations applied- Return type:
Examples
>>> normalise_for_display("0.3 * 0.7**x").result '0.3 · 0.7^x' # (superscript x is a letter, not digit — left as ^)
>>> normalise_for_display("x1**2 + y1").result 'x₁² + y₁'
- toful_parser.normalise_range_input(range_str)[source]
Light normalisation specifically for the range input field (discrete comma-separated values or continuous bounds).
- Handles::
“0, 1, 2, 3, …” — strip whitespace, normalise ellipsis “[0, inf]” — strip brackets for bound parsing “0 to inf” — convert ‘to’ notation to two separate bound strings “-∞ to ∞” — Unicode infinity to ‘inf’ “0..10” — Python-style range to explicit list hint
Returns the cleaned string; caller is responsible for further parsing.
- Parameters:
range_str (str)
- Return type:
- toful_parser.build_safe_dict(extra_params=None)[source]
Return the safe namespace dict used for
eval()calls throughout the backend.Centralising this here means the parser and core share a single source of truth for what names are available in user expressions.
- toful_parser.prepare_expression(raw_expr, extra_params=None)[source]
Full pipeline: normalise for eval, produce display form, validate syntax.
- Parameters:
- Returns:
eval_expr (str) – Expression ready for
eval().display_expr (str) – Expression ready for UI display.
changes (list[str]) – Human-readable list of transformations applied.
error (str or None) – Syntax error message if the eval_expr is not valid Python, else None.
- Return type:
Transformation Pipeline — Pass 1 (eval)
The following transformations are applied in this fixed order by
normalise_for_eval(). Order matters: later rules depend on earlier
ones having already run.
_apply_operator_aliasesReplaces Unicode operators with ASCII Python equivalents.≤ → <=,≥ → >=,≠ → !=,× → *,÷ → /,− → -(Unicode minus),∞ → inf._apply_greek_to_evalReplaces Unicode Greek letters with eval-context variable names.π → pi,λ → lam,μ → mu,σ → sigma._apply_subscript_normalisationConverts Unicode subscript digits to plain ASCII.x₁ → x1,y₂ → y2._apply_inline_subscriptRemoves underscore between letter and digits.x_1 → x1._apply_superscript_to_powerConverts Unicode superscript sequences to**nnotation.x² → x**2,(x+1)³ → (x+1)**3._apply_e_caretConvertse^xande^(expr)toexp(x)/exp(expr). Must run before the generic caret rule._apply_caret_powerConverts remaining^to**.2^n → 2**n._apply_absolute_valueConverts|expr|toabs(expr)for single-level expressions._apply_function_aliasesApplies the function alias table:ln( → log(,arcsin( → asin(,√( → sqrt(, etc._apply_implicit_multiplicationInserts*where multiplication is implied: digit × letter (2x → 2*x),)(→)*(,)letter→)*letter(excluding known function names)._apply_condition_sugarReserved for future piecewise shorthand. Currently a no-op.
Transformation Pipeline — Pass 2 (display)
Applied by normalise_for_display():
_apply_display_powers—**2 → ²,**10 → ¹⁰_apply_display_subscripts—x1 → x₁,y_2 → y₂_apply_display_operators—<= → ≤,>= → ≥,* → ·_apply_display_greek—lambda → λ,pi → π(whole words only)_apply_display_functions—exp( → e^(,sqrt( → √(
Safe Evaluation Namespace
The build_safe_dict() function returns the namespace passed as
locals= to every eval() call, with {"__builtins__": {}}
as globals to block access to Python builtins.
from toful_parser import build_safe_dict
ns = build_safe_dict(x_val=1.5, extra={"lam": 2.0})
code = compile("lam * exp(-lam * x)", "<expr>", "eval")
result = eval(code, {"__builtins__": {}}, ns)
# result ≈ 0.5413
Available names in the namespace:
Name |
Value |
|---|---|
|
numpy.pi |
|
numpy.e |
|
numpy.inf |
|
numpy.sqrt |
|
numpy.exp |
|
numpy.log (natural) |
|
numpy.log2 |
|
numpy.log10 |
|
numpy trigonometric functions |
|
numpy inverse trig |
|
numpy hyperbolic |
|
numpy.ceil / numpy.floor |
|
Python built-in abs |
|
math.factorial (integers only) |
|
scipy.special.gamma |
|
math.erf / math.erfc |
|
1.0 (default; override via extra_params) |
|
0.0 (default; override via extra_params) |
|
1.0 (default; override via extra_params) |
|
Set per evaluation |
Usage Examples
Basic normalisation
from toful_parser import normalise_for_eval, normalise_for_display
r = normalise_for_eval("0.3 * 0.7^x if x >= 0 else 0")
print(r.result) # "0.3 * 0.7**x if x >= 0 else 0"
print(r.changes) # ["Caret exponent (^) → Python power (**)"]
r2 = normalise_for_eval("e^(-2*x) if x ≥ 0 else 0")
print(r2.result) # "exp(-2*x) if x >= 0 else 0"
r3 = normalise_for_display("x1**2 + mu")
print(r3.result) # "x₁² + μ"
Full pipeline
from toful_parser import prepare_expression
eval_expr, display_expr, changes, error = prepare_expression(
"2x if 0 <= x <= 1 else 0"
)
print(eval_expr) # "2*x if 0 <= x <= 1 else 0"
print(display_expr) # "2·x if 0 ≤ x ≤ 1 else 0"
print(changes) # ["Implicit multiplication: digit × letter (e.g. 2x → 2*x)"]
print(error) # None
Range input
from toful_parser import normalise_range_input
r = normalise_range_input("0,1,2,3,…")
print(r.result) # "0,1,2,3,..."
print(r.changes) # ["Unicode ellipsis '…' → '...'"]
See also
core.py — Computational Backend — the computation engine that consumes parser output
Input Syntax Guide — user-facing syntax documentation