###
# Modern Albufeira Prolog Interpreter
#
# Warranty & Liability
# To the extent permitted by applicable law and unless explicitly
# otherwise agreed upon, XLOG Technologies AG makes no warranties
# regarding the provided information. XLOG Technologies AG assumes
# no liability that any problems might be solved with the information
# provided by XLOG Technologies AG.
#
# Rights & License
# All industrial property rights regarding the information - copyright
# and patent rights in particular - are the sole property of XLOG
# Technologies AG. If the company was not the originator of some
# excerpts, XLOG Technologies AG has at least obtained the right to
# reproduce, change and translate the information.
#
# Reproduction is restricted to the whole unaltered document. Reproduction
# of the information is only allowed for non-commercial uses. Selling,
# giving away or letting of the execution of the library is prohibited.
# The library can be distributed as part of your applications and libraries
# for execution provided this comment remains unchanged.
#
# Restrictions
# Only to be distributed with programs that add significant and primary
# functionality to the library. Not to be distributed with additional
# software intended to replace any components of the library.
#
# Trademarks
# Jekejeke is a registered trademark of XLOG Technologies AG.
##

import asyncio
import nova.store as store
from nova.store import (Provable, add, VAR_MASK_SEEN,
     MASK_PRED_CHECK, MASK_PRED_ARITHMETIC, Variable,
     VAR_MASK_STATE, is_frozen, is_compound,
     is_variable, Compound, is_structure, MASK_PRED_SPECIAL)
import nova.machine as machine
from nova.machine import (make_error, exec_deref, is_pending,
     deref, cont, bind, SYS_MASK_ASYNC_MODE, mark2_term,
     Choice, more, snap_setup, snap_cleanup, unify, check_callable,
     is_integer, is_atom, is_number, is_float, solve, VOID_ARGS,
     solve_signal, SYS_MASK_ALLOW_YIELD, register_interrupt,
     copy_term, check_nonvar, exec_build, exec_unify, register_signal)
from nova.unicode import code_category, code_numeric
import locale
import sys
import importlib

MAX_ARITY = 2147483647


################################################################
# Special Predicate                                            #
################################################################

###
# Return a built-in for a special.
#
# @param func The special.
# @return Provable The built-in.
##
def make_special(func):
    peek = Provable(MASK_PRED_SPECIAL)
    peek.func = func
    return peek


###
# Return a built-in for a check.
#
# @param func The check.
# @return Provable The built-in.
##
def make_check(func):
    peek = Provable(MASK_PRED_CHECK)
    peek.func = func
    return peek


###
# Return a built-in for an arithmetic.
#
# @param func The arithmetic.
# @return Provable The built-in.
##
def make_arithmetic(func):
    peek = Provable(MASK_PRED_ARITHMETIC)
    peek.func = func
    return peek


################################################################
# fail/0, '$CUT'/1 and '$MARK'/1                               #
################################################################

###
# fail: [ISO 7.8.2]
# The built-in fails.
##
def test_fail(args):
    return False


###
# '$CUT'(R): internal only
# The built-in removes the choice points up to R and succeeds.
##
def test_cut(args):
    choice = exec_build(args[0])
    more(choice)
    return True


###
# '$MARK'(R): Internal only
# The built-in binds R to the top choice point.
##
def test_mark(args):
    choice = machine.redo
    return exec_unify(args[0], choice)


################################################################
# '$SEQ'/2and '$ALT'/1                                         #
################################################################

###
# '$SEQ'(O, L): internal only
# If the Prolog term O has the form just(R) bind R the top choice point.
# Otherwise, do nothing. The built-in then sequentially adds the goals L
# to the continuation.
##
def special_seq(args):
    temp = deref(args[0])
    solve_mark(temp)
    temp = deref(args[1])
    solve_seq(temp)
    return True


###
# If the Prolog term has the form just(R) bind R the top choice point.
# Otherwise do nothing.
#
# @param temp The Prolog term.
##
def solve_mark(temp):
    if (is_structure(temp) and
            temp.functor == 'just' and
            len(temp.args) == 1):
        temp = deref(temp.args[0])
        bind(machine.redo, temp)


