public class Bindings {

    private static class BindingMap extends IdentityHashMap<Symbol, BoundEntryList> {

        private static final long serialVersionUID = 7424234288480959898L;

        @Override
        public BoundEntryList get(Object key) {
            BoundEntryList value = super.get(key);
            return value != null ? value : EMPTY_LIST;
        }
    }

    private static class BoundEntryList extends ArrayList<BoundEntry> {

        private static final long serialVersionUID = -2495819279714766796L;

        public BindingMap namespacedMap = EMPTY_MAP;
        public boolean hasVarArgs = false;

        @Override
        public BoundEntry get(int index) {
            return (index < size()) ? super.get(index) : NOT_FOUND;
        }

        @Override
        public BoundEntry set(int index, BoundEntry entry) {
            int size = size();
            if (index < size)
                return super.set(index, entry);
            while (size++ < index)
                add(NOT_FOUND);
            add(entry);
            return NOT_FOUND;
        }
    }

    private static final BoundEntry NOT_FOUND = new BoundEntry(NIL, 0);

    private static final BindingMap EMPTY_MAP = new BindingMap() {

        private static final long serialVersionUID = -3318238367668191888L;

        @Override
        public BoundEntryList get(Object key) {
            return EMPTY_LIST;
        }

        @Override
        public BoundEntryList put(Symbol key, BoundEntryList value) {
            throw new EvalException("EMPTY_MAP is not modifiable.");
        }
    };

    // "new BoundEntryList()" requires instanciated EMPTY_MAP.
    private static final BoundEntryList EMPTY_LIST = new BoundEntryList() {

        private static final long serialVersionUID = 3867360322809458687L;

        @Override
        public BoundEntry get(int index) {
            return NOT_FOUND;
        }

        @Override
        public BoundEntry set(int index, BoundEntry entry) {
            throw new EvalException("EMPTY_LIST is not modifiable.");
        }
    };

    public static boolean exists(BoundEntry entry) {
        return entry != NOT_FOUND;
    }

    private final Bindings parent;
    private final BindingMap map = new BindingMap();

    public Bindings(Bindings parent) {
        this.parent = parent;
        BoundEntryList nsEntry = new BoundEntryList();
        nsEntry.namespacedMap = map;
        map.put(NIL, nsEntry);
        bind(NIL, NIL, 0, NIL, READ_ONLY);
        bind(T, NIL, 0, T, READ_ONLY);
    }

    public boolean isTopLevel() {
        return parent == null;
    }

    public IndyObject bind(Symbol name, Symbol namespace, int arity, IndyObject value, int flags) {
        BoundEntryList nsEntry = map.get(namespace);
        if (nsEntry == EMPTY_LIST)
            map.put(namespace, nsEntry = new BoundEntryList());
        BindingMap lookupMap = nsEntry.namespacedMap;
        if (lookupMap == EMPTY_MAP)
            nsEntry.namespacedMap = lookupMap = new BindingMap();
        BoundEntryList entries = lookupMap.get(name);
        if (entries == EMPTY_LIST)
            lookupMap.put(name, entries = new BoundEntryList());
        BoundEntry entry = entries.get(arity);
        if (entry.isReadOnly())
            throw new EvalException(
                String.format("%s:%s:%d is read only. current value: %s / new value: %s",
                    namespace, name, arity, entry.getValue(), value));
        entry = new BoundEntry(value, flags);
        entries.set(arity, entry);
        if (entry.isVarArgs())
            entries.hasVarArgs = true;
        return value;
    }

    private BoundEntry lookupInternal(Symbol name, Symbol namespace, int arity) {
        BoundEntryList entries = map.get(namespace).namespacedMap.get(name);
        BoundEntry entry = entries.get(arity);
        if (entry != NOT_FOUND || !entries.hasVarArgs)
            return entry;
        for (int i = Math.min(arity - 1, entries.size() - 1); i >= 0; --i) {
            entry = entries.get(i);
            if (entry.isVarArgs())
                return entry;
        }
        return NOT_FOUND;
    }

    public BoundEntry lookup(Symbol name, Symbol namespace, int arity) {
        Bindings current = this;
        do {
            BoundEntry entry = current.lookupInternal(name, namespace, arity);
            if (entry != NOT_FOUND)
                return entry;
        } while ((current = current.parent) != null);
        return NOT_FOUND;
    }

    public boolean isReadOnly(Symbol name, Symbol namespace, int arity) {
        return map.get(namespace).namespacedMap.get(name).get(arity).isReadOnly();
    }

