gxjs
v0.0.0
Published
GxJS: Gerbil, a Meta-Scheme, for JavaScript runtimes
Downloads
2
Readme
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.