###
# Sequentially adds the literals L to the continuation. The list
# is unchecked, no validation that it ends on [].
#
# @param goal The literals.
##
def solve_seq(goal):
    back = None
    res = None
    while (is_structure(goal) and
            goal.functor == "." and
            len(goal.args) == 2):
        temp = deref(goal.args[0])
        temp = Compound(".", [temp, NotImplemented])
        if back is None:
            res = temp
        else:
            back.args[1] = temp
        back = temp
        goal = deref(goal.args[1])
    if back is None:
        res = machine.call.args[1]
    else:
        back.args[1] = machine.call.args[1]
    cont(res)


###
# '$ALT'(L): internal only
# The built-in alternatively adds the variants L to the
# continuation and succeeds.
##
def special_alt(args):
    goal = deref(args[0])
    return solve_alt(goal, -1, None)


###
# Alternatively adds the variants to the continuation.
#
# @param goal The variants.
# @param at This argument is ignored.
# @param choice The choice point for reuse or null.
# @return True if a variant could be added, otherwise false.
##
def solve_alt(goal, at, choice):
    if (is_structure(goal) and
            goal.functor == "." and
            len(goal.args) == 2):
        mark = machine.trail
        peek = deref(goal.args[0])
        temp = deref(peek.args[0])
        solve_mark(temp)
        goal = deref(goal.args[1])
        if (is_structure(goal) and
                goal.functor == "." and
                len(goal.args) == 2):
            if choice is None:
                choice = Choice(solve_alt, goal, -1, mark)
            else:
                choice.data = goal
            more(choice)
        temp = deref(peek.args[1])
        solve_seq(temp)
        return True
    else:
        return False


################################################################
# sys_raise/1, sys_trap/3                                      #
################################################################

###
# sys_raise(E): internal only
# The predicate raises the exception E.
##
def test_sys_raise(args):
    problem = exec_build(args[0])
    raise Exception(copy_term(problem))


###
# sys_trap(G, E, F): internal only
# The built-in succeeds whenever G succeeds. If
# there was an exception that unifies with E, the
# built-in further succeeds whenever F succeeds.
##
def special_sys_trap(args):
    goal = deref(args[0])
    snap = snap_setup()
    goal = Compound(".", [goal, "[]"])
    cont(goal)
    return solve_catch(snap, True, None)


###
# Call, redo or resume a goal.
# If there is an exception put the handler on the continuation.
#
# @param snap The surrounding choice point.
# @param found The call or redo flag.
# @param choice The choice point for reuse or null.
# @return True if goal succeeds, otherwise false.
##
def solve_catch(snap, found, choice):
    if choice is not None:
        choice.mark = None
        choice.cont = "[]"
        choice.tail = None
    try:
        found = solve(snap, found)
    except Exception as err:
        snap_cleanup(snap)
        goal = deref(machine.call.args[0])
        err = map_throwable(err)
        if not unify(goal.args[1], err.args[0]):
            raise err from None
        goal = deref(goal.args[2])
        goal = Compound(".", [goal, machine.call.args[1]])
        cont(goal)
        return True
    if found is False:
        return False
    if machine.redo is not snap:
        if choice is None:
            choice = Choice(solve_catch, snap, False, machine.trail)
        else:
            choice.mark = machine.trail
            choice.cont = machine.call
            choice.tail = machine.redo
        more(choice)
    else:
        more(snap.tail)
    if found is True:
        cont(snap.cont.args[1])
    return found


def map_throwable(err):
    if isinstance(err, RecursionError):
        err = make_error(Compound("system_error", ["stack_overflow"]))
    elif isinstance(err, MemoryError):
        err = make_error(Compound("system_error", ["out_of_memory"]))
    return err


################################################################
# os_sleep_promise/2 and os_import_promise/3                   #
################################################################

###
# os_sleep_promise(D, P):
# The predicate succeeds in P with a promise for a delay D.
##
def test_os_sleep_promise(args):
    delay = exec_build(args[0])
    check_integer(delay)
    if delay < 0:
        raise make_error(Compound("domain_error",
              ["not_less_than_zero", delay]))
    buf = machine.ctx
    return exec_unify(args[1], lambda: sleep_promise(buf, delay))


async def sleep_promise(buf, delay):
    register_interrupt(buf, asyncio.current_task().cancel)
    try:
        await asyncio.sleep(delay/1000.0)
    except asyncio.CancelledError:
        pass
    finally:
        register_interrupt(buf, lambda: None)


###
# os_import_promise(F, M, Q): internal only
# The predicate succeeds in Q with with a promise for the
# import M of the file F.
##
def test_os_import_promise(args):
    url = exec_build(args[0])
    check_atom(url)
    res = {}
    if not exec_unify(args[1], res):
        return False
    buf = machine.ctx
    return exec_unify(args[2], lambda: import_promise(buf, url, res))


