about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ccwl/ccwl.scm82
-rw-r--r--ccwl/cwl.scm55
-rw-r--r--ccwl/graphviz.scm57
-rw-r--r--doc/ccwl.skb12
-rw-r--r--doc/js-expression-iota.scm7
-rwxr-xr-xscripts/ccwl3
6 files changed, 176 insertions, 40 deletions
diff --git a/ccwl/ccwl.scm b/ccwl/ccwl.scm
index 97485da..d4f766b 100644
--- a/ccwl/ccwl.scm
+++ b/ccwl/ccwl.scm
@@ -47,6 +47,13 @@
             command-stdout
             command-requirements
             command-other
+            js-expression?
+            js-expression
+            js-expression-inputs
+            js-expression-expression
+            js-expression-outputs
+            js-expression-requirements
+            js-expression-other
             cwl-workflow?
             cwl-workflow
             cwl-workflow-file
@@ -269,6 +276,15 @@ compared using @code{equal?}."
   (requirements command-requirements)
   (other command-other))
 
+(define-immutable-record-type <js-expression>
+  (make-js-expression inputs expression outputs requirements other)
+  js-expression?
+  (inputs js-expression-inputs)
+  (expression js-expression-expression)
+  (outputs js-expression-outputs)
+  (requirements js-expression-requirements)
+  (other js-expression-other))
+
 (define-immutable-record-type <cwl-workflow>
   (make-cwl-workflow file inputs outputs)
   cwl-workflow?
@@ -285,10 +301,11 @@ compared using @code{equal?}."
   (other workflow-other))
 
 (define (function-outputs function)
-  "Return the outputs of FUNCTION---a <command>, <cwl-workflow> or
-<workflow> object."
+  "Return the outputs of FUNCTION---a <command>, <js-expression>,
+<cwl-workflow> or <workflow> object."
   ((cond
     ((command? function) command-outputs)
+    ((js-expression? function) js-expression-outputs)
     ((cwl-workflow? function) cwl-workflow-outputs)
     ((workflow? function) workflow-outputs)
     (else (error "Unrecognized ccwl function" function)))
@@ -500,6 +517,52 @@ identifiers defined in the commands."
                        '#,other)))
                 #'(args ...)))))))
 
+(define-syntax js-expression
+  (lambda (x)
+    (syntax-case x ()
+      ((_ args ...)
+       (guard (exception
+               ((unrecognized-keyword-assertion? exception)
+                (raise-exception
+                 (match (condition-irritants exception)
+                   ((irritant _ ...)
+                    (condition (ccwl-violation irritant)
+                               (formatted-message "Unrecognized keyword argument ~a in js-expression definition"
+                                                  (syntax->datum irritant)))))))
+               ((invalid-keyword-arity-assertion? exception)
+                (raise-exception
+                 (match (condition-irritants exception)
+                   ;; TODO: Report all extra arguments, not just the
+                   ;; first one.
+                   ((keyword _ extra _ ...)
+                    (condition (ccwl-violation extra)
+                               (formatted-message "Unexpected extra argument ~a for unary keyword argument ~a"
+                                                  (syntax->datum extra)
+                                                  (syntax->datum keyword)))))))
+               ((invalid-positional-arguments-arity-assertion? exception)
+                (raise-exception
+                 (match (condition-irritants exception)
+                   ;; TODO: Report all extra positional arguments, not
+                   ;; just the first one.
+                   ((extra _ ...)
+                    (condition (ccwl-violation extra)
+                               (formatted-message "Unexpected extra positional argument ~a in js-expression definition"
+                                                  (syntax->datum extra))))))))
+         (apply (syntax-lambda** (#:key expression (requirements #''()) (other #'()) #:key* inputs outputs)
+                  (unless expression
+                    (raise-exception
+                     (condition (ccwl-violation x)
+                                (formatted-message "Missing ~a key in command definition"
+                                                   #:expression))))
+                  (ensure-yaml-serializable other "#:other")
+                  #`(make-js-expression
+                     (list #,@(map input inputs))
+                     #,expression
+                     (list #,@(map output outputs))
+                     #,requirements
+                     '#,other))
+                #'(args ...)))))))
+
 (define-syntax cwl-workflow
   (lambda (x)
     (syntax-case x ()
@@ -546,17 +609,19 @@ identifiers defined in the commands."
 
 (define (function-inputs function)
   "Return the list of inputs accepted by @var{function}---a
-@code{<command>}, @code{<cwl-workflow>} or @code{<workflow> object."
+@code{<command>}, @code{<js-expression>}, @code{<cwl-workflow>} or
+@code{<workflow> object."
   ((cond
     ((command? function) command-inputs)
+    ((js-expression? function) js-expression-inputs)
     ((cwl-workflow? function) cwl-workflow-inputs)
     ((workflow? function) workflow-inputs)
     (else (error "Unrecognized ccwl function" function)))
    function))
 
 (define (function-input-keys function)
-  "Return the list of input keys accepted by FUNCTION, a <command>
-object or a <cwl-workflow> object."
+  "Return the list of input keys accepted by FUNCTION, a <command>,
+<js-expression>, <cwl-workflow> or <workflow> object."
   (map input-id
        (function-inputs function)))
 
@@ -597,14 +662,15 @@ an association list mapping keyword arguments to their values."
          (command-inputs command))))
 
 (define (function-object x)
-  "Return the ccwl function object (a <command>, <cwl-workflow>
-or <workflow> object) described by syntax X. If such a ccwl function
-is not defined, return #f."
+  "Return the ccwl function object (a <command>, <js-expression>,
+<cwl-workflow> or <workflow> object) described by syntax X. If such a
+ccwl function is not defined, return #f."
   ;; TODO: What if function object is defined in lexical scope?
   (let ((result (false-if-exception
                  (eval (syntax->datum x)
                        (interaction-environment)))))
     (and (or (command? result)
+             (js-expression? result)
              (cwl-workflow? result)
              (workflow? result))
          result)))
diff --git a/ccwl/cwl.scm b/ccwl/cwl.scm
index 3479492..baef519 100644
--- a/ccwl/cwl.scm
+++ b/ccwl/cwl.scm
@@ -41,7 +41,9 @@
     (((? workflow? workflow) port)
      (workflow->cwl workflow port))
     (((? command? command) port)
-     (command->cwl command port))))
+     (command->cwl command port))
+    (((? js-expression? expression) port)
+     (js-expression->cwl expression port))))
 
 (define (workflow->cwl workflow port)
   "Render WORKFLOW, a <workflow> object, to PORT as a CWL YAML
@@ -87,6 +89,8 @@ association list."
                        (run . ,(match (step-run step)
                                  ((? command? command)
                                   (command->cwl-scm command))
+                                 ((? js-expression? expression)
+                                  (js-expression->cwl-scm expression))
                                  ((? cwl-workflow? cwl-workflow)
                                   (cwl-workflow-file cwl-workflow))
                                  ((? workflow? workflow)
@@ -147,22 +151,27 @@ CWL YAML specification."
           '())
     ,@(input-other input)))
 
+(define (staging-requirements inputs)
+  "Return @samp{InitialWorkDirRequirement} to stage any @var{inputs} that
+must be staged."
+  (if (any input-stage? inputs)
+      ;; Stage any inputs that need to be.
+      `((InitialWorkDirRequirement
+         (listing . ,(list->vector
+                      (filter-map (lambda (input)
+                                    (and (input-stage? input)
+                                         (string-append "$(inputs."
+                                                        (symbol->string (input-id input))
+                                                        ")")))
+                                  inputs)))))
+      '()))
+
 (define (command->cwl-scm command)
   "Render COMMAND, a <command> object, into a CWL tree."
   `((cwlVersion . ,%cwl-version)
     (class . CommandLineTool)
     (requirements
-     ,@(if (any input-stage? (command-inputs command))
-           ;; Stage any inputs that need to be.
-           `((InitialWorkDirRequirement
-              (listing . ,(list->vector
-                           (filter-map (lambda (input)
-                                         (and (input-stage? input)
-                                              (string-append "$(inputs."
-                                                             (symbol->string (input-id input))
-                                                             ")")))
-                                       (command-inputs command))))))
-           '())
+     ,@(staging-requirements (command-inputs command))
      ,@(command-requirements command))
     ,@(command-other command)
     (arguments . ,(list->vector
@@ -187,3 +196,25 @@ CWL YAML specification."
     ,@(if (command-stdout command)
           `((stdout . ,(command-stdout command)))
           '())))
+
+(define (js-expression->cwl expression port)
+  "Render @var{expression}, a @code{<js-expression>} object, to
+@var{port} as a CWL YAML specification."
+  (scm->yaml (js-expression->cwl-scm expression)
+             port))
+
+(define (js-expression->cwl-scm expression)
+  "Render @var{expression}, a @code{<js-expression>} object, into
+a CWL tree."
+  `((cwlVersion . ,%cwl-version)
+    (class . ExpressionTool)
+    (requirements
+     (InlineJavascriptRequirement)
+     ,@(staging-requirements (js-expression-inputs expression))
+     ,@(js-expression-requirements expression))
+    ,@(js-expression-other expression)
+    (inputs . ,(map input->cwl-scm
+                    (js-expression-inputs expression)))
+    (outputs . ,(map output->cwl-scm
+                     (js-expression-outputs expression)))
+    (expression . ,(js-expression-expression expression))))
diff --git a/ccwl/graphviz.scm b/ccwl/graphviz.scm
index 2d39156..970af72 100644
--- a/ccwl/graphviz.scm
+++ b/ccwl/graphviz.scm
@@ -41,7 +41,9 @@
     (((? workflow? workflow) port)
      (workflow->dot workflow port))
     (((? command? command) port)
-     (command->dot command port))))
+     (command->dot command port))
+    (((? js-expression? expression) port)
+     (js-expression->dot expression port))))
 
 (define (workflow->dot workflow port)
   "Render WORKFLOW, a <workflow> object, to PORT in the graphviz dot
@@ -121,18 +123,13 @@ language."
          #:subgraphs (list (inputs-cluster (workflow-inputs workflow))
                            (outputs-cluster (workflow-outputs workflow)))))
 
-(define (command->dot command port)
-  "Render @var{command}, a @code{<command>} object, to @var{port} in the
-graphviz dot language."
-  (graph->dot (command->graph command)
-              port))
-
-(define (command->graph command)
-  "Convert @var{command}, a @code{<command>} object, to a @code{<graph>}
-object."
+(define (single-node-workflow->graph node-name inputs outputs)
+  "Convert a single node workflow (usually a @code{<command>} or
+@code{<js-expression>}) with @var{node-name}, @var{inputs} and
+@var{outputs}, to a @code{<graph>} object."
   (graph 'workflow
          #:properties '((bgcolor . "#eeeeee"))
-         #:nodes (list (graph-node 'command
+         #:nodes (list (graph-node node-name
                                    '((fillcolor . "lightgoldenrodyellow")
                                      (shape . "record")
                                      (style . "filled"))))
@@ -140,15 +137,41 @@ object."
                   ;; Connect inputs to command.
                   (map (lambda (input)
                          (cons (input-id input)
-                               'command))
-                       (command-inputs command))
+                               node-name))
+                       inputs)
                   ;; Connect command to outputs.
                   (map (lambda (output)
-                         (cons 'command
+                         (cons node-name
                                (output-id output)))
-                       (command-outputs command)))
-         #:subgraphs (list (inputs-cluster (command-inputs command))
-                           (outputs-cluster (command-outputs command)))))
+                       outputs))
+         #:subgraphs (list (inputs-cluster inputs)
+                           (outputs-cluster outputs))))
+
+(define (command->dot command port)
+  "Render @var{command}, a @code{<command>} object, to @var{port} in the
+graphviz dot language."
+  (graph->dot (command->graph command)
+              port))
+
+(define (command->graph command)
+  "Convert @var{command}, a @code{<command>} object, to a @code{<graph>}
+object."
+  (single-node-workflow->graph 'command
+                               (command-inputs command)
+                               (command-outputs command)))
+
+(define (js-expression->dot expression port)
+  "Render @var{expression}, a @code{<js-expression>} object, to
+@var{port} in the graphviz dot language."
+  (graph->dot (js-expression->graph expression)
+              port))
+
+(define (js-expression->graph expression)
+  "Convert @var{expression}, a @code{<js-expression>} object, to a
+@code{<graph>} object."
+  (single-node-workflow->graph 'js-expression
+                               (js-expression-inputs expression)
+                               (js-expression-outputs expression)))
 
 (define (step-node id)
   "Return graph node describing step with @var{id}."
diff --git a/doc/ccwl.skb b/doc/ccwl.skb
index cb78fd5..23bd798 100644
--- a/doc/ccwl.skb
+++ b/doc/ccwl.skb
@@ -1,5 +1,5 @@
 ;;; ccwl --- Concise Common Workflow Language
-;;; Copyright © 2021, 2023 Arun Isaac <arunisaac@systemreboot.net>
+;;; Copyright © 2021, 2023–2024 Arun Isaac <arunisaac@systemreboot.net>
 ;;;
 ;;; This file is part of ccwl.
 ;;;
@@ -389,7 +389,15 @@ the external CWL workflow.]
 keys forward to the output. This is what the ,(code [identity])
 construct is for. An example follows.]
          (scheme-source "doc/identity-construct.scm")
-         (image :file "doc/identity-construct.png"))))
+         (image :file "doc/identity-construct.png")))
+    (section :title [Javascript expressions via ExpressionTool]
+             :ident "javascript-expressions-via-expressiontool"
+      (p [ccwl supports CWL's ,(samp "ExpressionTool") using its
+,(code "js-expression") construct. The ,(code "js-expression")
+construct may be invoked from within workflows just like ,(code
+"command") constructs can be. Here's a workflow that uses ,(code
+"js-expression") to construct an array of numbers from 0 to n-1.]
+         (scheme-source "doc/js-expression-iota.scm"))))
 
   (chapter :title [Contributing]
            :ident "chapter-contributing"
diff --git a/doc/js-expression-iota.scm b/doc/js-expression-iota.scm
new file mode 100644
index 0000000..066e4ae
--- /dev/null
+++ b/doc/js-expression-iota.scm
@@ -0,0 +1,7 @@
+(define iota
+  (js-expression #:inputs (n #:type int)
+                 #:expression "$({\"sequence\": Array.from(Array(inputs.n).keys())})"
+                 #:outputs (sequence #:type (array int))))
+
+(workflow ((n #:type int))
+  (iota #:n n))
diff --git a/scripts/ccwl b/scripts/ccwl
index 43cf224..984d22a 100755
--- a/scripts/ccwl
+++ b/scripts/ccwl
@@ -123,10 +123,11 @@ Compile SOURCE-FILE.
                 (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 neither workflow nor command"
+                       (condition (formatted-message "Last expression in file ~a returns none of workflow, command or js-expression"
                                                      (assq-ref args 'source-file)))))))
               (current-output-port)))))
         ((program args ...)