gxjs

0.0.0 • Public • Published

GxJS: (G)erbil E(x)tended (J)avaScript (S)cheme

A Meta-Scheme runtime for JavaScript

Installation

Easy! I choose yarn, but npm also works.

yarn add gxjs
yarn add gxjs-loader --dev

Documentation

For details see the repo, man! https://github.com/drewc/gxjs

Usage

Writing js applications in Gerbil rules! This is what we add.

Most things are inside the js. They are all in the runtime and globally available.

To make the syntax work all files should import the :js gerbil module.

(import :js)

Loading

When using gxjs-loader things just happen automagically and rarely do we need to cast another spell.

Most of the following code is tangled into a test-gxjs.ss file. We load that in a JavaScript file and execute the function.

We’ll even make it an es module with no semicolons in order to be as modern as we want! :)

import testGxJS from 'gxjs-loader!./test-gxjs.ss'

testGxJS()

export default testGxJS;

FFI Starting Points: declaration, statement, expression.

Every Scheme object is also a javascript object or type. At the same time, for the most part, we want to ignore that and stick to gerbil.

But, at the same time, we need to use js libraries and modules to develop with, and need to interact with the “host” system.

For that there are three forms that matter.

  • js#declaration: A toplevel only form that puts the string passed as a toplevel JavaScript form.

  • js#statement: A form that if toplevel runs after the file is loaded while the Gambit Module is initializing, otherwise runs within the function/syntax is it used within. Can take objects as arguments.

  • js#expression: Similar to js#statement only returns the value of the JavaScript form to Gerbil as-is.

So let us define a global variable using js#declaration.