async def import_promise(buf, url, res):
    name = find_name(url)
    if name is None:
        register_signal(buf,
            Compound("existence_error", ["module", url]))
    else:
        try:
            module = await asyncio.to_thread(blocking_import, name)
            res["module"] = module
        except SyntaxError:
            register_signal(buf,
                Compound("syntax_error", ["bad_module"]))
        except ModuleNotFoundError:
            register_signal(buf,
                Compound("existence_error", ["module", url]))


def blocking_import(name):
    return importlib.import_module(name)


###
# Find a Python module name from an url
#
# @param url The url
# @return The Python module name or None
##
def find_name(url):
    if not url.endswith(".py"):
        return None
    path = find_path(url)
    if path is None:
        return None
    url = url[len(path)+1:len(url)-3]
    url = url.replace("/", ".")
    url = url.replace("\\", ".")
    return url


###
# Find a filepath entry for an url
#
# @param url The url
# @return The filepath entry or None
##
def find_path(url):
    i = 0
    while i < len(sys.path):
        path = sys.path[i]
        if (url.startswith(path) and
                (url.startswith("/", len(path)) or
                 url.startswith("\\", len(path)))):
            return path
        i = i+1
    return None


###
# os_invoke_main(M): internal only
# Invoke the main method of the module M.
##
def test_os_invoke_main(args):
    module = exec_build(args[0])
    module.main()
    return True


################################################################
# '$YIELD'/1, shield/1 and unshield/1                          #
################################################################

###
# '$YIELD'(R): Internal only
# The built-in stops the interpreter loop with return value R.
##
def special_yield(args):
    if (store.engine.flags & SYS_MASK_ALLOW_YIELD) == 0:
        raise make_error(Compound("system_error", ["illegal_yield"]))
    cont(machine.call.args[1])
    more(Choice(solve_signal, None, 0, machine.trail))
    return deref(args[0])


###
# shield(G):
# The predicate succeeds whenever the goal G succeeds.
# The goal is executed without auto-yield.
##
def special_shield(args):
    goal = deref(args[0])
    snap = snap_setup()
    goal = Compound(".", [goal, "[]"])
    cont(goal)
    return solve_shield(snap, True, None)


###
# Call, redo or resume a goal.
# If there is an exception put the handler on the continuation.
#
# @param snap The surrounding choice point.
# @param found The call or redo flag.
# @param choice The choice point for reuse or null.
# @return True if goal succeeds, otherwise false.
##
def solve_shield(snap, found, choice):
    if choice is not None:
        choice.mark = None
        choice.cont = "[]"
        choice.tail = None
    back = store.engine.flags & SYS_MASK_ASYNC_MODE
    store.engine.flags &= ~SYS_MASK_ASYNC_MODE
    try:
        found = solve(snap, found)
    except Exception as err:
        store.engine.flags &= ~SYS_MASK_ASYNC_MODE
        store.engine.flags |= back
        snap_cleanup(snap)
        raise err from None
    store.engine.flags &= ~SYS_MASK_ASYNC_MODE
    store.engine.flags |= back
    if found is False:
        return False
    if machine.redo is not snap:
        if choice is None:
            choice = Choice(solve_shield, snap, False, machine.trail)
        else:
            choice.mark = machine.trail
            choice.cont = machine.call
            choice.tail = machine.redo
        more(choice)
    else:
        more(snap.tail)
    if found is True:
        cont(snap.cont.args[1])
    return found


###
# unshield(G):
# The predicate succeeds whenever the goal G succeeds.
# The goal is executed with auto-yield.
##
def special_unshield(args):
    goal = deref(args[0])
    snap = snap_setup()
    goal = Compound(".", [goal, "[]"])
    cont(goal)
    return solve_unshield(snap, True, None)


