浏览代码

Redo JS reserved words mangling: mangle identifiers in code generation
instead of lexer. It also resolves strange diagnostic for variables like
'var' etc.
Fix issue #50: Module reader called with 'Math$' name, expected 'Math'.

Vladislav Folts 8 年之前
父节点
当前提交
b5c9786bf3

二进制
bin/compiled.zip


+ 2 - 2
src/eberon/EberonConstructor.ob

@@ -1,6 +1,6 @@
 MODULE EberonConstructor;
 IMPORT 
-    Cast, EberonCast, EberonRecord, Errors, Expression, 
+    Cast, CodeGenerator, EberonCast, EberonRecord, Errors, Expression, 
     LanguageContext, Procedure, Record, Stream, TypeId, Types;
 TYPE
     ConstructorCall = RECORD(Procedure.StdCall)
@@ -39,7 +39,7 @@ END;
 PROCEDURE BaseConstructorCall.make(args: ARRAY OF Expression.PType; cx: LanguageContext.PType): Expression.PType;
 BEGIN
     argCode <- checkArgs(SELF, args, cx);
-    code <- Record.constructor(cx.cx^, SELF.recordType^) + ".call(this, " + argCode + ");" + Stream.kCR;
+    code <- CodeGenerator.mangleId(Record.constructor(cx.cx^, SELF.recordType^)) + ".call(this, " + argCode + ");" + Stream.kCR;
     RETURN Expression.makeSimple(code, NIL);
 END;
 

+ 9 - 1
src/eberon/EberonContextDesignator.ob

@@ -32,6 +32,10 @@ TYPE
         var: PTypeNarrowVariable;
     END;
 
+    SelfVariable* = RECORD(Variable.Declared)
+        PROCEDURE SelfVariable(type: Types.PStorageType);
+    END;
+
     SelfAsPointer = RECORD(Types.Id)
     END;
 
@@ -205,7 +209,7 @@ PROCEDURE Type.handleLiteral(s: STRING);
 BEGIN
     IF s = "SELF" THEN
         type <- SELF.handleMessage(getMethodSelfMsg)(Types.PStorageType);
-        info <- NEW Variable.Declared("this", type, NIL);
+        info <- NEW SelfVariable(type);
         ContextDesignator.advance(SELF, type, info, "this", FALSE);
     ELSIF s = "POINTER" THEN
         type <- SELF.handleMessage(getSelfAsPointerMsg)(Types.PStorageType);
@@ -317,6 +321,10 @@ PROCEDURE SelfAsPointer.idType(): STRING;
     RETURN "SELF(POINTER)";
 END;
 
+PROCEDURE SelfVariable.SelfVariable(type: Types.PStorageType)
+    | SUPER("SELF", type, NIL);
+END;
+
 PROCEDURE ExpressionProcedureCall.ExpressionProcedureCall(parent: ContextHierarchy.PNode)
     | SUPER(parent);
 BEGIN

+ 1 - 1
src/eberon/EberonContextInPlace.ob

@@ -27,7 +27,7 @@ END;
 
 PROCEDURE VariableInit.handleLiteral(s: STRING);
 BEGIN
-    SELF.code := "var " + SELF.id + " = ";
+    SELF.code := "var " + CodeGenerator.mangleId(SELF.id) + " = ";
 END;
 
 PROCEDURE VariableInit.handleExpression(e: Expression.PType);

+ 4 - 3
src/eberon/EberonContextProcedure.ob

@@ -92,7 +92,7 @@ BEGIN
     
     RETURN NEW EberonContextDesignator.SuperMethodInfo(
         procId,
-        d.qualifyScope(baseType.scope) + baseType.description() + ".prototype." + id + ".call");
+        CodeGenerator.mangleId(d.qualifyScope(baseType.scope) + baseType.description()) + ".prototype." + id + ".call");
 END;
 
 PROCEDURE handleFieldInit(d: PProcOrMethodDeclaration; id: STRING): Procedure.PCallGenerator;
@@ -162,10 +162,11 @@ VAR
     result: STRING;
 BEGIN
     IF SELF.boundType # NIL THEN
+        boundTypeCode <- CodeGenerator.mangleId(SELF.boundType.name);
         IF SELF.isConstructor THEN
