Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WASM import all non-compile axioms with alphanum names in entrypoint #1426

Merged
merged 4 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/org/SUMMARY.org
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- [[./getting-started/quick-start.md][Quick start]]
- [[./getting-started/dependencies.md][Installing dependencies]]
- [[./examples/README.md][Juvix Examples]]
- [[./tutorials/README.md][Tutorials]]
- [[./tutorials/nodejs-interop.md][NodeJS Interop]]

- [[./language-reference/README.md][Language reference]]
- [[./language-reference/comments.md][Comments]]
Expand Down
1 change: 1 addition & 0 deletions docs/org/tutorials/README.org
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [[./nodejs-interop.md][NodeJS Interop]]
108 changes: 108 additions & 0 deletions docs/org/tutorials/nodejs-interop.org
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
* NodeJS Interop

A Juvix module can be compiled to a Wasm module. When a Wasm module is
instantiated by a host, functions from the host can be injected into a Wasm
module and functions from the Wasm module can be called by the host.

In this tutorial you will see how to call host functions in Juvix and call Juvix
functions from the host using the Wasm mechanism.

** The Juvix module

The following Juvix module has two functions.

The function =hostDisplayString= is an =axiom= with no corresponding =compile=
block that implements it. We will inject an implementation for this function
when we instantiate the module from NodeJS.

The function =juvixRender= is a normal Juvix function. We will call this from
NodeJS.

#+begin_src
-- NodeJsInterop.juvix
module NodeJsInterop;

open import Stdlib.Prelude;

axiom hostDisplayString : String → IO;

juvixRender : IO;
juvixRender ≔ hostDisplayString "Hello World from Juvix!";

end;
#+end_src

** Compiling the Juvix module

The Juvix module can be compiled using the following command:

#+begin_src
juvix compile -r standalone NodeJsInterop.juvix
#+end_src

This will create a file containing a Wasm module called =NodeJsInterop.wasm=.

** The NodeJS module

The following NodeJS module demonstrates both calling a Juvix function from
NodeJS and injecting a NodeJS function into a Juvix module.

The NodeJS function =hostDisplayString= is passed to the Wasm module
=NodeJSInterop.wasm= when it is instantiated. After instantiation the Juvix
function =juvixRender= is called.

The functions =ptrToCstr= and =cstrlen= are necessary to convert the =char=
pointer passed from Juvix to a JS =String=.

#+begin_src
-- NodeJSInterop.js
const fs = require('fs');
let wasmModule = null;

function cstrlen(mem, ptr) {
let len = 0;
while (mem[ptr] != 0) {
len++;
ptr++;
}
return len;
}

function ptrToCstr(ptr) {
const wasmMemory = wasmModule.instance.exports.memory.buffer;
const mem = new Uint8Array(wasmMemory);
const len = cstrlen(mem, ptr);
const bytes = new Uint8Array(wasmMemory, ptr, len);
return new TextDecoder().decode(bytes);
}

function hostDisplayString(strPtr) {
const text = ptrToCstr(strPtr);
console.log(text);
}

const wasmBuffer = fs.readFileSync("NodeJsInterop.wasm");
WebAssembly.instantiate(wasmBuffer, {
env: {
hostDisplayString,
}
}).then((w) => {
wasmModule = w;
wasmModule.instance.exports.juvixRender();
});
#+end_src

** Running the Wasm module

Now you should have the files =NodeJsInterop.wasm= and =NodeJsInterop.js= in the
same directory. Run the following command to execute the module:

#+begin_src
node NodeJsInterop.js
#+end_src

You should see the following output:

#+begin_src
Hello World from Juvix!
#+end_src
2 changes: 1 addition & 1 deletion src/Juvix/Syntax/MiniC/Language.hs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ data Qualifier
-- Attributes
--------------------------------------------------------------------------------

newtype Attribute = ExportName Text
data Attribute = ExportName Text | ImportName Text

--------------------------------------------------------------------------------
-- Types
Expand Down
6 changes: 5 additions & 1 deletion src/Juvix/Syntax/MiniC/Serialization.hs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ prettyCpp = \case

prettyAttribute :: Attribute -> HP.Doc
prettyAttribute = \case
ExportName n -> "__attribute__" HP.<> HP.parens (HP.parens ("export_name" HP.<> HP.parens (HP.doubleQuotes (prettyText n))))
ExportName n -> attr "export_name" n
ImportName n -> attr "import_name" n
where
attr :: Text -> Text -> HP.Doc
attr n v = "__attribute__" HP.<> HP.parens (HP.parens (prettyText n HP.<> HP.parens (HP.doubleQuotes (prettyText v))))

