Skip to content

Instantly share code, notes, and snippets.

@dckc
Last active April 13, 2020 19:20
Show Gist options
  • Save dckc/fb6c0a49361e8502b0be418ec71b44c1 to your computer and use it in GitHub Desktop.
Save dckc/fb6c0a49361e8502b0be418ec71b44c1 to your computer and use it in GitHub Desktop.
node_modules/
dist/
*~

Section 3. INTERPRETING CAPABILITIES of Policies as Types sketches compiling JavaScript to RHO-calculus.

We've got a small amount of it working:

const decr = amount => {
    balance = Nat(balance - amount);
};

gets compiled (using fresh names taken from line and column numbers) to

contract decr(amount, l2c13_0) = {
  new l3c14_1 in {
    new l3c14_5, l3c18_2 in {
      new l3c18_3, l3c28_4 in {
	balance!("get", *l3c18_3) | amount!("get", *l3c28_4) | l3c18_2!(*l3c18_3 - *l3c28_4)
      }
      | Nat!("get", *l3c14_5) | l3c14_5!(*l3c18_2, l3c14_1)
    }
    | balance!("set", *l3c14_1, *l2c13_0)
  }
}
  | top!(Nil)

See also bounty issue 427.

// @ts-check
/// <reference path="node_modules/@types/node/index.d.ts" />
/// <reference path="node_modules/@types/esprima/index.d.ts" />
import esprima from "esprima";
const { parseModule, Program } = esprima;
//import { Node } from "estree";
import poolSrc from './testSuite.js';
// console.log(process.argv); TODO: get filename from process.argv
/**
* @typedef {Object} Miranda
* @property {(out: Printer) => void} _printOn
*
* @typedef {Object} Printer
* @property {(s: string) => void} print
*
* @typedef {Miranda} Process
* @property {() => Name} quote
*
* @typedef {Object} Name
* @property {(out: Printer) => void} _printOn
* @property {() => Process} deref
*/
/**
* @typedef {"==" | "!="| "and" | "or"| "+"| "*" | "%"| "-" | "/"| "<=" | "<" | ">=" | ">"| "bitand" | "bitor"} BinOp
// Structurally equivalent processes: "==" | "!="
// Boolean operators: "and" | "or"
// Integers, strings, and datetimes: "+"
// Integers and strings: "*" | "%"
// Integers: "-" | "/"
// All ordered types: "<=" | "<" | ">=" | ">"
// Bitfields: "bitand" | "bitor"
*
* // TODO: types, iopairs
* @typedef {string | [string, string]} vdecl
*/
/**
* @typedef {Object} RhoBuilder is a thingy
* @property {() => Process} Nil
* @property {(v: boolean | number | string) => Process} primitive
* @property {(dest: Name, procs: Array<Process>) => Process} send
* @property {(op: BinOp, lhs: Process, rhs: Process) => Process} binop
* @property {(lhs: Array<Name>, rhs: Name, proc: Process) => Process} receiving
* @property {(name: Name, args: Array<Name>, body: Process) => Process} contract
* @property {(n: Name) => Process} Drop
* @property {(p: Process, q: Process) => Process} Par
* @property {(id: string) => Name} Var
* @property {(p: Process) => Name} Quote
* @property {(vars: Array<vdecl>, body: Process) => Process} new_
*/
/**
* @returns {RhoBuilder}
*/
function rhoBuilder() {
const Nil = () => Object.freeze({
_printOn: (out) => out.print("Nil"),
quote: () => Quote(Nil())
});
const primitive = (v) => Object.freeze({
_printOn: (out) => out.print(JSON.stringify(v)),
quote: () => Quote(primitive(v))
});
const Quote = (p) => Object.freeze({
_printOn: (out) => {
out.print("@{ ");
p._printOn(out)
out.print(" }")
},
deref: () => p
});
const Drop = (name) => Object.freeze({
_printOn: (out) => {
out.print("*");
name._printOn(out)
},
quote: () => name
});
const printList = (out, items) => {
let first = true;
for (let item of items) {
if (!first) {
out.print(", ")
}
item._printOn(out)
first = false
}
}
const send = (dest, procs) => Object.freeze({
_printOn: (out) => {
dest._printOn(out)
out.print(`!(`) // TODO: !!
printList(out, procs)
out.print(")")
},
quote: () => Quote(send(dest, procs))
});
const binop = (op, lhs, rhs) => Object.freeze({
_printOn: (out) => {
lhs._printOn(out)
out.print(" " + op + " ")
rhs._printOn(out)
},
quote: () => Quote(binop(op, lhs, rhs))
})
const receiving = (lhs, rhs, proc) => Object.freeze({
_printOn: (out) => {
out.print("for(")
printList(out, lhs)
out.print(` <- `)
rhs._printOn(out)
out.print(`) {\n `)
proc._printOn(out)
out.print("\n}\n")
},
quote: () => Quote(receiving(lhs, rhs, proc))
})
const contract = (name, args, body) => Object.freeze({
_printOn: (out) => {
out.print("contract ")
name._printOn(out)
out.print(`(`)
printList(out, args)
out.print(`) = {\n`)
body._printOn(out)
out.print("\n}\n")
},
quote: () => Quote(contract(name, args, body))
})
const Par = (p, q) => Object.freeze({
_printOn: (out) => {
p._printOn(out);
out.print(" | ");
q._printOn(out);
},
quote: () => Quote(Par(p, q))
});
const Var = (v) => Object.freeze({
_printOn: (out) => out.print(v),
deref: () => Drop(Var(v))
})
const fmtvdecl = (vd) => typeof vd === 'string' ? vd : `${vd[0]}(\`${vd[1]}\`)`;
/** @type {(vlist: vdecl[], body: Process) => Process} */
const new_ = (vlist, body) => Object.freeze({
_printOn: (out) => {
out.print("\nnew ")
out.print(vlist.map(fmtvdecl).join(",\n "))
out.print("\nin {\n")
body._printOn(out)
out.print("\n}\n")
},
quote: () => Quote(new_(vlist, body))
})
return Object.freeze({
Nil,
primitive,
send,
binop,
receiving,
contract,
Drop,
Var,
Quote,
Par,
new_,
});
}
const builtins = {
'@rchain-community/js2rho': ['bundlePlus', 'tuple', 'console'],
'@agoric/eventual-send': ['E'],
};
const rhoConsole = ['console', 'rho:io:stdout'];
/**
*
* @param {RhoBuilder} bld
* @returns {(js: Program, k: Name) => Process}
*/
function makeCompiler(bld) {
const loc = (n) => `${n.loc.start.line}c${n.loc.start.column}`
let tmp_ix = 0;
const fresh = (n) => `l${loc(n)}_${tmp_ix++}`
/** @type {(n: string) => Name} */
const vn = (n) => bld.Var(n)
const ignore = vn("_") // TODO: this is a pattern, not a name, no?
/** @type {(ps: Process[] ) => Process } */
const par = (ps) => ps.reduce((acc, next) => bld.Par(acc, next));
/**
*
* @param {Node[]} body
*/
function ofProgram(body) {
console.log({ Program: body.length });
if (body.length === 0) {
return bld.Nil();
}
const defaultExport = body[body.length - 1];
if (defaultExport.type !== 'ExportDefaultDeclaration') {
//throw new Error(`${loc(defaultExport)}: expected default export; found ${defaultExport.type}`);
console.warn(`${loc(defaultExport)}: expected default export; found ${defaultExport.type}`);
return bld.primitive(defaultExport.type);
}
/** @type {vdecl[]} */
const newNames = body.slice(0, body.length - 1).flatMap(decl => {
if (decl.type !== 'ImportDeclaration') {
throw new Error(`${loc(decl)}: expected import; found ${decl.type}`);
}
if (decl.source.type !== 'Literal') {
throw new Error(decl.source.type);
}
const specifier = decl.source.value;
if (typeof specifier === 'string' && specifier.startsWith('rho:')) {
if (decl.specifiers.length !== 1) {
throw new Error(`must import 1 name from ${specifier}`);
}
return [[decl.specifiers[0].local.name, specifier]];
} else {
const candidates = builtins[specifier];
if (!candidates) {
throw new Error(`not supported: import ... from ${specifier}`);
}
const unknowns = decl.specifiers.map(s => s.local.name).filter(n => !candidates.includes(n));
if (unknowns.length > 0) {
throw new Error(`unrecognized name(s) ${unknowns} from ${specifier}`);
}
return [];
}
});
console.log('@@new names', newNames);
if (defaultExport.declaration.type !== 'FunctionDeclaration') {
throw new Error(`${loc(defaultExport.declaration)}: expected function declartion; got ${defaultExport.declaration.type}`)
}
return bld.new_(newNames.concat([rhoConsole]),
js2rho(defaultExport.declaration.body, vn(fresh(defaultExport))));
}
/** @type {(js: Program, k: Name) => Process} */
function js2rho(js, k) {
// console.log("DEBUG: node:", JSON.stringify(js));
console.log("DEBUG: js.type", js.type);
const recur = (js) => {
const kr = fresh(js)
return { v: kr, p: js2rho(js, vn(kr)) }
}
const kont = (proc) => bld.send(k, [proc]);
const patname = (pat) => {
if (pat.type != "Identifier") {
throw (pat) // TODO: other param patterns
}
return pat.name
}
switch (js.type) {
case "Literal":
const v = js.value
if (typeof (v) == "object") {
throw (v) // TODO: RegExp
}
return kont(bld.primitive(v))
case "Identifier":
// Assume each js ident refers to a cell
return bld.send(vn(js.name),
[bld.primitive("get"),
bld.Drop(k)])
case "BinaryExpression":
const op = js.operator
if (op == "===" || op == "!==" ||
op == "<<" || op == ">>" ||
op == ">>>" ||
op == "**" ||
op == "|" || op == "&" ||
op == "^" ||
op == "in" ||
op == "instanceof") {
throw (op) // TODO
}
// We assume order of arguments doesn't matter.
const lt = recur(js.left),
rt = recur(js.right)
return bld.new_([lt.v, rt.v], par([lt.p, rt.p,
bld.send(k, [bld.binop(op, vn(lt.v).deref(), vn(rt.v).deref())])]))
case "CallExpression":
const args = js.arguments.map(recur)
let target;
let verb = []
/* special case for obj.prop(...) */
if (js.callee.type == "MemberExpression" &&
js.callee.property.type == "Identifier") {
target = recur(js.callee.object)
const prop = js.callee.property
verb = [bld.primitive(prop.name)]
} else {
target = recur(js.callee)
}
const vs = [].concat([target.v], args.map(a => a.v));
const call = bld.send(vn(target.v), [].concat(verb, args.map(a => vn(a.v).deref()), [k]))
return bld.new_(vs,
par([].concat(args.map(a => a.p), [target.p, call])))
case "ExpressionStatement":
return js2rho(js.expression, k);
case "VariableDeclaration":
const decls = js.declarations
/* special case for const f = (a, b, c, ...) => e */
if (js.kind == "const" && decls.length == 1) {
const decl = decls[0]
if (decl.type == "VariableDeclarator") {
const id = decl.id
if (id.type == "Identifier") {
const lhs = bld.Var(id.name)
const init = decl.init
if (init.type == "ArrowFunctionExpression") {
const ret = fresh(init)
const params = [].concat(
init.params.length == 0 ?
[ignore] : (
init.params.map(pat => vn(patname(pat)))),
[vn(ret)])
const contract = bld.contract(lhs, params, js2rho(init.body, vn(ret)))
const done = bld.send(k, [bld.Nil()])
return bld.Par(contract, done)
}
}
}
}
console.log("@@VariableDeclaration not impl:", JSON.stringify(js, null, 2))
return bld.primitive(js.type)
case "AssignmentExpression":
const lhs = patname(js.left)
const rhs = recur(js.right)
const set = bld.send(vn(lhs), [bld.primitive("set"), vn(rhs.v).deref(), k.deref()])
return bld.new_([rhs.v], par([rhs.p, set]))
case "BlockStatement":
if (js.body.length == 1) {
return js2rho(js.body[0], k)
}
console.log("BlockStatement @@not impl:", JSON.stringify(js).slice(0, 480))
return bld.primitive(js.type)
case "Program":
return ofProgram(js.body);
default:
console.log("@@not impl:", JSON.stringify(js, null, 2))
return bld.primitive(js.type)
}
}
return js2rho;
}
const tests = [
{
src: poolSrc,
},
{
src: `
m.set(purse, decr);
`,
},
{
src: `
const decr = amount => {
balance = Nat(balance - amount);
};
`,
},
{
src: `
const makeMint = () => {
const m = WeakMap();
const makePurse = () => mint(0);
const mint = balance => {
const purse = def({
getBalance: () => balance,
makePurse: makePurse,
deposit:
(amount, srcP) => Q(srcP).then(src => {
Nat(balance + amount);
m.get(src)(Nat(amount));
balance += amount;
})
});
const decr = amount => {
balance = Nat(balance - amount);
};
m.set(purse, decr);
return purse;
};
return mint;
};
`,
},
];
export default
function unittest(out) {
const bld = rhoBuilder();
const compiler = makeCompiler(bld);
const printer = Object.freeze({
print: (txt) => out.write(txt)
})
for (const item of tests) {
console.log('\n==== JS SOURCE CODE ====\n', item.src);
const prog = parseModule(item.src, { loc: true });
console.log('==== AST ====\n', JSON.stringify(prog, null, 2));
console.log('==== RHO ====\n');
compiler(prog, bld.Var("top"))._printOn(printer);
}
}
unittest(process.stdout);
# mt2rho -- Monte to Rholang compiler
# https://github.com/rchain/bounties/issues/427
#
# tabs:
# http://monte.readthedocs.io/en/latest/
# https://github.com/monte-language/typhon/blob/master/mast/fun/mli.mt
# https://developer.rchain.coop/tutorial
# https://github.com/rchain/rchain/blob/dev/rholang/examples/linking/packages/IArrayApi.rho
# https://github.com/rchain/rchain/blob/dev/rholang/src/main/bnfc/rholang_mercury.cf
exports (main)
def Expr :DeepFrozen := astBuilder.getExprGuard()
def letter :DeepFrozen := 'a'..'z' | 'A'..'Z'
def digit :DeepFrozen := '0'..'9'
def cr := fn c :Char { c..c }
def varFirst :DeepFrozen := [
for ch in (letter | cr('_') | cr('\'') ) ch].asSet()
def varRest :DeepFrozen := [
for ch in (letter | digit | cr('_') | cr('\'') ) ch].asSet()
object RhoVar as DeepFrozen:
"token Var ((letter | '_')(letter | digit | '_' | '\'')*) ;"
to coerce(x, ej):
def s:Str exit ej := x
if (s.size() == 0):
throw.eject(ej, "empty string is not a RhoVar")
def [first, rest] := [s[0], s.slice(1)]
if (!varFirst.contains(first)):
throw.eject(ej, `RhoVar ${s} cannot start with ${s[0]}`)
for ch in (rest):
if (! varRest.contains(ch)):
throw.eject(ej, `RhoVar ${s} cannot contain $ch`)
return s
def printList(out, items :List, "sep" => sep := ", ") as DeepFrozen:
for ix => item in (items):
if (ix > 0):
out.print(sep)
out.print(item)
# marker interfaces
interface Proc :DeepFrozen {}
interface RhoBuilder :DeepFrozen {}
object rhoBuilder as DeepFrozen implements RhoBuilder:
to Nil():
return object Nil as Proc:
to _printOn(out):
out.print("Nil")
to Value(v :Any[Bool, Int, Double, Str]):
if (v =~ s:Str):
return object Value as Proc:
to _printOn(out):
out.print(M.toQuote(s)) # ISSUE: same string escapes?
else:
return object Value as Proc:
to _printOn(out):
out.print(v)
to Lift(chan: RhoVar, proc: Proc):
return object Lift as Proc:
to _printOn(out):
out.print(`$chan!(`)
proc._printOn(out)
out.print(")")
to Input(pat: RhoVar, chan: RhoVar, proc: Proc):
return object Input as Proc:
to _printOn(out):
out.print(`for($pat <- $chan) {$\n`)
proc._printOn(out)
out.print("\n}")
to New(nameDecls :List[RhoVar], proc: Proc):
return object New as Proc:
to _printOn(out):
out.print("new ")
printList(out, nameDecls)
out.print(" in {\n")
proc._printOn(out)
out.print("\n}")
to Par(lhs: Proc, rhs: Proc):
return object Par as Proc:
to _printOn(out):
lhs._printOn(out)
out.print(" | ")
rhs._printOn(out)
def butLast(specimen: List, ej) as DeepFrozen:
def qty := specimen.size()
if (qty == 0):
throw.eject(ej, "butLast on empty list")
return [specimen.slice(0, qty - 1), specimen.last()]
def exprLoc(expr :Expr) :RhoVar as DeepFrozen:
def [_, =="run",
[==true, l0, c0, l1, c1], _] := expr.getSpan()._uncall()
return `at_l${l0}c${c0}_to_l${l1}c${c1}`
def makeCompiler(builder: RhoBuilder) as DeepFrozen:
return def compile(expr :Expr) :Proc:
def doExprs(exs, dest):
return switch(exs):
match []:
builder.Nil()
match [ex]:
builder.Lift(dest, compile(ex))
match via (butLast) [init, tail]:
# ISSUE: uniqueness
def pat :RhoVar := `step${init.size()}`
def chan :RhoVar := `${pat}Ch`
def rhoInit := doExprs(init, chan)
def rhoTail := builder.Lift(dest, compile(tail))
builder.New([chan],
builder.Par(
rhoInit,
builder.Input(pat, chan, rhoTail)))
return switch (expr.getNodeName()):
match =="LiteralExpr":
builder.Value(expr.getValue())
match =="SeqExpr":
def chan := exprLoc(expr)
builder.New([chan], doExprs(expr.getExprs(), chan))
match =="MethodCallExpr":
def receiver := compile(expr.getReceiver())
def args := [for a in (expr.getArgs()) compile(a)]
# def namedArgs := [for namedArg in (expr.getNamedArgs())
# [compile(namedArg.getKey()),
# compile(namedArg.getValue())]]
fn env {
M.call(receiver(env), expr.getVerb(),
[for arg in (args) arg(env)],
[for [k, v] in (namedArgs) k(env) => v(env)])
}
match _:
throw("not implemented: ", expr.getNodeName(), expr)
def monte0 :DeepFrozen := m`"feed"; "water"; "grow"`
def monte1 :DeepFrozen := m`1 + 1`
def monte2 :DeepFrozen := m`object origin {
method getX() { 0 }
method getY() { 0 }
}
# Now invoke the methods
origin.getY()
`
# TODO: real unit test
def testRhoVar() as DeepFrozen:
for specimen in (["abc", "123", "a*b"]):
escape notVar:
def _ :RhoVar exit notVar := specimen
traceln("name ok:", specimen)
catch oops:
traceln(oops)
def main(_argv) :Int as DeepFrozen:
testRhoVar()
def compile := makeCompiler(rhoBuilder)
for expr in ([monte0, monte1, monte2]):
traceln("monte:", expr)
def rho := compile(expr.expand())
traceln("rholang", rho)
return 0
{
"name": "js2rho",
"version": "0.1.0",
"description": "JavaScript to Rholang translator",
"main": "index.js",
"type": "module",
"dependencies": {
"esprima": "^4.0.0"
},
"devDependencies": {
"@types/esprima": "^4.0.1",
"@types/estree": "0.0.38",
"@types/node": "^13.11.1",
"typescript": "^3.8.3"
},
"scripts": {
"test": "node index.js"
},
"author": "Dan Connolly",
"license": "MIT"
}
// @ts-check
import * as fs from 'fs';
const source = fs.readFileSync('StakingPool.js', { encoding: 'utf-8' });
export default source;
{
"compileOnSave": true,
"compilerOptions": {
"target": "es6",
"outDir": "dist",
}
}
@dckc
Copy link
Author

dckc commented Apr 11, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment