TouchDevelop/embedded/emitter.ts

600 строки
22 KiB
TypeScript

///<reference path='refs.ts'/>
module TDev {
export module Embedded {
import J = AST.Json
import H = Helpers
// --- Environments
export interface EmitterEnv extends H.Env {
indent: string;
}
export function emptyEnv(): EmitterEnv {
return {
indent: "",
ident_of_id: {},
id_of_ident: {},
};
}
export function indent(e: EmitterEnv) {
return {
indent: e.indent + " ",
ident_of_id: e.ident_of_id,
id_of_ident: e.id_of_ident,
};
}
function assert(x: boolean) {
if (!x)
throw new Error("Assert failure");
}
// --- The code emitter.
export class Emitter extends JsonAstVisitor<EmitterEnv, string> {
// Output "parameters", written to at the end.
public prototypes = "";
public code = "";
public prelude = "";
// Type stubs. Then, type definitions. To allow for any kind of insane
// type-level recursion.
public tPrototypes = "";
public tCode = "";
private libraryMap: H.LibMap = {};
private imageLiterals = [];
// All the libraries needed to compile this [JApp].
constructor(
private libRef: J.JCall,
public libName: string,
private libs: J.JApp[],
private resolveMap: { [index:string]: string }
) {
super();
}
public visitMany(e: EmitterEnv, ss: J.JNode[]) {
var code = [];
ss.forEach((s: J.JNode) => { code.push(this.visit(e, s)) });
return code.join("\n");
}
public visitComment(env: EmitterEnv, c: string) {
return env.indent+"// "+c.replace("\n", "\n"+env.indent+"// ");
}
public visitBreak(env: EmitterEnv) {
return env.indent + "break;";
}
public visitContinue(env: EmitterEnv) {
return env.indent + "continue;";
}
public visitShow(env: EmitterEnv, expr: J.JExpr) {
// TODO hook this up to "post to wall" handling if any
return env.indent + "serial.print(" + this.visit(env, expr) + ");";
}
public visitReturn(env: EmitterEnv, expr: J.JExpr) {
return env.indent + H.mkReturn(this.visit(env, expr));
}
public visitExprStmt(env: EmitterEnv, expr: J.JExpr) {
return env.indent + this.visit(env, expr)+";";
}
public visitExprHolder(env: EmitterEnv, locals: J.JLocalDef[], expr: J.JExprHolder) {
var decls = locals.map(d => {
var x = H.defaultValueForType(this.libraryMap, d.type);
return this.visit(env, d) + (x ? " = " + x : "") + ";";
});
return decls.join("\n"+env.indent) +
(decls.length ? "\n" + env.indent : "") +
this.visit(env, expr);
}
public visitLocalRef(env: EmitterEnv, name: string, id: string) {
// In our translation, referring to a TouchDevelop identifier never
// requires adding a reference operator (&). Things passed by reference
// are either:
// - function pointers (a.k.a. "handlers" in TouchDevelop lingo), for
// which C and C++ accept both "f" and "&f" (we hence use the former)
// - arrays, strings, user-defined objects, which are in fact of type
// "shared_ptr<T>", no "&" operator here.
return H.mangleUnique(env, name, id);
}
public visitLocalDef(env: EmitterEnv, name: string, id: string, type: J.JTypeRef) {
return H.mkType(env, this.libraryMap, type)+" "+H.mangleUnique(env, name, id);
}
// Allows the target to redefine their own string type.
public visitStringLiteral(env: EmitterEnv, s: string) {
return 'touch_develop::mk_string("'+s.replace(/["\\\n\r]/g, c => {
if (c == '"') return '\\"';
if (c == '\\') return '\\\\';
if (c == "\n") return '\\n';
if (c == "\r") return '\\r';
}) + '")';
}
public visitNumberLiteral(env: EmitterEnv, n: number) {
return n+"";
}
public visitBooleanLiteral(env: EmitterEnv, b: boolean) {
return b+"";
}
public visitWhile(env: EmitterEnv, cond: J.JExprHolder, body: J.JStmt[]) {
var condCode = this.visit(env, cond);
var bodyCode = this.visitMany(indent(env), body);
return env.indent + "while ("+condCode+") {\n" + bodyCode + "\n" + env.indent + "}";
}
public visitFor(env: EmitterEnv, index: J.JLocalDef, bound: J.JExprHolder, body: J.JStmt[]) {
var indexCode = this.visit(env, index) + " = 0";
var testCode = H.mangleDef(env, index) + " < " + this.visit(env, bound);
var incrCode = "++"+H.mangleDef(env, index);
var bodyCode = this.visitMany(indent(env), body);
return (
env.indent + "for ("+indexCode+"; "+testCode+"; "+incrCode+") {\n" +
bodyCode + "\n" +
env.indent + "}"
);
}
public visitIf(
env: EmitterEnv,
cond: J.JExprHolder,
thenBranch: J.JStmt[],
elseBranch: J.JStmt[],
isElseIf: boolean)
{
var isIfFalse = cond.tree.nodeType == "booleanLiteral" && (<J.JBooleanLiteral> cond.tree).value === false;
// TouchDevelop abuses "if false" to comment out code. Commented out
// code is not type-checked, so don't try to compile it. However, an
// "if false" followed by an "else" is *not* understood to be a comment.
return [
env.indent, isElseIf ? "else " : "", "if (" + this.visit(env, cond) + "){\n",
isIfFalse ? "" : this.visitMany(indent(env), thenBranch) + "\n",
env.indent, "}",
elseBranch ? " else {\n" : "",
elseBranch ? this.visitMany(indent(env), elseBranch) + "\n" : "",
elseBranch ? env.indent + "}" : ""
].join("");
}
private resolveCall(env: EmitterEnv, receiver: J.JExpr, name: string) {
if (!receiver)
return null;
// Is this a call in the current scope?
var scoped = H.isScopedCall(receiver);
if (scoped)
if (this.libRef)
// If compiling a library, no scope actually means the library's
// scope. This step is required to possibly find a shim. This means
// that we may generate "lib::f()" where we could've just written
// "f()", but since the prototypes have been written out already,
// that's fine.
return this.resolveCall(env, this.libRef, name);
else
// Call to a function from the current script.
return H.mangleName(name);
// Is this a call to a library?
var n = H.isLibrary(receiver);
if (n) {
// I expect all libraries and all library calls to be properly resolved.
var key = this.resolveMap[n] || n;
var lib = this.libs.filter(l => l.name == key)[0];
var action = lib.decls.filter((d: J.JDecl) => d.name == name)[0];
var s = H.isShimBody((<J.JAction> action).body);
if (s != null) {
// Call to a built-in C++ function
if (!s.length)
throw new Error("Library author: some (compiled) function is trying to call "+name+" "+
"which is marked as {shim:}, i.e. for simulator purposes only.\n\n"+
"Hint: break on exceptions in the debugger and walk up the call stack to "+
"figure out which action it is.");
return s;
} else {
// Actual call to a library function
return H.manglePrefixedName(env, [n], name);
}
}
return null;
}
// Some conversions cannot be expressed using the simple "enums" feature
// (which maps a string literal to a constant). This function transforms
// the arguments for some known specific C++ functions.
private specialTreatment(e: EmitterEnv, f: string, actualArgs: J.JExpr[]) {
if (f == "micro_bit::createImage" || f == "micro_bit::showAnimation" || f == "micro_bit::plotImage") {
var x = H.isStringLiteral(actualArgs[0]);
if (x === "")
x = "0 0 0 0 0\n0 0 0 0 0\n0 0 0 0 0\n0 0 0 0 0\n0 0 0 0 0\n";
if (!x)
throw new Error("create image / show animation / plot image takes a string literal only");
var r = "literals::bitmap"+this.imageLiterals.length;
var otherArgs = actualArgs.splice(1).map((x: J.JExpr) => this.visit(e, x));
var code = f+"("+r+"_w, "+r+"_h, "+r+
(otherArgs.length ? ", "+otherArgs : "")+
")";
this.imageLiterals.push(x);
return code;
} else {
return null;
}
}
private safeGet(x: string, f: string): string {
return (
"("+x+".operator->() != NULL "+
"? "+x+"->"+f+" "+
": (uBit.panic(MICROBIT_INVALID_VALUE), "+x+"->"+f+"))"
);
}
public visitCall(env: EmitterEnv,
name: string,
args: J.JExpr[],
typeArgs: J.JTypeRef[],
parent: J.JTypeRef,
callType: string,
typeArgument: string = null)
{
var mkCall = (f: string, skipReceiver: boolean) => {
var actualArgs = skipReceiver ? args.slice(1) : args;
var s = this.specialTreatment(env, f, actualArgs);
if (s)
return s;
else {
var argsCode =
actualArgs.map(a => {
var k = H.isEnumLiteral(a);
if (k)
return k+"";
else
return this.visit(env, a)
});
var t = typeArgument ? "<" + typeArgument + ">" : "";
return f + t + "(" + argsCode.join(", ") + ")";
}
};
// The [JCall] node has several, different, often unrelated purposes.
// This function identifies (tentatively) the different cases and
// compiles each one of them into something that makes sense.
// 0a) Some methods take a type-level argument at the end, e.g.
// "create -> collection of -> number". TouchDevelop represents this as
// calling the method "Number" on "create -> collection of". C++ wants
// the type argument to be passed as a template parameter to "collection of"
// so we pop the type arguments off and call ourselves recursively with
// the extra type argument.
// Extra bonus subtlety: we are able to get the "complete" type argument
// at the root of the call sequence, but we need skip "intermediate"
// nodes (that have types such as Collection<T> in there) until we hit
// the actual code arguments.
if (<any> parent == "Unfinished Type") {
var newTypeArg = typeArgument || H.mkType(env, this.libraryMap, typeArgs[0]);
assert(args.length && args[0].nodeType == "call");
var call = <J.JCall> args[0];
return this.visitCall(env, call.name, call.args, call.typeArgs, call.parent, call.callType, newTypeArg);
}
// 0b) Ha ha! But actually, guess what? For records, it's the opposite,
// and TouchDevelop writes "÷point -> create".
if (H.isRecordConstructor(name, args)) {
// Note: we cannot call new on type definitions from other libraries.
// So the type we're looking for is always in the current scope's
// "user_types" namespace.
var struct_name = H.manglePrefixedName(env, ["user_types"], <any> parent)+"_";
return "ManagedType<"+struct_name+">(new "+struct_name+"())";
}
// 1) A call to a function, either in the current scope, or belonging to
// a TouchDevelop library. Resolves to a C++ function call.
var resolvedName = this.resolveCall(env, args[0], name);
if (resolvedName)
return mkCall(resolvedName, true);
// 2) A call to the assignment operator on the receiver. C++ assignment.
else if (name == ":=")
return this.visit(env, args[0]) + " = " + this.visit(env, args[1]);
// 3) Reference to a variable in the global scope.
else if (args.length && H.isSingletonRef(args[0]) == "data")
return H.manglePrefixedName(env, ["globals"], name);
// 4) Extension method, where p(x) is represented as x→ p. In case we're
// actually referencing a function from a library, go through
// [resolveCall] again, so that we find the shim if any.
else if (callType == "extension") {
var t = H.resolveTypeRef(this.libraryMap, parent);
var prefixedName = t.libs.length > 1
? this.resolveCall(env, H.mkLibraryRef(t.libs[0]), name)
: this.resolveCall(env, H.mkCodeRef(), name);
return mkCall(prefixedName, false);
}
// 5) Field access for an object.
else if (callType == "field")
return this.safeGet(this.visit(env, args[0]), H.mangleName(name));
// 6a) Lone reference to a library (e.g. ♻ micro:bit just by itself).
else if (args.length && H.isSingletonRef(args[0]) == "♻")
return "";
// 6b) Reference to a built-in library method, e.g. Math→ max. The
// first call to lowercase avoids a conflict between Number (the type)
// and Number (the namespace). The second call to lowercase avoids a
// conflict between Collection_of (the type) and Collection_of (the
// function).
else if (args.length && H.isSingletonRef(args[0]))
return H.isSingletonRef(args[0]).toLowerCase() + "::" + mkCall(H.mangleName(name).toLowerCase(), true);
// 7) Instance method (e.g. Number's > operator, for which the receiver
// is the number itself). Lowercase so that "number" is the namespace
// that contains the functions that operate on typedef "Number".
else {
var t = H.resolveTypeRef(this.libraryMap, parent);
return t.type.toLowerCase()+"::"+mkCall(H.mangleName(name), false);
}
}
public visitSingletonRef(e, n: string) {
if (n == "$skip")
return "";
else
// Reference to "data", "Math" (or other namespaces), that makes no
// sense. TouchDevelop allows these.
return "";
}
public visitGlobalDef(e: EmitterEnv, name: string, t: J.JTypeRef, comment: string) {
H.reserveName(e, name);
// TODO: we skip definitions marked as shims, but we do not do anything
// meaningful when we *refer* to them.
var s = H.isShim(comment);
if (s !== null)
return null;
var x = H.defaultValueForType(this.libraryMap, t);
// A reference to a global is already unique (i.e. un-ambiguous).
// [mkType] calls [mangleName] (NOT [mangleUnique], and so should we).
return e.indent + H.mkType(e, this.libraryMap, t) + " " + H.mangleName(name) +
(x ? " = " + x : "") + ";"
}
public visitAction(
env: EmitterEnv,
name: string,
id: string,
inParams: J.JLocalDef[],
outParams: J.JLocalDef[],
body: J.JStmt[])
{
// This function is always called with H.willCompile == true, meaning
// it's not a shim.
if (outParams.length > 1)
throw new Error("Not supported (multiple return parameters)");
var env2 = indent(env);
var bodyText = [
outParams.length ? env2.indent + this.visit(env2, outParams[0]) + ";" : "",
this.visitMany(env2, body),
outParams.length ? env2.indent + H.mkReturn(H.mangleDef(env, outParams[0])) : "",
].filter(x => x != "").join("\n");
// The name of a function is unique per library, so don't go through
// [mangleUnique].
var head = H.mkSignature(env, this.libraryMap, H.mangleName(name), inParams, outParams);
return env.indent + head + " {\n" + bodyText + "\n"+env.indent+"}";
}
private compileImageLiterals(e: EmitterEnv) {
if (!this.imageLiterals.length)
return "";
return e.indent + "namespace literals {\n" +
this.imageLiterals.map((s: string, n: number) => {
var x = 0;
var w = 0;
var h = 0;
var lit = "{ ";
for (var i = 0; i < s.length; ++i) {
switch (s[i]) {
case "0":
case "1":
lit += s[i]+", ";
x++;
break;
case " ":
break;
case "\n":
if (w == 0)
w = x;
else if (x != w)
// Sanity check
throw new Error("Malformed string literal");
x = 0;
h++;
break;
default:
throw new Error("Malformed string literal");
}
}
h++;
lit += "}";
var r = "bitmap"+n;
return e.indent + " const int "+r+"_w = "+w+";\n" +
e.indent + " const int "+r+"_h = "+h+";\n"+
e.indent + " const uint8_t "+r+"[] = "+lit+";\n";
}).join("\n") +
e.indent + "}\n\n";
}
private typeDecl(e: EmitterEnv, r: J.JRecord) {
// Comments on record definitions can't be set via the TouchDevelop UI.
// Instead, fire up the console, and do something like:
//
// TDev.Script.things.filter(function (x) {
// return x instanceof TDev.AST.RecordDef
// })[0].description = "{shim:}"
//
var s = H.isShim(r.comment);
if (s !== null)
return null;
var n = H.mangleName(r.name);
var fields = r.fields.map((f: J.JRecordField) => {
var t = H.mkType(e, this.libraryMap, f.type);
return e.indent + " " + t + " " + H.mangleName(f.name) + ";";
}).join("\n");
return [
e.indent + "struct " + n + "_ {",
fields,
e.indent + "};",
].join("\n");
}
private typeStub(e: EmitterEnv, r: J.JRecord) {
var s = H.isShim(r.comment);
if (s !== null)
return null;
var n = H.mangleName(r.name);
return [
e.indent + "struct " + n + "_;",
e.indent + "typedef ManagedType<" + n + "_> " + n + ";",
].join("\n");
}
private wrapNamespaceIf(s: string) {
if (this.libName != null)
return (s.length
? " namespace "+this.libName+" {\n"+
s +
"\n }"
: "");
else
return s;
}
private wrapNamespaceDecls(e: EmitterEnv, n: string, s: string[]) {
return (s.length
? e.indent + "namespace "+n+" {\n"+
s.join("\n") + "\n" +
e.indent + "}"
: "");
}
// This function runs over all declarations. After execution, the three
// member fields [prelude], [prototypes] and [code] are filled accordingly.
public visitApp(e: EmitterEnv, decls: J.JDecl[]) {
e = indent(e);
if (this.libName)
e = indent(e);
// Some parts of the emitter need to lookup library names by their id
decls.forEach((x: J.JDecl) => {
if (x.nodeType == "library") {
var l: J.JLibrary = <J.JLibrary> x;
this.libraryMap[l.id] = l.name;
}
});
// Compile type "stubs". Because there may be any kind of recursion
// between types, we first declare the structs, then the resulting
// ref-counted type (which the TouchDevelop type maps onto):
// struct Thing_;
// typedef ManagedType<Thing_> Thing;
var typeStubs = decls.map((f: J.JDecl) => {
var e1 = indent(e)
if (f.nodeType == "record")
return this.typeStub(e1, <J.JRecord> f);
else
return null;
}).filter(x => x != null);
var typeStubsCode = this.wrapNamespaceDecls(e, "user_types", typeStubs);
// Then, we can emit the definition of the structs (Thing_) because they
// refer to TouchDevelop types (Thing).
var typeDefs = decls.map((f: J.JDecl) => {
var e1 = indent(e)
if (f.nodeType == "record")
return this.typeDecl(e1, <J.JRecord> f);
else
return null;
}).filter(x => x != null);
var typeDefsCode = this.wrapNamespaceDecls(e, "user_types", typeDefs);
// Globals are in their own namespace (otherwise they would collide with
// "math", "number", etc.).
var globals = decls.map((f: J.JDecl) => {
var e1 = indent(e)
if (f.nodeType == "data")
return this.visit(e1, f);
else
return null;
}).filter(x => x != null);
var globalsCode = this.wrapNamespaceDecls(e, "globals", globals);
// We need forward declarations for all functions (they're,
// by default, mutually recursive in TouchDevelop).
var forwardDeclarations = decls.map((f: J.JDecl) => {
if (f.nodeType == "action" && H.willCompile(<J.JAction> f)) {
H.reserveName(e, f.name, f.id);
return e.indent + H.mkSignature(e, this.libraryMap, H.mangleName(f.name), (<J.JAction> f).inParameters, (<J.JAction> f).outParameters)+";";
} else {
return null;
}
}).filter(x => x != null);
// Compile all the top-level functions.
var userFunctions = decls.map((d: J.JDecl) => {
if (d.nodeType == "action" && H.willCompile(<J.JAction> d)) {
return this.visit(e, d);
} else if (d.nodeType == "art" && d.name == "prelude.cpp") {
this.prelude += (<J.JArt> d).value;
} else {
// The typical library has other stuff mixed in (pictures, other
// resources) that are used, say, when running the simulator. Just
// silently ignore these.
return null;
}
return null;
}).filter(x => x != null);
// By convention, because we're forced to return a string, write the
// output parameters in the member variables. Image literals are scoped
// within our namespace.
this.prototypes = this.wrapNamespaceIf(globalsCode + forwardDeclarations.join("\n"));
this.code = this.wrapNamespaceIf(this.compileImageLiterals(e) + userFunctions.join("\n"));
this.tPrototypes = this.wrapNamespaceIf(typeStubsCode);
this.tCode = this.wrapNamespaceIf(typeDefsCode);
// [embedded.ts] now reads the three member fields separately and
// ignores this return value.
return null;
}
}
}
}
// vim: set ts=2 sw=2 sts=2: