Biweekly update from the GHC DevX team at IOG.
IOG GHC Update #8
· 3 min read
Haskell DevX Engineer @ IOG
Haskell DevX Engineer @ IOG
Haskell DevX Engineer @ IOG
Haskell DevX Engineer @ IOG
Haskell DevX Intern @ IOG
Biweekly update from the GHC DevX team at IOG.
The IOG Networking Team is pleased to announce that we published [io-sim
],
[io-classes
], [si-timers
], [strict-stm
], [strict-mvar
] and
[io-classes-mtl
] on Hackage. These are tools without which we could not
imagine writing a complex distributed system like [Cardano].
These packages support our goal of using the same code to run in production and
simulation, what greatly increases the reliability and quality of the final
system. [io-sim
] and its ecosystem is designed to let write a simulation
environment which provides provided things usually provided by an operating
system like networking stack or disk IO and develop as well as implement
& model complex applications/systems.
For developing a robust system one needs a proper testing framework which
allows one to model the key characteristics of the system. To achieve this
goal we needed to create an abstraction that captures the key aspects of the
Haskell runtime and operating system environment for distributed systems. The
Cardano [network stack][ouroboros-network] is a highly concurrent system, and
as a network application, it needs to deal with time: there are all sorts of
timeouts that guard resource usage: inactivity timeouts, message timeouts, or
an application level TCP
's WAIT_TIMEOUT
among others. The tools which we
provide permitted us to capture issues related to timing (which abound in
network programming) which, in production, would be extremely rare (things like
simultaneous TCP open or critical race conditions) and ensure that we can test
(in the simulation) these scenarios. Recently we caught
a [bug][sim-tcp-open-bug] in simultaneous TCP open when one side of the
connection crashed - a corner case of a corner case, that's how effective is
the combination of quickcheck style property-based testing & simulation!
Biweekly update from the GHC DevX team at IOG.
Biweekly update from the GHC DevX team at IOG.
Biweekly update from the GHC DevX team at IOG.
Biweekly update from the GHC DevX team at IOG.
Biweekly update from the GHC DevX team at IOG.
This is the second biweekly update of the IOG GHC DevX team. You can find the previous one here.
TL;DR: This blog post intends to sum up the why and how of cargo-cabal
and hs-bindgen
. If you’re looking for usage walkthroughs and code examples, check out project READMEs on GitHub!
N.B. quoted paragraphs in this article give straightforward motivation regarding some systems programming basic concepts. Feel free to skip them if you know you’re likely to be already comfortable with them ;)
In a previous post we introduced GHC's new JavaScript backend, which allows the compilation of Haskell code into JavaScript. This is the first tutorial in a new series about the JavaScript backend. In this post, we'll build GHC as a JavaScript cross-compiler and run a trivial Haskell program in the browser.
We plan to write more of those blog post in the coming weeks and months as we add new features (e.g. support for "foreign exports" that will allow JavaScript code to call into Haskell code, support for Template Haskell, etc.). For now it relies on our "insider" knowledge (e.g. how the FFI works) that isn't well documented elsewhere. We do plan to add a chapter about the JavaScript backend in GHC's user guide, but for now your best chance is to look at GHCJS's documentation or at the source code.
Please note: this is a technology preview of the in-development JavaScript backend for GHC. Not all Haskell features are implemented, and bugs are expected. It is currently rather complicated for JavaScript code to call into Haskell code ("foreign exports" aren't implemented). GHC isn't a multi-target compiler yet, so a GHC executable built for a native platform (Linux/x86-64, Windows/x86-64, Darwin/AArch64...) as currently distributed (via ghcup, Stack, binary distributions, etc.) won't be able to produce JavaScript. Official prebuilt binary distributions are likely to remain unavailable until GHC gains multi-target support - requiring the JavaScript backend to be built from source even after the backend matures. That's why we start this post with the required steps to build yourself a GHC compiler capable of producing JavaScript.
Starting in 2023 we–the IOG GHC DevX team–are going to provide biweekly updates about our work. This is the first edition.
A new JavaScript backend was merged into GHC on November 30th, 2022! This means that the next release of GHC will be able to emit code that runs in web browsers without requiring any extra tools, enabling Haskell for both front-end and back-end web applications.
In this post, we, the GHC DevX team at IOG, describe the challenges we faced bringing GHCJS to GHC, how we overcame those challenges, and what's left to do. This post is rather long so we've provided these links in case you would like to skip ahead:
Take me to the future of GHCJS
Tell me what to expect
Show me the product roadmap
Tell me how I can help
Just show me how to hello world! (Skip to build instructions)
To put it simply, the number of users on the internet is as low as it will ever be right now, and it is almost guaranteed that those users use JavaScript. At time of writing, JavaScript holds 97.3% of client-side programming market share (not to mention market share of front-end technologies). Furthermore, JavaScript is not going to disappear anytime soon. As more and more interactivity is pushed onto the internet, JavaScript will become more entrenched because of backwards compatibility, network effects and the amount of capital already devoted to it. JavaScript, like C and COBOL will be with us for the foreseeable future. This makes JavaScript an attractive target; it provides portability, allows us to capitalize on the massive investments in the language and platform, and essentially eliminates the risk that the we build our technology atop a disappearing or deprecating foundation.
WebAssembly is a promising target as well, and Tweag has just merged a WebAssembly backend into GHC (great work and congrats!). WebAssembly is not as ubiquitous as JavaScript yet, and has a harder time interacting with JavaScript directly. Hence, we believe that the WebAssembly and JavaScript backends provide different strengths, and it is to the Haskell community's benefit to have and support both code generation paths in GHC for different use cases and requirements.
quickcheck-dynamic is a library jointly developed by Quviq and IOG, whose purpose is to leverage QuickCheck to test stateful programs against a Model. In other words, it's a Model-Based Testing tool. This article wants to be a gentle introduction to the use of quickcheck-dynamic for Model-Based Testing. It describes the overall approach, how the library works, and how it's being applied within IOG to improve the reach of our testing efforts.
I recently gave a short presentation about heap objects representation in GHCJS and hence in the upcoming JS backend for GHC. This post is a summary of the content.
GHC implements Haskell code evaluation by using graph reduction. As such Haskell programs compiled by GHC use the heap to store nodes of the graph to be reduced and utility nodes participating in graph reduction. These nodes are:
The heap is also used to store other values:
Users of GHCJS enjoyed a rich FFI system for foreign JavaScript imports. However, this has changed during our adaptation of GHCJS to GHC 9.x. This short post goes over the GHCJS FFI system, the motivation for these changes and what the changes are. First, we must consider the design space of an FFI system.
FFI code is typically employed in high performance scenarios. Additionally, users of the FFI do not want to deal with the object language the compiler is compiling to. Instead, users want a simple way to call functions from the object language and use them in their own code as normal Haskell functions. However, users of the FFI system do tend to be power users, and so as a design principle we want to expose the tools they need to achieve their performance needs, whatever those needs may be. We can summarize these constraints as follows:
Int64#
representation, but should also be able to simply follow standard patterns we
have written in base
.GHCJS’s FFI sets a high (qualitative) benchmark on these three constraints. Let’s inspect them each in detail, in no particular order.
In GHCJS, a user could take advantage of JavaScript functions in their Haskell
code using the GHCJS’s FFI. However, the syntax was unique to GHCJS with place
holder variables like one might see in perl, nix, or bash. For example, here is
a foreign import from the base
library for st_size
:
-- base/System/Posix/Internal.hs
-- the JS FFI version
foreign import javascript unsafe "$r1 = h$base_st_size($1_1,$1_2); $r2 = h$ret1;"
st_size :: Ptr CStat -> IO Int64
The syntax is different from what we know and love in the normal Haskell world
but the grammar is straightforward. We declare a foreign import
from javascript
,
state that the import is unsafe
or interruptible
and then provide a string,
h$base_fstat(...)
for the code generator to use when compiling. Compare this
with the C version:
-- base/System/Posix/Internal.hs
-- the C FFI version
foreign import ccall unsafe "HsBase.h __hscore_st_size"
st_size :: Ptr CStat -> IO Int64
And we see that they are similar. The only difference is the strange $n
symbols in the referrent string. Contrast this with the C version, which simply
declares a name.
These symbols are place holder variables with special meaning in GHCJS. There
are two intractable reasons for the placeholder patterns. First, we require
these patterns to work around the limitations of JavaScript as a backend (1).
For example, consider the case where we need to return an Int64#
from an
imported foreign function. In C and Haskell this is not a problem because both
can represent Int64#
natively, however JavaScript only has native support for
32-bit values. Thus, to be able to return an Int64#
we need to have a method to
return two 32-bit numbers. Similarly, in order to apply a function to an Int64#
that function must take at least two arguments, one for the high bits and one
for the low. Second, the referrent string is untyped and can contain arbritrary
JavaScript code. So placeholder patterns provide a simply and lightweight way
for safety checks and eliminate classes of untyped, hard to understand errors.
For example, consider an arity mismatch error between a function definition and
call site. When this happens JavaScript happily continues processing with the
return value from the function application defined as NaN
(of course). Such
arity conflicts can easily occur, especially when dealing with 64-bit values
which require function arity assumptions.
Lightweight safety checks (3) are done by GHCJS by parsing the names of the place holder variables; each of which follows a specific naming convention. This convention is:
$n
: Used for unary arguments, i.e., arguments which require only a single register.$n_n
: Used for binary arguments, i.e., arguments which require two registers.$c
: A continuation argument, only valid for interruptible
foreign functions.$r
: a unary return$r1
, $r2
: a binary return$r1
, $r2
, $r3_1
, $r3_2
: unboxed tuple return"&value"
: simply emitted as value
by the code generator"someFunction"
: emitted as ret = someFunction(...)
, i.e., map the FFI to
the result of the function call."$r = $1.f($2)"
: emitted as r1 = a1.f(a2)
, i.e., a combination of a
function call and a property access.With this standard GHCJS then parses the FFI referrent string to ensure that it
conforms to this standard. If not then GHCJS can at least respond to the user
with an ill-formatted FFI message and say precisely where the issue is. For
example, it could respond that only half of an Int64#
is returned based on the
referrent string and the function type.
But what of performant code? GHCJS achieves performant FFI by not trying to
abstract away from the runtime system. Instead, an advantage of GHCJS’s FFI is
that we can specify exactly which registers the foreign function should dump its
results or even arbitrary global variables. This places more burden on the user
of the FFI in specific scenarios, but crucially allows the FFI system to get out
of the way of the user. The FFI system also exploits this capability to return
multiple values from a single function call, which is a common need when
compiling to JavaScript. For example, in the above code st_size
is declared to
return an IO Int64
, the JavaScript handler h$base_st_size
returns the Int64
using two registers $r1
and $r2
, but does so through the use of a special
purpose global variable called h$ret1
:
function h$base_st_size(stat, stat_off) {
h$ret1 = (stat.i3[(stat_off>>2)+2]);
return (stat.i3[(stat_off>>2)+1]);
}
The function inputs a pointer and an offset. Pointers in GHCJS are simply
pointers to ByteArrays so the function indexes into the ByteArray and retrieves
and stores the lower 32-bits in h$ret1
, then returns the higher 32-bits
directly. These results are picked up by the FFI code, which performs assignment
to set $r1
to the result of the function call (the higher 32-bits), and set $r2
to the value of h$ret1
(the lower 32-bits). Crucially, the runtime system needs
to do nothing. The registers are already handled ready to be consumed by
whatever the caller of the foreign function will do.
One might consider using a simpler design, which trades register juggling for a
more straightforward representation such as a ByteArray which stores the Int64#
.
However, such a design would trade speed for implementation simplicity. If we
passed ByteArrays then each foreign function would spend time wrapping and
unwrapping the array to get the payload; clearly an undesirable outcome for high
performance code.
So we see that GHCJS’s FFI system actually performs quite well in the design
space. Power users are well supported and can leverage enough unsafety to bind
global variables like h$ret1
and specific registers such as $r1
. The system
provides some lightweight checking through parsing. The nuances of the
JavaScript platform are generally abstracted over and the FFI system is tuned
for performance critical scenarios. So why change it?
The short answer is to hit deadlines. By skipping the FFI parsing the JS Backend team was able to produce a working (can output “Hello World!”, and compile GHC’s boot libraries), integrated, JS backend in GHC faster than had we finished the FFI system.
For the time being, we have opted to replaced each foreign function call with a JavaScript fat arrow, for example:
foreign import javascript unsafe "(($1_1,$1_2) => { return h$base_st_size($1_1,$1_2); })"
st_size :: Ptr CStat -> IO Int64
Of course, this situation is untenable, as argued above, FFI code is assumed to be used in performance critical code, and thus any extra overhead, such as a function closure and consequent indirection, must be avoided. But fear not! In the near future we’ll be overhauling the FFI system and returning it to its former glory.
This is the July 2022 monthly update from the GHC DevX team at IOG.
For a few months we have been merging GHCJS (Haskell to JavaScript compiler) into GHC. We set our first milestone to be the ability to compile and to run the usual "Hello World" program. This month we finally reached it!
We are now focusing on:
fixing failing tests in GHC's testsuite (~2800 unexpected failures). To do that, we have to implement new primops, to fix bugs we introduced while we ported the code from GHCJS, etc.
implementing support for the "js-sources" Cabal stanza in Hadrian. Currently the JS backend finds the JS sources required for the RTS and for base into explicitly defined location. It was only a stop-gap measure and we now need to implement proper support for user-provided JavaScript files.
documenting and refactoring the source code and making it similar to other GHC modules. As an example, GHCJS used the text package which isn't a boot package. Hence we first switched to use GHC's ShortText implementation and now we switched to a FastString based implementation.
adding back GHCJS's features that we haven't ported for some reasons (e.g. the compactor, TH, etc.).
You can follow our progress on our development branch here.
For the time being, we will focus blog post topics on GHCJS internals and related topics. A few of these blog posts are currently under review and should be published shortly.
I recently gave a short presentation on the workings of the GHCJS linker. This post is a summary of the content.
The task of a linker is collecting and organizing object files and resources into a loadable library or executable program. JavaScript can be run in various environments, for example the browser or node.js, and not in all of these the concept of an executable makes sense.
Therefore, when we link a Haskell program, we generate a jsexe
directory filled with various files that allow us to run the JavaScript result:
File | Description |
---|---|
out.js | compiled/linked Haskell code |
out.frefs.* | list of foreign calls from out.js |
out.stats | source code size origin statistics for out.js |
lib.js | non-Haskell code, from js-sources in packages and RTS. possibly preprocessed |
rts.js | generated part of RTS (apply functions and similarly repetitive things) |
runmain.js | single line just starts main |
all.js | complete runnable program, created by combining out.js , lib.js , rts.js and runmain.js |
Most of the work done by the linker is producing out.js
, and that's what we'll be focusing on in the next sections.
out.js
The linker builds out.js
by collecting all code reachable from main
(and a few other symbols required by the RTS) and generating the required initialization code for all top-level data. The code is found in object files. These object files have the following structure:
Section | Description |
---|---|
Header | version number and offsets of other sections |
String table | shared string table, referred to by Dependencies and Code , to avoid duplication in file and memory |
Dependencies | Dependency data, internally between binding groups and externally to symbols in other object files |
Code | Compiled Haskell code stored as serialized JavaScript AST and metadata. Code is organized in binding groups |
The object files contain binding groups of mutually dependent bindings. These are the smallest units of code that can be linked. Each binding group has some associated metadata required for initialization of the heap objects in the group. The metadata contains for example constructor tags (e.g. 1 for Nothing
, 2 for Just
), the arity of functions and static reference tables.
From a high level, the procedure that the linker follows is this:
Step |
---|
Read object files from dependencies into memory |
Decode dependency part of all object files in dependencies (includes reading the string tables) |
Using dependency data, find all code reachable from main |
Decode reachable binding groups |
Render AST to JavaScript |
Construct initializers from metadata |
We avoid decoding (deserializing) the binding groups that do end up in the linked result to keep the memory consumption lower. Still the linker requires a lot of memory for larger programs, so we may need to make more improvements in the future.
The compactor is an optional link-time transformation step that reduces code size. It consists of a lightweight (i.e. no expensive operations like dataflow analysis) rewrite of the code contained in the object files. The compactor is disabled when linking with the -debug
flag. There are a few steps involved.
Haskell names are quite long by default: they need to be globally unique, hence they contain their defining unit-id and module name. For example: mtl-2.2.2-somehash-Control.Monad.State.Lazy.execState_go1
(special characters would be z-encoded but it isn't shown here).
Private symbols are only referred to from within the same module. It doesn't matter which JavaScript name we pick for them, as long as there is no overlap between the names from different modules. The compactor renames all the private symbols using a global sequence to ensure short names that do not overlap.
Without the compactor, the linker generates an h$initObj
initialization call (or h$o
) call for each global Haskell heap value. The code for this can get quite big. The compactor collects all heap objects to be initialized in a single large array and encodes the metadata in a string. This makes the initialization code much more compact.
An optional step in the compactor is deduplication of code. When deduplication is enabled with the -dedupe
flag, the compactor looks for functionally equivalent pieces of JavaScript in the output and merges them. This can result in a significant reduction of code size.
The linker supports building programs that are loaded incrementally. This is used for example for Template Haskell. The process that runs the Template Haskell stays alive during compilation of a whole module. When the first Template Haskell expression is compiled, it is linked against all its dependencies (including the RTS) and the resulting JavaScript code is sent over to be run in the evaluator process.
As subsequent Template Haskell expressions are evaluated in the same process, there is no need to load already loaded dependencies (including the RTS) again and it is much more efficient to avoid doing so. Therefore the linker keeps track of which dependencies have already been linked and each subsequent TH expression is only linked against dependencies that are not already loaded in the evaluator process.
It's also possible for users to use this functionality directly, with the -generate-base
to create a "linker state" file along with the regular jsexe
files. Another program can then be linked with -use-base=state_file
, resulting in a program which leaves out everything already present in the first program.
Memory consumption is the biggest problem in the linker at the moment. Possible ways to achieve this are compression, more efficient representation of the data structures or more incremental loading of the parts from the object files that we need.
In terms of functionality, we don't take advantage of JavaScript modules yet. It would be good if we could improve the linker to support linking a library as a JavaScript module. We should also consider making use of foreign export javascript
for this purpose.
One of the key challenges in any novel backend is representing GHC primitive
types in the new backend. For JavaScript, this is especially tricky, as
JavaScript only has 8 primitive types and some of those types, such as number
do
not directly map to any Haskell primitive type, such as Int8#
. This post walks
through the most important GHC primitives and describes our implementation for
each in the JavaScript backend. This post is intended to be an
explanation-oriented post, light on details, but just enough to understand how
the system works.
There are 36 primtype
s that GHC defines in primops.txt.pp
:
Char#
Int8#
, Int16#
, Int32#
, Int64#
, Int#
Word8#
, Word16#
, Word32#
, Word64#
, Word#
Double#
, Float#
,Array#
, MutableArray#
,, SmallArray#
, SmallMutableArray#
ByteArray#
, MutableByteArray#
Addr#
MutVar#
, TVar#
, MVar#
,IOPort#
, State#
, RealWorld
, ThreadId#
Weak#
, StablePtr#
, StableName#
, Compact#
, BCO
,Fun
, Proxy#
StackSnapshot#
VECTOR
Some of these are unsupported in the JS-backend, such as VECTOR
or lower
priority such as StackSnapshot#
. We’ll begin with the easy cases.
The easy cases are the cases that are implemented as JavaScript objects. In general, this is the big hammer used when nothing else will do. We’ll expand on the use of objects—especially representing heap objects—in a future post, but for the majority of cases we mimic the STG-machine behavior for GHC heap objects using JavaScript heap objects. For example,
var someConstructor =
{ f = // entry function of the datacon worker
, m = 0 // garbage collector mark
, d1 = first arg // First data field for the constructor
, d2 = arity = 2: second arg // second field, or object containing the remaining fields
arity > 2: { d1, d2, ...} object with remaining args (starts with "d1 = x2"!)
}
This is the general recipe; we define a JavaScript object that contains
properties which correspond to the entry function of the heap object; in this
case that is the entry function, f
for a constructor, some meta data for garbage
collection m
, and pointers to the fields of the constructor or whatever else the
heap object might need. Using JavaScript objects allows straightforward
translations of several GHC types. For example TVar
s and MVar
s:
// stg.js.pp
/** @constructor */
function h$TVar(v) {
TRACE_STM("creating TVar, value: " + h$collectProps(v));
this.val = v; // current value
this.blocked = new h$Set(); // threads that get woken up if this TVar is updated
this.invariants = null; // invariants that use this TVar (h$Set)
this.m = 0; // gc mark
this._key = ++h$TVarN; // for storing in h$Map/h$Set
#ifdef GHCJS_DEBUG_ALLOC
h$debugAlloc_notifyAlloc(this);
#endif
}
// stm.js.pp
function h$MVar() {
TRACE_SCHEDULER("h$MVar constructor");
this.val = null;
this.readers = new h$Queue();
this.writers = new h$Queue();
this.waiters = null; // waiting for a value in the MVar with ReadMVar
this.m = 0; // gc mark
this.id = ++h$mvarId;
#ifdef GHCJS_DEBUG_ALLOC
h$debugAlloc_notifyAlloc(this);
#endif
}
Notice that both implementations defined properties specific to the semantics of
the Haskell type. JavaScript functions which create these objects follow the
naming convention h$<something>
and reside in Shim files. Shim files are
JavaScript files that the JS-backend links against and are written in pure
JavaScript. This allows us to save some compile time by not generating code
which doesn’t change, and decompose the backend into JavaScript modules.
This strategy is also how functions are implemented in the JS-backend. Function
objects are generated by StgToJS.Expr.genExpr
and StgToJS.Apply.genApp
but
follow this recipe:
var myFUN =
{ f = <function itself>
, m = <garbage collector mark>
, d1 = free variable 1
, d2 = free variable 2
}
To summarize; for most cases we write custom JavaScript objects which hold
whatever machinery is needed as properties to satisfy the expected semantics of
the Haskell type. This is the strategy that implements: TVar
, MVar
, MutVar
and
Fun
.
ByteArray#
and friends map to JavaScript's
ArrayBuffer
object. The ArrayBuffer
object provides a fixed-length, raw binary data
buffer. To index into the ArrayBuffer
we need to know the type of data the
buffer is expected to hold. So we make engineering tradeoff; we allocate typed
views of the buffer payload once at buffer allocation time. This prevents
allocations from views later when we might be handling the buffer in a hot loop,
at the cost of slower initialization. For example, consider the mem.js.pp
shim, which defines ByteArray#
:
// mem.js.pp
function h$newByteArray(len) {
var len0 = Math.max(h$roundUpToMultipleOf(len, 8), 8);
var buf = new ArrayBuffer(len0);
return { buf: buf
, len: len
, i3: new Int32Array(buf)
, u8: new Uint8Array(buf)
, u1: new Uint16Array(buf)
, f3: new Float32Array(buf)
, f6: new Float64Array(buf)
, dv: new DataView(buf)
, m: 0
}
}
buf
is the payload of the ByteArray#
, len
is the length of the
ByteArray#
. i3
to dv
are the views of the payload; each view is an
object which interprets the raw data in buf
differently according to type. For
example, i3
interprets buf
as holding Int32
, while dv
interprets buf
as a
DataView
and so on. The final property, m
, is the garbage collector marker.
Addr#
and StablePtr#
are implemented as a pair of ByteArray#
and an Int#
offset into the array. We’ll focus on Addr#
because StablePtr#
is the
same implementation, with the exception that the StablePtr#
is tracked in the
global variable h$stablePtrBuf
. Addr#
s do not have an explicit constructor,
rather they are implicitly constructed. For example, consider h$rts_mkPtr
which creates a Ptr
that contains an Addr#
:
function h$rts_mkPtr(x) {
var buf, off = 0;
if(typeof x == 'string') {
buf = h$encodeUtf8(x);
off = 0;
} else if(typeof x == 'object' &&
typeof x.len == 'number' &&
x.buf instanceof ArrayBuffer) {
buf = x;
off = 0;
} else if(x.isView) {
buf = h$wrapBuffer(x.buffer, true, 0, x.buffer.byteLength);
off = x.byteOffset;
} else {
buf = h$wrapBuffer(x, true, 0, x.byteLength);
off = 0;
}
return (h$c2(h$baseZCGHCziPtrziPtr_con_e, (buf), (off)));
}
The function does some type inspection to check for the special case on
string
. If we do not have a string then a Ptr
, which contains an Addr#
, is
returned. The Addr#
is implicitly constructed by allocating a new
ArrayBuffer
and an offset into that buffer. The object
case is an idempotent
check; if the input is already such a Ptr
, then just return the input. The
cases which do the work are the cases which call to h$wrapBuffer
:
// mem.js.pp
function h$wrapBuffer(buf, unalignedOk, offset, length) {
if(!unalignedOk && offset && offset % 8 !== 0) {
throw ("h$wrapBuffer: offset not aligned:" + offset);
}
if(!buf || !(buf instanceof ArrayBuffer))
throw "h$wrapBuffer: not an ArrayBuffer"
if(!offset) { offset = 0; }
if(!length || length < 0) { length = buf.byteLength - offset; }
return { buf: buf
, len: length
, i3: (offset%4) ? null : new Int32Array(buf, offset, length >> 2)
, u8: new Uint8Array(buf, offset, length)
, u1: (offset%2) ? null : new Uint16Array(buf, offset, length >> 1)
, f3: (offset%4) ? null : new Float32Array(buf, offset, length >> 2)
, f6: (offset%8) ? null : new Float64Array(buf, offset, length >> 3)
, dv: new DataView(buf, offset, length)
};
}
h$wrapBuffer
is a utility function that does some offset checks and performs
the allocation for the typed views as described above.
Translating numbers has three issues. First, JavaScript has no concept of
fixed-precision 64-bit types such as Int64#
and Word64#
. Second, JavaScript
bitwise operators only support signed 32-bit values (except the unsigned
right
shift
operator of course). Third, numbers are atomic types and do not require any
special properties for correct semantics, thus using wrapping objects gains us
nothing at the cost of indirection.
To express 64-bit numerics, we simply use two 32-bit numbers, one to express
the high bits, one for the low bits. For example, consider comparing two Int64#:
// arith.js.pp
function h$hs_ltInt64(h1,l1,h2,l2) {
if(h1 === h2) {
var l1s = l1 >>> 1;
var l2s = l2 >>> 1;
return (l1s < l2s || (l1s === l2s && ((l1&1) < (l2&1)))) ? 1 : 0;
} else {
return (h1 < h2) ? 1 : 0;
}
}
The less than comparison function expects four inputs, two for each Int64#
in
Haskell. The first number is represented by h1
and l1
(high and low),
and similarly the second number is represented by h2
and l2
. The comparison
is straightforward, we check equivalence of our high bits, if equal then we
check the lower bits while being careful with signedness. No surprises here.
For the bitwise operators we store both Word32#
and Word#
as 32-bit signed
values, and then map any values greater or equal 2^31
bits to negative values.
This way we stay within the 32-bit range even though in Haskell these types only
support nonnegative values.
The JS backend uses JavaScript values to represent both Haskell heap objects and
unboxed values (note that this isn't the only possible implementation, see
1). As such, it doesn't require that all heap objects have the same
representation (e.g. a JavaScript object with a "tag" field indicating its type)
because we can rely on JS introspection for the same purpose (especially
typeof
). Hence this optimization consists in using a more efficient JavaScript
type to represent heap objects when possible, and to fallback on the generic
representation otherwise.
This optimization particularly applies to Boxed
numeric values (Int
, Word
,
Int8
, etc.) which can be directly represented with a JavaScript number,
similarly to how unboxed Int#
, Word#
, Int8#
, etc. values are represented.
Pros:
Cons:
The optimization is applicable when:
If these invariants hold then, we remove the wrapping object and instead refer
to the value held by the constructor directly. Int8
is the simplest case for
this optimization. In Haskell we have:
data Int8 = Int8 Int8#
Notice that this definition satisfies the requirements. A direct translation in the JS backend would be:
// An Int8 Thunk represented as an Object with an entry function, f
// and payload, d1.
var anInt8 = { d1 = <Int8# payload>
, f : entry function which would scrutinize the payload
}
We can operationally distinguish between a Thunk
and an Int8
because these
will have separate types in the StgToJS
GHC pass and will have separate types
(object
vs number
) at runtime. In contrast, in Haskell an Int8
may
actually be a Thunk
until it is scrutinized and then becomes the Int8
payload (i.e., call-by-need). So this means that we will always know when we
have an Int8
rather than a Thunk
and therefore we can omit the wrapper
object and convert this code to just:
// no object, just payload
var anInt8 = = <Int8# payload>
For the interested reader, this optimization takes place in the JavaScript code
generator module GHC.StgToJS.Arg
, specifically the functions allocConStatic
,
isUnboxableCon
, and primRepVt
.
Char#
: is represented by a number
, i.e., the code pointFloat#/Double#
: Both represented as a JavaScript Double. This means that
Float#
has excess precision and thus we do not generate exactly the same
answers as other platforms which are IEEE754 compliant. Full emulation of
single precision Floats does not seem to be worth the effort as of writing.
Our implementation represents these in a ByteArray#
, where each Float#
takes 4 bytes in the ByteArray#
. This means that the precision is reduced
to a 32-bit Float.I recently gave a short presentation on the topic of threads in GHCJS to the GHC team at IOG. This blog post is a summary of the content.
JavaScript is fundamentally single threaded. There are ways to share specific data between tasks but it's not possible to run multiple threads that have access to a shared memory space of JavaScript data.
The single JavaScript thread is often responsible for multiple tasks. For example a node.js server handles multiple simultaneous connections and a web application may be dealing with user input while downloading new data in the background.
This means that any single task should take care to never block execution of the other task. JavaScript's canonical answer is to use asynchronous programming. A function reading a file returns immediately without waiting for the file data to be loaded in memory. When the data is ready, a user-supplied callback is called to continue processing the data.
Concurrent Haskell supports lightweight threads through forkIO
. These threads are scheduled on top of one more more operating system thread. A blocking foreign call blocks an OS thread but other lightweight threads can still run on other OS threads if available.
There is no built-in support for foreign calls with a callback in the style of JavaScript. Functions imported with foreign import ccall interruptible
can be interrupted by sending an asynchronous exception to the corresponding lightweight thread.
GHCJS implements lightweight threads on top of the single JavaScript thread. The scheduler switches between threads and handles synchronization through MVar
and STM
as expected from other Haskell platforms.
Foreign calls that don't block can be handled in the usual way. We extend the foreign function interface with a new type foreign import javascript interruptible
that conveniently supports the callback mechanism used by JavaScript frameworks. The foreign call is supplied with an additional argument $c
representing a callback to be called with the result when ready. From the Haskell side the corresponding lightweight thread is blocked until $c
is called. This type of foreign call can be interrupted with an asynchronous exception to the lightweight Haskell thread.
By default, Haskell threads in the JS environment run asynchronously. A call to h$run
returns immediately and starts the thread in the background. This works for tasks that does not require immediate actions. For situations that require more immediate action, such as dealing with event handler propagation, there is h$runSync
. This starts a synchronous thread that is not interleaved with other task. If possible, the thread runs to completion before the call to h$runSync
returns. If the thread blocks for any reason, such as waiting for an MVar
or a foreign import javascript interruptible
call, synchronous execution cannot complete. The blocking task is then either interrupted with an exception or the thread is "demoted" to a regular asynchronous thread.
When a Haskell value is evaluated, its heap object is overwritten by a black hole. This black hole marks the value as being evaluated and prevents other threads from doing the same. "black holing" can be done either immediately or "lazily", when the garbage collector is run. GHCJS implements immediate blackholing.
Black holes give rise to an interesting problem in the presence of synchronous and asynchronous threads. Typically if we use h$runSync
, we want to have some guarantee that at least part of the task will run succesfully without blocking. For the most past it's fairly clear which parts of our task depends on potentially blocking IO or thread synchronization. But black holes throw a spanner in the works: Suddenly any "pure" data structure can be a source of blocking if it is under evaluation by another thread.
To regain some predictability and usability of synchronous threads, the h$runSync
scheduler can run other Haskell threads in order to "clear" a black hole. The process ends all black holes have been cleared or when any of the black holes is impossible to clear because of a blocking situation.
This all happens transparantly to the caller of h$runSync
, if the black holes could be cleared it appears as if they were never there.
We have lightweight Haskell threads in the single-threaded JavaScript environment and extend the foreign function interface to easily support foreign calls that depend on an asynchronous callback. This way, only the Haskell lightweight thread blocks.
By default, Haskell threads are asynchronous and run in the background: The scheduler interleaves the tasks and synchronization between threads. For situations that require immediate results or actions there are synchronous threads. Synchronous threads cannot block and are not interleaved with other tasks except when a black hole is encountered.
This is the June 2022 monthly update from the GHC DevX team at IOG.
For a few months we have been merging GHCJS (Haskell to JavaScript compiler) into GHC. We set our first milestone to be the ability to compile and to run the usual "Hello World" program. It turned out to be much more involved than we initially thought (requiring FFI support, etc.), but we should be getting there soon.
This month we have made the following progress:
Linking: GHCJS requires some functions to be directly implemented in
JavaScript (e.g. the RTS, some low-level functions in base). We have added support
for linking .js
files. We've also added support for a preprocessing pass with CPP
for .js.pp
files.
js-sources: there is some ongoing work to load these external JavaScript
files from installed libraries. Cabal provides a js-sources
stanza for this,
we need to adapt Hadrian to make use of it.
Binary vs Objectable: GHCJS used its own ByteString-based Objectable type-class: we replaced it with GHC's similar Binary type-class. Josh has published a blog post about their differences.
64-bit primops: we've added support for 64-bit primops (Word64# and Int64# types). In GHCJS (GHC 8.10), these were still implemented as foreign function calls. It's no longer true on GHC head.
base library: added CPP as required to support the JS backend. Ported and converted FFI imports from GHCJS to use JavaScript fat arrows (we haven't implemented GHCJS's fancy import syntax yet).
Now we can compile and link the "HelloWorld" program. To reach the first milestone we only have to fix the remaining runtime errors.
You can follow our progress on our development branch here. We now rebase this branch every Friday to avoid lagging too much behind GHC head.
The "Haskell Optimization Handbook" is an accepted proposal of the Haskell Foundation. Jeff has been steadily writing some initial material as per the project plan.