From 0157de026e802e94a2d0db0421b02ffca986c616 Mon Sep 17 00:00:00 2001 Message-Id: <0157de026e802e94a2d0db0421b02ffca986c616.1715539991.git.mdw@distorted.org.uk> From: Mark Wooding Date: Mon, 25 Jun 2018 01:37:03 +0100 Subject: [PATCH] Some more infrastructure. Maybe other things. Organization: Straylight/Edgeware From: Mark Wooding --- Makefile | 27 +-- admin.scala | 16 +- dep.scala | 428 +++++++++++++++++++++++++++++++++++++++++++++ sys.scala | 6 +- tar.scala | 16 +- toy-activity.scala | 1 + util.scala | 193 +++++++++++++++++++- 7 files changed, 650 insertions(+), 37 deletions(-) create mode 100644 dep.scala diff --git a/Makefile b/Makefile index 0675cd4..c6a85ba 100644 --- 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 diff --git a/admin.scala b/admin.scala index 52a2912..321fb56 100644 --- a/admin.scala +++ b/admin.scala @@ -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 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 . + */ + +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) => "" + case None => "" + 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 -------------------------------------------------*/ + +} diff --git a/sys.scala b/sys.scala index 8dab4c6..c449414 100644 --- 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; diff --git a/tar.scala b/tar.scala index 30a3a4a..986eeaa 100644 --- 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 diff --git a/toy-activity.scala b/toy-activity.scala index 868b3d2..dce975c 100644 --- a/toy-activity.scala +++ b/toy-activity.scala @@ -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.)"); diff --git a/util.scala b/util.scala index 8ede691..99ec790 100644 --- a/util.scala +++ b/util.scala @@ -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] { -- [mdw]