Some more infrastructure. Maybe other things.
authorMark Wooding <mdw@distorted.org.uk>
Mon, 25 Jun 2018 00:37:03 +0000 (01:37 +0100)
committerMark Wooding <mdw@distorted.org.uk>
Mon, 25 Jun 2018 02:57:58 +0000 (03:57 +0100)
Makefile
admin.scala
dep.scala [new file with mode: 0644]
sys.scala
tar.scala
toy-activity.scala
util.scala

index 0675cd4..c6a85ba 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -127,18 +127,14 @@ MINAPI                     = 15
 TARGETAPI               = 23
 TOOLVERSION             = 4.9
 
-## Android ABI definitions.
-ANDROID_ABIS           += armeabi
+## Android ABI definitions.  We don't bother with `armeabi-v7a': we'll use
+## fancy CPU features if we detect that they're available at runtime anyway.
+#ANDROID_ABIS          += armeabi
 GNUARCH.armeabi                 = arm-linux-androideabi
 PLATARCH.armeabi        = arm
 CFLAGS.ndk-armeabi      =
 
-ANDROID_ABIS.inhibit   += armeabi-v7a
-GNUARCH.armeabi-v7a     = arm-linux-androideabi
-PLATARCH.armeabi-v7a    = arm
-CFLAGS.ndk-armeabi-v7a  =
-
-ANDROID_ABIS.inhibit   += arm64-v8a
+#ANDROID_ABIS          += arm64-v8a
 GNUARCH.arm64-v8a       = aarch64-linux-android
 PLATARCH.arm64-v8a      = arm64
 MINAPI.arm64-v8a        = 21
@@ -148,7 +144,7 @@ TOOLCHAINDIR.x86     = x86
 GNUARCH.x86             = i686-linux-android
 PLATARCH.x86            = x86
 
-ANDROID_ABIS.inhibit   += x86_64
+#ANDROID_ABIS          += x86_64
 TOOLCHAINDIR.x86_64     = x86_64
 GNUARCH.x86_64          = x86_64-linux-android
 PLATARCH.x86_64                 = x86_64
@@ -159,7 +155,7 @@ FLAVOURS            += $(ANDROID_ABIS)
 ## Build variants.
 VARIANTS               += debug
 AAPTFLAGS.debug                 = --debug-mode \
-       --rename-manifest-package uk.org.distorted.tripe-debug
+       --rename-manifest-package uk.org.distorted.tripe.debug
 KEYSTORE.debug          = debug.keystore
 JARSIGNERFLAGS.debug    = -storepass public -keypass public
 
@@ -179,8 +175,8 @@ ndk-toolchain-bin    = \
 ENV.ndk                         = env PATH=$(call ndk-toolchain-bin,$1):$$PATH
 CC.ndk                  = $(GNUARCH.$1)-gcc
 LD.ndk                  = $(CC.ndk)
-CFLAGS.ndk              = $(CFLAGS) -fPIC -D__ANDROID_API__=$(MINAPI) \
-       $(CFLAGS.ndk-$1) \
+CFLAGS.ndk              = $(CFLAGS) -fPIC $(CFLAGS.ndk-$1) \
+       -D__ANDROID_API__=$(call defaulting,MINAPI.$1,$(MINAPI)) \
        --sysroot=$(call ndk-sysroot,$1) \
        -isystem $(ANDROID_NDKDIR)/sysroot/usr/include \
        -isystem $(ANDROID_NDKDIR)/sysroot/usr/include/$(GNUARCH.$1)
@@ -315,6 +311,7 @@ $(foreach f,$(FLAVOURS),$(foreach e,$(EXTERNALS),clean-$e.$f)): clean-%:
        rm -f $(STAMPDIR)/$*-stamp
        rm -rf $(call ext-stamp-builddir,$*)
 .PHONY: $(foreach f,$(FLAVOURS),$(foreach e,$(EXTERNALS),clean-$e.$f))
+$(foreach e,$(EXTERNALS),clean-$e): clean-%: $(foreach f,$(FLAVOURS),clean-%.$f)
 $(foreach f,$(FLAVOURS),clean-inst.$f): clean-inst.%:
        rm -rf $(call ext-prefix,$*)
 .PHONY: $(foreach f,$(FLAVOURS),clean-inst.$f)
@@ -439,6 +436,7 @@ CLASSES                     += util
 CLASSES                        += sys:util
 CLASSES                        += admin:sys,util
 CLASSES                        += tar:util
+CLASSES                        += dep:util
 CLASSES                        += progress:sys,util
 CLASSES                        += keys:progress,tar,sys,util
 CLASSES                        += terminal:progress,sys,util