-            result := "function " + SELF.boundType.name + "(";
+            result := "function " + boundTypeCode + "(";
         ELSE
-            result := SELF.boundType.name + ".prototype." + SELF.methodId.id() + " = function(";
+            result :=  boundTypeCode + ".prototype." + SELF.methodId.id() + " = function(";
         END;
     ELSE
         result := SUPER();

+ 9 - 12
src/eberon/EberonLanguageContext.ob

@@ -1,22 +1,19 @@
 MODULE EberonLanguageContext;
 IMPORT 
-	EberonMap, EberonTypePromotion, Expression, LanguageContext, Types;
+	EberonContextDesignator, EberonMap, EberonTypePromotion, Expression, LanguageContext, Types;
 TYPE
 	CodeTraits* = RECORD(LanguageContext.CodeTraits)
 	END;
 
 PROCEDURE CodeTraits.referenceCode(VAR info: Types.Id): STRING;
-VAR
-    result: STRING;
-BEGIN
-	IF info IS EberonTypePromotion.Variable THEN
-		result := info.id();
-	ELSIF (info IS EberonMap.ElementVariable) & ~info.elementType.isScalar() THEN
-		result := info.rval;
-	ELSE
-		result := SUPER(info);
-	END;
-    RETURN result;
+	RETURN 
+		info IS EberonContextDesignator.SelfVariable 
+				? "this"
+		: info IS EberonTypePromotion.Variable 			
+				? info.id()
+		: ((info IS EberonMap.ElementVariable) & ~info.elementType.isScalar()) 
+				? info.rval
+		: SUPER(info);
 END;
 
 PROCEDURE CodeTraits.assign(VAR info: Types.Id; right: Expression.PType): STRING;

+ 2 - 1
src/nodejs.js

@@ -2,6 +2,7 @@
 
 var Class = require("rtl.js").Class;
 var Code = require("js/Code.js");
+var CodeGenerator = require("js/CodeGenerator.js");
 var Errors = require("js/Errors.js");
 var ContextHierarchy = require("js/ContextHierarchy.js");
 var LanguageContext = require("js/LanguageContext.js");