###
# Call, redo or resume a goal.
# If there is an exception put the handler on the continuation.
#
# @param snap The surrounding choice point.
# @param found The call or redo flag.
# @param choice The choice point for reuse or null.
# @return True if goal succeeds, otherwise false.
##
def solve_unshield(snap, found, choice):
    if choice is not None:
        choice.mark = None
        choice.cont = "[]"
        choice.tail = None
    back = store.engine.flags & SYS_MASK_ASYNC_MODE
    store.engine.flags |= SYS_MASK_ASYNC_MODE
    try:
        found = solve(snap, found)
    except Exception as err:
        store.engine.flags &= ~SYS_MASK_ASYNC_MODE
        store.engine.flags |= back
        snap_cleanup(snap)
        raise err from None
    store.engine.flags &= ~SYS_MASK_ASYNC_MODE
    store.engine.flags |= back
    if found is False:
        return False
    if machine.redo is not snap:
        if choice is None:
            choice = Choice(solve_unshield, snap, False, machine.trail)
        else:
            choice.mark = machine.trail
            choice.cont = machine.call
            choice.tail = machine.redo
        more(choice)
    else:
        more(snap.tail)
    if found is True:
        cont(snap.cont.args[1])
    return found


###
# call(A): [ISO 7.8.3]
# The predicate succeeds whenever the goal A succeeds.
##
def special_call(args):
    goal = deref(args[0])
    goal = Compound(".", [goal, machine.call.args[1]])
    cont(goal)
    return True


################################################################
# Type Assertions                                              #
################################################################

###
# Assure that the object is an atom.
#
# @param beta The object.
##
def check_atom(beta):
    if not is_atom(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["atom", beta]))


###
# Assure that the object is a number.
#
# @param beta The object.
##
def check_number(beta):
    if not is_number(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["number", beta]))


###
# Assure that the object is an integer.
#
# @param beta The object.
##
def check_integer(beta):
    if not is_integer(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["integer", beta]))


###
# Assure that the object is atomic.
#
# @param beta The object.
##
def check_atomic(beta):
    if is_variable(beta) or is_structure(beta):
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["atomic", beta]))


###
# Assure that the object is nil.
#
# @param beta The object.
##
def check_nil(beta):
    if beta != "[]":
        if (is_structure(beta) and beta.functor == "."
                and len(beta.args) == 2):
            raise make_error(Compound("representation_error",
                              ["max_arity"]))
        else:
            check_nonvar(beta)
            beta = copy_term(beta)
            raise make_error(Compound("type_error",
                              ["list", beta]))


################################################################
# =/2 and copy_term/2                                          #
################################################################

###
# S = T: [ISO 8.2.1]
# The built-in succeeds when the Prolog terms S and T unify,
# otherwise the built-in fails.
##
def test_unify(args):
    alpha = exec_build(args[0])
    return exec_unify(args[1], alpha)


###
# copy_term(S, T): [ISO 8.5.4]
# The built-in succeeds in T with a copy of S.
##
def test_copy_term(args):
    alpha = exec_build(args[0])
    alpha = copy_term(alpha)
    return exec_unify(args[1], alpha)


################################################################
# =../2, functor/3 and arg/3                                   #
################################################################

###
# T =.. [F|L]: [ISO 8.5.3]
# If T is a variable, the built-in succeeds in T with the Prolog term
# from the functor F and arguments L. Otherwise the built-in succeeds in
# F and L with the functor and arguments of the Prolog term T.
##
def test_univ(args):
    alpha = exec_deref(args[0])
    if is_variable(alpha) or is_pending(alpha):
        beta = exec_build(args[1])
        beta = special_univ_pack(beta)
        return exec_unify(alpha, beta)
    else:
        alpha = exec_build(alpha)
        alpha = special_univ_unpack(alpha)
        return exec_unify(args[1], alpha)


def special_univ_pack(beta):
    if (is_structure(beta) and
            "." == beta.functor and
            len(beta.args) == 2):
        pass
    else:
        check_nonvar(beta)
        beta = copy_term(beta)
        raise make_error(Compound("type_error", ["list", beta]))
    functor = deref(beta.args[0])
    check_atomic(functor)
    beta = deref(beta.args[1])
    args = list_objects(beta)
    if len(args) == 0:
        pass
    else:
        if is_number(functor):
            raise make_error(Compound("type_error", ["atom", functor]))
        functor = Compound(functor, args)
    return functor


def list_objects(obj):
    peek = obj
    arity = 0
    while (is_structure(peek) and
            "." == peek.functor and
            len(peek.args) == 2 and
            arity < MAX_ARITY):
        arity += 1
        peek = deref(peek.args[1])
    check_nil(peek)
    if arity == 0:
        return VOID_ARGS
    else:
        args = [NotImplemented] * arity
        peek = obj
        arity = 0
        while (is_structure(peek) and
                "." == peek.functor and
                len(peek.args) == 2):
            args[arity] = deref(peek.args[0])
            arity += 1
            peek = deref(peek.args[1])
        return args


