toful_parser.py — Input Normalisation

The toful_parser module implements a two-pass transformation pipeline for user-supplied mathematical expressions:

  • Pass 1normalise_for_eval() transforms raw input into valid Python that can be safely passed to eval().

  • Pass 2normalise_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

class toful_parser.NormalisationResult(original, result, changes=<factory>)[source]

Returned by every normalise_* function.

Parameters:
original: str
result: str
changes: List[str]
property was_changed: bool

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:

NormalisationResult

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:

NormalisationResult

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:

NormalisationResult

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.

Parameters:

extra_params (dict, optional) – Additional names to inject (e.g. {"lam": 2.0, "mu": 3.0} for distribution parameters the user has specified separately).

Returns:

Namespace safe for use as the locals argument to eval() with {"__builtins__": {}} as globals.

Return type:

dict

toful_parser.prepare_expression(raw_expr, extra_params=None)[source]

Full pipeline: normalise for eval, produce display form, validate syntax.

Parameters:
  • raw_expr (str) – User-supplied expression.

  • extra_params (dict, optional) – Extra variable bindings to test syntax against.

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:

Tuple[str, str, List[str], str | None]


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.

  1. _apply_operator_aliases Replaces Unicode operators with ASCII Python equivalents. <=, >=, !=, × *, ÷ /, - (Unicode minus), inf.

  2. _apply_greek_to_eval Replaces Unicode Greek letters with eval-context variable names. π pi, λ lam, μ mu, σ sigma.

  3. _apply_subscript_normalisation Converts Unicode subscript digits to plain ASCII. x₁ x1, y₂ y2.

  4. _apply_inline_subscript Removes underscore between letter and digits. x_1 x1.

  5. _apply_superscript_to_power Converts Unicode superscript sequences to **n notation. x**2, (x+1)³ (x+1)**3.

  6. _apply_e_caret Converts e^x and e^(expr) to exp(x) / exp(expr). Must run before the generic caret rule.

  7. _apply_caret_power Converts remaining ^ to **. 2^n 2**n.

  8. _apply_absolute_value Converts |expr| to abs(expr) for single-level expressions.

  9. _apply_function_aliases Applies the function alias table: ln( log(, arcsin( asin(, √( sqrt(, etc.

  10. _apply_implicit_multiplication Inserts * where multiplication is implied: digit × letter (2x 2*x), )()*(, )letter)*letter (excluding known function names).

  11. _apply_condition_sugar Reserved for future piecewise shorthand. Currently a no-op.


Transformation Pipeline — Pass 2 (display)

Applied by normalise_for_display():

  1. _apply_display_powers**2 ², **10 ¹⁰

  2. _apply_display_subscriptsx1 x₁, y_2 y₂

  3. _apply_display_operators<= , >= , * ·

  4. _apply_display_greeklambda λ, pi π (whole words only)

  5. _apply_display_functionsexp( 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

pi

numpy.pi

e

numpy.e

inf

numpy.inf

sqrt

numpy.sqrt

exp

numpy.exp

log

numpy.log (natural)

log2

numpy.log2

log10

numpy.log10

sin/cos/tan

numpy trigonometric functions

asin/acos/atan

numpy inverse trig

sinh/cosh/tanh

numpy hyperbolic

ceil/floor

numpy.ceil / numpy.floor

abs

Python built-in abs

factorial

math.factorial (integers only)

gamma

scipy.special.gamma

erf/erfc

math.erf / math.erfc

lam

1.0 (default; override via extra_params)

mu

0.0 (default; override via extra_params)

sigma

1.0 (default; override via extra_params)

x

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