@@ -503,7 +501,7 @@ $$(OUTDIR)/pkg/assets/bin/$1/$2: $$$$(call ext-stamps,$$$$(EXTERNALS),$1)
        $$(V_AT)mkdir -p $$(dir $$@)
        $$(call v_tag,CP)cp $$(call ext-prefix,$1)/bin/$2 $$@
 endef
-$(foreach f,$(FLAVOURS), \
+$(foreach f,$(ANDROID_ABIS), \
 $(foreach b,$(BINS), \
        $(eval $(call bin-rule,$f,$b))))
 
@@ -547,6 +545,9 @@ all:: debug
 clean::; rm -f $(CLEANFILES)
 realclean::; rm -f $(REALCLEANFILES)
 
+repl: $(CLASSSTAMPS) $(foreach a,$(APKLIBS),$(OUTDIR)/$a)
+       $(SCALA) -cp $(CLASSDIR) -Djava.lib.path=$(OUTDIR) -Yno-load-impl-class
+
 t:; : $(show)
 .PHONY: t
 
index 52a2912..321fb56 100644 (file)
@@ -30,7 +30,7 @@ package uk.org.distorted.tripe; package object admin {
 import java.io.{BufferedReader, InputStreamReader, OutputStreamWriter};
 import java.util.concurrent.locks.{Condition, ReentrantLock => Lock};
 
-import scala.collection.mutable.{HashMap, Publisher};
+import scala.collection.mutable.HashMap;
 import scala.concurrent.Channel;
 import scala.util.control.Breaks;
 
@@ -78,7 +78,7 @@ class CommandFailed(val msg: Seq[String]) extends Exception {
 
 class ConnectionLostException extends Exception;
 
-object Connection extends Publisher[AsyncMessage]
+object Connection extends Hook[AsyncMessage]
 {
   /* Synchronization.
    *
@@ -102,7 +102,7 @@ object Connection extends Publisher[AsyncMessage]
     private[this] var nextmsg: Option[JobMessage] = None;
 
     private[this] def fetchNext()
-      { if (nextmsg == None) nextmsg = Some(ch.read); }
+      { if (!nextmsg) nextmsg = Some(ch.read); }
     override def hasNext: Boolean = {
       fetchNext();
       nextmsg match {
@@ -114,9 +114,9 @@ object Connection extends Publisher[AsyncMessage]
       fetchNext();
       nextmsg match {
        case None => ???
-       case Some(JobOK) => throw new NoSuchElementException
-       case Some(JobFail(msg)) => throw new CommandFailed(msg)
-       case Some(JobLostConnection) => throw new ConnectionLostException
+       case Some(JobOK) => throw new NoSuchElementException;
+       case Some(JobFail(msg)) => throw new CommandFailed(msg);
+       case Some(JobLostConnection) => throw new ConnectionLostException;
        case Some(JobInfo(msg)) => nextmsg = None; msg
       }
     }
@@ -248,7 +248,7 @@ println(s";; line: $line");
              j
            }
          case msg: AsyncMessage =>
-           publish(msg);
+           callHook(msg);
          case _: ServiceMessage =>
            ok;
        }
@@ -267,7 +267,7 @@ println(s";; line: $line");
          case None => ok;
        }
       }
-      publish(ConnectionLost);
+      callHook(ConnectionLost);
     }
   }
 }
diff --git a/dep.scala b/dep.scala
new file mode 100644 (file)
index 0000000..d94925d
--- /dev/null
+++ b/dep.scala
@@ -0,0 +1,428 @@
+/* -*-scala-*-
+ *
+ * Dependency-based computation
+ *
+ * (c) 2018 Straylight/Edgeware
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of the Trivial IP Encryption (TrIPE) Android app.
+ *
+ * TrIPE is free software: you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License as published by the Free
+ * Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * TrIPE is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with TrIPE.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package uk.org.distorted.tripe; package object dep {
+
+/*----- Imports -----------------------------------------------------------*/
+
+import scala.collection.mutable.{ArrayBuffer, Queue};
+
+import java.lang.ref.WeakReference;
+
+import Implicits.{truish, bitwiseImplicits};
+
+/*----- Main code ---------------------------------------------------------*/
+
+object Generation {
+  private var nextseq: Int = 0;
+}
+class Generation(what: String) extends Brand(what) {
+  /* Formally, a generation marker has no interesting properties except for
+   * its identity, so we could just as well use a plain `Brand'.  For
+   * diagnostic purposes though, we include a sequence number which we can
+   * include in the object printout.
+   */
+
+  import Generation._;
+  private val seq =
+    Generation synchronized { val v = nextseq; nextseq += 1; v };
+  override def toString(): String = s"${getClass.getName}($what, #$seq)";
+}
+
+class BadDep extends Throwable;
+  /* Thrown when you try to read a `bad' `Dep' object. */
+
+class CircularDependency extends Exception;
+  /* Thrown if a `Dep' depends on itself, possibly indirectly. */
+
+/* Some type aliases because otherwise we need to mess with existential
+ * types.
+ */
+type AbstractDep = Dep[_];
+type AbstractComputedDep = ComputedDep[_];
+
+object Dep {
+
+  /* Event types for hook clients. */
+  sealed abstract class Event;
+  case object Changed extends Event;
+
+  /* Flags for `Dep' objects. */
+  private[dep] final val F_VALUE = 1;  // has a value
+  private[dep] final val F_DEPS = 2;   // dependencies are know
+  private[dep] final val F_CHANGED = 4;        // changed in this update cycle
+  private[dep] final val F_RECOMPUTING = 8; // currently recomputing
+  private[dep] final val F_QUEUED = 16;        // queued for recomputation
+
+  /* Overall system state. */
+  object DepState extends Enumeration
+    { val READY, FROZEN, RECOMPUTING = Value; }
+  import DepState.{READY, FROZEN, RECOMPUTING, Value => State};
+
+  private[dep] var generation: Generation = new Generation("dep-generation");
+    /* The current generation.   Updated in `withDepsFrozen'. */
+
+  private[dep] val state = new SharedFluid(READY);
+    /* The current system state.  Must be `let'-bound. */
+
+  private[dep] val evaluating = new SharedFluid[AbstractComputedDep](null);
+    /* The `ComputedDep' object which is being evaluated, or null.  Must be
+     * `let'-bound.
+     */
+
+  private[dep] val delayed = new SharedFluid[Queue[() => Unit]](null);
+    /* Delayed thunks from `withDepsDelayed'.  Must be `let'-bound to a fresh
+     * `Queue', and then mutated in place.
+     */
+
+  private[dep] val pending =
+    new SharedFluid[Queue[AbstractComputedDep]](null);
+    /* `ComputedDep' objects awaiting recomputation.  Must be `let'-bound to
+     * a fresh `Queue', and then mutated in place.
+     */
+
+  private def recomputePending() {
+    /* Recalculate the deps on the `pending' queue.
+     *
+     * While this is running, we are in the `RECOMPUTING' state.
+     */
+
+    let(state -> RECOMPUTING) {
+      try {
+       while (pending.v) {
+         val d = pending.v.dequeue();
+         val f = d._flags;
+         d._flags = f&~F_QUEUED;
+         if (!(f&F_VALUE)) d.recompute();
+         else if (!(f&F_DEPS)) { d.recompute(); d.flags = f | F_DEPS; }
+       }
+      } finally {
+       while (pending.v) pending.v.dequeue()._val = None;
+      }
+    }
+  }
+
+  def withDepsFrozen[T](body: => T): T = state.v match {
+    /* Evaluate the BODY, allowing it to modify `Dep' objects.  When the BODY
+     * completes, but not before, all dependent `Dep's are recalculated.
+     * This can be used to improve performance if a big batch of changes is
+     * planned.
+     *
+     * It's not permitted to modify a `Dep' while recomputation is in
+     * progress.  See `withDepsDelayed'.
+     */
+
+    case FROZEN => body
+    case RECOMPUTING =>
+      throw new IllegalStateException("currently recomputing");
+    case READY =>
+      let(state -> FROZEN,
+         delayed -> new Queue[() => Unit],
+         pending -> new Queue[AbstractComputedDep]) {
+       generation = new Generation("dep-generation");
+       val r = body;
+       while ({ recomputePending(); delayed.v }) delayed.v.dequeue()();
+       r
+      }
+    }
+
+  def withDepsDelayed(body: => Unit) { state.v match {
+    /* Evaluate the BODY, allowing it to modify `Dep' objects.  If
+     * recomputation is in progress, then save the BODY in a queue to be
+     * evaluated later.
+     */
+
+    case RECOMPUTING => delayed.v += { () => body };
+    case _ => withDepsFrozen { body };
+  } }
+
+  /* Various constructures for basic `Dep' objects. */
+  def apply[T: Equiv](name: String, init: T): Dep[T] =
+    new Dep(name, Some(init));
+  def apply[T: Equiv](name: String): Dep[T] = new Dep(name, None);
+  def apply[T: Equiv](init: T): Dep[T] = new Dep(null, Some(init));
+  def apply[T: Equiv](): Dep[T] = new Dep(null, None);
+}
+
+/* Import these things here so that they're included in the scope of `Dep''s
+ * additional constructor bodies.
+ */
+import Dep._;
+
+/* tryDep { BODY } ifBad { ALT }
+ *
+ * Evaluate BODY.  If it tries to read a bad `Dep', then evaluate ALT
+ * instead.
+ */
+class PendingAttempt[T] private[dep](body: => T)
+  { def ifBad(alt: => T): T = try { body } catch { case _: BadDep => alt } }
+def tryDep[T](body: => T): PendingAttempt[T] = new PendingAttempt(body);
+
+def bad: Nothing = throw new BadDep;
+  /* Call from a `Dep' expression to cause the `Dep' to be marked bad. */
+
+class Dep[T: Equiv] protected(val name: String,
+                             var _val: Option[T],
+                             var _flags: Int)
+       extends Hook[Dep.Event]
+{
+  /* A leaf `Dep'.
+   *
+   * A `Dep' has a value, of some type T, and maybe a name.  The value is
+   * available in the `v' property.  A `Dep' may be `bad', in which case an
+   * exception, `BadDep', is thrown when an attempt is made to read its
+   * value; this can be hedged against either by calling `goodp' in advance,
+   * or by using the `tryDep' function.
+   *
+   * The value of a leaf `Dep' changes only as a result of direct assignments
+   * to its `v' property.
+   */
+
+  /* Internal constructor, for the benefit of the companion module. */
+  private def this(name: String, init: Option[T])
+    { this(name, init, F_CHANGED | F_VALUE); }
+
+  /* Other useful definitions. */
+  import DepState.{READY, FROZEN, RECOMPUTING, Value => State};
+
+  protected var gen: Generation = generation;
+    /* The generation during which this `Dep' was most recently updated. */
+
+  protected val dependents =
+    new ArrayBuffer[WeakReference[AbstractComputedDep]];
+    /* A collection of other `Dep's which depend (directly) on this one. */
+
+  override def toString(): String = {
+    /* Convert this `Dep' to a string.  The contents are useful only for
+     * diagnostic purposes.
+     */
+
+    val b = new StringBuilder;
+    val f = flags;
+
+    b ++= f"${getClass.getName}%s@${hashCode}%x(";
+
+    b ++= (_val match {
+      case _ if !(f&F_VALUE) => "<out-of-date>"
+      case None => "<bad>"
+      case Some(x) => x.toString
+    })
+
+    if (name != null) b ++= s" $name";
+
+    if (f&F_DEPS) b ++= " :recompute-deps";
+    if (f&F_QUEUED) b ++= " :queued";
+    if (f&F_CHANGED) b ++= " :changed";
+
+    b += ')'; b.result
+  }
+
+  /* A property for accessing the `Dep' flags.
+   *
+   * The flags stored are only relevant during recomputation and if they're
+   * fresh.  Otherwise we must synthesize appropriate flags.
+   */
+  protected[dep] def flags: Int =
+    if (state.v == READY || gen != generation) F_VALUE | F_DEPS
+    else _flags;
+  protected[dep] def flags_=(f: Int) { _flags = f; }
+
+  def update(v: Option[T]): Boolean = (v, _val) match {
+    /* Set this `Dep''s value to V; return true if this is a substantive
+     * change.
+     */
+    case (Some(x), Some(y)) if implicitly[Equiv[T]].equiv(x, y) => false
+    case _ => _val = v; true
+  }
+
+  protected def propagate() {
+    /* Notify all of our dependents that this `Dep' has changed its value. */
+    for {
+      dweak <- dependents;
+      d = dweak.get;
+      if d != null;
+      f = d.flags;
+      if !(f&(F_QUEUED | F_DEPS))
+    } {
+      pending.v += d;
+      d.flags = (f&F_VALUE) | F_QUEUED;
+    }
+    dependents.clear();
+    callHook(Changed);
+  }
+
+  private[dep] def force(): Boolean = flags&F_CHANGED;
+    /* Force this `Dep' to update its value if it hasn't done so already in
+     * the current recomputation cycle.  Return true if its value has changed
+     * in the current cycle.
+     *
+     * The implementation here is trivial, but subclasses will need to
+     * override it.
+     */
+
+  def v: T = {
+    /* Return the value of this `Dep', recalculating it if necessary.
+     *
+     * Throws `BadDep' if the `Dep is bad.
+     */
+
+    if (state.v == RECOMPUTING) {
+      if (evaluating.v != null) {
+       dependents += evaluating.v.weakref;
+       evaluating.v.dependencies += this;
+      }
+      force();
+    }
+    _val match {
+      case None => bad
+      case Some(v) => v
+    }
+  }
+
+  /* The obvious good/bad predicates. */
+  def goodp: Boolean = { if (state.v == RECOMPUTING) force(); _val != bad }
+  def badp: Boolean = { if (state.v == RECOMPUTING) force(); _val == bad }
+
+  private def set(v: Option[T]) {
+    /* Low-level operation to change the value of this `Dep', and trigger
+     * recomputation as necessary.
+     */
+
+    withDepsFrozen {
+      update(v);
+      gen = generation;
+      _flags = F_VALUE | F_CHANGED;
+      propagate();
+    }
+  }
+
+  /* Modify the `Dep' value. */
+  def v_=(x: T) { set(Some(x)); }
+  def makeBad() { set(None); }
+}
+
+object ComputedDep {
+
+  /* Cooked constructors. */
+  def apply[T: Equiv](expr: => T) = new ComputedDep(null, expr, None);
+  def apply[T: Equiv](name: String)(expr: => T) =
+    new ComputedDep(name, expr, None);
+  def apply[T: Equiv](init: T)(expr: => T) =
+    new ComputedDep(null, expr, Some(init));
+  def apply[T: Equiv](name: String, init: T)(expr: => T) =
+    new ComputedDep(name, expr, Some(init));
+}
+
+class ComputedDep[T: Equiv] protected(name: String,
+                                     expr: => T,
+                                     init: Option[T])
+       extends Dep[T](name, init,
+                      F_CHANGED | F_QUEUED | F_DEPS | (init match {
+                        case Some(_) => F_VALUE
+                        case None => 0
+                      }))
+{
+  /* A `Dep' which calculates its value based on other `Dep' objects.
+   *
+   * During this calculation, we keep track of the dependency structure so
+   * that, in the future, we can determine whether this `Dep' needs to be
+   * recalculated as a result of other changes.
+   */
+
+  private[dep] val dependencies = new ArrayBuffer[AbstractDep];
+    /* A collection of other `Dep' objects; if any of them change, we must
+     * recalculate.
+     */
+
+  private[dep] val weakref: WeakReference[AbstractComputedDep] =
+    new WeakReference(this);
+    /* A weak reference to this `Dep'.
+     *
+     * A `Dep' maintains only weak references to those other `Dep's which
+     * depend on it: just because X's value is determined (partially) by Y
+     * doesn't mean that we should keep X alive just because Y is alive.
+     *
+     * The weak reference is captured once to reduce consing.
+     */
+
+  /* Arrange recalculation at the earliest opportunity. */
+  withDepsFrozen { pending.v += this; }
+
+  /* Other useful definitions. */
+  import DepState.{READY, FROZEN, RECOMPUTING, Value => State};
+
+  /* Synthesize different flags when we aren't fresh. */
+  override protected[dep] def flags: Int =
+    if (state.v == READY) F_VALUE | F_DEPS
+    else if (gen == generation) _flags
+    else 0;
+
+  def newValue(): Option[T] = {
+    /* Determine the new value of this `Dep', keeping track of other `Dep'
+     * objects which we look at.
+     */
+
+    try { let(evaluating -> this) { dependencies.clear(); Some(expr)} }
+    catch { case _: BadDep => None }
+  }
+
+  private[this] def _recompute(v: Option[T], nf: Int): Boolean =
+    if (update(v)) { flags = nf | Dep.F_CHANGED; propagate(); true }
+    else { flags = nf; false }
+
+  private[dep] def recompute(): Boolean = {
+    /* Recalculate the value of this `Dep'.  Catch exceptions and mark the
+     * `Dep' as bad if it encounters any.
+     *
+     * Note that the special case of `BadDep' is trapped lower down in
+     * `newValue'.
+     */
+
+    val nf = (flags&F_QUEUED) | F_VALUE | F_DEPS;
+    try { _recompute(newValue(), nf) }
+    catch { case e: Exception => _recompute(None, nf); throw e; }
+  }
+
+  private[dep] override def force(): Boolean = {
+    /* Force this `Dep' to update its value if it hasn't done so already in
+     * the current recomputation cycle.  Return true if its value has changed
+     * in the current cycle.
+     */
+
+    val f = flags;
+    if (f&F_RECOMPUTING) throw new CircularDependency;
+    else if (f&F_VALUE) f&F_CHANGED
+    else {
+      gen = generation;
+      flags = (f&F_QUEUED) | F_RECOMPUTING;
+      if (dependencies.exists { _.force() }) recompute();
+      else { flags = f; false }
+    }
+  }
+}
+
+/*----- That's all, folks -------------------------------------------------*/
+
+}
index 8dab4c6..c449414 100644 (file)
--- a/sys.scala
+++ b/sys.scala
@@ -38,6 +38,8 @@ import java.nio.{ByteBuffer, CharBuffer};
 import java.nio.charset.Charset;
 import java.util.Date;
 
+import Implicits.truish;
+
 /*----- Some magic for C strings ------------------------------------------*/
 
 type CString = Array[Byte];
@@ -797,7 +799,7 @@ def runCommand(cmd: String*): (String, String) = {
 
     /* Check the exit status. */
     val rc = kid.exitValue;
-    if (rc != 0) throw new SubprocessFailed(cmd, rc, berr.result);
+    if (rc) throw new SubprocessFailed(cmd, rc, berr.result);
 
     /* We're all done. */
     return (bout.result, berr.result);
@@ -818,7 +820,7 @@ private var triggers: List[Wrapper] = Nil;
 
 private def getTrigger(): Wrapper = {
   triggerLock synchronized {
-    if (nTriggers == 0)
+    if (!nTriggers)
       make_trigger()
     else {
       val trig = triggers.head;
index 30a3a4a..986eeaa 100644 (file)
--- a/tar.scala
+++ b/tar.scala
@@ -35,6 +35,8 @@ import java.util.Date;
 import sys.FileInfo;
 import sys.FileInfo.{Value, FIFO, CHR, DIR, BLK, REG, LNK, HDLNK, UNK};
 
+import Implicits.truish;
+
 /*----- Main code ---------------------------------------------------------*/
 
 class TarFormatError(msg: String) extends Exception(msg);
@@ -98,12 +100,10 @@ trait TarEntry {
 
     /* Then the permissions bits.  Ugh, the permissions bits. */
     def perm(s: Int, r: Int, w: Int, x: Int, schar: Char, Schar: Char) {
-      sb += (if ((mode&r) != 0) 'r' else '-');
-      sb += (if ((mode&w) != 0) 'w' else '-');
-      sb += (if ((mode&s) != 0)
-              if ((mode&x) != 0) schar else Schar;
-            else
-              if ((mode&x) != 0) 'x' else '-');
+      sb += (if (mode&r) 'r' else '-');
+      sb += (if (mode&w) 'w' else '-');
+      sb += (if (mode&s) { if (mode&x) schar else Schar; }
+            else { if (mode&x) 'x' else '-' });
     }
     perm(0x800, 0x100, 0x080, 0x040, 's', 'S');
     perm(0x400, 0x020, 0x010, 0x008, 's', 'S');
@@ -337,7 +337,7 @@ class TarFile(in: InputStream)
       val b = hdr(i);
 
       /* See if we're done now. */
-      if (b == ' ' || b == 0) return n;
+      if (!b || b == ' ') return n;
       else if (b < '0' || b > '7')
        throw new TarFormatError(s"bad octal digit (at ${offset + off + i})");
 
@@ -407,7 +407,7 @@ class TarFile(in: InputStream)
      */
     val name = {
       val tail = string(0, 100);
-      if (!posixp || hdr(345) == 0) tail
+      if (!posixp || !hdr(345)) tail
       else {
        val prefix = string(345, 155);
        prefix + '/' + tail
index 868b3d2..dce975c 100644 (file)
@@ -81,6 +81,7 @@ class ToyActivity extends Activity {
     super.onCreate(joy);
     Setup.setup(this);
     setContentView(R.layout.toy);
+    Log.d(TAG, s"created ${this}");
   }
   def clickOk(v: View) {
     Log.d(TAG, "OK, OK.  (Scala was here.)");
index 8ede691..99ec790 100644 (file)
@@ -27,6 +27,9 @@ package uk.org.distorted; package object tripe {
 
 /*----- Imports -----------------------------------------------------------*/
 
+import scala.language.{existentials, implicitConversions};
+
+import scala.collection.mutable.{HashSet, WeakHashMap};
 import scala.concurrent.duration.{Deadline, Duration};
 import scala.util.control.{Breaks, ControlThrowable};
 
@@ -47,7 +50,9 @@ val rng = new java.security.SecureRandom;
 def unreachable(msg: String): Nothing = throw new AssertionError(msg);
 def unreachable(): Nothing = unreachable("unreachable");
 final val ok = ();
-final class Brand;
+class Brand(val what: String) {
+  override def toString(): String = s"<${getClass.getName} $what>";
+}
 
 /*----- Various pieces of implicit magic ----------------------------------*/
 
@@ -104,8 +109,55 @@ object Implicits {
       else try { body; } finally lk.unlock();
     }
   }
+
+  /* Implicit conversions to `Boolean'.  I miss the way C integers and
+   * pointers convert to boolean, so let's do that here.
+   *
+   * Numeric zero, null, and empty containers are all false; other objects
+   * are true.
+   */
+  implicit def truish(n: Byte): Boolean = n != 0;
+  implicit def truish(n: Char): Boolean = n != 0;
+  implicit def truish(n: Short): Boolean = n != 0;
+  implicit def truish(n: Int): Boolean = n != 0;
+  implicit def truish(n: Long): Boolean = n != 0;
+  implicit def truish(n: Float): Boolean = n != 0;
+  implicit def truish(n: Double): Boolean = n != 0;
+  implicit def truish(x: AnyRef): Boolean = x != null;
+  implicit def truish(s: String): Boolean = s != null && s != "";
+  implicit def truish(o: Option[_]): Boolean = o != None;
+  implicit def truish(i: Iterator[_]): Boolean = i != null && i.hasNext;
+  implicit def truish(c: Traversable[_]): Boolean =
+    c != null && c.nonEmpty;
+
+  /* Some additional bitwise operators.
+   *
+   * For now, just the `bic' operator `&~', because typing `& ~' is
+   * inconsistent with my current style.
+   */
+  class BitwiseIntImplicits(x: Int) {
+    def &~(y: Byte): Int = x & ~y;
+    def &~(y: Char): Int = x & ~y;
+    def &~(y: Short): Int = x & ~y;
+    def &~(y: Int): Int = x & ~y;
+    def &~(y: Long): Long = x & ~y;
+  }
+  class BitwiseLongImplicits(x: Long) {
+    def &~(y: Byte): Long = x & ~y;
+    def &~(y: Char): Long = x & ~y;
+    def &~(y: Short): Long = x & ~y;
+    def &~(y: Int): Long = x & ~y;
+    def &~(y: Long): Long = x & ~y;
+  }
+  implicit def bitwiseImplicits(x: Byte) = new BitwiseIntImplicits(x);
+  implicit def bitwiseImplicits(x: Char) = new BitwiseIntImplicits(x);
+  implicit def bitwiseImplicits(x: Short) = new BitwiseIntImplicits(x);
+  implicit def bitwiseImplicits(x: Int) = new BitwiseIntImplicits(x);
+  implicit def bitwiseImplicits(x: Long) = new BitwiseLongImplicits(x);
 }
 
+import Implicits.truish;
+
 /*----- Cleanup assistant -------------------------------------------------*/
 
 class Cleaner {
@@ -154,7 +206,7 @@ def block[T](body: (T => Nothing) => T): T = {
    * you'll have to write it explicitly.
    */
 
-  val mybrand = new Brand;
+  val mybrand = new Brand("block-exit");
   try { body { result => throw new ExitBlock(mybrand, result) } }
   catch {
     case ExitBlock(brand, result) if brand eq mybrand =>
@@ -169,7 +221,7 @@ def blockUnit(body: (=> Nothing) => Unit) {
    * exit(ok); ...'.
    */
 
-  val mybrand = new Brand;
+  val mybrand = new Brand("block-exit");
   try { body { throw new ExitBlock(mybrand, null) }; }
   catch { case ExitBlock(brand, result) if brand eq mybrand => ok; }
 }
@@ -427,13 +479,13 @@ def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
   if (i >= n) return None;
 
   /* There is something there.  Unpick the quoting and escaping. */
-  while (i < n && (q != 0 || !s(i).isWhitespace)) {
+  while (i < n && (q || !s(i).isWhitespace)) {
     s(i) match {
       case '\\' =>
        if (i + 1 >= n) throw new InvalidQuotingException("trailing `\\'");
        b += s(i + 1); i += 2;
       case ch@('"' | ''') =>
-       if (q == 0) q = ch;
+       if (!q) q = ch;
        else if (q == ch) q = 0;
        else b += ch;
        i += 1;
@@ -444,7 +496,7 @@ def nextToken(s: String, pos: Int = 0): Option[(String, Int)] = {
   }
 
   /* Check that the quoting was valid. */
-  if (q != 0) throw new InvalidQuotingException(s"unmatched `$q'");
+  if (q) throw new InvalidQuotingException(s"unmatched `$q'");
 
   /* Skip whitespace before the next token. */
   while (i < n && s(i).isWhitespace) i += 1;
@@ -466,6 +518,135 @@ def splitTokens(s: String, pos: Int = 0): Seq[String] = {
   b.result
 }
 
+/*----- Hooks -------------------------------------------------------------*/
+
+/* This is a really simple publisher/subscriber system.  The only slight
+ * tweak -- and the reason I'm not just using the Scala machinery -- is that
+ * being attached to a hook doesn't prevent the client from being garbage
+ * collected.
+ */
+
+trait BaseHookClient[E] {
+  /* The minimal requirements for a hook client.  Honestly you should be
+   * using `HookClient' instead.
+   */
+
+  type H = Hook[E];                    // the type of hook we attach to
+  def hook(hk: H, evt: E);             // called with events from the hook
+}
+
+trait HookClient[E] extends BaseHookClient[E] {
+  /* The properly cooked hook client.  This keeps track of which hooks we're
+   * attached to so we can release them all easily.
+   */
+
+  private val hooks = new HashSet[H];
+  protected def attachHook(hk: H) { hk.addHookClient(this); hooks += hk; }
+  protected def detachHook(hk: H) { hk.rmHookClient(this); hooks -= hk; }
+  protected def detachAllHooks()
+    { for (hk <- hooks) hk.rmHookClient(this); hooks.clear(); }
+}
+
+trait Hook[E] {
+  type C = BaseHookClient[E];
+  private val clients = new WeakHashMap[C, Unit];
+  def addHookClient(c: C) { clients(c) = (); }
+  def rmHookClient(c: C) { clients -= c; }
+  protected def callHook(evt: E)
+    { for (c <- clients.keys) c.hook(this, evt); }
+}
+
+/*----- Fluid variables ---------------------------------------------------*/
+
+object BaseFluid {
+  /* The multi-fluid `let' form is defined here so that it can access the
+   * `capture' method of individual fluids, but users should use the
+   * package-level veneer.
+   */
+
+  private[tripe] def let[U](fxs: (BaseFluid[T], T) forSome { type T }*)
+                          (body: => U): U = {
+      /* See the package-level `let' for details. */
+    val binds = for ((f, _) <- fxs) yield f.capture;
+    try { for ((f, x) <- fxs) f.v = x; body }
+    finally { for (b <- binds) b.restore(); }
+  }
+}
+def let[U](fxs: (BaseFluid[T], T) forSome { type T }*)(body: => U): U = {
+  /* let(F -> X, ...) { BODY }
+   *
+   * Evaluate BODY in a dynamic context where each fluid F is bound to the
+   * corresponding value X.
+   */
+
+  BaseFluid.let(fxs: _*)(body);
+}
+
+trait BaseFluid[T] {
+  /* The basic fluid protocol. */
+
+  override def toString(): String =
+    f"${getClass.getName}%s@${hashCode}%x($v%s)";
+
+  protected trait Binding {
+    /* A captured binding which can be restored later.  Implementing this is
+     * a subclass responsibility.
+     */
+
+    def restore();
+      /* Restore the fluid's state to the state captured here. */
+  }
+
+  /* Fetch and modify the current binding. */
+  def v: T;
+  def v_=(x: T);
+
+  protected def capture: Binding;
+    /* Capture and the current state of the fluid. */
+
+  def let[U](x: T)(body: => U): U = {
+    /* let(X) { BODY }
+     *
+     * Evaluate BODY in a dynamic context where the fluid is bound to the
+     * value X.
+     */
+
+    val b = capture;
+    try { v = x; body } finally { b.restore(); }
+  }
+}
+
+class SharedFluid[T](init: T) extends BaseFluid[T] {
+  /* A simple global fluid.  It's probably a mistake to try to access a
+   * `SharedFluid' from multiple threads without serious synchronization.
+   */
+
+  var v: T = init;
+  private class Binding(old: T) extends super.Binding
+    { def restore() { v = old; } }
+  protected def capture: super.Binding = new Binding(v);
+}
+
+class ThreadFluid[T](init: T) extends BaseFluid[T] {
+  /* A thread-aware fluid.  The top-level binding is truly global, shared by
+   * all threads, but `let'-bindings are thread-local.
+   */
+
+  private[this] var global: T = init;
+  private[this] var bound: ThreadLocal[Option[T]] = new ThreadLocal;
+  bound.set(None);
+
+  def v: T = bound.get match { case None => global; case Some(x) => x; };
+  def v_=(x: T) { bound.get match {
+    case None => global = x;
+    case _ => bound.set(Some(x));
+  } }
+
+  private class Binding(old: Option[T]) extends super.Binding
+    { def restore() { bound.set(old); } }
+  protected def capture: super.Binding = new Binding(bound.get);
+}
+
 /*----- Other random things -----------------------------------------------*/
 
 trait LookaheadIterator[T] extends BufferedIterator[T] {