Kaynağa Gözat

support read-onle export for excord fields using '-' mark

Vladislav Folts 11 yıl önce
ebeveyn
işleme
56b16a615e

BIN
bin/compiled.zip


+ 32 - 0
doc/wiki/eberon-record-fields-read-only-export.md

@@ -0,0 +1,32 @@
+Read-only export is introduced for record fields using '-' mark - in the same way as it is done in Component Pascal.
+The major reason for this extension is to maintain data integrity without obligation to write down corresponfing procedures-accessors. I.e. instead of hiding (not exporting) record field and giving procedure-accessor (exported) for reading field value it is possble to just mark a field as exported for reading only.
+
+### Syntax
+    identdef = ident ["*" | "-"].
+
+Example:
+
+    MODULE m1;
+    TYPE 
+        T* = RECORD
+            i-: INTEGER
+        END;
+        TP* = POINTER TO T;
+
+        PROCEDURE make*(): TP;
+        VAR result: TP;
+        BEGIN
+            NEW(result);
+            result.i := 123; (* field 'T.i' can be modified in this module *)
+            RETURN result
+        END make;
+    END m1.    
+
+    MODULE m2;
+    IMPORT m1;
+    VAR p: m1.TP; 
+    BEGIN
+        p := m1.make();
+        ASSERT(p.i = 123); (* field 'T.i' can be accessed in this module *) 
+        p.i := 321; (* compiller error, 'T.i' cannot be modified in this module *)
+    END m2.        

+ 1 - 0
doc/wiki/eberon.md

@@ -5,5 +5,6 @@ Eberon extends original Oberon so any valid oberon program is also a valid ebero
 ### Extensions
 * [Methods](/vladfolts/oberonjs/wiki/eberon-methods)
 * [Strings](/vladfolts/oberonjs/wiki/eberon-strings)
+* [Record fields read-only export](/vladfolts/oberonjs/wiki/eberon-record-fields-read-only-export)
 * [Procedure call result can be denoted](/vladfolts/oberonjs/wiki/eberon-procedure-call-result)
 * Non-scalar variables (arrays and records) can be exported (forbidden in oberon for some unknown reason).

+ 34 - 16
src/context.js

