from fractions import Fraction
from functools import partial, wraps
from itertools import chain
from pathlib import Path
import sys
from typing import (
cast, Callable, Dict, List, NamedTuple, Sequence, TypeVar, Union
)
import amalgam.amalgams as am
import amalgam.engine as en
import amalgam.environment as ev
FUNCTIONS: Dict[str, am.Function] = {}
T = TypeVar("T", bound=am.Amalgam)
[docs]def _make_function(
name: str,
func: Callable[..., T] = None,
defer: bool = False,
contextual: bool = False,
allows: Sequence[str] = None,
) -> Union[partial, Callable[..., T]]:
"""
Transforms a given function `func` into a `Function`
and stores it inside of the `FUNCTIONS` mapping.
"""
if func is None:
return partial(
_make_function,
name,
defer=defer,
contextual=contextual,
allows=allows,
)
if allows is None:
allows = []
@wraps(func)
def _func(env, *arguments, **keywords):
with env.search_at(depth=-1):
fns = [env[allow] for allow in allows]
for fn in fns:
cast(am.Function, fn).in_context = True
result = func(env, *arguments, **keywords)
for fn in fns:
cast(am.Function, fn).in_context = False
return result
FUNCTIONS[name] = am.Function(name, _func, defer, contextual)
return _func
[docs]@_make_function("+")
def _add(_env: ev.Environment, *nums: am.Numeric) -> am.Numeric:
"""Returns the sum of :data:`nums`."""
return am.Numeric(sum(num.value for num in nums))
[docs]@_make_function("-")
def _sub(_env: ev.Environment, *nums: am.Numeric) -> am.Numeric:
"""
Subtracts :data:`nums[0]` and the summation of :data:`nums[1:]`.
"""
x, *ns = (num.value for num in nums)
y: Union[float, Fraction] = 0
for n in ns:
y += n
return am.Numeric(x - y)
[docs]@_make_function("*")
def _mul(_env: ev.Environment, *nums: am.Numeric) -> am.Numeric:
"""Returns the product of :data:`nums`."""
prod: Union[float, Fraction] = 1
for num in nums:
prod *= num.value
return am.Numeric(prod)
[docs]@_make_function("/")
def _div(_env: ev.Environment, *nums: am.Numeric) -> am.Numeric:
"""
Divides :data:`nums[0]` and the product of :data:`nums[1:]`
"""
x, *ns = (num.value for num in nums)
y: Union[float, Fraction] = 1
for n in ns:
y *= n
return am.Numeric(x / y)
[docs]@_make_function("setn", defer=True)
def _setn(
env: ev.Environment,
name: am.Quoted[am.Symbol],
amalgam: am.Quoted[am.Amalgam],
) -> am.Amalgam:
"""
Binds :data:`name` to the evaluated :data:`amalgam` value in the
immediate :data:`env` and returns that value.
"""
env[name.value.value] = amalgam.value.evaluate(env)
return env[name.value.value]
[docs]@_make_function("fn", defer=True)
def _fn(
env: ev.Environment,
args: am.Quoted[am.Vector[am.Symbol]],
body: am.Quoted[am.Amalgam],
) -> am.Function:
"""
Creates an anonymous function using the provided arguments.
Binds :data:`env` to the created :class:`.amalgams.Function` if a
closure is formed.
"""
fn = am.create_fn("~lambda~", [arg.value for arg in args.value.vals], body.value)
if env.parent is not None:
fn.bind(env)
return fn
[docs]@_make_function("mkfn", defer=True)
def _mkfn(
env: ev.Environment,
name: am.Quoted[am.Symbol],
args: am.Quoted[am.Vector[am.Symbol]],
body: am.Quoted[am.Amalgam],
) -> am.Amalgam:
"""
Creates a named function using the provided arguments.
Composes :func:`._fn` and :func:`._setn`.
"""
return _setn(env, name, am.Quoted(_fn(env, args, body).with_name(name.value.value)))
[docs]@_make_function("let", defer=True)
def _let(
env: ev.Environment,
qpairs: am.Quoted[am.Vector[am.Vector]],
body: am.Quoted[am.Amalgam],
) -> am.Amalgam:
"""
Creates temporary bindings of names to values specified in
:data:`qpairs` before evaluating :data:`body`.
"""
names = []
values = []
for pos, pair in enumerate(qpairs.value.vals):
if not isinstance(pair, am.Vector) or len(pair.vals) != 2:
raise ValueError(f"{pair} at {pos} is not a pair")
name, value = pair.vals
if not isinstance(name, am.Symbol):
raise TypeError(f"{name} at {pos} is not a symbol")
names.append(name)
values.append(value)
return _fn(env, am.Quoted(am.Vector(*names)), body).call(env, *values)
[docs]@_make_function("bool")
def _bool(_env: ev.Environment, expr: am.Amalgam) -> am.Atom:
"""Checks for the truthiness of an :data:`expr`."""
if expr == am.String(""):
return am.Atom("FALSE")
elif expr == am.Numeric(0):
return am.Atom("FALSE")
elif expr == am.Vector():
return am.Atom("FALSE")
elif expr == am.Atom("FALSE"):
return am.Atom("FALSE")
elif expr == am.Atom("NIL"):
return am.Atom("FALSE")
return am.Atom("TRUE")
[docs]@_make_function(">")
def _gt(_env: ev.Environment, x: am.Amalgam, y: am.Amalgam) -> am.Atom:
"""Performs a `greater than` comparison."""
if x > y: # type: ignore
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function("<")
def _lt(_env: ev.Environment, x: am.Amalgam, y: am.Amalgam) -> am.Atom:
"""Performs a `less than` comparison."""
if x < y: # type: ignore
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function("=")
def _eq(_env: ev.Environment, x: am.Amalgam, y: am.Amalgam) -> am.Atom:
"""Performs an `equals` comparison."""
if x == y:
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function("/=")
def _ne(_env: ev.Environment, x: am.Amalgam, y: am.Amalgam) -> am.Atom:
"""Performs a `not equals` comparison."""
if x != y:
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function(">=")
def _ge(env: ev.Environment, x: am.Amalgam, y: am.Amalgam) -> am.Atom:
"""Performs a `greater than or equal` comparison."""
if x >= y: # type: ignore
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function("<=")
def _le(env: ev.Environment, x: am.Amalgam, y: am.Amalgam) -> am.Atom:
"""Performs a `less than or equal` comparison."""
if x <= y: # type: ignore
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function("not")
def _not(_env: ev.Environment, expr: am.Amalgam) -> am.Atom:
"""Checks and negates the truthiness of :data:`expr`."""
if _bool(_env, expr) == am.Atom("TRUE"):
return am.Atom("FALSE")
return am.Atom("TRUE")
[docs]@_make_function("and", defer=True)
def _and(env: ev.Environment, *qexprs: am.Quoted[am.Amalgam]) -> am.Atom:
"""
Checks the truthiness of the evaluated :data:`qexprs` and performs
an `and` operation. Short-circuits when :data:`:FALSE` is returned
and does not evaluate subsequent expressions.
"""
for qexpr in qexprs:
cond = _bool(env, qexpr.value.evaluate(env))
if cond == am.Atom("FALSE"):
return cond
return am.Atom("TRUE")
[docs]@_make_function("or", defer=True)
def _or(env: ev.Environment, *qexprs: am.Quoted[am.Amalgam]) -> am.Atom:
"""
Checks the truthiness of the evaluated :data:`qexprs` and performs
an `or` operation. Short-circuits when :data:`:TRUE` is returned
and does not evaluate subsequent expressions.
"""
for qexpr in qexprs:
cond = _bool(env, qexpr.value.evaluate(env))
if cond == am.Atom("TRUE"):
return cond
return am.Atom("FALSE")
[docs]@_make_function("if", defer=True)
def _if(
env: ev.Environment,
qcond: am.Quoted[am.Amalgam],
qthen: am.Quoted[am.Amalgam],
qelse: am.Quoted[am.Amalgam],
) -> am.Amalgam:
"""
Checks the truthiness of the evaluated :data:`qcond`, evaluates and
returns :data:`qthen` if :data:`:TRUE`, otherwise, evaluates and
returns :data:`qelse`.
"""
cond = _bool(env, qcond.value.evaluate(env))
if cond == am.Atom("TRUE"):
return qthen.value.evaluate(env)
return qelse.value.evaluate(env)
[docs]@_make_function("cond", defer=True)
def _cond(env: ev.Environment, *qpairs: am.Quoted[am.Vector[am.Amalgam]]) -> am.Amalgam:
"""
Traverses pairs of conditions and values. If the condition evaluates
to :data:`:TRUE`, returns the value pair and short-circuits
evaluation. If no conditions are met, :data:`:NIL` is returned.
"""
for qpair in qpairs:
pred, expr = qpair.value.vals
if _bool(env, pred.evaluate(env)) == am.Atom("TRUE"):
return expr.evaluate(env)
return am.Atom("NIL")
[docs]@_make_function("exit")
def _exit(env: ev.Environment, exit_code: am.Numeric = am.Numeric(0)) -> am.Amalgam:
"""Exits the program with the given :data:`exit_code`."""
print("Goodbye.")
sys.exit(int(exit_code.value))
[docs]@_make_function("print")
def _print(_env: ev.Environment, amalgam: am.Amalgam) -> am.Amalgam:
"""Prints the provided :data:`amalgam` and returns it."""
print(amalgam)
return amalgam
[docs]@_make_function("putstrln")
def _putstrln(_env: ev.Environment, string: am.String) -> am.String:
"""Prints the provided :data:`string` and returns it."""
if not isinstance(string, am.String):
raise TypeError("putstrln only accepts a string")
print(string.value)
return string
[docs]@_make_function("do", defer=True)
def _do(env: ev.Environment, *qexprs: am.Quoted[am.Amalgam]) -> am.Amalgam:
"""
Evaluates a variadic amount of :data:`qexprs`, returning the final
expression evaluated.
"""
accumulator = am.Atom("NIL")
for qexpr in qexprs:
accumulator = qexpr.value.evaluate(env)
return accumulator
[docs]@_make_function("require")
def _require(env: ev.Environment, module_name: am.String) -> am.Atom:
"""
Runs a given :data:`module_name` and imports the exposed symbols to
the current :data:`env` with respect to the `~provides~` key created
in :func:`._provide`.
"""
module_path = Path(module_name.value).absolute()
with module_path.open("r", encoding="UTF-8") as f:
text = f.read()
snapshot = env.bindings.copy()
internal_engine = cast(am.Internal[en.Engine], env["~engine~"])
internal_engine.value.parse_and_run(text)
if "~provides~" in env:
symbols = cast(am.Vector[am.Symbol], env["~provides~"])
exports = {symbol.value for symbol in symbols.vals}
changes = {
name: env[name]
for name in exports.intersection(env.bindings)
}
snapshot.update(changes)
env.bindings = snapshot
return am.Atom("NIL")
[docs]@_make_function("provide", defer=True)
def _provide(env: ev.Environment, *qsymbols: am.Quoted[am.Symbol]) -> am.Atom:
"""Sets the `~provides~` key to be used in :func:`._require`."""
env["~provides~"] = am.Vector(*(qsymbol.value for qsymbol in qsymbols))
return am.Atom("NIL")
[docs]@_make_function("concat")
def _concat(_env: ev.Environment, *strings: am.String) -> am.String:
"""Concatenates the given :data:`strings`."""
return am.String("".join(string.value for string in strings))
[docs]@_make_function("merge")
def _merge(_env: ev.Environment, *vectors: am.Vector) -> am.Vector:
"""Merges the given :data:`vectors`."""
return am.Vector(*chain.from_iterable(vector.vals for vector in vectors))
[docs]@_make_function("slice")
def _slice(
_env: ev.Environment,
vector: am.Vector,
start: am.Numeric,
stop: am.Numeric,
step: am.Numeric = am.Numeric(1),
) -> am.Vector:
"""Returns a slice of the given :data:`vector`."""
return am.Vector(*vector.vals[start.value:stop.value:step.value])
[docs]@_make_function("at")
def _at(_env: ev.Environment, index: am.Numeric, vector: am.Vector) -> am.Amalgam:
"""Indexes :data:`vector` with :data:`index`."""
return vector.vals[index.value]
[docs]@_make_function("remove")
def _remove(_env: ev.Environment, index: am.Numeric, vector: am.Vector) -> am.Vector:
"""Removes an item in :data:`vector` using :data:`index`."""
vals = list(vector.vals)
del vals[index.value]
return am.Vector(*vals)
[docs]@_make_function("len")
def _len(_env: ev.Environment, vector: am.Vector) -> am.Numeric:
"""Returns the length of a :data:`vector`."""
return am.Numeric(len(vector.vals))
[docs]@_make_function("cons")
def _cons(_env: ev.Environment, amalgam: am.Amalgam, vector: am.Vector) -> am.Vector:
"""Preprends an :data:`amalgam` to :data:`vector`."""
return am.Vector(amalgam, *vector.vals)
[docs]@_make_function("snoc")
def _snoc(_env: ev.Environment, vector: am.Vector, amalgam: am.Amalgam) -> am.Vector:
"""Appends an :data:`amalgam` to :data:`vector`."""
return am.Vector(*vector.vals, amalgam)
[docs]@_make_function("is-map")
def _is_map(_env: ev.Environment, vector: am.Vector) -> am.Atom:
"""Verifies whether :data:`vector` is a mapping."""
if vector.mapping:
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function("map-in")
def _map_in(_env: ev.Environment, vector: am.Vector, atom: am.Atom) -> am.Atom:
"""Checks whether :data:`atom` is a member of :data:`vector`."""
if not vector.mapping:
raise ValueError("the given vector is not a mapping")
if atom.value in vector.mapping:
return am.Atom("TRUE")
return am.Atom("FALSE")
[docs]@_make_function("map-at")
def _map_at(_env: ev.Environment, vector: am.Vector, atom: am.Atom) -> am.Amalgam:
"""Obtains the value bound to :data:`atom` in :data:`vector`."""
if not vector.mapping:
raise ValueError("the given vector is not a mapping")
return vector.mapping[atom.value]
[docs]@_make_function("map-up")
def _map_up(
_env: ev.Environment,
vector: am.Vector,
atom: am.Atom,
amalgam: am.Amalgam,
) -> am.Vector:
"""
Updates the :data:`vector mapping with :data:`atom`, and
:data:`amalgam`.
"""
if not vector.mapping:
raise ValueError("the given vector is not a mapping")
new_vector: am.Vector[am.Amalgam] = am.Vector()
mapping = {**vector.mapping}
mapping[atom.value] = amalgam
vals: List[am.Amalgam] = []
for name, value in mapping.items():
vals += (am.Atom(name), value)
new_vector.vals = tuple(vals)
new_vector.mapping = mapping
return new_vector
class _Return(NamedTuple):
"""Internal utility class for signalling a return value."""
return_value: am.Amalgam
[docs]@_make_function("return", contextual=True)
def _return(env: ev.Environment, result: am.Amalgam) -> am.Internal:
"""Exits a context with a :data:`result`."""
return am.Internal(_Return(result))
[docs]@_make_function("break", contextual=True)
def _break(env: ev.Environment) -> am.Internal:
"""Exits a loop with :data:`:NIL`."""
return am.Internal(_Return(am.Atom("NIL")))
[docs]@_make_function("loop", defer=True, allows=("break", "return"))
def _loop(env: ev.Environment, *qexprs: am.Quoted[am.Amalgam]) -> am.Amalgam:
"""
Loops through and evaluates :data:`qexprs` indefinitely until a
:data:`break` or :data:`return` is encountered.
"""
return_value = None
while return_value is None:
for qexpr in qexprs:
result = qexpr.value.evaluate(env)
if isinstance(result, am.Internal):
if isinstance(result.value, _Return): # pragma: no branch
return_value = result.value.return_value
break
return return_value
[docs]@_make_function("when", defer=True)
def _when(
env: ev.Environment, qcond: am.Quoted[am.Amalgam], qbody: am.Quoted[am.Amalgam],
) -> am.Amalgam:
"""
Synonym for :func:`._if` that defaults :data:`qelse` to
:data:`:NIL`.
"""
cond = _bool(env, qcond.value.evaluate(env))
if cond == am.Atom("TRUE"):
return qbody.value.evaluate(env)
return am.Atom("NIL")
[docs]@_make_function("eval")
def _eval(env: ev.Environment, amalgam: am.Amalgam) -> am.Amalgam:
"""Evaluates a given :data:`amalgam`."""
if isinstance(amalgam, am.Quoted):
amalgam = amalgam.value
return amalgam.evaluate(env)
[docs]@_make_function("unquote")
def _unquote(_env: ev.Environment, qamalgam: am.Quoted[am.Amalgam]) -> am.Amalgam:
"""Unquotes a given :data:`qamalgam`."""
if not isinstance(qamalgam, am.Quoted):
raise TypeError("unquotable value provided")
return qamalgam.value
[docs]@_make_function("setr", defer=True)
def _setr(
env: ev.Environment,
qrname: am.Quoted[am.Symbol],
qamalgam: am.Quoted[am.Amalgam],
) -> am.Amalgam:
"""
Attemps to resolve :data:`qrname` to a :class:`.amalgams.Symbol`
and binds it to the evaluated :data:`qamalgam` in the immediate
:data:`env`.
"""
rname = qrname.value.evaluate(env)
if not isinstance(rname, am.Symbol):
raise TypeError("could not resolve to a symbol")
amalgam = qamalgam.value.evaluate(env)
env[rname.value] = amalgam
return amalgam
[docs]@_make_function("macro", defer=True)
def _macro(
env: ev.Environment,
name: am.Quoted[am.Symbol],
args: am.Quoted[am.Vector[am.Symbol]],
body: am.Quoted[am.Amalgam],
) -> am.Amalgam:
"""Creates a named macro using the provided arguments."""
fn = am.create_fn(
name.value.value,
[arg.value for arg in args.value.vals],
body.value,
defer=True,
)
if env.parent is not None:
fn.bind(env)
return _setn(env, name, am.Quoted(fn))