summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ravanan/command-line-tool.scm51
-rw-r--r--ravanan/javascript.scm163
-rw-r--r--tests/javascript.scm53
3 files changed, 238 insertions, 29 deletions
diff --git a/ravanan/command-line-tool.scm b/ravanan/command-line-tool.scm
index 4c1fa43..a789079 100644
--- a/ravanan/command-line-tool.scm
+++ b/ravanan/command-line-tool.scm
@@ -37,6 +37,7 @@
   #:use-module (guix store)
   #:use-module (json)
   #:use-module (ravanan config)
+  #:use-module (ravanan javascript)
   #:use-module (ravanan job-state)
   #:use-module (ravanan reader)
   #:use-module (ravanan slurm-api)
@@ -170,19 +171,6 @@ class. Else, return @code{#f}."
 (define (interpolate-parameter-references str)
   "Interpolate @var{str} with one or more parameter references into a javascript
 expression suitable for evaluation."
-  (define (tokenize str)
-    "Split @var{str} into alternating tokens of parameter reference and literal
-strings."
-    (let ((end (if (string-prefix? "$(" str)
-                   (1+ (string-index str #\)))
-                   (string-index str #\$))))
-      (if end
-          (cons (substring str 0 end)
-                (tokenize (substring str end)))
-          (if (string-null? str)
-              (list)
-              (list str)))))
-
   (string-join (map (lambda (token)
                       (if (and (string-prefix? "$(" token)
                                (string-suffix? ")" token))
@@ -193,7 +181,7 @@ strings."
                                         (string-length ")")))
                           ;; Surround with double quotes.
                           (string-append "\"" token "\"")))
-                    (tokenize str))
+                    (tokenize-parameter-references str))
                " + "))
 
 (define* (coerce-expression expression #:optional context)
@@ -209,21 +197,26 @@ context and return the value. @var{context} must be an association list with
 keys @code{input}, @code{self} and @code{runtime}."
   (if (and (string? expression)
            (javascript-expression? expression))
-      (if context
-          ;; Evaluate immediately.
-          (evaluate-parameter-reference %node
-                                        (interpolate-parameter-references expression)
-                                        inputs
-                                        'null
-                                        (list)
-                                        (list))
-          ;; Compile to a G-expression that evaluates expression.
-          #~(evaluate-parameter-reference #$%worker-node
-                                          #$(interpolate-parameter-references expression)
-                                          inputs
-                                          'null
-                                          runtime
-                                          (list)))
+      (from-maybe
+       ;; Try evaluating expression as a simple parameter reference that uses a
+       ;; subset of javascript.
+       (evaluate-simple-parameter-reference expression context)
+       ;; Perhaps this is a more complex javascript expression. Fall back to node.
+       (if context
+           ;; Evaluate immediately.
+           (evaluate-parameter-reference %node
+                                         (interpolate-parameter-references expression)
+                                         (assq-ref context 'inputs)
+                                         'null
+                                         (list)
+                                         (list))
+           ;; Compile to a G-expression that evaluates expression.
+           #~(evaluate-parameter-reference #$%worker-node
+                                           #$(interpolate-parameter-references expression)
+                                           inputs
+                                           'null
+                                           runtime
+                                           (list))))
       ;; Not a javascript expression, but some other JSON tree. Return it as is.
       expression))
 
diff --git a/ravanan/javascript.scm b/ravanan/javascript.scm
new file mode 100644
index 0000000..46160b3
--- /dev/null
+++ b/ravanan/javascript.scm
@@ -0,0 +1,163 @@
+;;; ravanan --- High-reproducibility CWL runner powered by Guix
+;;; Copyright © 2024 Arun Isaac <arunisaac@systemreboot.net>
+;;;
+;;; This file is part of ravanan.
+;;;
+;;; ravanan 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.
+;;;
+;;; ravanan 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 ravanan.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; CWL allows parameter references that use a subset of Javascript/ECMAScript
+;; 5.1 syntax. This module implements that subset in scheme without resorting to
+;; a full-blown javascript engine.
+
+;;; Code:
+
+(define-module (ravanan javascript)
+  #:use-module (srfi srfi-1)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 peg)
+  #:use-module (guix gexp)
+  #:use-module (json)
+  #:use-module (ravanan work monads)
+  #:use-module (ravanan work ui)
+  #:use-module (ravanan work utils)
+  #:export (evaluate-simple-parameter-reference
+            tokenize-parameter-references))
+
+(define-peg-pattern symbol body
+  (+ (or (range #\a #\z)
+         (range #\A #\Z)
+         (range #\0 #\9)
+         "_")))
+
+(define-peg-pattern singleq body
+  (and (ignore "['")
+       (* (and (not-followed-by (or "|" "\\" "'" "}"))
+               peg-any))
+       (ignore "']")))
+
+(define-peg-pattern doubleq body
+  (and (ignore "[\"")
+       (* (and (not-followed-by (or "|" "\\" "\"" "}"))
+               peg-any))
+       (ignore "\"]")))
+
+(define-peg-pattern index all
+  (and (ignore "[")
+       (+ (range #\0 #\9))
+       (ignore "]")))
+
+(define-peg-pattern segment all
+  (and (or (and (ignore ".") symbol) singleq doubleq index)))
+
+(define-peg-pattern parameter-reference all
+  (and (ignore "(") symbol (* segment) (ignore ")")))
+
+(define* (evaluate-parameter-reference-1 expression context)
+  "Compile parameter reference @var{expression} to a G-expression that references
+the variables @code{inputs}, @code{self} or @code{runtime}. @var{expression}
+must strictly be a single parameter reference and will not be subject to string
+interpolation.
+
+If @var{context} is not @code{#f}, evaluate the parameter reference in that
+context and return the value. @var{context} must be an association list with
+keys @code{\"inputs\"}, @code{\"self\"} and @code{\"runtime\"}.
+
+The returned value is maybe-monadic. If @var{expression} fails to parse,
+@code{Nothing} is returned."
+  (match (peg:tree (match-pattern parameter-reference expression))
+    ;; Special case for null
+    (('parameter-reference "null")
+     (just (if context
+               'null
+               #~'null)))
+    ;; Disallow referencing anything other than inputs, self or runtime.
+    (('parameter-reference (and (not (or "inputs" "self" "runtime"))
+                                symbol)
+                           _ ...)
+     (user-error "Invalid parameter reference; `~a' unknown"
+                 symbol))
+    ;; Parse parameter reference and produce a G-expression. The strange and
+    ;; complex matching pattern for segments accounts for how (ice-9 peg) adds
+    ;; an additional layer of parentheses to the tree when there are 2 or more
+    ;; segments.
+    (('parameter-reference symbol . (or (('segment segments) ...)
+                                        ((('segment segments) ...))))
+     (let ((segments (map (match-lambda
+                            (('index key) (string->number key))
+                            (key key))
+                          segments)))
+       (just (if context
+                 (apply json-ref context symbol segments)
+                 #~(json-ref #$symbol #$@segments)))))
+    ;; Perhaps this is a more complex javascript expression.
+    (#f %nothing)))
+
+(define (tokenize-parameter-references str)
+  "Split @var{str} into alternating tokens of parameter reference and literal
+strings."
+  (let ((end (if (string-prefix? "$(" str)
+                 (1+ (string-index str #\)))
+                 (string-index str #\$))))
+    (if end
+        (cons (substring str 0 end)
+              (tokenize-parameter-references (substring str end)))
+        (if (string-null? str)
+            (list)
+            (list str)))))
+
+(define* (evaluate-simple-parameter-reference str #:optional context)
+  "Compile parameter reference @var{str} to a G-expression that references
+the variables @code{inputs}, @code{self} or @code{runtime}. @var{str} may be
+subject to string interpolation.
+
+If @var{context} is not @code{#f}, evaluate the parameter reference in that
+context and return the value. @var{context} must be an association list with
+keys @code{\"inputs\"}, @code{\"self\"} and @code{\"runtime\"}.
+
+The returned value is maybe-monadic. If any of the parameter references in
+@var{str} fail to parse, @code{Nothing} is returned."
+  (define (evaluate-token token)
+    (if (and (string-prefix? "$(" token)
+             (string-suffix? ")" token))
+        ;; Drop the leading "$" and evaluate.
+        (evaluate-simple-parameter-reference-1 (string-drop token 1)
+                                               context)
+        ;; token is a string literal.
+        (just token)))
+
+  (maybe-let* ((evaluated-tokens
+                (fold (lambda (token maybe-result)
+                        (maybe-let* ((result maybe-result))
+                          (maybe-let* ((evaluated-token (evaluate-token token)))
+                            (just (cons evaluated-token result)))))
+                      (just (list))
+                      (tokenize-parameter-references str))))
+    (just (if context
+              ;; Evaluate immediately.
+              (string-join (map (lambda (token)
+                                  (if (string? token)
+                                      token
+                                      (scm->json-string (canonicalize-json token))))
+                                (reverse evaluated-tokens))
+                           "")
+              ;; Compile to a G-expression that interpolates parameter reference
+              ;; string.
+              #~(string-join (map (lambda (token)
+                                    (if (string? token)
+                                        token
+                                        (scm->json-string (canonicalize-json token))))
+                                  (list #$@(reverse evaluated-tokens)))
+                             "")))))
diff --git a/tests/javascript.scm b/tests/javascript.scm
new file mode 100644
index 0000000..5f23153
--- /dev/null
+++ b/tests/javascript.scm
@@ -0,0 +1,53 @@
+;;; ravanan --- High-reproducibility CWL runner powered by Guix
+;;; Copyright © 2024 Arun Isaac <arunisaac@systemreboot.net>
+;;;
+;;; This file is part of ravanan.
+;;;
+;;; ravanan 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.
+;;;
+;;; ravanan 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 ravanan.  If not, see <https://www.gnu.org/licenses/>.
+
+(use-modules (srfi srfi-64)
+             (ravanan work monads)
+             (ravanan javascript))
+
+(test-begin "javascript")
+
+(test-equal "evaluate parameter reference"
+  "c"
+  (from-maybe
+   (evaluate-simple-parameter-reference "$(inputs.message['bar'][\"foo\"][2])"
+                                        '(("inputs" ("message" ("bar" ("foo" . #("a" "b" "c" "d")))))))
+   #f))
+
+(test-equal "evaluate parameter reference with string interpolation"
+  "24foo12foobar"
+  (from-maybe
+   (evaluate-simple-parameter-reference "$(runtime.cores)foo$(inputs.threads)$(inputs.output_filename)"
+                                        '(("inputs"
+                                           ("threads" . 12)
+                                           ("output_filename" . "foobar"))
+                                          ("runtime" ("cores" . 24))))
+   #f))
+
+(test-equal "evaluate parameter reference with string interpolation of JSON trees"
+  "foo[0,1,2,3]{\"bar\":2,\"foo\":1}"
+  (from-maybe
+   (evaluate-simple-parameter-reference "foo$(inputs.vector)$(inputs.object)"
+                                        '(("inputs"
+                                           ("object"
+                                            ("foo" . 1)
+                                            ("bar" . 2))
+                                           ("vector" . #(0 1 2 3)))))
+   #f))
+
+(test-end "javascript")