@@ -246,13 +246,16 @@ var IdentdefInfo = Class.extend({
 exports.Identdef = ChainedContext.extend({
     init: function IdentdefContext(context){
         ChainedContext.prototype.init.call(this, context);
-        this.__id = undefined;
-        this.__export = false;
+        this._id = undefined;
+        this._export = false;
     },
-    handleIdent: function(id){this.__id = id;},
-    handleLiteral: function(){this.__export = true;},
+    handleIdent: function(id){this._id = id;},
+    handleLiteral: function(){this._export = true;},
     endParse: function(){
-        this.parent().handleIdentdef(new IdentdefInfo(this.__id, this.__export));
+        this.parent().handleIdentdef(this._makeIdendef());
+    },
+    _makeIdendef: function(){
+        return new IdentdefInfo(this._id, this._export);
     }
 });
 
@@ -299,16 +302,16 @@ exports.Designator = ChainedContext.extend({
             isReadOnly = false;
         }
         else if (!(t instanceof Type.Record
-                || t instanceof Type.Module
                 || t instanceof Module.AnyType))
             throw new Errors.Error("cannot designate '" + t.description() + "'");
 
-        this.__denote(id, pointerType);
-        this.__info = this._makeDenoteVar(this.__currentType, isReadOnly);
+        var field = this.__denote(id, pointerType);
+        this.__info = this._makeDenoteVar(field, isReadOnly);
+        this.__currentType = field.type();
         this.__scope = undefined;
     },
-    _makeDenoteVar: function(type, isReadOnly){
-        return Type.makeVariable(type, isReadOnly);
+    _makeDenoteVar: function(field, isReadOnly){
+        return Type.makeVariable(field.type(), isReadOnly);
     },
     handleExpression: function(e){this.__indexExpression = e;},
     __handleIndexExpression: function(){
@@ -405,8 +408,8 @@ exports.Designator = ChainedContext.extend({
     },
     __denote: function(id, pointerType){
         var t = this.__currentType;
-        var fieldType = t.findSymbol(id);
-        if (!fieldType){
+        var field = t.findSymbol(id);
+        if (!field){
             var typeDesc = !Type.typeName(t) && pointerType && Type.typeName(pointerType)
                 ? Type.typeName(pointerType)
                 : t.description();
@@ -415,7 +418,7 @@ exports.Designator = ChainedContext.extend({
         this.__derefCode = this.__code;
         this.__propCode = "\"" + id + "\"";
         this.__code += "." + id;
-        this.__currentType = fieldType;
+        return field;
     },
     endParse: function(){
         var code = this.__code;
@@ -1669,7 +1672,7 @@ function isTypeRecursive(type, base){
             return true;
         var fields = Type.recordOwnFields(type);
         for(var fieldName in fields){
-            if (isTypeRecursive(fields[fieldName], base))
+            if (isTypeRecursive(fields[fieldName].type(), base))
                 return true;
         }
     }
@@ -1678,6 +1681,19 @@ function isTypeRecursive(type, base){
     return false;
 }
 
+var RecordField = Class.extend({
+    init: function Context$RecordField(identdef, type, recordType){
+        this.__identdef = identdef;
+        this.__type = type;
+        this.__refcordType = recordType;
+    },
+    id: function(){return this.__identdef.id();},
+    exported: function(){return this.__identdef.exported();},
+    identdef: function(){return this.__identdef;},
+    type: function(){return this.__type;},
+    recordType: function(){return this.__refcordType;}
+});
+
 exports.RecordDecl = ChainedContext.extend({
     init: function RecordDeclContext(context, makeRecord){
         ChainedContext.prototype.init.call(this, context);
@@ -1693,7 +1709,7 @@ exports.RecordDecl = ChainedContext.extend({
         if (isTypeRecursive(type, this.__type))
             throw new Errors.Error("recursive field definition: '"
                 + field.id() + "'");
-        this.__type.addField(field, type);
+        this.__type.addField(new RecordField(field, type, this.__type));
         if (field.exported())
             this.parent().exportField(field.id());
     },
@@ -1723,7 +1739,7 @@ exports.RecordDecl = ChainedContext.extend({
             gen.write(qualifiedBase + ".prototype.init.call(this);\n");
         var ownFields = Type.recordOwnFields(type);
         for(var f in ownFields)
-            gen.write("this." + f + " = " + ownFields[f].initializer(this) + ";\n");
+            gen.write("this." + f + " = " + ownFields[f].type().initializer(this) + ";\n");
 
         gen.closeScope("");
         gen.closeScope(");\n");
@@ -1962,7 +1978,9 @@ exports.endCallMsg = endCallMsg;
 exports.Chained = ChainedContext;
 exports.endParametersMsg = endParametersMsg;
 exports.getSymbolAndScope = getSymbolAndScope;
+exports.IdentdefInfo = IdentdefInfo;
 exports.makeProcCall = makeProcCall;
 exports.unwrapType = unwrapType;
 exports.IdentdefInfo = IdentdefInfo;
+exports.RecordField = RecordField;
 exports.RelationOps = RelationOps;

+ 75 - 10
src/eberon/eberon_context.js

@@ -65,6 +65,7 @@ var ProcOrMethodId = Context.Chained.extend({
     handleIdentdef: function(id){
         if (this.__type && id.exported())
             throw new Errors.Error("method implementation cannot be exported: " + id.id());
+        checkOrdinaryExport(id, "procedure");
         this.handleMessage(new MethodOrProcMsg(id, this.__type));
     }
 });
@@ -105,6 +106,29 @@ var ResultVariable = Type.Variable.extend({
     idType: function(){return "procedure call " + (this.type() ? "result" : "statement");}
 });
 
+var IdentdefInfo = Context.IdentdefInfo.extend({
+    init: function(id, exported, ro){
+        Context.IdentdefInfo.prototype.init.call(this, id, exported);
+        this.__ro = ro;
+    },
+    isReadOnly: function(){return this.__ro;}
+});
+
+var Identdef = Context.Identdef.extend({
+    init: function(parent){
+        Context.Identdef.prototype.init.call(this, parent);
+        this.__ro = false;
+    },
+    handleLiteral: function(l){
+        if (l == "-")
+            this.__ro = true;  
+        Context.Identdef.prototype.handleLiteral.call(this, l);
+    },
+    _makeIdendef: function(){
+        return new IdentdefInfo(this._id, this._export, this.__ro);
+    }
+});
+
 var Designator = Context.Designator.extend({
     init: function EberonContext$Designator(parent){
         Context.Designator.prototype.init.call(this, parent);
@@ -120,10 +144,13 @@ var Designator = Context.Designator.extend({
         }
         return Context.Designator.prototype._indexSequence.call(this, type, info);
     },
-    _makeDenoteVar: function(type, isReadOnly){
-        return (type instanceof MethodType)
-            ? new MethodVariable(type)
-            : Context.Designator.prototype._makeDenoteVar(type, isReadOnly);
+    _makeDenoteVar: function(field, isReadOnly){
+        var type = field.type();
+        if (type instanceof MethodType)
+            return new MethodVariable(type);
+        if (!isReadOnly && this.qualifyScope(Type.recordScope(field.recordType())))
+            isReadOnly = field.identdef().isReadOnly();
+        return Context.Designator.prototype._makeDenoteVar(field, isReadOnly);
     },
     handleMessage: function(msg){
         if (msg == Context.beginCallMsg)
@@ -244,12 +271,12 @@ var RecordType = Type.Record.extend({
         var existingField = this.findSymbol(id);
         if (existingField)
             throw new Errors.Error(
-                  existingField instanceof MethodType
+                  existingField.type() instanceof MethodType
                 ?   "cannot declare a new method '" + id 
                   + "': method already was declared"
                 : "cannot declare method, record already has field '" + id + "'");
 
-        this.__declaredMethods[id] = type;
+        this.__declaredMethods[id] = new Context.RecordField(methodId, type);
 
         if (!methodId.exported())
             this.__nonExportedMethods.push(id);
@@ -257,14 +284,13 @@ var RecordType = Type.Record.extend({
     defineMethod: function(methodId, type){
         var base = Type.recordBase(this);
         var id = methodId.id();
-        var existing = this.findSymbol(id);
-        if (!(existing instanceof MethodType)){
+        var existingField = this.findSymbol(id);
+        if (!existingField || !(existingField.type() instanceof MethodType)){
             throw new Errors.Error(
                   "'" + Type.typeName(this) + "' has no declaration for method '" + id 
                 + "'");
         }
-        //if (this.__definedMethods.indexOf(id) != -1)
-        //    throw new Error.Error("method definition duplicate");
+        var existing = existingField.type();
         if (!Cast.areProceduresMatch(existing, type))
             throw new Errors.Error(
                   "overridden method '" + id + "' signature mismatch: should be '"
@@ -367,6 +393,41 @@ var RecordType = Type.Record.extend({
     }
 });
 
+function checkOrdinaryExport(id, hint){
+    if (id.isReadOnly())
+        throw new Errors.Error(hint + " cannot be exported as read-only using '-' mark (did you mean '*'?)");
+}
+
+var ConstDecl = Context.ConstDecl.extend({
+    init: function EberonContext$ConstDecl(context){
+        Context.ConstDecl.prototype.init.call(this, context);
+    },
+    handleIdentdef: function(id){
+        checkOrdinaryExport(id, "constant");
+        Context.ConstDecl.prototype.handleIdentdef.call(this, id);
+    }
+});
+
+var VariableDeclaration = Context.VariableDeclaration.extend({
+    init: function EberonContext$VariableDeclaration(context){
+        Context.VariableDeclaration.prototype.init.call(this, context);
+    },
+    handleIdentdef: function(id){
+        checkOrdinaryExport(id, "variable");
+        Context.VariableDeclaration.prototype.handleIdentdef.call(this, id);
+    }
+});
+
+var TypeDeclaration = Context.TypeDeclaration.extend({
+    init: function EberonContext$TypeDeclaration(context){
+        Context.TypeDeclaration.prototype.init.call(this, context);
+    },
+    handleIdentdef: function(id){
+        checkOrdinaryExport(id, "type");
+        Context.TypeDeclaration.prototype.handleIdentdef.call(this, id);
+    }
+});
+
 var RecordDecl = Context.RecordDecl.extend({
     init: function EberonContext$RecordDecl(context){
         var makeRecord = function(name, cons, scope){return new RecordType(name, cons, scope);};
@@ -532,11 +593,15 @@ var Expression = Context.Expression.extend({
 });
 
 exports.AddOperator = AddOperator;
+exports.ConstDecl = ConstDecl;
 exports.Designator = Designator;
 exports.Expression = Expression;
 exports.ExpressionProcedureCall = ExpressionProcedureCall;
+exports.Identdef = Identdef;
 exports.MethodHeading = MethodHeading;
 exports.AssignmentOrProcedureCall = AssignmentOrProcedureCall;
 exports.ProcOrMethodId = ProcOrMethodId;
 exports.ProcOrMethodDecl = ProcOrMethodDecl;
 exports.RecordDecl = RecordDecl;
+exports.TypeDeclaration = TypeDeclaration;
+exports.VariableDeclaration = VariableDeclaration;

+ 20 - 11
src/eberon/eberon_grammar.js

@@ -25,6 +25,10 @@ function makeAssignmentOrProcedureCall(designator, assignment){
                    EbContext.AssignmentOrProcedureCall);
 }
 
+function makeIdentdef(ident){
+    return context(and(ident, optional(or("*", "-"))), EbContext.Identdef);
+}
+
 function makeDesignator(qualident, selector, actualParameters){
     var designator = context(
         and(or("SELF", "SUPER", qualident), repeat(or(selector, actualParameters))), EbContext.Designator);
@@ -59,17 +63,22 @@ function makeFieldList(identdef, identList, type, formalParameters){
 }
 
 exports.language = {
-  grammar: Grammar.make(
-      makeDesignator,
-      makeProcedureHeading,
-      makeProcedureDeclaration,
-      makeFieldList,
-      EbContext.RecordDecl,
-      Context.VariableDeclaration,
-      EbContext.AddOperator,
-      EbContext.Expression,
-      Grammar.reservedWords + " SELF SUPER"
-      ),
+    grammar: Grammar.make(
+        makeIdentdef,
+        makeDesignator,
+        makeProcedureHeading,
+        makeProcedureDeclaration,
+        makeFieldList, 
+        { 
+            constDeclaration:   EbContext.ConstDecl, 
+            typeDeclaration:    EbContext.TypeDeclaration,
+            recordDecl:         EbContext.RecordDecl,
+            variableDeclaration: EbContext.VariableDeclaration,
+            addOperator:        EbContext.AddOperator,
+            expression:         EbContext.Expression
+        },
+        Grammar.reservedWords + " SELF SUPER"
+        ),
     stdSymbols: Symbols.makeStd(),
     types: {
         implicitCast: Cast.implicit

+ 10 - 13
src/grammar.js

@@ -22,14 +22,12 @@ var required = Parser.required;
 
 var reservedWords = "ARRAY IMPORT THEN BEGIN IN TO BY IS TRUE CASE MOD TYPE CONST MODULE UNTIL DIV NIL VAR DO OF WHILE ELSE OR ELSIF POINTER END PROCEDURE FALSE RECORD FOR REPEAT IF RETURN";
 
-function make(makeDesignator,
+function make(makeIdentdef,
+              makeDesignator,
               makeProcedureHeading, 
               makeProcedureDeclaration,
               makeFieldList,
-              recordDeclContext,
-              varDeclContext,
-              addOperatorContext,
-              expressionContext,
+              contexts,
               reservedWords
               ){
 var result = {};
@@ -46,8 +44,7 @@ var ModuleDeclContext = Context.ModuleDeclaration.extend({
 
 var qualident = context(and(optional(and(ident, ".")), ident),
                         Context.QualifiedIdentificator);
-var identdef = context(and(ident, optional("*")),
-                       Context.Identdef);
+var identdef = makeIdentdef(ident);
 
 var selector = or(and(point, ident)
                 // break recursive declaration of expList
@@ -65,7 +62,7 @@ var type = or(context(qualident, Context.Type),
               function(stream, context){return strucType(stream, context);} // break recursive declaration of strucType
              );
 var identList = and(identdef, repeat(and(",", identdef)));
-var variableDeclaration = context(and(identList, ":", type), varDeclContext);
+var variableDeclaration = context(and(identList, ":", type), contexts.variableDeclaration);
 
 var integer = or(context(and(digit, repeat(hexDigit), "H", separator), Context.HexInteger)
                , context(and(digit, repeat(digit), separator), Context.Integer));
@@ -90,7 +87,7 @@ var factor = context(
      )
     , Context.Factor);
 
-var addOperator = context(or("+", "-", "OR"), addOperatorContext);
+var addOperator = context(or("+", "-", "OR"), contexts.addOperator);
 var mulOperator = context(or("*", "/", "DIV", "MOD", "&"), Context.MulOperator);
 var term = context(and(factor, repeat(and(mulOperator, factor))), Context.Term);
 var simpleExpression = context(
@@ -100,7 +97,7 @@ var simpleExpression = context(
       , Context.SimpleExpression);
 var relation = or("=", "#", "<=", "<", ">=", ">", "IN", "IS");
 var expression = context(and(simpleExpression, optional(and(relation, simpleExpression)))
-                       , expressionContext);
+                       , contexts.expression);
 var constExpression = expression;
 
 var element = context(and(expression, optional(and("..", expression))), Context.SetElement);
@@ -165,7 +162,7 @@ var arrayType = and("ARRAY", context(and(
 
 var baseType = context(qualident, Context.BaseType);
 var recordType = and("RECORD", context(and(optional(and("(", baseType, ")")), optional(fieldListSequence)
-                                     , "END"), recordDeclContext));
+                                     , "END"), contexts.recordDecl));
 
 var pointerType = and("POINTER", "TO", context(type, Context.PointerDecl));
 
@@ -181,9 +178,9 @@ var procedureType = and("PROCEDURE"
                       , context(optional(formalParameters), Context.FormalParameters)
                         );
 var strucType = or(arrayType, recordType, pointerType, procedureType);
-var typeDeclaration = context(and(identdef, "=", strucType), Context.TypeDeclaration);
+var typeDeclaration = context(and(identdef, "=", strucType), contexts.typeDeclaration);
 
-var constantDeclaration = context(and(identdef, "=", constExpression), Context.ConstDecl);
+var constantDeclaration = context(and(identdef, "=", constExpression), contexts.constDeclaration);
 
 var imprt = and(ident, optional(and(":=", ident)));
 var importList = and("IMPORT", imprt, repeat(and(",", imprt)));

+ 26 - 3
src/ob/Module.ob

@@ -8,7 +8,11 @@ TYPE
 
     AnyType* = RECORD(Types.StorageType)
         PROCEDURE callGenerator(cx: LanguageContext.PType; id: STRING): Procedure.PCallGenerator;
-        PROCEDURE findSymbol(id: STRING): Types.PType
+        PROCEDURE findSymbol(id: STRING): Types.PField;
+        asField: POINTER TO AnyField
+    END;
+
+    AnyField = RECORD(Types.Field)
     END;
 
     AnyTypeProc* = RECORD(Types.DefinedProcedure)
@@ -34,10 +38,26 @@ PROCEDURE AnyType.callGenerator(cx: LanguageContext.PType; id: STRING): Procedur
     RETURN Procedure.makeProcCallGenerator(cx, id, anyProc)
 END AnyType.callGenerator;
 
-PROCEDURE AnyType.findSymbol(id: STRING): Types.PType;
-    RETURN any
+PROCEDURE AnyType.findSymbol(id: STRING): Types.PField;
+    RETURN any.asField
 END AnyType.findSymbol;
 
+PROCEDURE AnyField.id(): STRING;
+    RETURN "any field"
+END AnyField.id;
+
+PROCEDURE AnyField.exported(): BOOLEAN;
+    RETURN FALSE
+END AnyField.exported;
+
+PROCEDURE AnyField.type(): Types.PType;
+    RETURN any
+END AnyField.type;
+
+PROCEDURE AnyField.recordType(): Types.PType;
+    RETURN any
+END AnyField.recordType;
+
 PROCEDURE AnyTypeProc.args(): JsArray.Type;
     RETURN NIL
 END AnyTypeProc.args;
@@ -115,7 +135,10 @@ END makeJS;
 BEGIN
     doProcId := "do$";
     varTypeId := "var$";
+    
     NEW(any);
+    NEW(any.asField); 
+
     doProcSymbol := makeDoProcSymbol();
     varTypeSymbol := makeVarTypeSymbol();
 END Module.

+ 12 - 9
src/ob/Types.ob

@@ -131,14 +131,17 @@ TYPE
 
     PBasicType* = POINTER TO BasicType;
 
-    Field = RECORD
-        id: PROCEDURE(): STRING;
-        exported: PROCEDURE(): BOOLEAN
+    Field* = RECORD(Object.Type)
+        PROCEDURE id*(): STRING;
+        PROCEDURE exported*(): BOOLEAN;
+        PROCEDURE type*(): PType;
+        PROCEDURE recordType*(): PType 
     END;
+    PField* = POINTER TO Field;
 
     Record* = RECORD(NamedType)
-        PROCEDURE addField(f: Field; type: PType);
-        PROCEDURE findSymbol(id: STRING): PType;
+        PROCEDURE addField(f: PField);
+        PROCEDURE findSymbol(id: STRING): PField;
         PROCEDURE finalize();
 
         fields: JsMap.Type;
@@ -388,7 +391,7 @@ PROCEDURE Record.initializer(cx: Context.Type): STRING;
     RETURN "new " + cx.qualifyScope(SELF.scope) + SELF.cons + "()"
 END Record.initializer;
 
-PROCEDURE Record.addField(f: Field; type: PType);
+PROCEDURE Record.addField(f: PField);
 BEGIN
     IF JsMap.has(SELF.fields, f.id()) THEN
         Errors.raise("duplicated field: '" + f.id() + "'");
@@ -396,15 +399,15 @@ BEGIN
     IF (SELF.base # NIL) & (SELF.base.findSymbol(f.id()) # NIL) THEN
         Errors.raise("base record already has field: '" + f.id() + "'");
     END;
-    JsMap.put(SELF.fields, f.id(), type);
+    JsMap.put(SELF.fields, f.id(), f);
     IF ~f.exported() THEN
         JsArray.addString(SELF.notExported, f.id());
     END;
 END Record.addField;
 
-PROCEDURE Record.findSymbol(id: STRING): PType;
+PROCEDURE Record.findSymbol(id: STRING): PField;
 VAR
-    result: PType;
+    result: PField;
 BEGIN
     IF ~JsMap.find(SELF.fields, id, result) & (SELF.base # NIL) THEN
         result := SELF.base.findSymbol(id);

+ 19 - 10
src/oberon/oberon_grammar.js

@@ -27,6 +27,10 @@ function makeAssignmentOrProcedureCall(designator, actualParameters, assignment)
               );
 }
 
+function makeIdentdef(ident){
+    return context(and(ident, optional("*")), Context.Identdef);
+}
+
 function makeDesignator(qualident, selector, actualParameters){
     var designator = context(and(qualident, repeat(selector)), Context.Designator);
     return { 
@@ -50,16 +54,21 @@ function makeFieldList(identdef, identList, type){
 
 exports.language = {
     grammar: Grammar.make(
-            makeDesignator,
-            makeProcedureHeading,
-            makeProcedureDeclaration,
-            makeFieldList,
-            ObContext.RecordDecl,
-            ObContext.VariableDeclaration,
-            Context.AddOperator,
-            Context.Expression,
-            Grammar.reservedWords
-            ),
+        makeIdentdef,
+        makeDesignator,
+        makeProcedureHeading,
+        makeProcedureDeclaration,
+        makeFieldList,
+        {
+            constDeclaration:   Context.ConstDecl, 
+            typeDeclaration:    Context.TypeDeclaration,
+            recordDecl:         ObContext.RecordDecl,
+            variableDeclaration: ObContext.VariableDeclaration,
+            addOperator:        Context.AddOperator,
+            expression:         Context.Expression
+        },
+        Grammar.reservedWords
+        ),
     stdSymbols: Symbols.makeStd(),
     types: {
         implicitCast: Cast.implicit

+ 28 - 0
test/test_unit_eberon.js

@@ -191,6 +191,34 @@ exports.suite = {
          "VAR a*: A;"),
     fail()
     ),
+"export as read-only": testWithContext(
+    context(grammar.declarationSequence, ""),
+    pass("TYPE T* = RECORD i-: INTEGER END;"),
+    fail(["TYPE T- = RECORD END;", 
+          "type cannot be exported as read-only using '-' mark (did you mean '*'?)"],
+         ["PROCEDURE p-(); END p;", 
+          "procedure cannot be exported as read-only using '-' mark (did you mean '*'?)"],
+         ["CONST c- = 123;", 
+          "constant cannot be exported as read-only using '-' mark (did you mean '*'?)"],
+         ["VAR i-: INTEGER;", 
+          "variable cannot be exported as read-only using '-' mark (did you mean '*'?)"])
+    ),
+"field exported as read-only is writable in current module": testWithContext(
+    context(grammar.statement,
+              "TYPE T* = RECORD i-: INTEGER END;"
+            + "VAR r: T;"
+            ),
+    pass("r.i := 123"),
+    fail()
+    ),
+"import as read-only": testWithModule(
+    "MODULE test; TYPE T* = RECORD f-: INTEGER END; END test.",
+    pass(),
+    fail(["MODULE m; IMPORT test; VAR r: test.T; BEGIN r.f := 123; END m.",
+          "cannot assign to read-only variable"],
+         ["MODULE m; IMPORT test; TYPE D = RECORD(test.T) END; VAR r: D; BEGIN r.f := 123; END m.",
+          "cannot assign to read-only variable"]
+        )),
 "STRING variable": testWithGrammar(
     grammar.variableDeclaration,
     pass("s: STRING")