about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.guix/ccwl-distribution.scm8
-rw-r--r--.guix/ccwl-package.scm73
-rw-r--r--.guix/ccwl-website.scm74
-rw-r--r--HACKING.md27
-rw-r--r--Makefile10
-rw-r--r--NEWS.org4
-rw-r--r--README.org11
-rw-r--r--build-aux/build-home-page.el5
-rw-r--r--build-aux/test-driver.scm92
-rw-r--r--ccwl/ccwl.scm119
-rw-r--r--ccwl/conditions.scm4
-rw-r--r--ccwl/cwl.scm101
-rw-r--r--ccwl/utils.scm28
-rw-r--r--doc/ccwl.skb10
-rw-r--r--doc/prefix-arguments.scm5
-rw-r--r--doc/unseparated-prefix-arguments.scm2
-rwxr-xr-xscripts/ccwl101
-rw-r--r--tests/ccwl.scm32
-rw-r--r--tests/cwl.scm16
-rw-r--r--tests/ui.scm5
-rw-r--r--tests/utils.scm14
-rw-r--r--website/releases/ccwl-0.5.0.tar.lzbin0 -> 47416 bytes
-rw-r--r--website/releases/ccwl-0.5.0.tar.lz.asc11
23 files changed, 446 insertions, 306 deletions
diff --git a/.guix/ccwl-distribution.scm b/.guix/ccwl-distribution.scm
index 2b97f94..0598a00 100644
--- a/.guix/ccwl-distribution.scm
+++ b/.guix/ccwl-distribution.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2024, 2026 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -54,14 +54,14 @@
            "GUILE_LOAD_COMPILED_PATH"
            (list (string-append "/lib/guile/" (target-guile-effective-version) "/site-ccache"))
            (list #$development-profile))
-          (invoke "git" "clone" #$ccwl-git-repo (getcwd))
+          (invoke "git" "clone" (string-append "file://" #$ccwl-git-repo) (getcwd))
           (invoke "sh" "configure")
           (invoke "make" "dist")
           (match (scandir (getcwd) (cut string-suffix? ".tar.lz" <>))
             ((tarball)
-             (install-file tarball #$output)))))))
+             (copy-file tarball #$output)))))))
 
 (define-public ccwl-distribution
-  (computed-file "ccwl-distribution" ccwl-distribution-gexp))
+  (computed-file "ccwl.tar.lz" ccwl-distribution-gexp))
 
 ccwl-distribution
diff --git a/.guix/ccwl-package.scm b/.guix/ccwl-package.scm
index a70e3e0..2a73dbd 100644
--- a/.guix/ccwl-package.scm
+++ b/.guix/ccwl-package.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2023–2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021, 2023–2026 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -18,15 +18,27 @@
 
 (define-module (ccwl-package)
   #:use-module ((gnu packages bioinformatics) #:prefix guix:)
-  #:use-module ((gnu packages emacs) #:select (emacs-minimal))
-  #:use-module ((gnu packages fonts) #:select (font-charter font-fira-code))
-  #:use-module ((guix build-system guile) #:select (%guile-build-system-modules))
+  #:use-module ((gnu packages skribilo)
+                #:select (skribilo) #:prefix guix:)
+  #:use-module (guix download)
   #:use-module (guix gexp)
   #:use-module (guix git-download)
   #:use-module (guix packages)
-  #:use-module (guix profiles)
   #:use-module (guix utils))
 
+(define skribilo
+  (package
+    (inherit guix:skribilo)
+    (name "skribilo")
+    (version "0.10.0")
+    (source (origin
+             (method url-fetch)
+             (uri (string-append "mirror://savannah/skribilo/skribilo-"
+                                 version ".tar.gz"))
+             (sha256
+              (base32
+               "03pm2a9a5k0wkj10ywh6xi8flawm8sd396k4698gvvbc2zp4izwc"))))))
+
 (define-public ccwl
   (package
     (inherit guix:ccwl)
@@ -34,52 +46,9 @@
                         "ccwl-checkout"
                         #:recursive? #t
                         #:select? (or (git-predicate (dirname (current-source-directory)))
-                                      (const #t))))))
-
-(define ccwl-website-gexp
-  (let ((development-profile
-         (profile
-          (content (package->development-manifest ccwl))
-          (allow-collisions? #t))))
-    (with-imported-modules %guile-build-system-modules
-      #~(begin
-          (use-modules (guix build guile-build-system)
-                       (guix build utils))
-
-          (set-path-environment-variable
-           "PATH" (list "/bin") (list #$development-profile #$emacs-minimal))
-          (set-path-environment-variable
-           "LIBRARY_PATH" (list "/lib") (list #$development-profile))
-          (set-path-environment-variable
-           "GUILE_LOAD_PATH"
-           (list (string-append "/share/guile/site/"
-                                (target-guile-effective-version)))
-           (list #$development-profile))
-          (set-path-environment-variable
-           "GUILE_LOAD_COMPILED_PATH"
-           (list (string-append "/lib/guile/" (target-guile-effective-version) "/site-ccache"))
-           (list #$development-profile))
-          (copy-recursively #$(package-source ccwl)
-                            (getcwd))
-          ;; Emacs modifies README.org presumably for the contained
-          ;; org dynamic block. So, grant write permissions.
-          (chmod "README.org" #o644)
-          (for-each patch-shebang
-                    (list "pre-inst-env"
-                          "build-aux/generate-cwl-output.sh"
-                          "scripts/ccwl"))
-          (substitute* "Makefile"
-            (("\\$\\(GUIX_ENVIRONMENT\\)")
-             #$(profile
-                (content (packages->manifest (list font-charter font-fira-code))))))
-          (invoke "sh" "configure")
-          (invoke "make"
-                  "--jobs" (number->string (parallel-job-count)))
-          (invoke "make" "website"
-                  "--jobs" (number->string (parallel-job-count)))
-          (copy-recursively "website" #$output)))))
-
-(define-public ccwl-website
-  (computed-file "ccwl-website" ccwl-website-gexp))
+                                      (const #t))))
+    (native-inputs
+     (modify-inputs (package-native-inputs guix:ccwl)
+       (replace "skribilo" skribilo)))))
 
 ccwl
diff --git a/.guix/ccwl-website.scm b/.guix/ccwl-website.scm
new file mode 100644
index 0000000..754a0d5
--- /dev/null
+++ b/.guix/ccwl-website.scm
@@ -0,0 +1,74 @@
+;;; ccwl --- Concise Common Workflow Language
+;;; Copyright © 2025 Arun Isaac <arunisaac@systemreboot.net>
+;;;
+;;; This file is part of ccwl.
+;;;
+;;; ccwl 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.
+;;;
+;;; ccwl 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 ccwl.  If not, see <https://www.gnu.org/licenses/>.
+
+(define-module (ccwl-website)
+  #:use-module ((ccwl-package) #:select (ccwl))
+  #:use-module ((gnu packages emacs) #:select (emacs-minimal))
+  #:use-module ((gnu packages fonts) #:select (font-charter font-fira-code))
+  #:use-module ((guix build-system guile) #:select (%guile-build-system-modules))
+  #:use-module (guix gexp)
+  #:use-module (guix packages)
+  #:use-module (guix profiles))
+
+(define ccwl-website-gexp
+  (let ((development-profile
+         (profile
+          (content (package->development-manifest ccwl))
+          (allow-collisions? #t))))
+    (with-imported-modules %guile-build-system-modules
+      #~(begin
+          (use-modules (guix build guile-build-system)
+                       (guix build utils))
+
+          (set-path-environment-variable
+           "PATH" (list "/bin") (list #$development-profile #$emacs-minimal))
+          (set-path-environment-variable
+           "LIBRARY_PATH" (list "/lib") (list #$development-profile))
+          (set-path-environment-variable
+           "GUILE_LOAD_PATH"
+           (list (string-append "/share/guile/site/"
+                                (target-guile-effective-version)))
+           (list #$development-profile))
+          (set-path-environment-variable
+           "GUILE_LOAD_COMPILED_PATH"
+           (list (string-append "/lib/guile/" (target-guile-effective-version) "/site-ccache"))
+           (list #$development-profile))
+          (copy-recursively #$(package-source ccwl)
+                            (getcwd))
+          ;; Emacs modifies README.org presumably for the contained
+          ;; org dynamic block. So, grant write permissions.
+          (chmod "README.org" #o644)
+          (for-each patch-shebang
+                    (list "pre-inst-env"
+                          "build-aux/generate-cwl-output.sh"
+                          "scripts/ccwl"))
+          (substitute* "Makefile"
+            (("\\$\\(GUIX_ENVIRONMENT\\)")
+             #$(profile
+                (content (packages->manifest (list font-charter font-fira-code))))))
+          (invoke "sh" "configure")
+          (invoke "make"
+                  "--jobs" (number->string (parallel-job-count)))
+          (invoke "make" "website"
+                  "--jobs" (number->string (parallel-job-count)))
+          (copy-recursively "website" #$output)))))
+
+(define-public ccwl-website
+  (computed-file "ccwl-website" ccwl-website-gexp))
+
+ccwl-website
diff --git a/HACKING.md b/HACKING.md
new file mode 100644
index 0000000..bb822b3
--- /dev/null
+++ b/HACKING.md
@@ -0,0 +1,27 @@
+# Set up development environment
+
+Drop into a development environment using `guix shell`.
+```
+guix shell -Df guix.scm
+```
+
+# Make a release
+## Tag a release
+Tag a release `vx.x.x` putting news into the tag message.
+## Create a release tarball, test it, and sign it
+```
+cp $(guix build -L .guix -f .guix/ccwl-distribution.scm) ccwl-x.x.x.tar.lz
+guix build --with-source=ccwl=ccwl-x.x.x.tar.lz -f guix.scm
+make distsign
+```
+## Build guix pack, docker and singularity images
+```
+guix pack --with-source=ccwl=ccwl-x.x.x.tar.lz --file=guix.scm
+guix pack -f docker -S /bin=bin --with-source=ccwl=ccwl-x.x.x.tar.lz --file=guix.scm
+guix pack -f squashfs --with-source=ccwl=ccwl-x.x.x.tar.lz bash --file=guix.scm
+```
+## Publish release tarball
+Add release tarball and signature to website. Publish release tarball, guix pack, docker and singularity images to GitHub.
+## Update Guix package
+## Publicize
+Publicize on the ccwl@systemreboot.net and guix-science@gnu.org mailing lists, and on the [CWL Discourse forum](https://cwl.discourse.group/).
diff --git a/Makefile b/Makefile
index f4487d9..5a9b07f 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 # ccwl --- Concise Common Workflow Language
-# Copyright © 2022, 2024 Arun Isaac <arunisaac@systemreboot.net>
+# Copyright © 2022, 2024–2025 Arun Isaac <arunisaac@systemreboot.net>
 #
 # This file is part of ccwl.
 #
@@ -24,12 +24,12 @@ GIT = git
 GPG = gpg
 GUILD = guild
 GUILE = guile
+GUILE_RUN64 = guile-run64
 LZIP = lzip
 SKRIBILO = skribilo
 
 FIND_DEPENDENCIES = build-aux/find-dependencies.scm
 GENERATE_CWL_OUTPUT = build-aux/generate-cwl-output.sh
-TEST_DRIVER = build-aux/test-driver.scm
 
 top_level_module_dir = $(project)
 sources = $(wildcard $(top_level_module_dir)/*.scm)
@@ -46,7 +46,7 @@ fonts = $(addprefix $(GUIX_ENVIRONMENT)/share/fonts/web/, charter_regular.woff2
 distribute_files = $(sources) $(scripts) $(tests) $(test_data) \
                    $(doc_sources) doc/skribilo.scm $(doc_data) $(DOC_SCM) $(DOC_OTHER) \
                    pre-inst-env guix.scm Makefile configure configure.scm \
-		   $(FIND_DEPENDENCIES) $(GENERATE_CWL_OUTPUT) $(TEST_DRIVER) \
+		   $(FIND_DEPENDENCIES) $(GENERATE_CWL_OUTPUT) \
 		   COPYING NEWS.org README.org
 
 scmdir = $(datarootdir)/guile/site/$(guile_effective_version)/$(top_level_module_dir)
@@ -63,8 +63,8 @@ all: $(objects)
 
 # Run tests
 
-check: $(tests) $(TEST_DRIVER)
-	$(GUILE) --no-auto-compile -L . $(TEST_DRIVER) $(tests)
+check:
+	./pre-inst-env $(GUILE_RUN64) $(tests)
 
 # Build documentation
 
diff --git a/NEWS.org b/NEWS.org
index c351602..02fe372 100644
--- a/NEWS.org
+++ b/NEWS.org
@@ -1,6 +1,8 @@
 #+TITLE: ccwl NEWS – History of user-visible changes
 
-Copyright © 2021, 2024, 2025 Arun Isaac <arunisaac@systemreboot.net>
+Copyright © 2021, 2024–2026 Arun Isaac <arunisaac@systemreboot.net>
+
+Changes in 0.5.0 and later are in the git tag messages.
 
 * Changes in 0.4.0 (since 0.3.0)
 ** Documentation
diff --git a/README.org b/README.org
index 3d8e31a..3084234 100644
--- a/README.org
+++ b/README.org
@@ -36,14 +36,14 @@ Download the Docker image from [[https://github.com/arunisaac/ccwl/releases][the
 it. Then, run ccwl with command-line arguments of your choice.
 #+BEGIN_SRC shell
   $ docker load -i ccwl-docker.tar.gz
-  $ docker run ccwl [ARGS...]
+  $ docker run ccwl ccwl [ARGS...]
 #+END_SRC
 
 When passing files into the Docker container, remember to share the
 filesystem. The following command is one way to share the current
 directory with the container.
 #+BEGIN_SRC shell
-  $ docker run -v "$PWD:$PWD" -w "$PWD" ccwl compile foo.scm
+  $ docker run -v "$PWD:$PWD" -w "$PWD" ccwl ccwl compile foo.scm
 #+END_SRC
 
 ** Using Guix
@@ -80,8 +80,11 @@ introduction to ccwl.
 * Contributing
 
 Feedback, suggestions, feature requests, bug reports and pull requests
-are all welcome. Unclear and unspecific error messages are considered
-a bug. Do report them!
+are all welcome. Please write to [[mailto:ccwl@systemreboot.net][ccwl@systemreboot.net]]. You may also
+browse the [[https://lists.systemreboot.net/ccwl][archives]] of previous discussions. Unclear and unspecific
+error messages are considered a bug. Do report them!
+
+We do not accept issues or pull requests on GitHub. Thank you!
 
 To hack on ccwl, you can use GNU Guix to quickly drop into a
 development environment by running
diff --git a/build-aux/build-home-page.el b/build-aux/build-home-page.el
index fd76a9e..15867bd 100644
--- a/build-aux/build-home-page.el
+++ b/build-aux/build-home-page.el
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2022, 2024, 2025 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021, 2022, 2024–2026 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -41,7 +41,8 @@
 Download release tarballs.
 
 ")
-  (dolist (release '(("2025-01-28" "0.4.0")
+  (dolist (release '(("2026-01-13" "0.5.0")
+                     ("2025-01-28" "0.4.0")
                      ("2024-01-26" "0.3.0")
                      ("2021-11-05" "0.2.0")
                      ("2021-07-06" "0.1.0")))
diff --git a/build-aux/test-driver.scm b/build-aux/test-driver.scm
deleted file mode 100644
index adac481..0000000
--- a/build-aux/test-driver.scm
+++ /dev/null
@@ -1,92 +0,0 @@
-;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2022 Arun Isaac <arunisaac@systemreboot.net>
-;;;
-;;; This file is part of ccwl.
-;;;
-;;; ccwl 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.
-;;;
-;;; ccwl 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 ccwl.  If not, see <https://www.gnu.org/licenses/>.
-
-;;; Commentary:
-
-;; This is a better test driver for Guile's (srfi srfi-64).
-;;
-;; TODO: Improve Guile's test driver so this module won't be
-;; necessary.
-
-;;; Code:
-
-(use-modules (ice-9 format)
-             (ice-9 match)
-             (srfi srfi-26)
-             (srfi srfi-64)
-             (term ansi-color))
-
-(define (color color-symbol str color?)
-  (if color?
-      (colorize-string str color-symbol)
-      str))
-
-(define red (cut color 'RED <> <>))
-(define green (cut color 'GREEN <> <>))
-(define magenta (cut color 'MAGENTA <> <>))
-
-(define (my-gnu-runner color?)
-  (let ((runner (test-runner-null)))
-    (test-runner-on-group-begin! runner
-      (lambda (runner suite-name count)
-        (format #t (magenta "%%%% ~a~%" color?) suite-name)))
-    (test-runner-on-group-end! runner
-      (lambda _
-        (newline)))
-    (test-runner-on-test-end! runner
-      (lambda (runner)
-        (let ((name (test-runner-test-name runner))
-              (result (string-upcase
-                       (symbol->string (test-result-kind runner))))
-              (result-alist (test-result-alist runner)))
-          (format #t "~a ~a~%"
-                  (case (test-result-kind runner)
-                    ((pass) (green result color?))
-                    (else (red result color?)))
-                  name)
-          ;; If test did not pass, print details.
-          (unless (eq? (test-result-kind runner) 'pass)
-            (format (current-error-port)
-                    "~a:~a~%expected: ~s~%actual: ~s~%"
-                    (assq-ref result-alist 'source-file)
-                    (assq-ref result-alist 'source-line)
-                    (match (assq-ref result-alist 'source-form)
-                      (('test-assert _ ...) #t)
-                      (_ (assq-ref result-alist 'expected-value)))
-                    (assq-ref result-alist 'actual-value))))))
-    runner))
-
-(match (command-line)
-  ((_ test-files ...)
-   (let ((runner (my-gnu-runner #t)))
-     (test-with-runner runner
-       (for-each load-from-path test-files)
-       (display (magenta "SUMMARY" #t))
-       (newline)
-       (format #t "PASS: ~a
-FAIL: ~a
-XPASS: ~a
-XFAIL: ~a
-SKIP: ~a
-"
-               (test-runner-pass-count runner)
-               (test-runner-fail-count runner)
-               (test-runner-xpass-count runner)
-               (test-runner-xfail-count runner)
-               (test-runner-skip-count runner))
-       (exit (zero? (test-runner-fail-count runner)))))))
diff --git a/ccwl/ccwl.scm b/ccwl/ccwl.scm
index 3fc5364..075ae0c 100644
--- a/ccwl/ccwl.scm
+++ b/ccwl/ccwl.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021–2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021–2026 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -72,6 +72,7 @@
             input-default
             input-position
             input-prefix
+            input-separate?
             input-separator
             input-stage?
             input-other
@@ -93,7 +94,7 @@
             unspecified-default?))
 
 (define-immutable-record-type <input>
-  (make-input id type label default position prefix stage? other)
+  (make-input id type label default position prefix separate? separator stage? other)
   input?
   (id input-id)
   (type input-type)
@@ -101,6 +102,7 @@
   (default input-default set-input-default)
   (position input-position set-input-position)
   (prefix input-prefix set-input-prefix)
+  (separate? input-separate? set-input-separate?)
   (separator input-separator set-input-separator)
   (stage? input-stage?)
   (other input-other))
@@ -206,7 +208,8 @@ compared using @code{equal?}."
                                 #,(if (unspecified-default? default)
                                       #'(make-unspecified-default)
                                       default)
-                                #,position #,prefix #,stage? '#,other)))
+                                #,position #,prefix #f #f
+                                #,stage? '#,other)))
               #'(id args ...))))
     (id (identifier? #'id) (input #'(id)))
     (_ (error "Invalid input:" (syntax->datum input-spec)))))
@@ -279,7 +282,7 @@ compared using @code{equal?}."
 (define-immutable-record-type <js-expression>
   (make-js-expression inputs expression outputs requirements other)
   js-expression?
-  (inputs js-expression-inputs)
+  (inputs js-expression-inputs set-js-expression-inputs)
   (expression js-expression-expression)
   (outputs js-expression-outputs)
   (requirements js-expression-requirements)
@@ -289,14 +292,14 @@ compared using @code{equal?}."
   (make-cwl-workflow file inputs outputs)
   cwl-workflow?
   (file cwl-workflow-file)
-  (inputs cwl-workflow-inputs)
+  (inputs cwl-workflow-inputs set-cwl-workflow-inputs)
   (outputs cwl-workflow-outputs))
 
 (define-immutable-record-type <workflow>
   (make-workflow steps inputs outputs other)
   workflow?
   (steps workflow-steps)
-  (inputs workflow-inputs)
+  (inputs workflow-inputs set-workflow-inputs)
   (outputs workflow-outputs)
   (other workflow-other))
 
@@ -350,13 +353,13 @@ return @code{#f}."
                    (input (identifier? #'input)
                           (syntax->datum #'input))
                    ;; prefixed input
-                   ((_ input) (identifier? #'input)
+                   ((_ input _ ...) (identifier? #'input)
                     (syntax->datum #'input))
                    ;; array input specifier
                    ((array input _ ...) (identifier? #'input)
                     (syntax->datum #'input))
                    ;; prefixed array input specifier
-                   ((_ (array input _ ...)) (identifier? #'input)
+                   ((_ (array input _ ...) _ ...) (identifier? #'input)
                     (syntax->datum #'input))
                    (_ #f))))
             (and run-arg-input
@@ -367,7 +370,27 @@ return @code{#f}."
   "Return the prefix specified in @var{run-arg} syntax. If not a prefixed
 input, return #f."
   (syntax-case run-arg ()
-    ((prefix _) #'prefix)
+    ((prefix _ ...) (string? (syntax->datum #'prefix))
+     #'prefix)
+    (_ #f)))
+
+(define (validate-separate? separate?)
+  "Validate @var{separate?} and raise an exception if it is not valid."
+  (unless (boolean? separate?)
+    (raise-exception
+     (condition (ccwl-violation separate?)
+                (formatted-message "Invalid #:separate? flag ~a. #:separate? flag must be a boolean."
+                                   (syntax->datum separate?))))))
+
+(define (run-arg-separate? run-arg)
+  "Return the separate? specified in @var{run-arg} syntax. If not a
+prefixed input, return #f."
+  (syntax-case run-arg (array)
+    ((prefix _ args ...) (string? (syntax->datum #'prefix))
+     (apply (syntax-lambda** (#:key (separate? #'#t))
+              (validate-separate? (syntax->datum separate?))
+              separate?)
+            #'(args ...)))
     (_ #f)))
 
 (define (run-arg-separator run-arg)
@@ -385,7 +408,7 @@ input, return #f."
                 separator))
             #'(args ...)))
     ;; prefixed array input specifier
-    ((_ (array input args ...))
+    ((_ (array input args ...) _ ...)
      (run-arg-separator #'(array input args ...)))
     (_ #f)))
 
@@ -415,14 +438,20 @@ identifiers defined in the commands."
        (syntax->run-arg #'input))
       ;; Flatten prefixed string arguments. They have no
       ;; special meaning.
-      ((prefix string-arg) (and (string? (syntax->datum #'prefix))
-                                (string? (syntax->datum #'string-arg)))
-       (list #'prefix #'string-arg))
+      ((prefix string-arg args ...) (and (string? (syntax->datum #'prefix))
+                                         (string? (syntax->datum #'string-arg)))
+       (apply (syntax-lambda** (#:key (separate? #'#t))
+                (validate-separate? (syntax->datum separate?))
+                (if (syntax->datum separate?)
+                    (list #'prefix #'string-arg)
+                    (list #`#,(string-append (syntax->datum #'prefix)
+                                             (syntax->datum #'string-arg)))))
+              #'(args ...)))
       ;; Recurse on prefixed inputs.
-      ((prefix input) (string? (syntax->datum #'prefix))
+      ((prefix input _ ...) (string? (syntax->datum #'prefix))
        (syntax->run-arg #'input))
       ;; Prefixes that are not strings
-      ((prefix _)
+      ((prefix _ ...)
        (raise-exception
         (condition (ccwl-violation #'prefix)
                    (formatted-message "Invalid prefix ~a. Prefixes must be strings."
@@ -482,18 +511,21 @@ identifiers defined in the commands."
                                        (let* ((id (input-spec-id input-spec))
                                               (run-arg (find-run-arg id run)))
                                          #`(set-input-separator
-                                            (set-input-prefix
-                                             (set-input-position
-                                              #,(input input-spec)
-                                              ;; `run-args' returns inputs as quoted symbols.
-                                              ;; So, we add quote.
-                                              #,(list-index (match-lambda
-                                                              (`(quote ,input)
-                                                               (eq? input id))
-                                                              (_ #f))
-                                                            (syntax->datum flattened-args)))
+                                            (set-input-separate?
+                                             (set-input-prefix
+                                              (set-input-position
+                                               #,(input input-spec)
+                                               ;; `run-args' returns inputs as quoted symbols.
+                                               ;; So, we add quote.
+                                               #,(list-index (match-lambda
+                                                               (`(quote ,input)
+                                                                (eq? input id))
+                                                               (_ #f))
+                                                             (syntax->datum flattened-args)))
+                                              #,(and run-arg
+                                                     (run-arg-prefix run-arg)))
                                              #,(and run-arg
-                                                    (run-arg-prefix run-arg)))
+                                                    (run-arg-separate? run-arg)))
                                             #,(and run-arg
                                                    (run-arg-separator run-arg)))))
                                      inputs))
@@ -600,7 +632,7 @@ identifiers defined in the commands."
                              ((id . type)
                               (with-syntax ((id (datum->syntax #f id))
                                             (type (datum->syntax #f type)))
-                                #`(make-input 'id 'type #f #f #f #f #f '()))))
+                                #`(make-input 'id 'type #f #f #f #f #f #f #f '()))))
                            (parameters->id+type (assoc-ref yaml "inputs"))))
               (list #,@(map (match-lambda
                              ((id . type)
@@ -612,7 +644,7 @@ identifiers defined in the commands."
 (define (function-inputs function)
   "Return the list of inputs accepted by @var{function}---a
 @code{<command>}, @code{<js-expression>}, @code{<cwl-workflow>} or
-@code{<workflow> object."
+@code{<workflow>} object."
   ((cond
     ((command? function) command-inputs)
     ((js-expression? function) js-expression-inputs)
@@ -621,6 +653,21 @@ identifiers defined in the commands."
     (else (error "Unrecognized ccwl function" function)))
    function))
 
+(define (set-function-inputs function inputs)
+  "Set inputs of @var{function} to @var{inputs}---a
+@code{<command>}, @code{<js-expression>}, @code{<cwl-workflow>} or
+@code{<workflow> object. @code{set-function-inputs} is purely
+functional. It returns a copy of @var{function}. @var{function} is not
+mutated."
+  ((cond
+    ((command? function) set-command-inputs)
+    ((js-expression? function) set-js-expression-inputs)
+    ((cwl-workflow? function) set-cwl-workflow-inputs)
+    ((workflow? function) set-workflow-inputs)
+    (else (error "Unrecognized ccwl function" function)))
+   function
+   inputs))
+
 (define (function-input-keys function)
   "Return the list of input keys accepted by FUNCTION, a <command>,
 <js-expression>, <cwl-workflow> or <workflow> object."
@@ -647,11 +694,11 @@ identifiers defined in the commands."
       ;; Global input/output
       (symbol->string (key-cwl-id key))))
 
-(define (apply-partially command partial-arguments)
-  "Return a new command that is a partial application of
-@var{partial-arguments} to @var{command}. @var{partial-arguments} is
+(define (apply-partially function partial-arguments)
+  "Return a new function that is a partial application of
+@var{partial-arguments} to @var{function}. @var{partial-arguments} is
 an association list mapping keyword arguments to their values."
-  (set-command-inputs command
+  (set-function-inputs function
     (map (lambda (input)
            (set-input-default input
              (or (any (match-lambda
@@ -661,7 +708,7 @@ an association list mapping keyword arguments to their values."
                               value)))
                       partial-arguments)
                  (input-default input))))
-         (command-inputs command))))
+         (function-inputs function))))
 
 (define (function-object x)
   "Return the ccwl function object (a <command>, <js-expression>,
@@ -711,7 +758,7 @@ represented by <step> objects."
             (list)))
     ;; tee
     ((tee expressions ...)
-     (let ((key-lists step-lists (mapn (cut collect-steps <> input-keys)
+     (let ((key-lists step-lists (map2 (cut collect-steps <> input-keys)
                                        #'(expressions ...))))
        (values
         ;; Global workflow input keys may be duplicated across the
@@ -858,7 +905,9 @@ represented by <step> objects."
                                                           input-keys)))))
                                           (pairify (syntax->datum #'(args ...))))))))
            ;; If literal values are provided as arguments, partially
-           ;; apply those literal values to the command and recurse.
+           ;; apply those literal values to the function object
+           ;; (command, workflow, js-expression, cwl-workflow) and
+           ;; recurse.
            (_
             (collect-steps #`(((module-ref (resolve-module '(ccwl ccwl))
                                            'apply-partially)
diff --git a/ccwl/conditions.scm b/ccwl/conditions.scm
index c6523cb..abbd404 100644
--- a/ccwl/conditions.scm
+++ b/ccwl/conditions.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2022 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2022, 2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -63,7 +63,7 @@
   (arguments formatted-message-arguments))
 
 (define (formatted-message format-string . arguments)
-  "Return &ccwl-message condition for FORMAT-STRING with ARGUMENTS."
+  "Return &formatted-message condition for FORMAT-STRING with ARGUMENTS."
   (make-formatted-message (apply format format-string arguments)
                           format-string
                           arguments))
diff --git a/ccwl/cwl.scm b/ccwl/cwl.scm
index baef519..f954f08 100644
--- a/ccwl/cwl.scm
+++ b/ccwl/cwl.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2023–2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021, 2023–2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -51,16 +51,34 @@ specification."
   (scm->yaml (workflow->cwl-scm workflow)
              port))
 
-(define (filter-alist alist)
-  "Filter ALIST removing entries with #f as the value. If the
-resulting association list is empty, return #f. Else, return that
-association list."
-  (match (filter (match-lambda
-                   ((_ . #f) #f)
-                   (_ #t))
-                 alist)
-    (() #f)
-    (result result)))
+(define (vector-filter-map proc vec)
+  "Map @var{proc} over @var{vec} and return a vector of the results that
+are not @code{#f}."
+  (list->vector (filter-map proc (vector->list vec))))
+
+(define (prune-tree tree)
+  "Prune JSON-like @var{tree} removing dictionary keys without a mapping."
+  (match tree
+    ;; Array
+    (#(elements ...)
+     (vector-filter-map (lambda (element)
+                          (match (prune-tree element)
+                            (() #f)
+                            (pruned-element pruned-element)))
+                        tree))
+    ;; Dictionary
+    ((pairs ...)
+     (filter-map (match-lambda
+                   ;; Key with a mapping
+                   ((key . value)
+                    (match (prune-tree value)
+                      (() #f)
+                      (pruned-value (cons key pruned-value))))
+                   ;; Key without a mapping
+                   ((key) #f))
+                 pairs))
+    ;; Atom
+    (atom atom)))
 
 (define* (workflow->cwl-scm workflow)
   "Render WORKFLOW, a <workflow> object, into a CWL tree."
@@ -114,17 +132,17 @@ association list."
   "Render @var{output}, a @code{<output>} object, into a CWL tree. If
 @var{workflow?} is @code{#t}, this is a workflow output."
   `(,(output-id output)
-    ,@(or (filter-alist
-           `((type . ,(type->cwl (output-type output)))
-             ;; outputBinding is relevant only to commands, and
-             ;; outputSource is relevant only to workflows.
-             ,@(if workflow?
-                   `((outputSource . ,(match (output-source output)
-                                        ((? string? source) source)
-                                        ((? input? input) (input-id input)))))
-                   `((outputBinding . ,(output-binding output))))))
-          '())
-    ,@(output-other output)))
+    ,@(prune-tree
+       `((type . ,(type->cwl (output-type output)))
+         ;; outputBinding is relevant only to commands, and
+         ;; outputSource is relevant only to workflows.
+         ,@(if workflow?
+               `((outputSource . ,(match (output-source output)
+                                    ((? string? source) source)
+                                    ((? input? input) (input-id input)))))
+               `((outputBinding . ,(or (output-binding output)
+                                       '()))))
+         ,@(output-other output)))))
 
 (define (command->cwl command port)
   "Render @var{command}, a @code{<command>} object, to @var{port} as a
@@ -135,21 +153,30 @@ CWL YAML specification."
 (define (input->cwl-scm input)
   "Render @var{input}, a @code{<input>} object, into a CWL tree."
   `(,(input-id input)
-    (type . ,(type->cwl (input-type input)))
-    ,@(or (filter-alist
-           `((label . ,(input-label input))
-             (default . ,(and (not (unspecified-default? (input-default input)))
-                              (input-default input)))
-             ;; inputBinding is only relevant to commands, not
-             ;; workflows. But, the input position and prefix are not set
-             ;; for worklow inputs and therefore this sub-expression has
-             ;; no effect. So, leave this be.
-             (inputBinding . ,(filter-alist
-                               `((position . ,(input-position input))
-                                 (prefix . ,(input-prefix input))
-                                 (itemSeparator . ,(input-separator input)))))))
-          '())
-    ,@(input-other input)))
+    ,@(prune-tree
+       `((type . ,(type->cwl (input-type input)))
+         (default . ,(if (unspecified-default? (input-default input))
+                         '()
+                         (input-default input)))
+         (label . ,(or (input-label input)
+                       '()))
+         ;; inputBinding is only relevant to commands, not workflows.
+         ;; But, the input position and prefix are not set for worklow
+         ;; inputs and therefore this sub-expression has no effect.
+         ;; So, leave this be.
+         (inputBinding
+          (position . ,(or (input-position input)
+                           '()))
+          (prefix . ,(or (input-prefix input)
+                         '()))
+          ;; separate? has a meaningful value only with prefix.
+          (separate . ,(if (input-prefix input)
+                           (and (input-separate? input)
+                                '())
+                           '()))
+          (itemSeparator . ,(or (input-separator input)
+                                '())))
+         ,@(input-other input)))))
 
 (define (staging-requirements inputs)
   "Return @samp{InitialWorkDirRequirement} to stage any @var{inputs} that
diff --git a/ccwl/utils.scm b/ccwl/utils.scm
index 3c18efd..2abc404 100644
--- a/ccwl/utils.scm
+++ b/ccwl/utils.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2022, 2023 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021, 2022, 2023, 2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -39,6 +39,7 @@
             lambda**
             syntax-lambda**
             mapn
+            map2
             foldn
             filter-mapi))
 
@@ -311,22 +312,31 @@ element. For example,
               lst
               (iota (length lst))))
 
-(define (mapn proc lst)
+(define (mapn proc lst n)
   "Map the procedure PROC over list LST and return a list containing
-the results. PROC can return multiple values, in which case, an equal
-number of lists are returned. For example,
+the results. PROC must return N values, in which case, N lists are
+returned. For example,
 
 (mapn (lambda (n)
         (values (expt n 2)
                 (expt n 3)))
-      (iota 5))
+      (iota 5)
+      2)
 => (0 1 4 9 16)
 => (0 1 8 27 64)"
   (apply values
-         (apply zip
-                (map (lambda (x)
-                       (call-with-values (cut proc x) list))
-                     lst))))
+         (match lst
+           ;; With an empty list, we cannot know the number of values
+           ;; proc would return. Hence this special case.
+           (() (make-list n '()))
+           (_
+            (apply zip
+                   (map (lambda (x)
+                          (call-with-values (cut proc x) list))
+                        lst))))))
+
+(define map2
+  (cut mapn <> <> 2))
 
 (define (foldn proc lst . inits)
   "Apply PROC to the elements of LST to build a result, and return
diff --git a/doc/ccwl.skb b/doc/ccwl.skb
index 9001f20..7096711 100644
--- a/doc/ccwl.skb
+++ b/doc/ccwl.skb
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2023–2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021, 2023–2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -363,6 +363,14 @@ prefix. For example, in the following example, we associate the input
 ,(code "output_filename") to the prefix ,(code "-o"). Notice the
 parentheses around ,(code "-o output_filename").]
          (scheme-source "doc/prefix-arguments.scm")))
+    (section :title [Unseparated prefix arguments]
+             :ident "section-unseparated-prefix-arguments"
+      (p [Some programs don't like it when you separate arguments from
+their prefixes. You can specify this using the ,(code [#:separate?])
+flag.]
+         (scheme-source "doc/unseparated-prefix-arguments.scm")
+         [This is executed as ,(samp [gcc foo.c -ofoo]), rather than
+as ,(samp [gcc foo.c -o foo]).]))
     (section :title [Array types]
              :ident "section-array-types"
       (p [ccwl supports array types using the following syntax.]
diff --git a/doc/prefix-arguments.scm b/doc/prefix-arguments.scm
index ec95aeb..bb54620 100644
--- a/doc/prefix-arguments.scm
+++ b/doc/prefix-arguments.scm
@@ -1,5 +1,2 @@
 (command #:inputs (source #:type File) (output_filename #:type string)
-         #:run "gcc" source ("-o" output_filename)
-         #:outputs (executable
-                    #:type File
-                    #:binding ((glob . "$(inputs.output_filename)"))))
+         #:run "gcc" source ("-o" output_filename))
diff --git a/doc/unseparated-prefix-arguments.scm b/doc/unseparated-prefix-arguments.scm
new file mode 100644
index 0000000..9e1e767
--- /dev/null
+++ b/doc/unseparated-prefix-arguments.scm
@@ -0,0 +1,2 @@
+(command #:inputs (source #:type File) (output_filename #:type string)
+         #:run "gcc" source ("-o" output_filename #:separate? #f))
diff --git a/scripts/ccwl b/scripts/ccwl
index 984d22a..fbd549d 100755
--- a/scripts/ccwl
+++ b/scripts/ccwl
@@ -3,7 +3,7 @@
 exec guile --no-auto-compile -e main -s "$0" "$@"
 !#
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021–2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021–2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -28,6 +28,7 @@ exec guile --no-auto-compile -e main -s "$0" "$@"
 
 (use-modules (rnrs conditions)
              (rnrs exceptions)
+             (srfi srfi-26)
              (srfi srfi-28)
              (srfi srfi-37)
              (ice-9 match)
@@ -49,6 +50,52 @@ exec guile --no-auto-compile -e main -s "$0" "$@"
           (lambda (opt name arg result)
             (acons 'help #t result))))
 
+(define %options
+  (list (option (list #\t "to") #t #f
+                (lambda (opt name arg result)
+                  (let ((supported (list "cwl" "dot")))
+                    (unless (member arg supported)
+                      (raise-exception
+                       (formatted-message "Invalid target ~a argument ~s. Supported targets are ~a."
+                                          (if (char? name)
+                                              (string #\- name)
+                                              (string-append "--" name))
+                                          arg
+                                          (string-join supported ", ")))))
+                  (acons 'to (string->symbol arg)
+                         result)))
+        (option (list #\o "output") #t #f
+                (lambda (opt name arg result)
+                  (acons 'output-file arg result)))
+        %help-option))
+
+(define (ccwl-compile source to port)
+  "Compile @var{source} file to @var{to} format writing output to
+@var{port}. @var{to} is either @code{'cwl} or @code{'dot}."
+  ;; We don't need to compile ccwl files. Loading is sufficient for
+  ;; our purposes. Besides, compiling would fail since the workflow
+  ;; macro cannot access command definitions.
+  (set! %load-should-auto-compile #f)
+  ((case to
+     ((cwl) function->cwl)
+     ((dot) function->dot))
+   (let ((result (guard (exception
+                         ;; Handle syntax violation exceptions by
+                         ;; reporting them and exiting.
+                         ((ccwl-violation? exception)
+                          (report-ccwl-violation exception)
+                          (exit #f)))
+                   (load (canonicalize-path source)
+                         read-syntax))))
+     (if (or (command? result)
+             (js-expression? result)
+             (workflow? result))
+         result
+         (raise-exception
+          (formatted-message "Last expression in file ~a returns none of workflow, command or js-expression"
+                             source))))
+   port))
+
 (define (main args)
   (with-exception-handler
       (lambda (condition)
@@ -75,30 +122,17 @@ Thank you!
       (match args
         ((program "compile" args ...)
          (let* ((args (args-fold args
-                                 (list (option (list #\t "to") #t #f
-                                               (lambda (opt name arg result)
-                                                 (let ((supported (list "cwl" "dot")))
-                                                   (unless (member arg supported)
-                                                     (scm-error 'misc-error
-                                                                #f
-                                                                "Invalid target ~A argument ~S. Supported targets are ~A."
-                                                                (list (if (char? name)
-                                                                          (string #\- name)
-                                                                          (string-append "--" name))
-                                                                      arg
-                                                                      (string-join supported ", "))
-                                                                #f)))
-                                                 (acons 'to arg result)))
-                                       %help-option)
+                                 %options
                                  invalid-option
                                  (lambda (arg result)
                                    (acons 'source-file arg result))
-                                 '((to . "cwl")))))
+                                 '((to . cwl)))))
            (when (or (assq 'help args)
                      (not (assq-ref args 'source-file)))
              (display (format "Usage: ~a compile [OPTIONS] SOURCE-FILE
 Compile SOURCE-FILE.
 
+  -o, --output=FILE  write compiled output to file
   -t, --to=TARGET    compile SOURCE-FILE to TARGET language;
                      Supported targets are cwl (default) and dot.
 
@@ -106,30 +140,15 @@ Compile SOURCE-FILE.
                               program)
                       (current-error-port))
              (exit (assq 'help args)))
-           ;; We don't need to compile ccwl files. Loading is sufficient
-           ;; for our purposes. Besides, compiling would fail since the
-           ;; workflow macro cannot access command definitions.
-           (set! %load-should-auto-compile #f)
-           (let ((to (assq-ref args 'to)))
-             ((cond
-               ((string=? to "cwl") function->cwl)
-               ((string=? to "dot") function->dot))
-              (guard (exception
-                      ;; Handle syntax violation exceptions by reporting
-                      ;; them and exiting.
-                      ((ccwl-violation? exception)
-                       (report-ccwl-violation exception)
-                       (exit #f)))
-                (let ((result (load (canonicalize-path (assq-ref args 'source-file))
-                                      read-syntax)))
-                  (if (or (command? result)
-                          (js-expression? result)
-                          (workflow? result))
-                      result
-                      (raise-exception
-                       (condition (formatted-message "Last expression in file ~a returns none of workflow, command or js-expression"
-                                                     (assq-ref args 'source-file)))))))
-              (current-output-port)))))
+           (if (assq-ref args 'output-file)
+               (call-with-output-file (assq-ref args 'output-file)
+                 (cut ccwl-compile
+                      (assq-ref args 'source-file)
+                      (assq-ref args 'to)
+                      <>))
+               (ccwl-compile (assq-ref args 'source-file)
+                             (assq-ref args 'to)
+                             (current-output-port)))))
         ((program args ...)
          (let ((args (args-fold args
                                 (list %help-option)
diff --git a/tests/ccwl.scm b/tests/ccwl.scm
index 2d755ad..85ec322 100644
--- a/tests/ccwl.scm
+++ b/tests/ccwl.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021–2024 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021–2026 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -20,6 +20,7 @@
              (srfi srfi-1)
              (srfi srfi-64)
              (srfi srfi-71)
+             (ice-9 match)
              (ccwl ccwl)
              (ccwl conditions))
 
@@ -60,17 +61,14 @@
 
 (test-begin "ccwl")
 
-(test-assert "stdin input should not have inputBinding"
-  (not (assoc-ref
-        (assoc-ref
-         (assoc-ref
-          ((@@ (ccwl cwl) command->cwl-scm)
-           (command #:inputs (file #:type File)
-                    #:run "wc" "-c"
-                    #:stdin file))
-          'inputs)
-         'file)
-        'inputBinding)))
+(test-equal "stdin input should not have inputBinding"
+  '((file (type . File)))
+  (assoc-ref
+   ((@@ (ccwl cwl) command->cwl-scm)
+    (command #:inputs (file #:type File)
+             #:run "wc" "-c"
+             #:stdin file))
+   'inputs))
 
 (test-equal "read all forms of inputs and outputs from a CWL workflow"
   '(((spam string))
@@ -189,6 +187,16 @@
   (workflow ()
     (print-int #:number 42)))
 
+;; TODO: Define this in the lexical scope of the test that requires
+;; it.
+(define print-workflow
+  (workflow ((message #:type string))
+    (print #:message message)))
+
+(test-assert "allow literals as arguments to workflows"
+  (workflow ()
+    (print-workflow #:message "foo")))
+
 (test-condition "step supplied with an unknown key must raise a &ccwl-violation condition"
   ccwl-violation?
   (macroexpand
diff --git a/tests/cwl.scm b/tests/cwl.scm
index ba619ab..ae8fa6b 100644
--- a/tests/cwl.scm
+++ b/tests/cwl.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2023 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2023, 2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -18,6 +18,12 @@
 
 (use-modules (srfi srfi-64))
 
+(define make-input
+  (@@ (ccwl ccwl) make-input))
+
+(define input->cwl-scm
+  (@@ (ccwl cwl) input->cwl-scm))
+
 (define type->cwl
   (@@ (ccwl cwl) type->cwl))
 
@@ -41,4 +47,12 @@
               (items . File))))
   (type->cwl (make-array-type (make-array-type 'File))))
 
+(test-equal "Serialize #f defaults in input values"
+  '("foo"
+    (type . boolean)
+    (default . #f)
+    (label . "foo"))
+  (input->cwl-scm
+   (make-input "foo" 'boolean "foo" #f #f #f #t #f #f '())))
+
 (test-end "cwl")
diff --git a/tests/ui.scm b/tests/ui.scm
index a5741c0..115a20e 100644
--- a/tests/ui.scm
+++ b/tests/ui.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2023 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2023, 2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -16,7 +16,8 @@
 ;;; You should have received a copy of the GNU General Public License
 ;;; along with ccwl.  If not, see <https://www.gnu.org/licenses/>.
 
-(use-modules (srfi srfi-64)
+(use-modules (srfi srfi-26)
+             (srfi srfi-64)
              (term ansi-color)
              (ccwl ui)
              (ccwl conditions))
diff --git a/tests/utils.scm b/tests/utils.scm
index 50c3396..d786d02 100644
--- a/tests/utils.scm
+++ b/tests/utils.scm
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2022, 2023 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021, 2022, 2023, 2025 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -194,7 +194,17 @@
   (let ((squares cubes (mapn (lambda (n)
                                (values (expt n 2)
                                        (expt n 3)))
-                             (iota 5))))
+                             (iota 5)
+                             2)))
+    (list squares cubes)))
+
+(test-equal "mapn on an empty list"
+  '(() ())
+  (let ((squares cubes (mapn (lambda (n)
+                               (values (expt n 2)
+                                       (expt n 3)))
+                             '()
+                             2)))
     (list squares cubes)))
 
 (test-equal "foldn"
diff --git a/website/releases/ccwl-0.5.0.tar.lz b/website/releases/ccwl-0.5.0.tar.lz
new file mode 100644
index 0000000..d8b622a
--- /dev/null
+++ b/website/releases/ccwl-0.5.0.tar.lz
Binary files differdiff --git a/website/releases/ccwl-0.5.0.tar.lz.asc b/website/releases/ccwl-0.5.0.tar.lz.asc
new file mode 100644
index 0000000..d99264d
--- /dev/null
+++ b/website/releases/ccwl-0.5.0.tar.lz.asc
@@ -0,0 +1,11 @@
+-----BEGIN PGP SIGNATURE-----
+
+iQEzBAABCAAdFiEEf3MDQ/Lwnzx3v3nTLiXui2GAK7MFAmllmDoACgkQLiXui2GA
+K7MseQgAnkLn1tTRRzv9q02vmnLBLhNf/BN1yHFJnbxzRRzezJK4n3eGGw0fWq8j
+vlwlX+uWsGEPZ4Ru7jMfLRlunT4SnJYQTBgZy5rLYyFNRs6ltCaz6DL9JQbS0Hqo
+5AY6MWPWSXim1YnKK/cAhIxcL1Lzq65ka5aaeqhTEi8L0/wgjWZHsMLG07dsejsm
+dcPwyKCwjyWT7+ypyo61axfiuImlrrfNH0pwRNhhThIXLBCCPffdYkUnsakNg0aY
+l3nRKUpbUWMXXMMDABthAFLG1ny6Aw346Irk03Nc/JYsODEiaYTO5+2fdLiipPpY
+CiwR7ftRkqXdoR6BH2kwhUCmlgsmeA==
+=BrLX
+-----END PGP SIGNATURE-----