Skip to content

Commit

Permalink
safe locals-clearing lazy-seq
Browse files Browse the repository at this point in the history
  • Loading branch information
frenchy64 committed May 31, 2024
1 parent c6e50f6 commit c0a87e3
Show file tree
Hide file tree
Showing 2 changed files with 335 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,319 @@
/**
* Copyright (c) Rich Hickey. All rights reserved.
* The use and distribution terms for this software are covered by the
* Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
* which can be found in the file epl-v10.html at the root of this distribution.
* By using this software in any fashion, you are agreeing to be bound by
* the terms of this license.
* You must not remove this notice, or any other, from this software.
**/

/* rich Jan 31, 2009 */

package io.github.frenchy64.fully_satisfies.safe_locals_clearing;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.*;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import clojure.lang.Obj;
import clojure.lang.ISeq;
import clojure.lang.Sequential;
import clojure.lang.IPending;
import clojure.lang.IHashEq;
import clojure.lang.IFn;
import clojure.lang.RT;
import clojure.lang.Util;
import clojure.lang.SeqIterator;
import clojure.lang.Murmur3;
import clojure.lang.PersistentList;
import clojure.lang.IPersistentCollection;
import clojure.lang.IPersistentMap;

public final class LazySeq extends Obj implements ISeq, Sequential, List, IPending, IHashEq{

private static final long serialVersionUID = -7345643944998411680L;

private IFn fn;
private Object sv;
private ISeq s;
private ReentrantLock lock;

public LazySeq(IFn f){
fn = f;
lock = new ReentrantLock();
}

private LazySeq(IPersistentMap meta, ISeq seq){
super(meta);
fn = null;
s = seq;
}

public Obj withMeta(IPersistentMap meta){
if(meta() == meta)
return this;
return new LazySeq(meta, seq());
}

// MUST be locked when called!
final private void force() {
if (fn != null) {
sv = fn.invoke();
fn = null;
}
}

final private void lockAndForce() {
Lock l = lock;
if(l != null) {
l.lock();
try {
force();
} finally {
l.unlock();
}
}
}

final private Object sval() {
if(fn != null)
lockAndForce();
if(sv != null)
return sv;
return s;
}

final private Object unwrap(Object ls){
while(ls instanceof LazySeq) {
ls = ((LazySeq) ls).sval();
}
return ls;
}

final private void realize() {
ReentrantLock l = lock;
if(l != null) {
if(l.isHeldByCurrentThread()) {
throw Util.sneakyThrow(Util.runtimeException("Recursive lazy-seq realization"));
}
l.lock();
try {
//must re-examine under lock
if(lock != null) {
force();
Object ls = sv;
sv = null;
if(ls instanceof LazySeq)
ls = unwrap(ls);
s = RT.seq(ls);
lock = null;
}
}
finally {
l.unlock();
}
}
}

public final ISeq seq(){
if(lock != null)
realize();
return s;
}

public int count(){
int c = 0;
for(ISeq s = seq(); s != null; s = s.next())
++c;
return c;
}

public Object first(){
seq();
if(s == null)
return null;
return s.first();
}

public ISeq next(){
seq();
if(s == null)
return null;
return s.next();
}

public ISeq more(){
seq();
if(s == null)
return PersistentList.EMPTY;
return s.more();
}

public ISeq cons(Object o){
return RT.cons(o, seq());
}

public IPersistentCollection empty(){
return PersistentList.EMPTY;
}

public boolean equiv(Object o){
ISeq s = seq();
if(s != null)
return s.equiv(o);
else
return (o instanceof Sequential || o instanceof List) && RT.seq(o) == null;
}

public int hashCode(){
ISeq s = seq();
if(s == null)
return 1;
return Util.hash(s);
}

public int hasheq(){
return Murmur3.hashOrdered(this);
}

public boolean equals(Object o){
ISeq s = seq();
if(s != null)
return s.equals(o);
else
return (o instanceof Sequential || o instanceof List) && RT.seq(o) == null;
}


// java.util.Collection implementation

public Object[] toArray(){
return RT.seqToArray(seq());
}

public boolean add(Object o){
throw new UnsupportedOperationException();
}

public boolean remove(Object o){
throw new UnsupportedOperationException();
}

public boolean addAll(Collection c){
throw new UnsupportedOperationException();
}

public void clear(){
throw new UnsupportedOperationException();
}

public boolean retainAll(Collection c){
throw new UnsupportedOperationException();
}

public boolean removeAll(Collection c){
throw new UnsupportedOperationException();
}

public boolean containsAll(Collection c){
for(Object o : c)
{
if(!contains(o))
return false;
}
return true;
}

public Object[] toArray(Object[] a){
return RT.seqToPassedArray(seq(), a);
}

public int size(){
return count();
}

public boolean isEmpty(){
return seq() == null;
}

public boolean contains(Object o){
for(ISeq s = seq(); s != null; s = s.next())
{
if(Util.equiv(s.first(), o))
return true;
}
return false;
}

public Iterator iterator(){
return new SeqIterator(this);
}

//////////// List stuff /////////////////
private List reify(){
return new ArrayList(this);
}

public List subList(int fromIndex, int toIndex){
return reify().subList(fromIndex, toIndex);
}

public Object set(int index, Object element){
throw new UnsupportedOperationException();
}

public Object remove(int index){
throw new UnsupportedOperationException();
}

public int indexOf(Object o){
ISeq s = seq();
for(int i = 0; s != null; s = s.next(), i++)
{
if(Util.equiv(s.first(), o))
return i;
}
return -1;
}

public int lastIndexOf(Object o){
return reify().lastIndexOf(o);
}

public ListIterator listIterator(){
return reify().listIterator();
}

public ListIterator listIterator(int index){
return reify().listIterator(index);
}

public Object get(int index){
return RT.nth(this, index);
}

public void add(int index, Object element){
throw new UnsupportedOperationException();
}

public boolean addAll(int index, Collection c){
throw new UnsupportedOperationException();
}

public boolean isRealized(){
if(lock != null) {
Lock l = lock;
if(l != null) {
l.lock();
try {
return lock == null;
} finally {
l.unlock();
}
}
}
return true;
}
}
18 changes: 16 additions & 2 deletions src/io/github/frenchy64/fully_satisfies/safe_locals_clearing.clj
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
while preserving their locals-clearing capabilities.
The tradeoff being they now cannot be recursively dereferenced,
instead now throwing an exception instead of risking executing garbage.
instead now throwing an exception instead of risking executing
locals-cleared garbage.
Additional functions delay? and force are also included that only
work on delays in this namespace.
This article explains how locals clearing enables tail calls
to potentially have strong references to their arguments.
Expand All @@ -27,7 +31,7 @@
}"
(:refer-clojure :exclude [delay delay? force lazy-seq])
(:require [clojure.core :as cc])
(:import io.github.frenchy64.fully_satisfies.safe_locals_clearing.Delay))
(:import [io.github.frenchy64.fully_satisfies.safe_locals_clearing Delay LazySeq]))

(defmacro delay
"Takes a body of expressions and yields a Delay object that will
Expand All @@ -51,3 +55,13 @@
{:added "1.0"
:static true}
[x] (. io.github.frenchy64.fully_satisfies.safe_locals_clearing.Delay (force x)))

;;TODO unit test
(defmacro lazy-seq
"Takes a body of expressions that returns an ISeq or nil, and yields
a Seqable object that will invoke the body only the first time seq
is called, and will cache the result and return it on all subsequent
seq calls. See also - realized?"
{:added "1.0"}
[& body]
(list 'new 'io.github.frenchy64.fully_satisfies.safe_locals_clearing.LazySeq (list* '^{:once true} fn* [] body)))

0 comments on commit c0a87e3

Please sign in to comment.