Spec in Bootstrap ClojureScript

June 14, 2016

The new clojure.spec capability was released as cljs.spec in ClojureScript 1.9.14. In particular, the use of cljs.spec with bootstrap ClojureScript was supported on day one. It ships with Planck 1.14 and, via the App Store, with Replete 1.6. The cljs.spec namespace should work with any self-hosted environment.




Porting

When spec as announced on May 23rd, my immediate concern was whether it would be amenable to self-hosted ClojureScript, in particular with respect to macros. Alex Miller explained that, in order to capture predicate forms, most of the spec API is macros at the top.

David Nolen had a ClojureScript port ready by the weekend after the announcement, and I checked to see what might need to be changed for bootstrapped ClojureScript. No changes were needed.

Well, truth be told, there was one minor adjustment: Since spec introduces its own and and or macros, a short alias was employed to make it easier to refer to the core macros, with this being accomplished with the alias function. Since this doesn't exist in ClojureScript, manual expansion of the few use sites resulted in cljs.spec working in self-hosted ClojureScript!

Shortly after that, David sorted out how to hook spec'd macros into the ClojureScript compiler's Clojure-based macroexpansion phase. He indicated a patch would be welcome for the same capability for bootstrap. Again, the bootstrap story here was easy: We just needed to uncomment the original function that Rich and Alex had authored for Clojure, and hook it in. (This is an example of how—sometimes—bootstrap ClojureScript is closer to Clojure than JVM ClojureScript.)

An interesting aside with respect to specs on macros in ClojureScript: Because in ClojureScript you can have a macro and a function with the same name, it makes sense to have different specs on macros and functions. In JVM ClojureScript, a macro spec will perforce have to be in Clojure code, with a runtime spec separately being in ClojureScript code. A similar separation essentially occurs in bootstrap ClojureScript, with a macro spec internally working against the $macros psedudo-namespace mentioned near the end of this post and runtime specs working with the un-decorated namespaces. But, the important thing is that the same code will work for both JVM and bootstrap ClojureScript.

Bugs surrounding cljs.spec are still being discovered and fixed, as you might expect, given how new it is. In order to ensure that things don't regress for bootstrap as development proceeds, the spec unit tests have been hooked in to the self-host portion of the ClojureScript compiler test suite.

Generative Testing

One interesting aspect of spec is that it can automatically use test.check to create property-based generative tests based on the specifications of the relationships of the inputs and outputs of functions. So, being able to use the test.check library from bootstrapped ClojureScript environments will be key to completing this picture for self-host.

Fortuitously, in sorting out changes for Specter for bootstrap, it became clear that a port of test.check to bootstrap would be needed. (Specter uses test.check as part of its test suite.) As part of that effort, the changes needed for bootstrap were actually sorted out prior to the release of spec.

With test.check support for bootstrap, you can use generative testing with spec in self-hosted environments. A copy with the needed changes is shipping today in Replete—so you can mess with test.check and generative testing with spec right on your iOS device.

Planck

Since spec is now available in Planck, specs have been added for all of the ancillary namespaces that Planck exposes in Planck 1.15. This is great because some of the functions in those namespaces take either maps or keyword arguments of various shapes / types: Now your calls to these functions will be checked if you have enabled spec instrumentation in Planck.

Additionally, one thing I noticed with the spec section that appears in doc output, is that specs can get to be difficult to read when they get long. Such specs are already heavily abbreviated by the spec library, but they can grow long enough to wrap, etc.

So, I've added some code to Planck that employs FIPP to format specs appearing in doc output based on the terminal width. And, another technique (an idea offered by Tom Faulhaber as is used in Autodoc), is to even further shorten namespaced keywords that appear in specs. Here is the spec portion of doc for planck.http/get, for example:

Spec
 args: (cat
         :url string?
         :opts
           (? (keys
                :opt-un
                  [::timeout
                   ::debug
                   ::accepts
                   ::content-type
                   ::headers])))
 ret: (keys :req-un [::body ::headers ::status])

Without this formatting and namespaced-keyword shortening, this spec would look like the following, which is a bit more difficult to digest with a quick glance, IMHO:

Spec
 args: (cat :url string? :opts (? (keys :opt-un [:planck.http/timeout :planck.http/debug :planck.http/accepts :planck.http/content-type :planck.http/headers])))
 ret: (keys :req-un [:planck.http/body :planck.http/headers :planck.http/status])

I hope this all helps!

Since one of the primary use cases for Planck and Replete are their educational value when learning and Clojure / ClojureScript, I'm happy they've been updated and are ready to help with learning spec as well.

Tags: ClojureScript Bootstrap