prettyCCode :: CCode -> HP.Doc
prettyCCode = \case
Expand Down
20 changes: 17 additions & 3 deletions src/Juvix/Translation/MicroJuvixToMiniC.hs
Original file line number Diff line number Diff line change
Expand Up @@ -461,9 +461,7 @@ goAxiom a
| otherwise = do
backend <- runFail (lookupBackends (axiomName ^. Micro.nameId) >>= firstBackendMatch)
case backend of
Nothing -> do
sig <- ExternalFuncSig <$> (cFunTypeToFunSig defineName <$> typeToFunType (mkPolyType' (a ^. Micro.axiomType)))
return [sig]
Nothing -> genFunctionDef a
Just defineBody ->
return
[ ExternalMacro
Expand Down Expand Up @@ -495,6 +493,22 @@ goAxiom a
NameId ->
Sem r [BackendItem]
lookupBackends f = ask >>= failMaybe . fmap (^. Scoper.compileInfoBackendItems) . HashMap.lookup f
genFunctionDef ::
forall r.
Members [Reader Micro.InfoTable, Reader CompileInfoTable] r =>
Micro.AxiomDef ->
Sem r [CCode]
genFunctionDef d
| T.all isAlphaNum nameText = (ExternalAttribute (ImportName (axiomName ^. Micro.nameText)) :) <$> sig
| otherwise = sig
where
nameText :: Text
nameText = axiomName ^. Micro.nameText

sig :: Sem r [CCode]
sig = do
s <- cFunTypeToFunSig defineName <$> typeToFunType (mkPolyType' (d ^. Micro.axiomType))
return [ExternalFuncSig s]

goForeign :: ForeignBlock -> [CCode]
goForeign b = case b ^. foreignBackend of
Expand Down
12 changes: 2 additions & 10 deletions test/BackendC/Base.hs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ clangCompile mkClangArgs cResult execute step =
withTempDir
( \dirPath -> do
let cOutputFile = dirPath </> "out.c"
wasmOutputFile = dirPath </> "out.wasm"
wasmOutputFile = dirPath </> "Input.wasm"
TIO.writeFile cOutputFile (cResult ^. MiniC.resultCCode)
step "WASM generation"
P.callProcess
Expand All @@ -40,16 +40,8 @@ wasmClangAssertion WASMInfo {..} mainFile expectedFile step = do

expected <- TIO.readFile expectedFile

let execute :: FilePath -> IO Text
execute outputFile =
pack
<$> P.readProcess
"wasmer"
(["run", outputFile, "--invoke", unpack _wasmInfoFunctionName] <> (unpack <$> _wasmInfoFunctionArgs))
""

step "Compile C with wasm standalone runtime"
actualStandalone <- clangCompile standaloneArgs p execute step
actualStandalone <- clangCompile standaloneArgs p _wasmInfoActual step
step "Compare expected and actual program output"
assertEqDiff ("Check: WASM output = " <> expectedFile) actualStandalone expected

Expand Down
29 changes: 27 additions & 2 deletions test/BackendC/Positive.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module BackendC.Positive where

import BackendC.Base
import Base
import System.Process qualified as P

data PosTest = PosTest
{ _name :: String,
Expand All @@ -20,6 +21,29 @@ mainFile = "Input.juvix"
expectedFile :: FilePath
expectedFile = "expected.golden"

actualCallExport :: Text -> [Text] -> FilePath -> IO Text
actualCallExport funName funArgs outputFile =
pack
<$> P.readProcess
"wasmer"
(["run", outputFile, "--invoke", unpack funName] <> (unpack <$> funArgs))
""

actualCallNode :: FilePath -> FilePath -> IO Text
actualCallNode jsFile outputFile = do
assertCmdExists "node"
let outputDir = takeDirectory outputFile
outputJsFile = outputDir </> jsFile
copyFile jsFile outputJsFile
withCurrentDirectory
outputDir
( pack
<$> P.readProcess
"node"
[outputJsFile]
""
)

testDescr :: PosTest -> TestDescr
testDescr PosTest {..} =
let tRoot = root </> _relDir
Expand Down Expand Up @@ -55,6 +79,7 @@ tests =
PosTest "Builtin types and functions" "Builtins" (WASI StdlibExclude),
PosTest "Import from embedded standard library" "StdlibImport" (WASI StdlibInclude),
PosTest "Axiom without a compile block" "AxiomNoCompile" (WASI StdlibInclude),
PosTest "Invoke a function using exported name" "ExportName" (WASM (WASMInfo "fun" [])),
PosTest "Invoke a function using exported name with args" "ExportNameArgs" (WASM (WASMInfo "fun" ["0"]))
PosTest "Invoke a function using exported name" "ExportName" (WASM (WASMInfo (actualCallExport "fun" []))),
PosTest "Invoke a function using exported name with args" "ExportNameArgs" (WASM (WASMInfo (actualCallExport "fun" ["0"]))),
PosTest "Invoke an imported function in Juvix and exported function in JS" "ImportExportName" (WASM (WASMInfo (actualCallNode "input.js")))
]
5 changes: 2 additions & 3 deletions test/Base.hs
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@ data TestDescr = TestDescr
_testAssertion :: AssertionDescr
}

data WASMInfo = WASMInfo
{ _wasmInfoFunctionName :: Text,
_wasmInfoFunctionArgs :: [Text]
newtype WASMInfo = WASMInfo
{ _wasmInfoActual :: FilePath -> IO Text
}

makeLenses ''TestDescr
Expand Down
10 changes: 10 additions & 0 deletions tests/positive/MiniC/ImportExportName/Input.juvix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module Input;

open import Stdlib.Prelude;

axiom hostDisplayString : String → IO;

juvixRender : IO;
juvixRender ≔ hostDisplayString "Hello World from Juvix!";

end;
1 change: 1 addition & 0 deletions tests/positive/MiniC/ImportExportName/expected.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Hello World from Juvix!
35 changes: 35 additions & 0 deletions tests/positive/MiniC/ImportExportName/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const fs = require('fs');

let wasmModule = null;

function cstrlen(mem, ptr) {
let len = 0;
while (mem[ptr] != 0) {
len++;
ptr++;
}
return len;
}

function ptrToCstr(ptr) {
const wasmMemory = wasmModule.instance.exports.memory.buffer;
const mem = new Uint8Array(wasmMemory);
const len = cstrlen(mem, ptr);
const bytes = new Uint8Array(wasmMemory, ptr, len);
return new TextDecoder().decode(bytes);
}

function hostDisplayString(strPtr) {
const text = ptrToCstr(strPtr);
console.log(text);
}

const wasmBuffer = fs.readFileSync("Input.wasm");
WebAssembly.instantiate(wasmBuffer, {
env: {
hostDisplayString,
}
}).then((w) => {
wasmModule = w;
wasmModule.instance.exports.juvixRender();
});
Empty file.