    @Override
    public String toString() {
        return map.toString();
    }
} public class BoundEntry {

    public static final int READ_ONLY = 0b0000_0001;
    public static final int VAR_ARGS = 0b0000_0010;
    public static final int SPECIAL_FORM = 0b0000_0100;

    private final IndyObject value;
    private final boolean isReadOnly;
    private final boolean isVarArgs;
    private final boolean isSpecialForm;

    private static boolean isTrue(int value, int flags) {
        return (value & flags) != 0;
    }

    BoundEntry(IndyObject value, int type) {
        this.value = value;
        this.isReadOnly = isTrue(type, READ_ONLY);
        this.isVarArgs = isTrue(type, VAR_ARGS);
        this.isSpecialForm = isTrue(type, SPECIAL_FORM);
    }

    public IndyObject getValue() {
        return value;
    }

    public boolean isSpecialForm() {
        return isSpecialForm;
    }

    public boolean isVarArgs() {
        return isVarArgs;
    }

    public boolean isReadOnly() {
        return isReadOnly;
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder("[").append(value.toString());
        if (isSpecialForm)
            buffer.append(":SF");
        if (isVarArgs)
            buffer.append(":VA");
        if (isReadOnly)
            buffer.append(":RO");
        return buffer.append("]").toString();
    }
} public class Converters {

    private static final Logger log = LoggerFactory.getLogger(Converters.class);

    private final Converters global;
    private final Map<Class<?>, MethodHandle> rvMap = new IdentityHashMap<>();
    private final Map<Class<?>, MethodHandle> pvMap = new IdentityHashMap<>();

    protected Converters(Converters global) {
        this.global = global;
    }

    public void putRvConv(Class<?> rtype, MethodHandle mh) {
        if (rvMap.containsKey(rtype))
            throw new EvalException("RvConverter for " + rtype + " is already registered.");
        rvMap.put(rtype, mh);
    }

    public void putPvConv(Class<?> ptype, MethodHandle mh) {
        if (pvMap.containsKey(ptype))
            throw new EvalException("PvConverter for " + ptype + " is already registered.");
        pvMap.put(ptype, mh);
    }

    public MethodHandle getRvConv(Class<?> rtype) {
        MethodHandle result = rvMap.get(rtype);
        if (result != null)
            return result;
        else if (global != null)
            return global.getRvConv(rtype);
        else
            return null;
    }

    public MethodHandle getPvConv(Class<?> ptype) {
        MethodHandle result = pvMap.get(ptype);
        if (result != null)
            return result;
        else if (global != null)
            return global.getPvConv(ptype);
        else
            return null;
    }

    public void registerConverters(Lookup lookup, Class<?> convClass) {
        registerConverters(lookup, convClass, Converter.class);
    }

    protected void registerConverters(Lookup lookup, Class<?> convClass, Class<? extends Annotation> anno) {
        if (log.isDebugEnabled()) {
            log.debug("Converter: {} ({})", convClass.getSimpleName(), convClass.getPackage());
            log.debug("  Annotation: @{}", anno.getSimpleName());
        }
        Method[] methods = convClass.getDeclaredMethods();
        for (Method method : methods) {
            if (!method.isAnnotationPresent(anno)) {
                log.debug("  Skip (no annotation): {}", signature(method));
                continue;
            }
            int mod = method.getModifiers();
            if (!Modifier.isPublic(mod) || !Modifier.isStatic(mod)) {
                log.warn("  Skip (not public static): {}", signature(method));
                continue;
            }
            Class<?> rtype = method.getReturnType();
            if (rtype == void.class || rtype == Void.class) {
                log.warn("  Skip (void or Void): {}", signature(method));
                continue;
            }
            Class<?>[] ptypes = method.getParameterTypes();
            if (ptypes.length != 1) {
                log.warn("  Skip (not single parameter): {}", signature(method));
                continue;
            }
            registerConverterMethod(lookup, method, rtype, ptypes[0]);
        }
        if (log.isDebugEnabled())
            log.debug("Converter: Done.");
    }

    protected MethodHandle getArraytoIndyObject() {
        return global.getArraytoIndyObject();
    }

    protected MethodHandle getIndyObjectsToArray() {
        return global.getIndyObjectsToArray();
    }

    private void registerConverterMethod(Lookup lookup, Method method, Class<?> rtype, Class<?> ptype) {
        try {
            if (IndyObject.class.isAssignableFrom(rtype)) {
                // Java object to IndyObject
                MethodHandle mh = lookup.unreflect(method);
                putRvConv(ptype, mh);
                Class<?> aptype = Array.newInstance(ptype, 0).getClass();
                MethodHandle amh = MethodHandles.insertArguments(getArraytoIndyObject(), 0, mh)
                    .asType(MethodType.methodType(IndyObject.class, aptype));
                putRvConv(aptype, amh);
                log.debug("  J->I: {}", signature(method));
            } else if (IndyObject.class.isAssignableFrom(ptype)) {
                // IndyObject to Java object
                MethodHandle mh = lookup.unreflect(method);
                putPvConv(rtype, mh);
                Class<?> artype = Array.newInstance(rtype, 0).getClass();
                MethodHandle amh = MethodHandles.insertArguments(getIndyObjectsToArray(), 0, mh, rtype)
                    .asType(MethodType.methodType(artype, IndyObject[].class))
                    .asVarargsCollector(IndyObject[].class);
                putPvConv(artype, amh);
                log.debug("  I->J: {}", signature(method));
            } else {
                log.debug("  Skip: {}", signature(method));
            }
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
} public final class GlobalConverters extends Converters {

    final MethodHandle arraytoIndyObject;
    final MethodHandle indyObjectsToArray;

    public GlobalConverters() {
        super(null);
        Lookup lookup = MethodHandles.publicLookup();
        Class<?> thisClass = getClass();
        try {
            arraytoIndyObject = lookup.findStatic(thisClass, "arrayToIndyObject",
                methodType(IndyObject.class, MethodHandle.class, Object[].class));
            indyObjectsToArray = lookup.findStatic(thisClass, "indyObjectsToArray",
                methodType(Object[].class, MethodHandle.class, Class.class, IndyObject[].class));
            MethodHandle voidToIndyObject = lookup.findStatic(thisClass, "voidToIndyObject",
                methodType(IndyObject.class));
            putRvConv(void.class, voidToIndyObject);
        } catch (NoSuchMethodException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        registerConverters(lookup, thisClass, GlobalConverter.class);
        registerConverters(lookup, StringWrapper.class, GlobalConverter.class);
        registerConverters(lookup, NumberWrapper.class, GlobalConverter.class);
    }

    public Converters newConverters() {
        return new Converters(this);
    }

    @Override
    protected MethodHandle getArraytoIndyObject() {
        return arraytoIndyObject;
    }

    @Override
    protected MethodHandle getIndyObjectsToArray() {
        return indyObjectsToArray;
    }

    public static IndyObject arrayToIndyObject(MethodHandle rvConv, Object[] array) {
        CellList result = new CellList();
        try {
            for (Object elem : array)
                result.append((IndyObject) rvConv.invoke(elem));
        } catch (Throwable t) {
            throw new EvalException(t);
        }
        return result.toCell();
    }

    public static Object[] indyObjectsToArray(MethodHandle pvConv, Class<?> ctype, IndyObject... objs) {
        try {
            Object[] array = (Object[]) Array.newInstance(ctype, objs.length);
            for (int i = 0; i < objs.length; i++)
                array[i] = pvConv.invoke(objs[i]);
            return array;
        } catch (RuntimeException e) {
            throw e;
        } catch (Throwable t) {
            throw new RuntimeException(t);
        }
    }

    public static IndyObject voidToIndyObject() {
        return T;
    }

    @GlobalConverter
    public static IndyObject booleanToIndyObject(boolean value) {
        return value ? T : NIL;
    }

    @GlobalConverter
    public static boolean indyObjectToBoolean(IndyObject value) {
        return !value.isNil();
    }

    @GlobalConverter
    public static Object indyObjectToObject(IndyObject value) {
        return (value instanceof Wrapper<?>) ? value.getValue() : value;
    }
} public class Importer {

    private static final Logger log = LoggerFactory.getLogger(Importer.class);

    private static final String initImport = "initImport";
    private static final List<Class<?>[]> ALL = new ArrayList<>();

    private final Engine engine;
    private final Bindings bindings;
    private final Lookup lookup;
    private final Class<?> targetClass;
    private Symbol namespace = null;
    private Class<?> wrappedClass = null;
    private MethodHandle unwrapper = null;
    private final Converters converters;
    private final Map<String, List<Class<?>[]>> excludeMap = new HashMap<>();
    private final Map<String, String> aliasMap = new HashMap<>();

    public static void importClass(Engine engine, Class<?> targetClass) {
        Importer importer = new Importer(engine, targetClass);
        importer.importClass();
    }

    private Importer(Engine engine, Class<?> targetClass) {
        this.engine = engine;
        bindings = engine.getCurrentBindings();
        converters = engine.newConverters();
        lookup = MethodHandles.publicLookup();
        this.targetClass = targetClass;
    }

    public void setNamespace(String namespace) {
        this.namespace = engine.symbol(namespace);
    }

    public void setWrappedClass(Class<?> wrappedClass) {
        this.wrappedClass = wrappedClass;
    }

    public void exclude(String name, Class<?>... ptypes) {
        List<Class<?>[]> list = excludeMap.get(name);
        if (list == null) {
            list = new ArrayList<>();
            excludeMap.put(name, list);
        }
        list.add(ptypes);
    }

    public void excludeAllOf(String name) {
        excludeMap.put(name, ALL);
    }

    private boolean isExcluded(Method method) {
        if (method.isAnnotationPresent(Exclude.class))
            return true;
        String name = method.getName();
        List<Class<?>[]> ptypesList = excludeMap.get(name);
        if (ptypesList == null)
            return false;
        else if (ptypesList == ALL)
            return true;
        Class<?>[] ptypes = method.getParameterTypes();
        for (Class<?>[] exclPTypes : ptypesList)
            if (Arrays.equals(ptypes, exclPTypes))
                return true;
        return false;
    }

    public void addAlias(String origin, String alias) {
        aliasMap.put(origin, alias);
    }

    private Symbol getMethodName(Method method, boolean isStatic) {
        Alias alias = method.getAnnotation(Alias.class);
        if (alias != null)
            return engine.symbol(alias.value());
        String origin = method.getName();
        if (isStatic)
            origin = namespace.getValue() + "." + origin;
        String name = aliasMap.get(origin);
        return engine.symbol(name != null ? name : origin);
    }

    private static int getFlags(Method method) {
        int flags = READ_ONLY;
        if (method.isVarArgs())
            flags |= VAR_ARGS;
        if (method.isAnnotationPresent(SpecialForm.class))
            flags |= SPECIAL_FORM;
        return flags;
    }

    public void importClass() {
        try {
            lookup.findStatic(targetClass, initImport, methodType(void.class, Importer.class)).invoke(this);
            if (wrappedClass != null)
                unwrapper = lookup.findVirtual(targetClass, "getValue", methodType(Object.class));
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        if (log.isDebugEnabled())
            log.debug("Import: {} / {}", targetClass.getSimpleName(),
                (wrappedClass != null) ? wrappedClass.getSimpleName() : "-");
        if (namespace == null)
            throw new RuntimeException("Need call setNamespace() in "
                + targetClass.getSimpleName() + "." + initImport + "()");
        converters.registerConverters(lookup, targetClass);
        // register symbol as class name on global binding.
        bindings.bind(namespace, NIL, 0, namespace, READ_ONLY);
        if (wrappedClass != null)
            importWrappedClass();
        else
            importTargetClass();
        if (log.isDebugEnabled())
            log.debug("Import: Done.");
    }

    private void importWrappedClass() {
        Method[] methods = wrappedClass.getMethods(); // all methods are public
        for (Method method : methods) {
            if (isExcluded(method)) {
                log.debug("  Skip: {}", signature(method));
                continue;
            }
            if (!registerWrapperMethod(method))
                registerWrappedMethod(method);
        }
    }

    private boolean registerWrapperMethod(Method method) {
        boolean isStatic = Modifier.isStatic(method.getModifiers());
        int arity = method.getParameterTypes().length;
        MethodHandle mh;
        try {
            Class<?>[] wptypes = new Class<?>[arity];
            Arrays.fill(wptypes, IndyObject.class);
            if (method.isVarArgs())
                wptypes[arity - 1] = IndyObject[].class;
            // get wrapper method
            method = targetClass.getMethod(method.getName(), wptypes);
            mh = lookup.unreflect(method);
        } catch (NoSuchMethodException e) {
            return false;
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
        Symbol name = getMethodName(method, isStatic);
        Symbol className = isStatic ? NIL : namespace;
        if (bindings.isReadOnly(name, className, arity))
            return true; // already registered.
        String signature = signature(method);
        bindings.bind(name, className, arity, new JavaMethod(signature, mh), getFlags(method));
        if (log.isDebugEnabled()) {
            log.debug("  WrapperMethod: {}", signature);
            log.debug("    Registered as {}.", name);
        }
        return true;
    }

    private void registerWrappedMethod(Method method) {
        try {
            boolean isStatic = Modifier.isStatic(method.getModifiers());
            String signature = signature(method);
            if (log.isDebugEnabled())
                log.debug("  WrappedMethod: {}", signature);
            // is convertable return type?
            Class<?> rtype = method.getReturnType();
            MethodHandle rvConv = converters.getRvConv(rtype);
            if (rvConv == null) {
                log.debug("    Can't convert return type {}", rtype.getSimpleName());
                return;
            }
            // is convertable parameter type?
            MethodHandle mh = lookup.unreflect(method);
            List<MethodHandle> pvConvList = new ArrayList<>();
            if (!isStatic) {
                Class<?> recvType = mh.type().parameterType(0);
                pvConvList.add(unwrapper.asType(methodType(recvType, targetClass)));
            }
            Class<?>[] ptypes = method.getParameterTypes();
            for (Class<?> ptype : ptypes) {
                MethodHandle pvConv = converters.getPvConv(ptype);
                if (pvConv == null) {
                    log.debug("    Can't convert parameter type {}", ptype.getSimpleName());
                    return;
                }
                pvConvList.add(pvConv);
            }
            // wrap method
            MethodHandle[] pvConvs = new MethodHandle[pvConvList.size()];
            pvConvs = pvConvList.toArray(pvConvs);
            mh = MethodHandles.filterReturnValue(mh, rvConv);
            mh = MethodHandles.filterArguments(mh, 0, pvConvs);
            if (method.isVarArgs())
                mh = mh.asVarargsCollector(IndyObject[].class);
            JavaMethod javaMethod = new JavaMethod(signature, mh);
            Symbol name = getMethodName(method, isStatic);
            bindings.bind(name, isStatic ? NIL : namespace, ptypes.length, javaMethod, getFlags(method));
            if (log.isDebugEnabled())
                log.debug("    Registered as {}.", name);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private void importTargetClass() {
        Method[] methods = targetClass.getMethods(); // all methods are public
        next: for (Method method : methods) {
            boolean isStatic = Modifier.isStatic(method.getModifiers());
            String signature = signature(method);
            if (isExcluded(method)) {
                log.debug("  Skip: {}", signature);
                continue next;
            }
            Class<?> rtype = method.getReturnType();
            if (!IndyObject.class.isAssignableFrom(rtype))
                continue next;
            Class<?>[] ptypes = method.getParameterTypes();
            int arity = ptypes.length;
            if (arity > 0) {
                Class<?> tail = ptypes[arity - 1];
                if (method.isVarArgs()
                    ? !IndyObject[].class.isAssignableFrom(tail)
                    : !IndyObject.class.isAssignableFrom(tail))
                    continue next;
                for (int i = 0; i <= arity - 2; i++)
                    if (!IndyObject.class.isAssignableFrom(ptypes[i]))
                        continue next;
            }
            try {
                MethodHandle mh = lookup.unreflect(method);
                Symbol className;
                if (mh.type().parameterType(0) == Engine.class) {
                    mh = mh.bindTo(engine);
                    if (method.isVarArgs())
                        mh = mh.asVarargsCollector(ptypes[ptypes.length - 1]);
                    className = NIL; // as function
                    arity = mh.type().parameterCount();
                } else {
                    className = isStatic ? NIL : namespace;
                }
                JavaMethod javaMethod = new JavaMethod(signature, mh);
                Symbol name = getMethodName(method, isStatic);
                bindings.bind(name, className, arity, javaMethod, getFlags(method));
                if (log.isDebugEnabled()) {
                    log.debug("  IndyMethod: {}", signature);
                    log.debug("    Registered as {}.", name);
                }
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }
} public class BufferedReaderWrapper extends Wrapper<BufferedReader> {

    public static final String NAMESPACE = "BufferedReader";

    public BufferedReaderWrapper(BufferedReader value) {
        super(value);
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    @Override
    public String toExprString() {
        return "[" + NAMESPACE + "/" + getValue() + "]";
    }

    public static void initImport(Importer importer) {
        importer.setNamespace(NAMESPACE);
        importer.setWrappedClass(BufferedReader.class);
    }
} public class Cell extends IndyObject {

    public static final String NAMESPACE = "Cell";

    protected IndyObject car;
    protected IndyObject cdr;

    public Cell(IndyObject car, IndyObject cdr) {
        this.car = car;
        this.cdr = cdr;
    }

    public Cell(IndyObject car) {
        this(car, NIL);
    }

    public Cell() {
        this(null);
    }

    @Override
    public boolean isAtom() {
        return false;
    }

    @Override
    public IndyObject car() {
        return car;
    }

    @Override
    public IndyObject cdr() {
        return cdr;
    }

    @Override
    public <T extends IndyObject> T setcar(T car) {
        this.car = car;
        return car;
    }

    @Override
    public <T extends IndyObject> T setcdr(T cdr) {
        this.cdr = cdr;
        return cdr;
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    @Override
    public String toExprString() {
        return toExprString(this);
    }

    @Override
    public String toString() {
        return toExprString(this);
    }

    @Override
    public IndyObject[] toArray() {
        List<IndyObject> list = new ArrayList<>();
        for (IndyObject car : this)
            list.add(car);
        return list.toArray(new IndyObject[list.size()]);
    }

    public static void initImport(Importer importer) {
        importer.setNamespace(NAMESPACE);
    }
} public abstract class IndyObject implements Iterable<IndyObject> {

    //
    // Common methods
    //

    abstract public boolean isAtom();

    public boolean isSymbol() {
        return false;
    }

    public boolean isNil() {
        return false;
    }

    abstract public String getNamespace();

    public IndyObject car() {
        throw new EvalException("this is not cons cell.");
    }

    public IndyObject cdr() {
        throw new EvalException("this is not cons cell.");
    }

    public <T extends IndyObject> T setcar(T car) {
        throw new EvalException("this is not cons cell.");
    }

    public <T extends IndyObject> T setcdr(T cdr) {
        throw new EvalException("this is not cons cell.");
    }

    public Object getValue() {
        throw new EvalException("this is not atom.");
    }

    public IndyObject[] toArray() {
        throw new EvalException("this is not cons cell.");
    }

    public abstract String toExprString();

    //
    // Iterator
    //

    public static class IndyObjectIterator implements Iterator<IndyObject> {

        private IndyObject obj;

        private IndyObjectIterator(IndyObject obj) {
            this.obj = obj;
        }

        @Override
        public boolean hasNext() {
            return !obj.isNil();
        }

        @Override
        public IndyObject next() {
            IndyObject result = obj.car();
            obj = obj.cdr();
            return result;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("remove method is not supported.");
        }
    }

    @Override
    public Iterator<IndyObject> iterator() {
        return new IndyObjectIterator(this);
    }

    //
    // IndyObject to String
    //

    private static ThreadLocal<Map<Cell, Boolean>> scannedCellMap = new ThreadLocal<>();

    public static String toExprString(IndyObject obj) {
        if (obj.isAtom())
            return obj.toExprString();
        Cell cell = (Cell) obj;
        boolean needRemove = false;
        Map<Cell, Boolean> map = scannedCellMap.get();
        if (map == null) {
            map = new IdentityHashMap<>();
            needRemove = true;
            scannedCellMap.set(map);
        } else if (map.containsKey(obj))
            return "...";
        try {
            StringBuilder buf = new StringBuilder("(");
            while (true) {
                map.put(cell, Boolean.TRUE);
                IndyObject car = cell.car();
                if (car == null)
                    buf.append("[null]");
                else if (car == cell)
                    buf.append("[self]");
                else
                    buf.append(car.toExprString());
                IndyObject cdr = cell.cdr();
                if (cdr.isNil())
                    break;
                else if (cdr.isAtom()) {
                    buf.append(" . ").append(cdr.toExprString());
                    break;
                } else if (map.get(cdr) != null) {
                    buf.append(" ...");
                    break;
                }
                buf.append(' ');
                cell = (Cell) cdr;
            }
            buf.append(')');
            return buf.toString();
        } finally {
            if (needRemove)
                scannedCellMap.remove();
        }
    }
} public class Lambda extends IndyObject {

    public static final String NAMESPACE = Lambda.class.getSimpleName();

    private final Bindings parentBindings;
    private final IndyObject argNameList;
    private final IndyObject[] exprList;

    public Lambda(Bindings parentBindings, IndyObject argNameList, IndyObject[] exprList) {
        this.parentBindings = parentBindings;
        this.argNameList = argNameList;
        this.exprList = exprList;
    }

    public Bindings assignParameters(IndyObject[] argList) {
        Bindings newBindings = new Bindings(parentBindings);
        Iterator<IndyObject> iter = argNameList.iterator();
        for (IndyObject arg : argList) {
            if (!iter.hasNext())
                break;
            newBindings.bind((Symbol) iter.next(), NIL, 0, arg, 0);
        }
        while (iter.hasNext())
            newBindings.bind((Symbol) iter.next(), NIL, 0, NIL, 0);
        return newBindings;
    }

    public IndyObject[] getExprList() {
        return exprList;
    }

    @Override
    public boolean isAtom() {
        return true;
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    @Override
    public String toString() {
        return toExprString();
    }

    @Override
    public String toExprString() {
        StringBuilder buffer = new StringBuilder("(lambda ");
        argNameListToExprString(buffer, argNameList);
        buffer.append("\n");
        exprListToExprString(buffer, exprList);
        return buffer.append(")\n").toString();
    }

    public static void argNameListToExprString(StringBuilder buffer, IndyObject argNameList) {
        buffer.append("(");
        boolean needSep = false;
        for (IndyObject argName : argNameList) {
            if (needSep)
                buffer.append(" ");
            else
                needSep = true;
            buffer.append(argName.toString());
        }
        buffer.append(")");
    }

    public static void exprListToExprString(StringBuilder buffer, IndyObject[] exprList) {
        for (IndyObject expr : exprList)
            buffer.append("  ").append(expr.toExprString()).append("\n");
    }
} public class LazyExprList extends IndyObject {

    private final IndyObject[] exprList;

    public LazyExprList(IndyObject... exprList) {
        this.exprList = exprList;
    }

    public IndyObject[] getExprList() {
        return exprList;
    }

    @Override
    public boolean isAtom() {
        return true;
    }

    @Override
    public String getNamespace() {
        return null;
    }

    @Override
    public String toString() {
        return toExprString();
    }

    @Override
    public String toExprString() {
        StringBuilder buffer = new StringBuilder(getClass().getSimpleName()).append("(\n");
        Lambda.exprListToExprString(buffer, exprList);
        return buffer.append(")\n").toString();
    }
} public interface Method {
    IndyObject invoke(IndyObject... args);
} public class Nil extends Symbol {

    private static String NAMESPACE = "Nil";

    private static final IndyObject[] EMPTY_ARRAY = new IndyObject[0];

    Nil() {
        super("nil");
    }

    @Override
    public boolean isNil() {
        return true;
    }

    @Override
    public IndyObject car() {
        return this;
    }

    @Override
    public IndyObject cdr() {
        return this;
    }

    @Override
    public IndyObject[] toArray() {
        return EMPTY_ARRAY;
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    public static void initImport(Importer importer) {
        importer.setNamespace(NAMESPACE);
    }
} public class NumberWrapper extends Wrapper<BigDecimal> {

    public static final String NAMESPACE = "Number";

    private NumberWrapper(BigDecimal value) {
        super(value);
    }

    private NumberWrapper(long value) {
        super(BigDecimal.valueOf(value));
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    @Override
    public String toExprString() {
        return toString();
    }

    @GlobalConverter
    public static NumberWrapper getInstance(BigDecimal value) {
        return new NumberWrapper(value);
    }

    @GlobalConverter
    public static NumberWrapper getInstance(long value) {
        return new NumberWrapper(value);
    }

    @GlobalConverter
    public static NumberWrapper getInstance(int value) {
        return getInstance((long) value);
    }

    @GlobalConverter
    public static BigDecimal toBigDecimal(IndyObject obj) {
        if (obj instanceof NumberWrapper)
            return ((NumberWrapper) obj).getValue();
        throw new EvalException("Not number: " + obj);
    }

    @GlobalConverter
    public static long toLong(IndyObject obj) {
        if (obj instanceof NumberWrapper)
            return ((NumberWrapper) obj).getValue().longValue();
        throw new EvalException("Not number: " + obj);
    }

    @GlobalConverter
    public static int toInt(IndyObject obj) {
        if (obj instanceof NumberWrapper)
            return ((NumberWrapper) obj).getValue().intValue();
        throw new EvalException("Not number: " + obj);
    }

    @Converter
    public static RoundingMode toRoundingMode(IndyObject obj) {
        if (!obj.isSymbol())
            throw new EvalException("Not symbol: " + obj);
        switch (((Symbol) obj).getValue()) {
        case "UP":
            return RoundingMode.UP;
        case "DOWN":
            return RoundingMode.DOWN;
        case "CEILING":
            return RoundingMode.CEILING;
        case "FLOOR":
            return RoundingMode.FLOOR;
        case "HALF_UP":
            return RoundingMode.HALF_UP;
        case "HALF_DOWN":
            return RoundingMode.HALF_DOWN;
        case "HALF_EVEN":
            return RoundingMode.HALF_EVEN;
        case "UNNECESSARY":
            return RoundingMode.UNNECESSARY;
        default:
            throw new EvalException("Not RoundingMode constant: " + obj.getValue());
        }
    }

    @Converter
    public static MathContext toMathContext(IndyObject obj) {
        if (!obj.isSymbol())
            throw new EvalException("Not symbol: " + obj);
        switch (((Symbol) obj).getValue()) {
        case "DECIMAL32":
            return MathContext.DECIMAL32;
        case "DECIMAL64":
            return MathContext.DECIMAL64;
        case "DECIMAL128":
            return MathContext.DECIMAL128;
        case "UNLIMITED":
            return MathContext.UNLIMITED;
        default:
            throw new EvalException("Can't MathContext constant: " + obj.getValue());
        }
    }

    public IndyObject divide(IndyObject divisor, IndyObject symbol) {
        try {
            RoundingMode roundingMode = toRoundingMode(symbol);
            return getInstance(getValue().divide(((NumberWrapper) divisor).getValue(), roundingMode));
        } catch (EvalException e) {
            MathContext mathContext = toMathContext(symbol);
            return getInstance(getValue().divide(((NumberWrapper) divisor).getValue(), mathContext));
        }
    }

    public static void initImport(Importer importer) {
        importer.setNamespace(NAMESPACE);
        importer.setWrappedClass(BigDecimal.class);
        importer.exclude("setScale", int.class, int.class);
        importer.exclude("divide", BigDecimal.class, int.class);
        importer.exclude("divide", BigDecimal.class, int.class, int.class);
        importer.exclude("compareTo", Object.class);
        importer.excludeAllOf("valueOf");
    }
} public class PrintStreamWrapper extends Wrapper<PrintStream> {

    public static final String NAMESPACE = "PrintStream";

    public PrintStreamWrapper(PrintStream value) {
        super(value);
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    @Override
    public String toExprString() {
        return "[" + NAMESPACE + "/" + getValue() + "]";
    }

    @Converter
    public static IndyObject ignorePrintStream(PrintStream ps) {
        return T;
    }

    public static void initImport(Importer importer) {
        importer.setNamespace(NAMESPACE);
        importer.setWrappedClass(PrintStream.class);
        importer.exclude("print", boolean.class);
        importer.exclude("print", char.class);
        importer.exclude("print", int.class);
        importer.exclude("print", long.class);
        importer.exclude("print", float.class);
        importer.exclude("print", double.class);
        importer.exclude("print", char[].class);
        importer.exclude("print", String.class);
        importer.exclude("println", boolean.class);
        importer.exclude("println", char.class);
        importer.exclude("println", int.class);
        importer.exclude("println", long.class);
        importer.exclude("println", float.class);
        importer.exclude("println", double.class);
        importer.exclude("println", char[].class);
        importer.exclude("println", String.class);
        importer.exclude("printf", Locale.class, String.class, Object[].class);
    }
} public class StringWrapper extends Wrapper<String> {

    public static final String NAMESPACE = "String";

    private StringWrapper(String value) {
        super(value);
    }

    @Override
    public String getNamespace() {
        return NAMESPACE;
    }

    @Override
    public String toExprString() {
        return "\"" + getValue().replaceAll("\"", "\\\"") + "\"";
    }

    @GlobalConverter
    public static IndyObject getInstance(String value) {
        return new StringWrapper(value);
    }

    @GlobalConverter
    public static IndyObject getInstance(CharSequence value) {
        return new StringWrapper(value.toString());
    }

    @GlobalConverter
    public static String toString(IndyObject obj) {
        if (obj instanceof StringWrapper)
            return ((StringWrapper) obj).getValue();
        throw new EvalException("Not string: " + obj);
    }

    @GlobalConverter
    public static CharSequence toCharSequence(IndyObject obj) {
        return toString(obj);
    }

    public static void initImport(Importer importer) {
        importer.setNamespace(NAMESPACE);
        importer.setWrappedClass(String.class);
        importer.addAlias("String.format", "format");
        importer.exclude("indexOf", int.class);
        importer.exclude("indexOf", int.class, int.class);
        importer.exclude("lastIndexOf", int.class);
        importer.exclude("lastIndexOf", int.class, int.class);
        importer.exclude("compareTo", Object.class);
        importer.excludeAllOf("valueOf");
    }
} public class Symbol extends IndyObject {

    public static class SymbolTable extends HashMap<String, Symbol> {

        private static final long serialVersionUID = 1655796480738610490L;

        public SymbolTable() {
            super();
            put(NIL.getValue(), NIL);
            put(T.getValue(), T);
        }

        public Symbol symbol(String name) {
            Symbol symbol = get(name);
            if (symbol == null)
                put(name, symbol = new Symbol(name));
            return symbol;
        }
    }

    public static final Symbol NIL = new Nil();
    public static final Symbol T = new Symbol("t");

    private final String value;

    protected Symbol(String value) {
        this.value = value;
    }

    @Override
    public boolean isAtom() {
        return true;
    }

    @Override
    public boolean isSymbol() {
        return true;
    }

    @Override
    public String getNamespace() {
        return value;
    }

    @Override
    public String getValue() {
        return value;
    }

    @Override
    public String toExprString() {
        return value;
    }

    @Override
    public String toString() {
        return value;
    }
} public static class SymbolTable extends HashMap<String, Symbol> {

    private static final long serialVersionUID = 1655796480738610490L;

    public SymbolTable() {
        super();
        put(NIL.getValue(), NIL);
        put(T.getValue(), T);
    }

    public Symbol symbol(String name) {
        Symbol symbol = get(name);
        if (symbol == null)
            put(name, symbol = new Symbol(name));
        return symbol;
    }
} public class BoundEntry {

    public static final int READ_ONLY = 0b0000_0001;
    public static final int VAR_ARGS = 0b0000_0010;
    public static final int SPECIAL_FORM = 0b0000_0100;

    private final IndyObject value;
    private final boolean isReadOnly;
    private final boolean isVarArgs;
    private final boolean isSpecialForm;

    private static boolean isTrue(int value, int flags) {
        return (value & flags) != 0;
    }

    BoundEntry(IndyObject value, int type) {
        this.value = value;
        this.isReadOnly = isTrue(type, READ_ONLY);
        this.isVarArgs = isTrue(type, VAR_ARGS);
        this.isSpecialForm = isTrue(type, SPECIAL_FORM);
    }

    public IndyObject getValue() {
        return value;
    }

    public boolean isSpecialForm() {
        return isSpecialForm;
    }

    public boolean isVarArgs() {
        return isVarArgs;
    }

    public boolean isReadOnly() {
        return isReadOnly;
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder("[").append(value.toString());
        if (isSpecialForm)
            buffer.append(":SF");
        if (isVarArgs)
            buffer.append(":VA");
        if (isReadOnly)
            buffer.append(":RO");
        return buffer.append("]").toString();
    }
} public class Symbol extends IndyObject {

    public static class SymbolTable extends HashMap<String, Symbol> {

        private static final long serialVersionUID = 1655796480738610490L;

        public SymbolTable() {
            super();
            put(NIL.getValue(), NIL);
            put(T.getValue(), T);
        }

        public Symbol symbol(String name) {
            Symbol symbol = get(name);
            if (symbol == null)
                put(name, symbol = new Symbol(name));
            return symbol;
        }
    }

    public static final Symbol NIL = new Nil();
    public static final Symbol T = new Symbol("t");

    private final String value;

    protected Symbol(String value) {
        this.value = value;
    }

    @Override
    public boolean isAtom() {
        return true;
    }

    @Override
    public boolean isSymbol() {
        return true;
    }

    @Override
    public String getNamespace() {
        return value;
    }

    @Override
    public String getValue() {
        return value;
    }

    @Override
    public String toExprString() {
        return value;
    }

    @Override
    public String toString() {
        return value;
    }
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import jp.vmi.indylisp.annotations.SpecialForm;
import jp.vmi.indylisp.bindings.Bindings;
import jp.vmi.indylisp.bindings.BoundEntry;
import jp.vmi.indylisp.importer.Converters;
import jp.vmi.indylisp.importer.GlobalConverters;
import jp.vmi.indylisp.importer.Importer;
import jp.vmi.indylisp.objects.BufferedReaderWrapper;
import jp.vmi.indylisp.objects.Cell;
import jp.vmi.indylisp.objects.IndyObject;
import jp.vmi.indylisp.objects.Lambda;
import jp.vmi.indylisp.objects.LazyExprList;
import jp.vmi.indylisp.objects.Method;
import jp.vmi.indylisp.objects.Nil;
import jp.vmi.indylisp.objects.NumberWrapper;
import jp.vmi.indylisp.objects.PrintStreamWrapper;
import jp.vmi.indylisp.objects.StringWrapper;
import jp.vmi.indylisp.objects.Symbol;
import jp.vmi.indylisp.objects.Symbol.SymbolTable;
import static jp.vmi.indylisp.bindings.BoundEntry.*;
import static jp.vmi.indylisp.objects.Symbol.*;
package jp.vmi.indylisp;




public class Engine {

    public static final String NAMESPACE = "Engine";

    private static final Class<?>[] DEFAULT_IMPORTS = {
        Nil.class,
        Cell.class,
        StringWrapper.class,
        NumberWrapper.class,
        BufferedReaderWrapper.class,
        PrintStreamWrapper.class,
    };

    private final SymbolTable symbolTable = new SymbolTable();
    private final GlobalConverters globalConverters = new GlobalConverters();
    private Bindings currentBindings = new Bindings(null);