def special_univ_unpack(alpha):
    if is_structure(alpha):
        temp = alpha.args
        alpha = alpha.functor
        return Compound(".", [alpha,
            objects_list(temp, 0, len(temp))])
    else:
        return Compound(".", [alpha, "[]"])


def objects_list(args, off, count):
    res = "[]"
    i = off + count - 1
    while i >= off:
        res = Compound(".", [args[i], res])
        i -= 1
    return res


###
# functor(T, F, A): [ISO 8.5.1]
# If T is a variable, the built-in succeeds in T with a new Prolog term
# from the functor F and the arity A. Otherwise the built-in succeeds in
# F and L with the functor and arguments of the Prolog term T.
##
def test_functor(args):
    alpha = exec_deref(args[0])
    if is_variable(alpha) or is_pending(alpha):
        functor = exec_build(args[1])
        check_atomic(functor)
        arity = exec_build(args[2])
        check_integer(arity)
        if arity < 0:
            raise make_error(Compound("domain_error",
                     ["not_less_than_zero", arity]))
        if arity > MAX_ARITY:
            raise make_error(Compound("representation_error",
                     ["max_arity"]))
        if arity == 0:
            pass
        else:
            if is_number(functor):
                raise make_error(Compound("type_error", ["atom", functor]))
            temp = [NotImplemented] * arity
            i = 0
            while i < arity:
                temp[i] = Variable()
                i += 1
            functor = Compound(functor, temp)
        return exec_unify(alpha, functor)
    else:
        alpha = exec_build(alpha)
        if is_structure(alpha):
            functor = alpha.functor
            arity = len(alpha.args)
        else:
            functor = alpha
            arity = 0
        if not exec_unify(args[1], functor):
            return False
        return exec_unify(args[2], arity)


