Source code for amalgam.amalgams

from __future__ import annotations

from abc import ABC, abstractmethod
from dataclasses import dataclass
from fractions import Fraction
from typing import (
    cast,
    Any,
    Callable,
    Generic,
    Mapping,
    Sequence,
    Tuple,
    TypeVar,
)

import amalgam.environment as ev


[docs]class Amalgam(ABC): """The abstract base class for language constructs."""
[docs] @abstractmethod def evaluate(self, environment: ev.Environment) -> Any: """ Protocol for evaluating or unwrapping :class:`Amalgam` objects. """
[docs] def bind(self, environment: ev.Environment) -> Amalgam: # pragma: no cover """ Protocol for implementing environment binding for :class:`Function`. This base implementation is responsible for allowing `bind` to be called on other :class:`Amalgam` subclasses by performing no operation aside from returning :data:`self`. """ return self
[docs] def call( self, environment: ev.Environment, *arguments: Amalgam ) -> Amalgam: # pragma: no cover """ Protocol for implementing function calls for :class:`Function`. This base implementation is responsible for making the type signature of :attr:`SExpression.func` to properly type check when :meth:`SExpression.evaluate` is called, as well as raising :class:`NotImplementedError` for non-callable types. """ raise NotImplementedError(f"{self.__class__.__name__} is not callable")
[docs] def _make_repr(self, value: Any) -> str: # pragma: no cover """Helper method for creating a :meth:`__repr__`.""" return f"<{self.__class__.__name__} '{value!s}' @ {hex(id(self))}>"
[docs]@dataclass(repr=False) class Atom(Amalgam): """ An :class:`.Amalgam` that represents different atoms. Attributes: value (:class:`str`): The name of the atom. """ value: str
[docs] def evaluate(self, _environment: ev.Environment) -> Atom: """Evaluates to the same :class:`.Atom` reference.""" return self
def __repr__(self) -> str: # pragma: no cover return self._make_repr(self.value) def __str__(self) -> str: return f":{self.value}"
N = TypeVar("N", int, float, Fraction)
[docs]@dataclass(repr=False, order=True) class Numeric(Amalgam, Generic[N]): """ An :class:`.Amalgam` that wraps around numeric types. Parameterized as a :class:`Generic` by: :data:`N = TypeVar("N", int, float, Fraction)` Attributes: value (:data:`N`): The numeric value being wrapped. """ value: N
[docs] def evaluate(self, _environment: ev.Environment) -> Numeric: """Evaluates to the same :class:`.Numeric` reference.""" return self
def __repr__(self) -> str: # pragma: no cover return self._make_repr(self.value) def __str__(self) -> str: return str(self.value)
@dataclass(repr=False, order=True) class String(Amalgam): """ An :class:`.Amalgam` that wraps around strings. Attributes: value (:class:`str`): The string being wrapped. """ value: str def evaluate(self, _environment: ev.Environment) -> String: """Evaluates to the same :class:`.String` reference.""" return self def __repr__(self) -> str: # pragma: no cover return self._make_repr(f"\"{self.value}\"") def __str__(self) -> str: return f"\"{self.value}\""
[docs]@dataclass(repr=False) class Symbol(Amalgam): """ An :class:`.Amalgam` that wraps around symbols. Attributes: value (:class:`str`): The name of the symbol. """ value: str
[docs] def evaluate(self, environment: ev.Environment) -> Amalgam: """ Searches the provided `environment` fully with :attr:`Symbol.value`. Returns the :class:`.Amalgam` object bound to the :attr:`Symbol.value` in the environment. Raises :class:`.environment.SymbolNotFound` if a binding is not found. """ with environment.search_at(depth=-1): return environment[self.value]
def __repr__(self) -> str: # pragma: no cover return self._make_repr(self.value) def __str__(self) -> str: return self.value
[docs]class DisallowedContextError(Exception): """Raised on functions outside of their intended contexts."""
[docs]@dataclass(repr=False) class Function(Amalgam): """ An :class:`.Amalgam` that wraps around functions. Attributes: name (:class:`str`): The name of the function. fn (:class:`Callable[..., Amalgam]`): The function being wrapped. Must have the signature: `(env, amalgams...) -> amalgam`. defer (:class:`bool`): If set to :obj:`True`, arguments are wrapped in :class:`.Quoted` before being passed to :attr:`.Function.fn`. contextual (:class:`bool`): If set to :obj:`True`, disallows function calls when :attr:`.Function.in_context` is set to :obj:`False`. env (:class:`.environment.Environment`): The :class:`.environment.Environment` instance bound to the function. Overrides the `environment` parameter passed to the :meth:`.Function.call` method. in_context (:class:`bool`): Predicate that disallows functions to be called outside of specific contexts. Makes :meth:`.Function.call` raise :class:`.DisallowedContextError` when set to :obj:`False` and :attr:`.Function.contextual` is set to :obj:`True`. """ name: str fn: Callable[..., Amalgam] defer: bool = False contextual: bool = False def __post_init__(self): self.env = cast(ev.Environment, None) self.in_context = False
[docs] def evaluate(self, _environment: ev.Environment) -> Function: """Evaluates to the same :class:`.Function` reference.""" return self
[docs] def bind(self, environment: ev.Environment) -> Function: """ Sets the :attr:`.Function.env` attribute and returns the same :class:`.Function` reference. """ self.env = environment return self
[docs] def call(self, environment: ev.Environment, *arguments: Amalgam) -> Amalgam: """ Performs the call to the :attr:`.Function.fn` attribute. Performs pre-processing depending on the values of :attr:`.Function.defer`, :attr:`.Function.contextual`, and :attr:`.Function.in_context`, """ if self.contextual and not self.in_context: raise DisallowedContextError(f"invalid context for {self.name}") if self.env is not None: environment = self.env if self.defer: arguments = tuple(Quoted(arg) for arg in arguments) else: arguments = tuple(arg.evaluate(environment) for arg in arguments) return self.fn(environment, *arguments)
[docs] def with_name(self, name: str) -> Function: """ Sets the :attr:`.Function.name` attribute and returns the same :class:`.Function` reference. """ self.name = name return self
def __repr__(self) -> str: # pragma: no cover return self._make_repr(self.name) def __str__(self) -> str: # pragma: no cover return self.name
[docs]@dataclass(init=False, repr=False) class SExpression(Amalgam): """ An :class:`.Amalgam` that wraps around S-Expressions. Attributes: vals (:class:`Tuple[Amalgam, ...]`): Entities contained by the S-Expression. """ vals: Tuple[Amalgam, ...] def __init__(self, *vals: Amalgam) -> None: self.vals = vals @property def func(self) -> Amalgam: """The head of the :attr:`SExpression.vals`.""" return self.vals[0] @property def args(self) -> Tuple[Amalgam, ...]: """The rest of the :attr:`SExpression.vals`.""" return self.vals[1:]
[docs] def evaluate(self, environment: ev.Environment) -> Amalgam: """ Evaluates :attr:`func` using `environment` before invoking the :meth:`call` method with `environment` and :attr:`SExpression.args`. """ return self.func.evaluate(environment).call(environment, *self.args)
def __repr__(self) -> str: # pragma: no cover return self._make_repr(f"{self.func!r} {' '.join(map(repr, self.args))}") def __str__(self) -> str: return f"({' '.join(map(str, self.vals))})"
T = TypeVar("T", bound=Amalgam)
[docs]@dataclass(init=False, repr=False) class Vector(Amalgam, Generic[T]): """ An :class:`.Amalgam` that wraps around a homogenous vector. Parameterized as a :class:`Generic` by: :data:`T = TypeVar("T", bound=Amalgam)` Attributes: vals (:class:`Tuple[T, ...]`): Entities contained by the vector mapping (:class:`Mapping[str, Amalgam]`): Mapping representing vectors with :class:`.Atom` s for odd indices and :class:`.Amalgam` s for even indices. """ vals: Tuple[T, ...] def __init__(self, *vals: T) -> None: self.vals = vals self.mapping = self._as_mapping()
[docs] def evaluate(self, environment: ev.Environment) -> Vector: """ Creates a new :class:`.Vector` by evaluating every value in :attr:`Vector.vals`. """ return Vector(*(val.evaluate(environment) for val in self.vals))
[docs] def _as_mapping(self) -> Mapping[str, Amalgam]: """ Attemps to create a :class:`Mapping[str, Amalgam]` from :attr:`Vector.vals`. Odd indices must be :class:`.Atom` s and even indices must be :class:`.Amalgam` s. Returns an empty mapping if this form is not met. """ if len(self.vals) % 2 != 0 or len(self.vals) == 0: return {} mapping = {} atoms = self.vals[::2] amalgams = self.vals[1::2] for atom, amalgam in zip(atoms, amalgams): if not isinstance(atom, Atom): return {} mapping[atom.value] = amalgam return mapping
def __repr__(self) -> str: # pragma: no cover return self._make_repr(" ".join(map(repr, self.vals))) def __str__(self) -> str: return f"[{' '.join(map(str, self.vals))}]"
[docs]@dataclass(repr=False) class Quoted(Amalgam, Generic[T]): """ An :class:`Amalgam` that defers evaluation of other :class:`Amalgam` s. Parameterized as a :class:`Generic` by: :data:`T = TypeVar("T", bound=Amalgam)` Attributes: value (:data:`T`): The :class:`.Amalgam` being deferred. """ value: T
[docs] def evaluate(self, _environment: ev.Environment) -> Quoted: """Evaluates to the same :class:`.Quoted` reference.""" return self
def __repr__(self) -> str: # pragma: no cover return self._make_repr(repr(self.value)) def __str__(self) -> str: return f"'{self.value!s}"
P = TypeVar("P", bound=object)
[docs]@dataclass(repr=False) class Internal(Amalgam, Generic[P]): """ An :class:`Amalgam` that holds Python :class:`object` s. Parameterized as a :class:`Generic` by: :data:`P = TypeVar("P", bound=object)` Attributes: value (:data:`P`): The Python :class:`object` being wrapped. """ value: P
[docs] def evaluate(self, _environment: ev.Environment) -> Internal: """Evaluates to the same :class:`.Internal` reference.""" return self
def __repr__(self) -> str: # pragma: no cover return self._make_repr(repr(self.value)) def __str__(self) -> str: # pragma: no cover return f"~{self.value!s}~"
def create_fn( fname: str, fargs: Sequence[str], fbody: Amalgam, defer: bool = False, contextual: bool = False, ) -> Function: """Helper function for creating `Function` objects. Given the name of the function: `fname`, a sequence of argument names: `fargs`, and the `Amalgam` to be evaluated: `fbody`, creates a new `closure_fn` to be wrapped by a `Function`. """ def closure_fn(environment: ev.Environment, *arguments: Amalgam) -> Amalgam: """Callable responsible for evaluating `fbody`.""" # Create a child environment and bind args to their names. # TODO: Raise an error when missing arguments instead. cl_env = environment.env_push(dict(zip(fargs, arguments))) # Call the `evaluate` method on the function body with # `cl_env` and then call `bind` on the result with the # same environment. return fbody.evaluate(cl_env).bind(cl_env) return Function(fname, closure_fn, defer, contextual)