From 4eed4f7659aff422c1c67d610788ad08fb23d580 Mon Sep 17 00:00:00 2001 From: jespergravgaard Date: Fri, 15 Mar 2019 00:02:33 +0100 Subject: [PATCH] Implemented proper handling of referenced to constants inside inline ASM. Closes #146 --- .../model/ControlFlowGraphCopyVisitor.java | 2 +- .../kickc/model/iterator/ProgramValue.java | 22 ++ .../model/iterator/ProgramValueIterator.java | 10 +- .../kickc/model/statements/StatementAsm.java | 15 +- .../Pass0GenerateStatementSequence.java | 52 ++-- .../kickc/passes/Pass3AssertConstants.java | 17 ++ .../dk/camelot64/kickc/test/TestPrograms.java | 16 +- ...-problem.kc => inline-asm-refout-const.kc} | 2 +- src/test/kc/inline-asm-refout-illegal.kc | 14 ++ src/test/kc/inline-asm-refout-undef.kc | 6 + src/test/ref/inline-asm-refout-const.asm | 16 ++ src/test/ref/inline-asm-refout-const.cfg | 15 ++ src/test/ref/inline-asm-refout-const.log | 233 ++++++++++++++++++ src/test/ref/inline-asm-refout-const.sym | 10 + 14 files changed, 405 insertions(+), 25 deletions(-) rename src/test/kc/{inline-asm-refout-problem.kc => inline-asm-refout-const.kc} (80%) create mode 100644 src/test/kc/inline-asm-refout-illegal.kc create mode 100644 src/test/kc/inline-asm-refout-undef.kc create mode 100644 src/test/ref/inline-asm-refout-const.asm create mode 100644 src/test/ref/inline-asm-refout-const.cfg create mode 100644 src/test/ref/inline-asm-refout-const.log create mode 100644 src/test/ref/inline-asm-refout-const.sym diff --git a/src/main/java/dk/camelot64/kickc/model/ControlFlowGraphCopyVisitor.java b/src/main/java/dk/camelot64/kickc/model/ControlFlowGraphCopyVisitor.java index 22992a340..ce9ffd61f 100644 --- a/src/main/java/dk/camelot64/kickc/model/ControlFlowGraphCopyVisitor.java +++ b/src/main/java/dk/camelot64/kickc/model/ControlFlowGraphCopyVisitor.java @@ -190,7 +190,7 @@ public class ControlFlowGraphCopyVisitor extends ControlFlowGraphBaseVisitor referenced = statementAsm.getReferenced(); + for(String label : referenced.keySet()) { + execute(new ProgramValue.AsmReferenced(statementAsm, label), handler, statement, statementsIt, block); + } } } diff --git a/src/main/java/dk/camelot64/kickc/model/statements/StatementAsm.java b/src/main/java/dk/camelot64/kickc/model/statements/StatementAsm.java index 4f110f20b..a089f0080 100644 --- a/src/main/java/dk/camelot64/kickc/model/statements/StatementAsm.java +++ b/src/main/java/dk/camelot64/kickc/model/statements/StatementAsm.java @@ -2,9 +2,11 @@ package dk.camelot64.kickc.model.statements; import dk.camelot64.kickc.model.Comment; import dk.camelot64.kickc.model.Program; +import dk.camelot64.kickc.model.values.SymbolVariableRef; import dk.camelot64.kickc.parser.KickCParser; import java.util.List; +import java.util.Map; /** Inline ASM code */ public class StatementAsm extends StatementBase { @@ -12,9 +14,13 @@ public class StatementAsm extends StatementBase { /** ASM Fragment code. */ private KickCParser.AsmLinesContext asmLines; - public StatementAsm(KickCParser.AsmLinesContext asmLines, StatementSource source, List comments) { + /** All variables/constants referenced in the inline assembler. */ + private Map referenced; + + public StatementAsm(KickCParser.AsmLinesContext asmLines, Map referenced, StatementSource source, List comments) { super(null, source, comments); this.asmLines = asmLines; + this.referenced = referenced; } @Override @@ -32,4 +38,11 @@ public class StatementAsm extends StatementBase { return asmLines; } + public void setReferenced(Map referenced) { + this.referenced = referenced; + } + + public Map getReferenced() { + return referenced; + } } diff --git a/src/main/java/dk/camelot64/kickc/passes/Pass0GenerateStatementSequence.java b/src/main/java/dk/camelot64/kickc/passes/Pass0GenerateStatementSequence.java index 86a833bc1..67145bc72 100644 --- a/src/main/java/dk/camelot64/kickc/passes/Pass0GenerateStatementSequence.java +++ b/src/main/java/dk/camelot64/kickc/passes/Pass0GenerateStatementSequence.java @@ -18,10 +18,7 @@ import org.antlr.v4.runtime.tree.TerminalNode; import java.io.File; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Stack; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -73,7 +70,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { @Override public Void visitFile(KickCParser.FileContext ctx) { - if(program.getFileComments()==null) { + if(program.getFileComments() == null) { // Only set program file level comments for the first file. program.setFileComments(ensureUnusedComments(getCommentsFile(ctx))); } @@ -325,7 +322,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { ConstantInteger zero = new ConstantInteger(0l); Statement stmt = new StatementAssignment(lValue.getRef(), zero, new StatementSource(ctx), ensureUnusedComments(comments)); sequence.addStatement(stmt); - } else if(type instanceof SymbolTypeArray) { + } else if(type instanceof SymbolTypeArray) { // Add an zero-array initializer SymbolTypeArray typeArray = (SymbolTypeArray) type; RValue size = typeArray.getSize(); @@ -334,14 +331,14 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { } Statement stmt = new StatementAssignment(lValue.getRef(), new ArrayFilled(typeArray.getElementType(), size), new StatementSource(ctx), ensureUnusedComments(comments)); sequence.addStatement(stmt); - } else if(type instanceof SymbolTypePointer) { + } else if(type instanceof SymbolTypePointer) { // Add an zero value initializer SymbolTypePointer typePointer = (SymbolTypePointer) type; ConstantValue zero = new ConstantPointer(0l, typePointer.getElementType()); Statement stmt = new StatementAssignment(lValue.getRef(), zero, new StatementSource(ctx), ensureUnusedComments(comments)); sequence.addStatement(stmt); } else { - throw new CompileError("Default initializer not implemented for type "+type.getTypeName(), new StatementSource(ctx)); + throw new CompileError("Default initializer not implemented for type " + type.getTypeName(), new StatementSource(ctx)); } } @@ -538,7 +535,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { Label doJumpLabel = getCurrentSymbols().addLabelIntermediate(); Label endJumpLabel = getCurrentSymbols().addLabelIntermediate(); List comments = ensureUnusedComments(getCommentsSymbol(ctx)); - StatementLabel beginJumpTarget = new StatementLabel(beginJumpLabel.getRef(), new StatementSource(ctx),comments); + StatementLabel beginJumpTarget = new StatementLabel(beginJumpLabel.getRef(), new StatementSource(ctx), comments); sequence.addStatement(beginJumpTarget); PrePostModifierHandler.addPreModifiers(this, ctx.expr()); RValue rValue = (RValue) this.visit(ctx.expr()); @@ -694,7 +691,26 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { @Override public Object visitStmtAsm(KickCParser.StmtAsmContext ctx) { List comments = ensureUnusedComments(getCommentsSymbol(ctx)); - sequence.addStatement(new StatementAsm(ctx.asmLines(), new StatementSource(ctx), comments)); + + Map referenced = new LinkedHashMap<>(); + // Find all referenced symbols in the asm lines + KickCBaseVisitor visitor = new KickCBaseVisitor() { + @Override + public Void visitAsmExprLabel(KickCParser.AsmExprLabelContext ctxLabel) { + String label = ctxLabel.NAME().toString(); + Symbol symbol = getCurrentSymbols().getSymbol(ctxLabel.NAME().getText()); + if(symbol instanceof Variable) { + Variable variable = (Variable) symbol; + referenced.put(label, variable.getRef()); + } else { + throw new CompileError("Symbol referenced in inline ASM not found " + label, new StatementSource(ctxLabel)); + } + return super.visitAsmExprLabel(ctxLabel); + } + }; + visitor.visit(ctx.asmLines()); + StatementAsm statementAsm = new StatementAsm(ctx.asmLines(), referenced, new StatementSource(ctx), comments); + sequence.addStatement(statementAsm); return null; } @@ -775,7 +791,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { public Object visitExprAssignment(KickCParser.ExprAssignmentContext ctx) { Object val = visit(ctx.expr(0)); if(!(val instanceof LValue)) { - throw new CompileError("Error! Illegal assignment Lvalue " + val.toString(), new StatementSource(ctx)); + throw new CompileError("Error! Illegal assignment Lvalue " + val.toString(), new StatementSource(ctx)); } LValue lValue = (LValue) val; if(lValue instanceof VariableRef && ((VariableRef) lValue).isIntermediate()) { @@ -966,7 +982,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { if(hiddenToken.getChannel() == CHANNEL_WHITESPACE) { String text = hiddenToken.getText(); long newlineCount = text.chars().filter(ch -> ch == '\n').count(); - if(newlineCount > 1 && comments.size()>0) { + if(newlineCount > 1 && comments.size() > 0) { // Create new comment block commentBlocks.add(comments); comments = new ArrayList<>(); @@ -978,17 +994,17 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { text = text.substring(2); } if(text.startsWith("/*")) { - text = text.substring(2, text.length()-2); + text = text.substring(2, text.length() - 2); isBlock = true; } Comment comment = new Comment(text); comment.setBlock(isBlock); comment.setTokenIndex(hiddenToken.getTokenIndex()); - comments.add( comment); + comments.add(comment); } } } - if(comments.size()>0) { + if(comments.size() > 0) { commentBlocks.add(comments); } return commentBlocks; @@ -1004,7 +1020,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { * @return The comments if they are unused. An empty comment if they had already been used. */ private List ensureUnusedComments(List candidate) { - if(candidate.size()==0) { + if(candidate.size() == 0) { return candidate; } int tokenIndex = candidate.get(0).getTokenIndex(); @@ -1027,7 +1043,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { */ private List getCommentsFile(ParserRuleContext ctx) { List> commentBlocks = getCommentBlocks(ctx); - if(commentBlocks.size()==0) { + if(commentBlocks.size() == 0) { return new ArrayList<>(); } return commentBlocks.get(0); @@ -1042,7 +1058,7 @@ public class Pass0GenerateStatementSequence extends KickCBaseVisitor { */ private List getCommentsSymbol(ParserRuleContext ctx) { List> commentBlocks = getCommentBlocks(ctx); - if(commentBlocks.size()==0) { + if(commentBlocks.size() == 0) { return new ArrayList<>(); } return commentBlocks.get(commentBlocks.size() - 1); diff --git a/src/main/java/dk/camelot64/kickc/passes/Pass3AssertConstants.java b/src/main/java/dk/camelot64/kickc/passes/Pass3AssertConstants.java index c729c648e..7aa0510db 100644 --- a/src/main/java/dk/camelot64/kickc/passes/Pass3AssertConstants.java +++ b/src/main/java/dk/camelot64/kickc/passes/Pass3AssertConstants.java @@ -4,14 +4,22 @@ import dk.camelot64.kickc.model.CompileError; import dk.camelot64.kickc.model.ControlFlowBlock; import dk.camelot64.kickc.model.Program; import dk.camelot64.kickc.model.statements.Statement; +import dk.camelot64.kickc.model.statements.StatementAsm; import dk.camelot64.kickc.model.statements.StatementKickAsm; +import dk.camelot64.kickc.model.values.ConstantRef; import dk.camelot64.kickc.model.values.ConstantValue; import dk.camelot64.kickc.model.values.RValue; +import dk.camelot64.kickc.model.values.SymbolVariableRef; + +import java.util.Map; /** * Asserts that some RValues have been resolved to Constants. * Checks: * - KickAssembler locations + * - KickAssembler bytes + * - KickAssembler cycles + * - ASM referenced variables */ public class Pass3AssertConstants extends Pass2SsaAssertion { @@ -36,6 +44,15 @@ public class Pass3AssertConstants extends Pass2SsaAssertion { if(cycles!= null && !(cycles instanceof ConstantValue)) { throw new CompileError("Error! KickAssembler cycles is not constant " + cycles.toString(), statement); } + } else if(statement instanceof StatementAsm) { + StatementAsm statementAsm = (StatementAsm) statement; + Map referenced = statementAsm.getReferenced(); + for(String label : referenced.keySet()) { + SymbolVariableRef symbolRef = referenced.get(label); + if(!(symbolRef instanceof ConstantRef)) { + throw new CompileError("Error! Inline ASM reference is not constant " + label, statement); + } + } } } } diff --git a/src/test/java/dk/camelot64/kickc/test/TestPrograms.java b/src/test/java/dk/camelot64/kickc/test/TestPrograms.java index 962c5a261..063ec9737 100644 --- a/src/test/java/dk/camelot64/kickc/test/TestPrograms.java +++ b/src/test/java/dk/camelot64/kickc/test/TestPrograms.java @@ -44,12 +44,22 @@ public class TestPrograms { AsmFragmentTemplateUsages.logUsages(log, false, false, false, false, false, false); } - /* @Test - public void testInlineAsmRefoutProblem() throws IOException, URISyntaxException { - compileAndCompare("inline-asm-refout-problem"); + public void testInlineAsmRefoutIllegal() throws IOException, URISyntaxException { + assertError("inline-asm-refout-illegal", "Inline ASM reference is not constant"); } + @Test + public void testInlineAsmRefoutConst() throws IOException, URISyntaxException { + compileAndCompare("inline-asm-refout-const"); + } + + @Test + public void testInlineAsmRefoutUndef() throws IOException, URISyntaxException { + assertError("inline-asm-refout-undef", "Symbol referenced in inline ASM not found"); + } + + /* @Test public void testConstIfProblem() throws IOException, URISyntaxException { compileAndCompare("const-if-problem"); diff --git a/src/test/kc/inline-asm-refout-problem.kc b/src/test/kc/inline-asm-refout-const.kc similarity index 80% rename from src/test/kc/inline-asm-refout-problem.kc rename to src/test/kc/inline-asm-refout-const.kc index 5be98e098..e7726d398 100644 --- a/src/test/kc/inline-asm-refout-problem.kc +++ b/src/test/kc/inline-asm-refout-const.kc @@ -1,4 +1,4 @@ -// Illustrates how inline assembler can reference data from the outside program +// Illustrates how inline assembler can reference data from the outside program without the data being optimized away as unused const byte* SCREEN = $400; byte[] table = "cml!"; diff --git a/src/test/kc/inline-asm-refout-illegal.kc b/src/test/kc/inline-asm-refout-illegal.kc new file mode 100644 index 000000000..c48243a42 --- /dev/null +++ b/src/test/kc/inline-asm-refout-illegal.kc @@ -0,0 +1,14 @@ +// Illustrates how inline assembler referencing variables is illegal + +const byte* SCREEN = $400; + +void main() { + for( byte i: 0..10) { + asm { + lda #'a' + ldx i + sta SCREEN,x + } + } + +} \ No newline at end of file diff --git a/src/test/kc/inline-asm-refout-undef.kc b/src/test/kc/inline-asm-refout-undef.kc new file mode 100644 index 000000000..130ade2e1 --- /dev/null +++ b/src/test/kc/inline-asm-refout-undef.kc @@ -0,0 +1,6 @@ +// Reference to undefined symbol in inline asm +void main() { + asm { + lda qwe + } +} \ No newline at end of file diff --git a/src/test/ref/inline-asm-refout-const.asm b/src/test/ref/inline-asm-refout-const.asm new file mode 100644 index 000000000..d044e2960 --- /dev/null +++ b/src/test/ref/inline-asm-refout-const.asm @@ -0,0 +1,16 @@ +// Illustrates how inline assembler can reference data from the outside program without the data being optimized away as unused +.pc = $801 "Basic" +:BasicUpstart(main) +.pc = $80d "Program" + .label SCREEN = $400 +main: { + ldx #0 + !: + lda table,x + sta SCREEN+1,x + inx + cpx #4 + bne !- + rts +} + table: .text "cml!" diff --git a/src/test/ref/inline-asm-refout-const.cfg b/src/test/ref/inline-asm-refout-const.cfg new file mode 100644 index 000000000..f0fd9d09e --- /dev/null +++ b/src/test/ref/inline-asm-refout-const.cfg @@ -0,0 +1,15 @@ +@begin: scope:[] from + [0] phi() + to:@1 +@1: scope:[] from @begin + [1] phi() + [2] call main + to:@end +@end: scope:[] from @1 + [3] phi() +main: scope:[main] from @1 + asm { ldx#0 !: ldatable,x staSCREEN+1,x inx cpx#4 bne!- } + to:main::@return +main::@return: scope:[main] from main + [5] return + to:@return diff --git a/src/test/ref/inline-asm-refout-const.log b/src/test/ref/inline-asm-refout-const.log new file mode 100644 index 000000000..7e3ed5420 --- /dev/null +++ b/src/test/ref/inline-asm-refout-const.log @@ -0,0 +1,233 @@ + +CONTROL FLOW GRAPH SSA +@begin: scope:[] from + (byte*) SCREEN#0 ← ((byte*)) (word/signed word/dword/signed dword) $400 + (byte[]) table#0 ← (const string) $0 + to:@1 +main: scope:[main] from @1 + asm { ldx#0 !: ldatable,x staSCREEN+1,x inx cpx#4 bne!- } + to:main::@return +main::@return: scope:[main] from main + return + to:@return +@1: scope:[] from @begin + call main + to:@2 +@2: scope:[] from @1 + to:@end +@end: scope:[] from @2 + +SYMBOL TABLE SSA +(const string) $0 = (string) "cml!" +(label) @1 +(label) @2 +(label) @begin +(label) @end +(byte*) SCREEN +(byte*) SCREEN#0 +(void()) main() +(label) main::@return +(byte[]) table +(byte[]) table#0 + +Culled Empty Block (label) @2 +Successful SSA optimization Pass2CullEmptyBlocks +Constant (const byte*) SCREEN#0 = ((byte*))$400 +Constant (const byte[]) table#0 = $0 +Successful SSA optimization Pass2ConstantIdentification +Constant inlined $0 = (const byte[]) table#0 +Successful SSA optimization Pass2ConstantInlining +Adding NOP phi() at start of @begin +Adding NOP phi() at start of @1 +Adding NOP phi() at start of @end +CALL GRAPH +Calls in [] to main:2 + +Created 0 initial phi equivalence classes +Coalesced down to 0 phi equivalence classes +Adding NOP phi() at start of @begin +Adding NOP phi() at start of @1 +Adding NOP phi() at start of @end + +FINAL CONTROL FLOW GRAPH +@begin: scope:[] from + [0] phi() + to:@1 +@1: scope:[] from @begin + [1] phi() + [2] call main + to:@end +@end: scope:[] from @1 + [3] phi() +main: scope:[main] from @1 + asm { ldx#0 !: ldatable,x staSCREEN+1,x inx cpx#4 bne!- } + to:main::@return +main::@return: scope:[main] from main + [5] return + to:@return + + +VARIABLE REGISTER WEIGHTS +(byte*) SCREEN +(void()) main() +(byte[]) table + +Initial phi equivalence classes +Complete equivalence classes + +INITIAL ASM +//SEG0 File Comments +// Illustrates how inline assembler can reference data from the outside program without the data being optimized away as unused +//SEG1 Basic Upstart +.pc = $801 "Basic" +:BasicUpstart(bbegin) +.pc = $80d "Program" +//SEG2 Global Constants & labels + .label SCREEN = $400 +//SEG3 @begin +bbegin: +//SEG4 [1] phi from @begin to @1 [phi:@begin->@1] +b1_from_bbegin: + jmp b1 +//SEG5 @1 +b1: +//SEG6 [2] call main + jsr main +//SEG7 [3] phi from @1 to @end [phi:@1->@end] +bend_from_b1: + jmp bend +//SEG8 @end +bend: +//SEG9 main +main: { + //SEG10 asm { ldx#0 !: ldatable,x staSCREEN+1,x inx cpx#4 bne!- } + ldx #0 + !: + lda table,x + sta SCREEN+1,x + inx + cpx #4 + bne !- + jmp breturn + //SEG11 main::@return + breturn: + //SEG12 [5] return + rts +} + table: .text "cml!" + +REGISTER UPLIFT POTENTIAL REGISTERS +Statement asm { ldx#0 !: ldatable,x staSCREEN+1,x inx cpx#4 bne!- } always clobbers reg byte a reg byte x + +REGISTER UPLIFT SCOPES +Uplift Scope [main] +Uplift Scope [] + +Uplifting [main] best 39 combination +Uplifting [] best 39 combination + +ASSEMBLER BEFORE OPTIMIZATION +//SEG0 File Comments +// Illustrates how inline assembler can reference data from the outside program without the data being optimized away as unused +//SEG1 Basic Upstart +.pc = $801 "Basic" +:BasicUpstart(bbegin) +.pc = $80d "Program" +//SEG2 Global Constants & labels + .label SCREEN = $400 +//SEG3 @begin +bbegin: +//SEG4 [1] phi from @begin to @1 [phi:@begin->@1] +b1_from_bbegin: + jmp b1 +//SEG5 @1 +b1: +//SEG6 [2] call main + jsr main +//SEG7 [3] phi from @1 to @end [phi:@1->@end] +bend_from_b1: + jmp bend +//SEG8 @end +bend: +//SEG9 main +main: { + //SEG10 asm { ldx#0 !: ldatable,x staSCREEN+1,x inx cpx#4 bne!- } + ldx #0 + !: + lda table,x + sta SCREEN+1,x + inx + cpx #4 + bne !- + jmp breturn + //SEG11 main::@return + breturn: + //SEG12 [5] return + rts +} + table: .text "cml!" + +ASSEMBLER OPTIMIZATIONS +Removing instruction jmp b1 +Removing instruction jmp bend +Removing instruction jmp breturn +Succesful ASM optimization Pass5NextJumpElimination +Removing instruction b1_from_bbegin: +Removing instruction b1: +Removing instruction bend_from_b1: +Succesful ASM optimization Pass5RedundantLabelElimination +Removing instruction bend: +Removing instruction breturn: +Succesful ASM optimization Pass5UnusedLabelElimination +Updating BasicUpstart to call main directly +Removing instruction jsr main +Succesful ASM optimization Pass5SkipBegin +Removing instruction bbegin: +Succesful ASM optimization Pass5UnusedLabelElimination + +FINAL SYMBOL TABLE +(label) @1 +(label) @begin +(label) @end +(byte*) SCREEN +(const byte*) SCREEN#0 SCREEN = ((byte*))(word/signed word/dword/signed dword) $400 +(void()) main() +(label) main::@return +(byte[]) table +(const byte[]) table#0 table = (string) "cml!" + + + +FINAL ASSEMBLER +Score: 24 + +//SEG0 File Comments +// Illustrates how inline assembler can reference data from the outside program without the data being optimized away as unused +//SEG1 Basic Upstart +.pc = $801 "Basic" +:BasicUpstart(main) +.pc = $80d "Program" +//SEG2 Global Constants & labels + .label SCREEN = $400 +//SEG3 @begin +//SEG4 [1] phi from @begin to @1 [phi:@begin->@1] +//SEG5 @1 +//SEG6 [2] call main +//SEG7 [3] phi from @1 to @end [phi:@1->@end] +//SEG8 @end +//SEG9 main +main: { + //SEG10 asm { ldx#0 !: ldatable,x staSCREEN+1,x inx cpx#4 bne!- } + ldx #0 + !: + lda table,x + sta SCREEN+1,x + inx + cpx #4 + bne !- + //SEG11 main::@return + //SEG12 [5] return + rts +} + table: .text "cml!" + diff --git a/src/test/ref/inline-asm-refout-const.sym b/src/test/ref/inline-asm-refout-const.sym new file mode 100644 index 000000000..797e39068 --- /dev/null +++ b/src/test/ref/inline-asm-refout-const.sym @@ -0,0 +1,10 @@ +(label) @1 +(label) @begin +(label) @end +(byte*) SCREEN +(const byte*) SCREEN#0 SCREEN = ((byte*))(word/signed word/dword/signed dword) $400 +(void()) main() +(label) main::@return +(byte[]) table +(const byte[]) table#0 table = (string) "cml!" +