###
# arg(K, X, Y): [ISO 8.5.2]
# The predicate succeeds in Y with the K-th argument of X.
##
def test_arg(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    beta = exec_build(args[1])
    check_callable(beta)
    if is_structure(beta):
        arity = len(beta.args)
    else:
        arity = 0
    if alpha < 1 or arity < alpha:
        return False
    beta = beta.args[alpha - 1]
    return exec_unify(args[2], beta)


################################################################
# change_arg/3                                                 #
################################################################

###
# change_arg(K, X, Y):
# The predicate succeeds. As a side-effect the K-th argument of X is set to Y.
##
def test_change_arg(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    beta = exec_build(args[1])
    check_callable(beta)
    gamma = exec_build(args[2])
    if is_structure(beta):
        arity = len(beta.args)
    else:
        arity = 0
    if alpha < 1 or arity < alpha:
        return False
    linkarg(gamma, beta, alpha)
    return True


def linkarg(source, term, pos):
    if is_frozen(term):
        raise make_error(Compound("permission_error",
                  ["modify", "compound", term]))
    if (term.walk & VAR_MASK_STATE) == machine.gc_mask:
        mark2_term(source)
    term.args[pos-1] = source


################################################################
# term_variables/3                                             #
################################################################

###
# term_variables(T, L, E): [TC2 8.5.5]
# The built-in succeeds in L with a list with end E and
# with the variables of T as its list elements.
##
def test_term_variables(args):
    alpha = exec_build(args[0])
    res = None
    back = None

    def term_variables2(node):
        nonlocal res
        nonlocal back
        peek = Compound(".", [node, NotImplemented])
        if back is None:
            res = peek
        else:
            back.args[1] = peek
        back = peek
        return False

    walk_vars(alpha, term_variables2, VAR_MASK_SEEN)
    if back is None:
        res = "[]"
    else:
        back.args[1] = "[]"
    walk_vars(alpha, lambda node: False, 0)
    return exec_unify(args[1], res)


def walk_vars(first, acceptor, state):
    stack = None
    while True:
        first = deref(first)
        if is_variable(first):
            if (first.flags & VAR_MASK_SEEN) != state:
                if acceptor(first):
                    return True
                first.flags = (first.flags & ~VAR_MASK_SEEN) | state
        elif is_compound(first):
            if (first.walk & VAR_MASK_SEEN) != state:
                first.walk = (first.walk & ~VAR_MASK_SEEN) | state
                if 0 != len(first.args) - 1:
                    first.walk &= ~VAR_MASK_SERNO
                    stack = stack_push(stack, first)
                first = first.args[0]
                continue
        item = stack_peek(stack)
        if item is None:
            return False
        else:
            item.walk += 1
            first = item.args[item.walk & VAR_MASK_SERNO]
            if (item.walk & VAR_MASK_SERNO) == len(item.args) - 1:
                stack_pop(stack)


################################################################
# reference/1                                                  #
################################################################

###
# reference(A):
# The built-in succeeds if A is a Prolog reference. Otherwise, it fails.
##
def test_reference(args):
    alpha = exec_build(args[0])
    if is_variable(alpha) or is_structure(alpha):
        return False
    if is_atom(alpha) or is_number(alpha):
        return False
    return True


################################################################
# callable/1, var/1, nonvar/1, compound/1, atomic/1 and atom/1 #
################################################################

###
# callable(C): [TC2 8.3.9]
# The built-in succeeds if C is a Prolog compound or symbol. Otherwise, it fails.
##
def test_callable(args):
    alpha = exec_build(args[0])
    if is_variable(alpha) or is_number(alpha):
        return False
    return True


###
# var(V): [ISO 8.3.1]
# The built-in succeeds if V is a Prolog variable. Otherwise, it fails.
##
def test_var(args):
    alpha = exec_build(args[0])
    return is_variable(alpha)


###
# nonvar(V): [ISO 8.3.7]
# The built-in succeeds if V is not a Prolog variable. Otherwise, it fails.
##
def test_nonvar(args):
    alpha = exec_build(args[0])
    return not is_variable(alpha)


###
# compound(C): [ISO 8.3.6]
# The built-in succeeds if C is a Prolog compound. Otherwise, it fails.
##
def test_compound(args):
    alpha = exec_build(args[0])
    return is_structure(alpha)


###
# atomic(A): [ISO 8.3.5]
# The built-in succeeds if A is a Prolog symbol or number. Otherwise, it fails.
##
def test_atomic(args):
    alpha = exec_build(args[0])
    if is_structure(alpha) or is_variable(alpha):
        return False
    return True


###
# atom(A): [ISO 8.3.2]
# The built-in succeeds if A is a Prolog atom. Otherwise, it fails.
##
def test_atom(args):
    alpha = exec_build(args[0])
    return is_atom(alpha)


################################################################
# number/1, integer/1, float/1                                 #
################################################################

###
# number(A): [ISO 8.3.8]
# The built-in succeeds if A is a Prolog number. Otherwise, it fails.
##
def test_number(args):
    alpha = exec_build(args[0])
    return is_number(alpha)


###
# integer(A): [ISO 8.3.3]
# The built-in succeeds if A is a Prolog integer. Otherwise, it fails.
##
def test_integer(args):
    alpha = exec_build(args[0])
    return is_integer(alpha)


###
# float(A): [ISO 8.3.4]
# The built-in succeeds if A is a Prolog float. Otherwise, it fails.
##
def test_float(args):
    alpha = exec_build(args[0])
    return is_float(alpha)


################################################################
# Number Utility                                               #
################################################################

def narrow_float(alpha):
    if is_integer(alpha):
        try:
            return float(alpha)
        except OverflowError:
            raise make_error(Compound("evaluation_error", ["float_overflow"]))
    else:
        return alpha


################################################################
# code_category/2 and code_numeric/2                           #
################################################################

###
# code_category(C, T):
# The predicate succeeds in T with the Unicode general category of C.
# Otherwise, the predicate succeeds in T with 0.
##
def test_code_category(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    if alpha < 0 or alpha > 0x10FFFF:
        alpha = 0  # UNASSIGNED
    else:
        alpha = code_category(alpha)
    return exec_unify(args[1], alpha)


###
# code_numeric(C, V):
# The predicate succeeds in V with the Unicode numeric value of C,
# in case it is integer and between 0 and 35. Otherwise, the predicate
# succeeds in V with -1.
##
def test_code_numeric(args):
    alpha = exec_build(args[0])
    check_integer(alpha)
    if alpha < 0 or alpha > 0x10FFFF:
        alpha = -1  # UNASSIGNED
    else:
        alpha = code_numeric(alpha)
    return exec_unify(args[1], alpha)


################################################################
# atom_integer/3                                               #
################################################################

###
# atom_integer(A, R, N):
# If A is a variable, then the built-in succeeds in A with the
# atom for the Prolog integer N in radix R. Otherwise the
# built-in succeeds in N with the Prolog number from the
# atom A in radix R.
##
def test_atom_integer(args):
    radix = exec_build(args[1])
    check_integer(radix)
    if radix < 2 or radix > 36:
        raise make_error(Compound("domain_error", ["radix", radix]))
    text = exec_deref(args[0])
    if is_variable(text) or is_pending(text):
        beta = exec_build(args[2])
        check_integer(beta)
        if beta >= 0:
            beta = atom_integer_encode(beta, radix)
        else:
            beta = "-" + atom_integer_encode(- beta, radix)
        return exec_unify(text, beta)
    else:
        text = exec_build(text)
        check_atom(text)
        text = atom_integer_decode(text, radix)
        return exec_unify(args[2], text)


###
# Encode a Prolog integer to a string.
#
# @param num The Prolog integer.
# @param radix The radix.
# @return The string.
##
def atom_integer_encode(num, radix):
    if radix == 10:
        return str(num)
    if num.bit_length() < 32:
        return str_integer(num, radix)
    else:
        step = 32 // (radix - 1).bit_length()
        base = radix ** step
        count = 0
        res = str_integer(num % base, radix)
        num = num // base
        while num != 0:
            count += step
            res = res.rjust(count, "0")
            res = str_integer(num % base, radix) + res
            num = num // base
        return res


def str_integer(num, radix):
    res = chr_digit(num % radix)
    num = num // radix
    while num != 0:
        res = chr_digit(num % radix) + res
        num = num // radix
    return res


def chr_digit(num):
    if num < 10:
        return chr(num + 48)
    else:
        return chr(num + 87)


###
# Decode a Prolog integer from a string.
#
# @param text The string
# @param radix The radix.
# @param signed The signed flag.
# @return The Prolog integer.
##
def atom_integer_decode(text, radix):
    text = ascii_replace(text, radix, False)
    try:
        return int(text, radix)
    except ValueError:
        raise make_error(Compound("syntax_error", ["illegal_number"]))


###
# Convert and validate Unicode number values
# into capital ASCII number value.
#
# @param text The string.
# @param radix The radix.
# @param expo The float flag
# @return The The new string.
##
def ascii_replace(text, radix, expo):
    buf = ""
    last = 0
    pos = 0
    while pos < len(text):
        ch = ord(text[pos])
        val = code_numeric(ch)
        if val >= 0 and val < radix:
            if ch <= 127:  # ASCII
                pass
            else:
                if val < 10:
                    val += 48  # '0'
                else:
                    val += 55  # 'A'-10
                buf += text[last:pos] + chr(val)
                last = pos + 1
        elif (ch == 43 or ch == 45) and (pos == 0 or
                                         (expo and (ord(text[pos-1]) == 101 or ord(text[pos-1]) == 69))):
            pass
        elif expo and (ch == 46 or ch == 101 or ch == 69):
            pass
        else:
            raise make_error(Compound("syntax_error", ["illegal_number"]))
        pos += 1
    if last != 0:
        buf += text[last:]
        return buf
    else:
        return text


################################################################
# atom_number/2                                                #
################################################################

###
# atom_number(A, N):
# If A is a variable, then the built-in succeeds in A with the
# atom for the Prolog number N. Otherwise the built-in succeeds in N
# with the Prolog number from the atom A.
##
def test_atom_number(args):
    text = exec_deref(args[0])
    if is_variable(text) or is_pending(text):
        beta = exec_build(args[1])
        check_number(beta)
        beta = atom_number_encode(beta)
        return exec_unify(text, beta)
    else:
        text = exec_build(text)
        check_atom(text)
        text = atom_number_decode(text)
        return exec_unify(args[1], text)


###
# Encode a Prolog number to a string.
#
# @param num The Prolog number.
# @return The string.
##
def atom_number_encode(num):
    if is_integer(num):
        return str(num)
    else:
        res = "%.16g" % num
        if float(res) != num:
            res = "%.17g" % num
        return shape_number(res)


###
# Shape the number string so that it has always a period,
# no exponent positive sign, no exponent leading zeros and
# lower case exponent.
#
# @param res The ascii number string.
# @return The shaped number string.
##
def shape_number(res):
    peek = res.find("e")
    if peek != -1:
        res = shape_number_mantissa(res[0:peek]) + \
              "e" + shape_number_exponent(res[peek+1:])
    else:
        res = shape_number_mantissa(res)
    return res


def shape_number_mantissa(res):
    if "." not in res:
        res += ".0"
    return res


def shape_number_exponent(res):
    if res.startswith("+"):
        res = res[1:]
    if res.startswith("0") and 1 < len(res):
        res = res[1:]
    elif res.startswith("-0") and 2 < len(res):
        res = "-" + res[2:]
    return res


###
# Decode a Prolog number from a string. Unlike the ISO
# core standard and numbers without a period but with
# an exponent are accepted as float.
#
# @param str The string
# @return The Prolog number.
##
def atom_number_decode(text):
    text = ascii_replace(text, 10, True)
    if "." in text or "e" in text or "E" in text:
        try:
            return float(text)
        except ValueError:
            raise make_error(Compound("syntax_error", ["illegal_number"]))
    else:
        try:
            return int(text)
        except ValueError:
            raise make_error(Compound("syntax_error", ["illegal_number"]))


################################################################
# atom_reference/2                                             #
################################################################

###
# atom_reference(A, R):
# The built-in succeeds in A with the atom for the Prolog reference R.
##
def test_atom_reference(args):
    text = exec_deref(args[0])
    if is_variable(text) or is_pending(text):
        obj = exec_build(args[1])
        if obj is True:
            obj = "True"
        elif obj is False:
            obj = "False"
        elif obj is None:
            obj = "None"
        else:
            obj = "Reference"
        return exec_unify(text, obj)
    else:
        text = exec_build(text)
        check_atom(text)
        if text == "True":
            obj = True
        elif text == "False":
            obj = False
        elif text == "None":
            obj = None
        else:
            obj = NotImplemented
        if obj is NotImplemented:
            raise make_error(Compound("syntax_error", ["illegal_reference"]))
        return exec_unify(args[1], obj)


################################################################
# sys_get_locale/1 and sys_get_args/1                          #
################################################################

###
# sys_get_locale(L): Internal only
# The built-in succeeds in L with the initial locale
# of the Python environment.
##
def test_sys_get_locale(args):
    loc = locale.getlocale()[0]
    return exec_unify(args[0], loc if loc is not None else "")


###
# sys_get_args(L): Internal only
# The built-in succeeds in L with the initial command
# line arguments of the Python environment.
##
def test_sys_get_args(args):
    res = objects_list(sys.argv, 1, len(sys.argv)-1)
    return exec_unify(args[0], res)


################################################################
# Special Init                                                 #
################################################################

# Albufeira compiler, deterministic
add("fail", 0, make_check(test_fail))
add("$CUT", 1, make_check(test_cut))
add("$MARK", 1, make_check(test_mark))
add("$SEQ", 2, make_special(special_seq))
add("$ALT", 1, make_special(special_alt))
add("sys_raise", 1, make_check(test_sys_raise))
add("sys_trap", 3, make_special(special_sys_trap))

# Albufeira compiler, for call/1
add("os_sleep_promise", 2, make_check(test_os_sleep_promise))
add("os_import_promise", 3, make_check(test_os_import_promise))
add("os_invoke_main", 1, make_check(test_os_invoke_main))
add("$YIELD", 1, make_special(special_yield))
add("shield", 1, make_special(special_shield))
add("unshield", 1, make_special(special_unshield))
add("call", 1, make_special(special_call))

# term specials
add("=", 2, make_check(test_unify))
add("copy_term", 2, make_check(test_copy_term))
add("=..", 2, make_check(test_univ))
add("functor", 3, make_check(test_functor))
add("arg", 3, make_check(test_arg))
add("change_arg", 3, make_check(test_change_arg))
add("term_variables", 2, make_check(test_term_variables))
add("reference", 1, make_check(test_reference))

# type specials
add("callable", 1, make_check(test_callable))
add("var", 1, make_check(test_var))
add("nonvar", 1, make_check(test_nonvar))
add("compound", 1, make_check(test_compound))
add("atomic", 1, make_check(test_atomic))
add("atom", 1, make_check(test_atom))
add("number", 1, make_check(test_number))
add("integer", 1, make_check(test_integer))
add("float", 1, make_check(test_float))

# atom specials
add("code_category", 2, make_check(test_code_category))
add("code_numeric", 2, make_check(test_code_numeric))
add("atom_integer", 3, make_check(test_atom_integer))
add("atom_number", 2, make_check(test_atom_number))
add("atom_reference", 2, make_check(test_atom_reference))

# locale specials
add("sys_get_locale", 1, make_check(test_sys_get_locale))
add("sys_get_args", 1, make_check(test_sys_get_args))