(js#declaration
 "console.log('Declaring a global variable testGxJS');
  globalThis['testGxJS'] = 42;")

;;(##inline-host-expression "force error(@1@)")

If we compile that to a js file we end up with something similar to the following.

/ File generated by Gambit v4.9.3
// Link info: (409003 (js ((repr-module class) (namespace "__GxJS_"))) "testGxJS" (("testGxJS")) (module_register glo peps make_interned_symbol r0 r1 ffi wrong_nargs nargs) () (testGxJS#) () #f)

console.log('Declaring a global variable testGxJS');
  globalThis['testGxJS'] = 42;
__GxJS_testGxJS = function () {
};
// There are 20 or so lines here that set the initializing function for this
// Gambit Module, which at this point just returns void.

For testing we’ll use js#statement. Here’s the basic function.

(def (test> name i (predicate? eq?) (j #t))
  (let ((result (predicate? i j)))
    (js#statement "
     (() => {
     const name = @1@, i = @2@, j = @3@, res = !!@4@;
     const msg = name + ' ' + JSON.stringify(i) +
                 (res ? ' => ' : ' != ') + JSON.stringify(j);
     res ? console.log('Success!! :)', msg) : console.error('Failure :( ', msg);
   })()

" name i j result)))

And a simple use.

(test> "Testing test>" #t)
(test> "Testing test> expression" (js#expression "(@1@) === 42" 42))
(test> "Testing test> expression predicate"
       42 (lambda (x y) (js#expression "(@1@) === (@2@)" x y)) 42)

For a predicate, js#expression has a lot we need. For example, we want to use js#=== to compare things.

First, because a lot of scheme predicates can take multiple arguments we’ll make a js#declaration that has a function that can turn a binary predicate into a n-ary operand.

We’ll pass an option that defines what is returned if there are 0 or 1 values to compare and the option to recurse on down the rest of the vector if needed.

RTS.GxJS.make_nary_predicate =  (op, zeroOrOne = true, recurse = false) => {
  const pred = (...args) => {
    if (args.length < 2) {
     return zeroOrOne;
    } else {
      const x = args[0], ys = args.slice(1);
      const res = ys.every(y => op(x,y));

      if (!res && !recurse) {
        return false
      } else if (!recurse) {
        return true
      } else {
         return RTS.GxJS.make_nary_predicate(op, zeroOrOne, recurse)(...ys);
      }
    }
  }

  return pred;
}

RTS.GxJS.apply_predicate = (op, argsList, zeroOrOne = true, recurse = false) => {
   const args = RTS.list2vector(argsList);
   const pred = RTS.GxJS.make_nary_predicate(op, zeroOrOne, recurse);
   return pred(...args);
}

That introduces us to the Gambit runtime object, RTS, and our own sub-object, RTS.GxJS.

Now we can define === inside the namespace: js

(def (=== . args)
  (js#expression "RTS.GxJS.apply_predicate((x,y) => x === y, @1@);" args))

(js#declaration "console.error('HERE!');")

And test it.

(test> "Testing js#=== binary" 42 js#=== 42)
(test> "Testing js#=== N-ary" (js#=== 42 42 42))

js#jso and the {} syntax to make a JavaScript object.

We must interact with js all the time. While it is an FFI, trying to go between the two gets, odd. js#jso is the first step in trying to do so.

There is also a js#jso? predicate that just wraps typeof obj === 'object' and foreign?.

(def jso-jso (js#jso
                keyword: 42
                'symbol "String as value"
                "hyphen-or-dash" 'symbol-as-value
                42 "That was a number as a key"))

(test> "jso jso?" (js#jso? jso-jso))

Even better, there’s a {} syntax that closely resembles JSON only without the hockey mask, AKA comma.

(def first-jso { keyword: 42
                 'symbol "String as value"
                 "hyphen-or-dash" 'symbol-as-value
                 42 "That was a number as a key"
                })

(test> "jso first-jso?" (js#jso? first-jso))

All jso’s are also a foreign type by default.

(test> "First JSO is foreign?" (foreign? first-jso))

js#ref

We often need to reference properties from things in JavaScript. There are many things that have properties and can be accessed.with.dots.

While we could use an inline expression to do so that starts to be a headache.

So we have js#ref.

(test> "First JSO Keyword" (js#ref first-jso keyword:) ##fx= 42)

Just like js we can refer to the properties in various ways.

(test> "First JSO Keyword as String" (js#ref first-jso "keyword") ##fx= 42)

js#js->scm and js#scm->js

Things to start to get odd though as js#jso does its best to make a host object with what it is passed but js#ref does not do the inverse.

(test> "First JSO symbol as keyword but fail string"
       (string=? (js#ref first-jso symbol:) "String as value")
       eq? #f)

We have two functions to go back and forth.

(test> "First JSO symbol as keyword and js->scm"
       (js#js->scm (js#ref first-jso symbol:))
       string=? "String as value")

(test> "First JSO symbol as keyword and scm -> js"
       (##inline-host-expression
        "(@1@) === (@2@)"
        (js#ref first-jso symbol:)
        (js#scm->js "String as value")))

In case the latter did not make it obvious, true is #t and false is #f. That makes things easy.

Some things have no host value.

(test> "First JSO String as Symbol"
       (js#ref first-jso 'hyphen-or-dash) eq? 'symbol-as-value)

But, for almost all of them they are javascript objects.

(test> "First JSO String as keyword with ref on value which is a symbol"
       (string=?
        (js#js->scm (js#ref first-jso hyphen-or-dash: name:))
        "symbol-as-value"))

Also note that js#ref can have many refs.

Not just for foreigners!!

We sometimes need to access properties for non-foreign objects. js#ref checks for that.

((lambda ()
   (let ((obj (##inline-host-expression "{ JavaScript: 'object', with: 'commas! :P' };")))
     (test> "Not a foreigner" (not (foreign? obj)))
     (test> "Ref on non-foreign" (string=? "object" (js#js->scm (js#ref obj JavaScript:)))))))

js#jso-ref, compose js->scm and ref

Most of the time in Gerbil we want Gerbil objects. Because js#jso and {} turn them into javascript objects we simply need to turn them back.

(test> "First JSO symbol as keyword and jso-ref"
       (string=? (js#jso-ref first-jso symbol:) "String as value"))
  (##inline-host-statement "console.log('\\nFinished JSOREF \\n----------------------')")

That means that other jso objects become foreign

(test> "Nested JSO becomes foreign"
       (foreign? (js#jso-ref { jso: { nested: #t } }
                         jso:)))

js#foreign->js and vice versa

The back and forth between js and scheme can get very odd. Like most FFI’s, we want to interact, not interfere, and not be interfered with.

To make it easy any javascript object that is not of a type or instanceof a “class” that we swap with (i.e strings and functions and numbers and vectors etc), our RTS.host2scm turn it into a foreign object.

(test> "Automagic foreign?" (foreign? (js#js->scm (##inline-host-expression "{ foreign: 42 }"))))

By automagic, our js#jso and the syntax that follows it run RTS.scm2host on every value. That’s what our js#scm->js calls.

(def second-jso { string: "string value" number: 1.1 jso: { "this is a foreign" "that becomes an object" } })

(test> "Second JSO is foreign?" (foreign? second-jso))

Because of that, in this instance and many more, even though our second-jso is foreign that value, made by js#jso, is not.

(test> "Second JSO jso: property is not foreign!"
       (not (foreign? (js#ref second-jso jso:))))

That’s worth keeping in mind as, in general, we want to stick with scheme objects, where a foreign wrapper makes it a scheme object, versus JavaScript objects in and of themselves.

js#ref works with both, and does not attempt any conversion.

(test> "JS === from ref with foreign and not with foreign"
       (##inline-host-expression
        "(@1@) == (@2@)"
        (js#ref second-jso "this is a foreign")
        (js#ref (js#js->foreign second-jso) "this is a foreign")))

js#ref-set!, be very cautious!

js#ref-set!, like js#ref, can operate on foreign objects but does no conversion the the value. FFI really can be funny.

(test> "ref-set! does no conversion"
       (let ((js-string (js#ref second-jso string:)))
        (set! (js#ref second-jso string:) "Scheme String")

        (and (js#expression "typeof @1@ === 'string'" js-string)
             (js#expression "typeof @1@ === 'object'"
                            (js#ref second-jso string:))
             (string=? "Scheme String" (js#ref second-jso string:))
             (##fx= 13 (##vector-length (js#ref "Scheme String" codes:))))))

js#jso-ref-set!, caution can meet wind sometimes.

js#jso-ref-set!, like js#jso-ref, does the conversion. That allows us to use js ’objects’ like scheme objects a lot of the time. Sh

(test> "jso-ref-set! does conversion"
       (let ((scm-string (js#ref second-jso string:)))
        (set! (js#jso-ref second-jso string:) "Javascript String")

        (and (js#expression "typeof @1@ === 'object'" scm-string)
             (js#expression "typeof @1@ === 'string'"
                            (js#ref second-jso string:))
             (js#expression "(@1@) === 'Javascript String'"
                              (js#ref second-jso string:)))))

js#function with js#this and js#arguments

In JavaScript functions can take be passed arguments even if they do not accept them.

i.e:

> o = { bar: function () {return this}, baz: 42}
 {baz: 42, bar: ƒ}

> foo.bar('this is ignored').baz
 42

Then there’s the this variable.

foo.bar('this is ignored').bar().bar().baz
42

(def (foo t) 42)
(def this-jso { fn: (js#function () js#this)
                val: 42 })
(##inline-host-statement "")
(test> "Testing out (function () ...) syntax"
       (js#expression "(@1@).fn('ignored').fn().val === 42"
                                 (js#foreign->js this-jso)))

plist->jso

By default all javascript objects become RTS.Foreign.

(def jso-as-plist '(property: 42 "as a string" symbol-here))
(def new-jso (js#plist->jso jso-as-plist))

(test> "A Foreign?" (foreign? new-jso))

Support

Go to https://github.com/drewc/gxjs/issues or contact the author.

Readme

Keywords

none

Package Sidebar

Install

npm i gxjs

Weekly Downloads

0

Version

0.0.0

License

Apache-2.0

Unpacked Size

21.4 kB

Total Files

6

Last publish

Collaborators

  • drewc