@@ -24,7 +25,7 @@ var ModuleGenerator = Class.extend({
             var alias = modules[name];
             var importName = this.__importDir ? this.__importDir + "/" + name
                                               : name;
-            result += "var " + alias + " = " + (name == "this" 
+            result += "var " + CodeGenerator.mangleId(alias) + " = " + (name == "JS" 
                 ? "GLOBAL"
                 : "require(\"" + importName + ".js\")") + ";\n";
         }

+ 8 - 16
src/ob/Code.ob

@@ -66,17 +66,11 @@ BEGIN
 END;
 
 PROCEDURE genExport*(s: Symbols.Symbol): STRING;
-VAR
-    result: STRING;
 BEGIN
-    IF s.isVariable() THEN
-        result := "function(){return " + s.id() + ";}";
-    ELSIF ~s.isType() THEN
-        result := s.id();
-    ELSE
-        result := typeShouldBeExported(s.info()(TypeId.PType), s.id())
-    END;
-    RETURN result
+    codeId <- CodeGenerator.mangleId(s.id());
+    RETURN s.isVariable() ? "function(){return " + codeId + ";}"
+        : ~s.isType()     ? codeId
+                          : typeShouldBeExported(s.info()(TypeId.PType), codeId);
 END;
 
 PROCEDURE genCommaList(m: StringsMap; import: BOOLEAN): STRING;
@@ -87,17 +81,15 @@ BEGIN
         IF LEN(result) # 0 THEN
             result := result + ", ";
         END;
-        IF import THEN
-            result := result + alias;
-        ELSE
-            result := result + name;
-        END;
+        result := result + ((~import & (name = "JS")) 
+                                ? "this"
+                                : CodeGenerator.mangleId(import ? alias : name));
     END;
     RETURN result;
 END;
 
 PROCEDURE ModuleGenerator.prolog(): STRING;
-    RETURN "var " + SELF.name + " = function (" + genCommaList(SELF.imports, TRUE) + "){" + Stream.kCR
+    RETURN "var " + CodeGenerator.mangleId(SELF.name) + " = function (" + genCommaList(SELF.imports, TRUE) + "){" + Stream.kCR
 END;
 
 PROCEDURE exportId*(s: Symbols.Symbol): STRING;

+ 14 - 0
src/ob/CodeGenerator.ob

@@ -3,6 +3,16 @@ IMPORT
     Stream, String;
 CONST
     kTab* = 09X;
+
+    jsReservedWords = [
+        "break", "case", "catch", "const", "continue", "debugger", "default", "delete", "do", "else", "finally",
+        "for", "function", "if", "in", "instanceof", "new", "return", "switch", "this", "throw", "try", "typeof",
+        "var", "void", "while", "with", "false", "true", "null", "class", "enum", "export", "extends",
+        "import", "super", "implements", "interface", "let", "package", "private", "protected",
+        "public", "static", "yield",
+        "Object", "Math", "Number" (* Object, Math and Number are used in generated code for some functions so it is 
+                                      reserved word from code generator standpoint *)
+        ];
 TYPE
     Insertion* = RECORD
         PROCEDURE Insertion(index: INTEGER);
@@ -178,6 +188,10 @@ BEGIN
     SELF.indents.add(Indent(0));
 END;
 
+PROCEDURE mangleId*(id: STRING): STRING;
+    RETURN jsReservedWords.indexOf(id) # -1 ? id + "$" : id;
+END;
+
 BEGIN
     NEW(nullGenerator);
 END CodeGenerator.

+ 3 - 3
src/ob/ContextIdentdef.ob

@@ -1,6 +1,6 @@
 MODULE ContextIdentdef;
 IMPORT
-    Context, ContextDesignator, ContextHierarchy, ContextType, 
+    Context, CodeGenerator, ContextDesignator, ContextHierarchy, ContextType, 
     Module, Record, TypeId;
 TYPE
     Type* = RECORD(ContextHierarchy.Node)
@@ -64,7 +64,7 @@ END;
 PROCEDURE Qualified.handleModule(id: STRING; module: Module.PType);
 BEGIN
     SELF.module := module;
-    SELF.code := id + ".";
+    SELF.code := CodeGenerator.mangleId(id) + ".";
 END;
 
 PROCEDURE Qualified.endParse(): BOOLEAN;
@@ -72,7 +72,7 @@ VAR
     code: STRING;
 BEGIN
     IF LEN(SELF.code) = 0 THEN
-        code := SELF.id;
+        code := CodeGenerator.mangleId(SELF.id);
     ELSE
         code := SELF.code + Record.mangleJSProperty(SELF.id);
     END;

+ 3 - 6
src/ob/ContextModule.ob

@@ -1,6 +1,6 @@
 MODULE ContextModule;
 IMPORT
-    ContextHierarchy, ContextType, Errors, LanguageContext, 
+    CodeGenerator, ContextHierarchy, ContextType, Errors, LanguageContext, 
     Object, Scope, ScopeBase, String, Symbols, Types;
 TYPE
     Declaration* = RECORD(ContextHierarchy.Node)
@@ -77,11 +77,8 @@ BEGIN
            should not be used in code generation, 
            just return non-empty value to indicate this is not current module
         *)
-        IF ~(id IN SELF.imports) THEN
-            result := "module '" + id + "' is not imported";
-        ELSE
-            result := SELF.imports[id].id() + ".";
-        END;
+        result := ~(id IN SELF.imports) ? "module '" + id + "' is not imported"
+                                        : CodeGenerator.mangleId(SELF.imports[id].id()) + ".";
     END;
     RETURN result;
 END;

+ 2 - 2
src/ob/ContextProcedure.ob

@@ -85,7 +85,7 @@ BEGIN
 END;
 
 PROCEDURE Declaration.doProlog(): STRING;
-    RETURN Chars.ln + "function " + SELF.id.id() + "(";
+    RETURN Chars.ln + "function " + CodeGenerator.mangleId(SELF.id.id()) + "(";
 END;
 
 PROCEDURE Declaration.doEpilog(): STRING;
@@ -128,7 +128,7 @@ BEGIN
     ELSE
         declaration.multipleArguments := TRUE;
     END;
-    code.write(name + "/*" + arg.description() + "*/");
+    code.write(CodeGenerator.mangleId(name) + "/*" + arg.description() + "*/");
 END;
 
 PROCEDURE Declaration.doMakeArgumentVariable(arg: Types.ProcedureArgument; name: STRING): Types.PVariable;

+ 6 - 2
src/ob/ContextType.ob

@@ -364,7 +364,7 @@ END;
 PROCEDURE Record.doGenerateConstructor(): STRING;
 BEGIN
     gen <- NEW CodeGenerator.Generator();
-    gen.write("function " + SELF.cons + "()");
+    gen.write("function " + CodeGenerator.mangleId(SELF.cons) + "()");
     gen.openScope();
     gen.write(SELF.doGenerateBaseConstructorCallCode() 
             + generateFieldsInitializationCode(SELF));
@@ -385,7 +385,11 @@ BEGIN
         result := SELF.cons + ".prototype.$scope = " + scope + ";" + Chars.ln;
     ELSE
         qualifiedBase <- SELF.qualifyScope(base.scope) + base.name; 
-        result := SELF.root().language().rtl.extend(SELF.cons, qualifiedBase, scope) + ";" + Chars.ln;
+        result := SELF.root().language().rtl.extend(
+                    CodeGenerator.mangleId(SELF.cons), 
+                    CodeGenerator.mangleId(qualifiedBase), 
+                    scope) 
+                + ";" + Chars.ln;
     END;
     RETURN result;
 END;

+ 2 - 2
src/ob/ContextVar.ob

@@ -1,6 +1,6 @@
 MODULE ContextVar;
 IMPORT
-    Chars, Context, ContextHierarchy, ContextType, Errors, 
+    Chars, CodeGenerator, Context, ContextHierarchy, ContextType, Errors, 
     Object, Symbols, Types, Variable;
 TYPE
     Declaration* = RECORD(ContextType.DeclarationAndIdentHandle)
@@ -58,7 +58,7 @@ BEGIN
         scope <- SELF.root().currentScope();
         v <- NEW Variable.Declared(varName, SELF.type, scope);
         scope.addSymbol(NEW Symbols.Symbol(varName, v), id.exported());
-        gen.write("var " + varName + " = " + SELF.doInitCode() + ";");
+        gen.write("var " + CodeGenerator.mangleId(varName) + " = " + SELF.doInitCode() + ";");
     END;
 
     gen.write(Chars.ln);

+ 4 - 3
src/ob/LanguageContext.ob

@@ -112,7 +112,7 @@ VAR
     result: STRING;
 BEGIN
     IF info IS T.DeclaredVariable THEN
-        result := info.id();
+        result := CodeGenerator.mangleId(info.id());
         IF info.type().isScalar() & ~((info IS Variable.ArgumentVariable) & info.var) THEN
             result := "{set: function($v){" + result + " = $v;}, get: function(){return " + result + ";}}";
         END
@@ -144,10 +144,11 @@ VAR
 BEGIN
     rightCode <- Expression.deref(right).code();
     IF info IS T.DeclaredVariable THEN
+        idCode <- CodeGenerator.mangleId(info.id());
         IF (info IS Variable.ArgumentVariable) & info.var THEN
-            result := info.id() + ".set(" + rightCode + ")";
+            result := idCode + ".set(" + rightCode + ")";
         ELSE
-            result := info.id() + " = " + rightCode;
+            result := idCode + " = " + rightCode;
         END;
     ELSIF info IS Variable.PropertyVariable THEN
         result := SELF.putAt(info.leadCode, info.propCode, rightCode);

+ 2 - 12
src/ob/Lexer.ob

@@ -6,16 +6,6 @@ CONST
     commentBegin = "(*";
     commentEnd = "*)";
 
-    jsReservedWords = [
-        "break", "case", "catch", "const", "continue", "debugger", "default", "delete", "do", "else", "finally",
-        "for", "function", "if", "in", "instanceof", "new", "return", "switch", "this", "throw", "try", "typeof",
-        "var", "void", "while", "with", "false", "true", "null", "class", "enum", "export", "extends",
-        "import", "super", "implements", "interface", "let", "package", "private", "protected",
-        "public", "static", "yield",
-        "Object", "Math", "Number" (* Object, Math and Number are used in generated code for some functions so it is 
-                                      reserved word from code generator standpoint *)
-        ];
-
 TYPE
     Literal* = RECORD
         PROCEDURE Literal*(s: STRING);
@@ -272,9 +262,9 @@ BEGIN
             END;
 
             IF reservedWords.indexOf(s) = -1 THEN
-                IF jsReservedWords.indexOf(s) # -1 THEN
+                (*IF jsReservedWords.indexOf(s) # -1 THEN
                     s := s + "$";
-                END;
+                END;*)
                 context.handleIdent(s);
                 result := TRUE;
             END

+ 3 - 3
src/ob/Module.ob

@@ -169,7 +169,7 @@ BEGIN
 END makeDoProcSymbol;
 
 PROCEDURE makeJS*(): PType;
-    RETURN NEW JS("this");
+    RETURN NEW JS("JS");
 END;
 
 PROCEDURE AnyType.AnyType()
@@ -188,8 +188,8 @@ BEGIN
 END;
 
 BEGIN
-    doProcId := "do$";
-    varTypeId := "var$";
+    doProcId := "do";
+    varTypeId := "var";
     
     NEW(any);
     NEW(anyVar);

+ 0 - 0
src/ob/Object$.ob → src/ob/Object.ob


+ 34 - 0
test/expected/eberon/js_keyword.js

@@ -0,0 +1,34 @@
+<rtl code>
+var import$ = function (){
+function Math$(){
+}
+Math$.prototype.do = function(){
+};
+Math$.prototype.catch = function(){
+};
+return {
+	Math: Math$
+}
+}();
+var m = function (import$){
+RTL$.extend(Object$, import$.Math);
+RTL$.extend(Number$, Object$);
+function Object$(var$/*INTEGER*/){
+	import$.Math.call(this);
+	this.var = var$;
+}
+Object$.prototype.catch = function(){
+	import$.Math.prototype.catch.call(this);
+};
+Object$.prototype.throw = function(){
+};
+function Number$(){
+	Object$.call(this, 123);
+}
+Number$.prototype.throw = function(){
+	Object$.prototype.throw.call(this);
+};
+Number$.prototype.do = function(){
+	Object$.prototype.do.call(this);
+};
+}(import$);

+ 28 - 5
test/expected/js_keyword.js

@@ -1,10 +1,33 @@
-var do$ = function (){
-var break$ = 0;var case$ = 0;var catch$ = 0;var continue$ = 0;var debugger$ = 0;var default$ = 0;var delete$ = 0;var else$ = 0;var class$ = 0;var const$ = 0;var enum$ = 0;var export$ = 0;var extends$ = 0;var import$ = 0;var super$ = 0;var true$ = 0;var false$ = 0;var null$ = 0;var implements$ = 0;var interface$ = 0;var let$ = 0;var package$ = 0;var private$ = 0;var protected$ = 0;var public$ = 0;var static$ = 0;var yield$ = 0;var finally$ = 0;var for$ = 0;var if$ = 0;var in$ = 0;var instanceof$ = 0;var new$ = 0;var return$ = 0;var switch$ = 0;var this$ = 0;var try$ = 0;var typeof$ = 0;var var$ = 0;var void$ = 0;var while$ = 0;var with$ = 0;var Object$ = 0;var Math$ = 0;var Number$ = 0;
+<rtl code>
+var Math$ = function (){
+function Object$(){
+}
+var var$ = 0;
 
-function function$(){
+function throw$(){
+}
+return {
+	Object: Object$,
+	var: function(){return var$;},
+	throw: throw$
+}
+}();
+var do$ = function (Math$){
+function D(){
+	Math$.Object.call(this);
+}
+RTL$.extend(D, Math$.Object);
+var break$ = 0;var case$ = 0;var catch$ = 0;var continue$ = 0;var debugger$ = 0;var default$ = 0;var delete$ = 0;var else$ = 0;var class$ = 0;var const$ = 0;var enum$ = 0;var export$ = 0;var extends$ = 0;var import$ = 0;var super$ = 0;var true$ = 0;var false$ = 0;var null$ = 0;var implements$ = 0;var interface$ = 0;var let$ = 0;var package$ = 0;var private$ = 0;var protected$ = 0;var public$ = 0;var static$ = 0;var yield$ = 0;var finally$ = 0;var for$ = 0;var if$ = 0;var in$ = 0;var instanceof$ = 0;var new$ = 0;var return$ = 0;var switch$ = 0;var this$ = 0;var try$ = 0;var typeof$ = 0;var var$ = 0;var void$ = 0;var while$ = 0;var with$ = 0;var Number$ = 0;var Object$ = 0;
+var r = new Math$.Object();
+
+function function$(else$/*VAR INTEGER*/){
 	var i = null;
+	var Math$ = 0;
 	i = function$;
 	case$ = 123;
+	else$.set(456);
 }
-function$();
-}();
+function$({set: function($v){var$ = $v;}, get: function(){return var$;}});
+Math$.throw();
+var$ = Math$.var();
+}(Math$);

+ 57 - 0
test/input/eberon/js_keyword.ob

@@ -0,0 +1,57 @@
+MODULE import;
+TYPE
+	Math* = RECORD
+		PROCEDURE do*();
+		PROCEDURE catch*();
+	END;
+
+PROCEDURE Math.do();
+END;
+
+PROCEDURE Math.catch();
+END;
+
+END import.
+
+MODULE m;
+IMPORT import;
+
+TYPE 
+	Object = RECORD(import.Math)
+		PROCEDURE Object(var: INTEGER);
+		PROCEDURE throw();
+
+		var: INTEGER;
+	END;
+
+	Number = RECORD(Object)
+		PROCEDURE Number();
+	END;
+
+PROCEDURE Object.Object(var: INTEGER)
+	| var(var);
+END;
+
+PROCEDURE Object.catch();
+BEGIN
+	SUPER();
+END;
+
+PROCEDURE Object.throw();
+END;
+
+PROCEDURE Number.Number()
+	| SUPER(123);
+END;
+
+PROCEDURE Number.throw();
+BEGIN
+	SUPER();
+END;
+
+PROCEDURE Number.do();
+BEGIN
+	SUPER();
+END;
+
+END m.

+ 28 - 5
test/input/js_keyword.ob

@@ -1,20 +1,43 @@
+MODULE Math;
+
+TYPE
+    Object* = RECORD END;
+
+VAR var*: INTEGER;
+
+PROCEDURE throw*();
+END throw;
+
+END Math.
+
 MODULE do;
-TYPE throw = PROCEDURE;
+IMPORT Math;
+TYPE
+    throw = PROCEDURE(VAR catch: INTEGER);
+    D = RECORD(Math.Object) END;
 VAR
     break, case, catch, continue, debugger, default, delete, else,
     class, const, enum, export, extends, import, super,
     true, false, null,
     implements, interface, let, package, private, protected, public, static, yield,
     finally, for, if, in, instanceof, new, return, switch, this, 
-    try, typeof, var, void, while, with, Object, Math, Number
+    try, typeof, var, void, while, with, Number, Object
     : INTEGER;
-PROCEDURE function();
-VAR i: throw;
+
+    r: Math.Object;
+
+PROCEDURE function(VAR else: INTEGER);
+VAR 
+    i: throw;
+    Math: INTEGER;
 BEGIN
 	i := function;
 	case := 123;
+    else := 456;
 END function;
 
 BEGIN
-	function();
+	function(var);
+    Math.throw();
+    var := Math.var;
 END do.

+ 11 - 0
test/test_unit.js

@@ -1109,6 +1109,17 @@ return {
          ["ASSERT(TRUE, 123)", "1 argument(s) expected, got 2"],
          ["ASSERT(123)", "type mismatch for argument 1: 'INTEGER' cannot be converted to 'BOOLEAN'"])
     ),
+"import module with reserved name": testWithContext(
+    { grammar: grammar.module,
+      source: "",
+      moduleReader: function(name){
+        TestUnitCommon.expectEq(name, "Math"); 
+        return "MODULE " + name + "; END " + name + "."; 
+        }
+    },
+    pass("MODULE m; IMPORT Math; END m."),
+    fail()
+    ),
 "imported module without exports": testWithModule(
     "MODULE test; END test.",
     pass("MODULE m; IMPORT test; END m."),

+ 33 - 20
test/test_unit_common.js

@@ -30,7 +30,7 @@ var TestModuleGenerator = Class.extend({
 });
 
 var TestContextRoot = Class.extend.call(ContextHierarchy.Root, {
-    init: function TestContextRoot(language){
+    init: function TestContextRoot(language, moduleResolver){
         var rtl = new makeRTL(language.rtl);
         ContextHierarchy.Root.call(
                 this,
@@ -38,7 +38,8 @@ var TestContextRoot = Class.extend.call(ContextHierarchy.Root, {
                   moduleGenerator: function(){return new TestModuleGenerator();},
                   rtl: rtl,
                   types: language.types,
-                  stdSymbols: language.stdSymbols
+                  stdSymbols: language.stdSymbols,
+                  moduleResolver: moduleResolver
                 });
         this.pushScope(new Scope.Module("test", language.stdSymbols));
     },
@@ -51,13 +52,13 @@ var TestContextRoot = Class.extend.call(ContextHierarchy.Root, {
 });
 
 var TestContext = Class.extend.call(ContextExpression.ExpressionHandler, {
-    init: function TestContext(language){
-        ContextExpression.ExpressionHandler.call(this, new TestContextRoot(language));
+    init: function TestContext(language, moduleResolver){
+        ContextExpression.ExpressionHandler.call(this, new TestContextRoot(language, moduleResolver));
     },
     handleExpression: function(){}
 });
 
-function makeContext(language){return new TestContext(language);}
+function makeContext(language, moduleResolver){return new TestContext(language, moduleResolver);}
 
 function testWithSetup(setup, pass, fail){
     return function(){
@@ -135,9 +136,20 @@ function setupParser(parser, language, contextFactory){
     return setup(parseImpl);
 }
 
-function setupWithContext(grammar, contextGrammar, language, source){
+function compileModule(src, language){
+    var imported = oc.compileModule(language.grammar, new Stream.Type(src), makeContext(language));
+    return imported.symbol().info();
+}
+
+function makeModuleResolver(moduleReader, language){
+    return moduleReader ? function(name){ return compileModule(moduleReader(name), language); }
+                        : undefined;
+}
+
+function setupWithContext(fixture, contextGrammar, language){
     function innerMakeContext(){
-        var context = makeContext(language);
+        var context = makeContext(language, makeModuleResolver(fixture.moduleReader, language));
+        var source = fixture.source;
         try {
             parseInContext(contextGrammar, source, context);
         }
@@ -149,12 +161,12 @@ function setupWithContext(grammar, contextGrammar, language, source){
         return context;
     }
 
-    return setupParser(grammar, language, innerMakeContext);
+    return setupParser(fixture.grammar, language, innerMakeContext);
 }
 
-function testWithContext(context, contextGrammar, language, pass, fail){
+function testWithContext(fixture, contextGrammar, language, pass, fail){
     return testWithSetup(
-        function(){return setupWithContext(context.grammar, contextGrammar, language, context.source);},
+        setupWithContext.bind(undefined, fixture, contextGrammar, language),
         pass,
         fail);
 }
@@ -166,23 +178,15 @@ function testWithGrammar(parser, language, pass, fail){
         fail);
 }
 
-var TestContextWithModule = TestContext.extend({
-    init: function(module, language){
-        TestContext.prototype.init.call(this, language);
-        this.root().findModule = function(){return module;};
-    }
-});
-
 function testWithModule(src, language, pass, fail){
     var grammar = language.grammar;
     return testWithSetup(
         function(){
-            var imported = oc.compileModule(grammar, new Stream.Type(src), makeContext(language));
-            var module = imported.symbol().info();
+            var module = compileModule(src, language);
             return setup(function(s){
                 oc.compileModule(grammar,
                                  new Stream.Type(s),
-                                 new TestContextWithModule(module, language));
+                                 new TestContext(language, function(){return module;}));
             });},
         pass,
         fail);
@@ -204,7 +208,16 @@ function assert(cond){
     }
 }
 
+function expectEq(x1, x2){
+    if (x1 == x2)
+        return;
+
+    throw new TestError("'" + x1 + "' != '" + x2 + "'");
+    }
+
 exports.assert = assert;
+exports.expectEq = expectEq;
+
 exports.context = context;
 exports.pass = pass;
 exports.fail = fail;