diff --git a/src-tests/com/maccasoft/tools/AssemblerTest.java b/src-tests/com/maccasoft/tools/AssemblerTest.java new file mode 100644 index 0000000..d1763f6 --- /dev/null +++ b/src-tests/com/maccasoft/tools/AssemblerTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2019 Marco Maccaferri and others. + * All rights reserved. + * + * This program and the accompanying materials are made available under + * the terms of the Eclipse Public License v1.0 which accompanies this + * distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ + +package com.maccasoft.tools; + +import static org.junit.Assert.assertArrayEquals; + +import java.io.File; +import java.io.StringReader; +import java.util.ArrayList; + +import junit.framework.TestCase; +import nl.grauw.glass.Source; +import nl.grauw.glass.SourceBuilder; + +public class AssemblerTest extends TestCase { + + public void testGetHex() throws Exception { + Assembler builder = new Assembler(); + + builder.ram.put(0x0000, (byte) 0x01); + builder.ram.put(0x0001, (byte) 0x02); + builder.ram.put(0x0002, (byte) 0x03); + + StringBuilder sb = builder.getHex(); + assertEquals(":03000000010203F7\r\n", sb.toString()); + } + + public void testGetHexWithGap() throws Exception { + Assembler builder = new Assembler(); + + builder.ram.put(0x0000, (byte) 0x01); + builder.ram.put(0x0001, (byte) 0x02); + builder.ram.put(0x0003, (byte) 0x03); + + StringBuilder sb = builder.getHex(); + assertEquals(":020000000102FB\r\n:0100030003F9\r\n", sb.toString()); + } + + public void testGetHexWithOrigin() throws Exception { + Assembler builder = new Assembler(); + + builder.ram.put(0x0100, (byte) 0x01); + builder.ram.put(0x0101, (byte) 0x02); + builder.ram.put(0x0102, (byte) 0x03); + + StringBuilder sb = builder.getHex(); + assertEquals(":03010000010203F6\r\n", sb.toString()); + } + + public void testBuildHex() throws Exception { + Source source = assemble( + " xor a", + " ret"); + + Assembler builder = new Assembler(); + builder.build(source); + + StringBuilder sb = builder.getHex(); + assertEquals(":02000000AFC986\r\n", sb.toString()); + } + + public void testBuildHexWithOrigin() throws Exception { + Source source = assemble( + " .org 100H", + " xor a", + " ret"); + + Assembler builder = new Assembler(); + builder.build(source); + + StringBuilder sb = builder.getHex(); + assertEquals(":02010000AFC985\r\n", sb.toString()); + } + + public void testBuildHexWithDsGap() throws Exception { + Source source = assemble( + " xor a", + " .ds 1", + " ret"); + + Assembler builder = new Assembler(); + builder.build(source); + + StringBuilder sb = builder.getHex(); + assertEquals(":01000000AF50\r\n:01000200C934\r\n", sb.toString()); + } + + public void testGetBinary() throws Exception { + Assembler builder = new Assembler(); + + builder.ram.put(0x0000, (byte) 0x01); + builder.ram.put(0x0001, (byte) 0x02); + builder.ram.put(0x0002, (byte) 0x03); + + assertArrayEquals(b(0x01, 0x02, 0x03), builder.getBinary()); + } + + public void testGetBinaryWithGap() throws Exception { + Assembler builder = new Assembler(); + + builder.ram.put(0x0000, (byte) 0x01); + builder.ram.put(0x0001, (byte) 0x02); + builder.ram.put(0x0003, (byte) 0x03); + + assertArrayEquals(b(0x01, 0x02, 0x00, 0x03), builder.getBinary()); + } + + public void testGetBinaryWithOrigin() throws Exception { + Assembler builder = new Assembler(); + + builder.ram.put(0x0100, (byte) 0x01); + builder.ram.put(0x0101, (byte) 0x02); + builder.ram.put(0x0102, (byte) 0x03); + + assertArrayEquals(b(0x01, 0x02, 0x03), builder.getBinary()); + } + + public void testBuildBinary() throws Exception { + Source source = assemble( + " xor a", + " ret"); + + Assembler builder = new Assembler(); + builder.build(source); + + assertArrayEquals(b(0xAF, 0xC9), builder.getBinary()); + } + + public void testBuildBinaryWithOrigin() throws Exception { + Source source = assemble( + " .org 100H", + " xor a", + " ret"); + + Assembler builder = new Assembler(); + builder.build(source); + + assertArrayEquals(b(0xAF, 0xC9), builder.getBinary()); + } + + public void testBuildBinaryWithDsGap() throws Exception { + Source source = assemble( + " xor a", + " .ds 1", + " ret"); + + Assembler builder = new Assembler(); + builder.build(source); + + assertArrayEquals(b(0xAF, 0x00, 0xC9), builder.getBinary()); + } + + private Source assemble(String... sourceLines) { + StringBuilder builder = new StringBuilder(); + for (String lineText : sourceLines) { + builder.append(lineText).append("\n"); + } + SourceBuilder sourceBuilder = new SourceBuilder(new ArrayList()); + Source source = sourceBuilder.parse(new StringReader(builder.toString()), null); + source.register(); + source.expand(); + source.resolve(); + return source; + } + + private byte[] b(int... values) { + byte[] bytes = new byte[values.length]; + for (int i = 0; i < values.length; i++) { + bytes[i] = (byte) values[i]; + } + return bytes; + } + +} diff --git a/src/com/maccasoft/tools/Assembler.java b/src/com/maccasoft/tools/Assembler.java new file mode 100644 index 0000000..4824702 --- /dev/null +++ b/src/com/maccasoft/tools/Assembler.java @@ -0,0 +1,265 @@ +package com.maccasoft.tools; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import nl.grauw.glass.AssemblyException; +import nl.grauw.glass.Line; +import nl.grauw.glass.Scope; +import nl.grauw.glass.Source; +import nl.grauw.glass.SourceBuilder; +import nl.grauw.glass.directives.If; +import nl.grauw.glass.directives.Section; + +public class Assembler { + + public static final int RAM_SIZE = 65536; + + private static Assembler instance; + + Source source; + + Map ram; + + public static void main(String[] args) { + if (args.length == 0) { + System.out.println("Usage: java -jar glass.jar [OPTION] SOURCE [OBJECT]"); + System.exit(1); + } + + File sourcePath = null; + File objectPath = null; + List includePaths = new ArrayList(); + for (int i = 0; i < args.length; i++) { + if (args[i].equals("-I") && i + 1 < args.length) { + includePaths.add(new File(args[++i])); + } + else if (sourcePath == null) { + sourcePath = new File(args[i]); + } + else if (objectPath == null) { + objectPath = new File(args[i]); + } + else { + throw new AssemblyException("Too many arguments."); + } + } + + try { + instance = new Assembler(); + instance.assemble(sourcePath, includePaths, objectPath); + } catch (AssemblyException ex) { + StringBuilder sb = new StringBuilder(); + + Iterator iter = ex.contexts.iterator(); + if (iter.hasNext()) { + AssemblyException.Context context = iter.next(); + sb.append(context.file.getName()); + sb.append(":"); + sb.append(context.line + 1); + if (context.column != -1) { + sb.append(":"); + sb.append(context.column); + } + sb.append(": error: "); + sb.append(ex.getPlainMessage()); + } + + System.out.println(); + System.err.println(sb.toString()); + System.exit(1); + + } catch (Exception e) { + e.printStackTrace(); + System.exit(2); + } + } + + public Assembler() { + ram = new HashMap(RAM_SIZE); + } + + void assemble(File sourcePath, List includePaths, File objectPath) { + System.out.print("Compiling " + sourcePath.getName() + "..."); + System.out.flush(); + + source = new SourceBuilder(includePaths) { + + @Override + public Source parse(File sourceFile) { + if (!sourceFile.equals(sourcePath)) { + System.out.print("\r\nCompiling " + sourceFile.getName() + "..."); + System.out.flush(); + } + return super.parse(sourceFile); + } + + }.parse(sourcePath); + + source.register(); + source.expand(); + source.resolve(); + + build(source); + writeObject(objectPath); + + int lower = Integer.MAX_VALUE; + int higher = Integer.MIN_VALUE; + for (Line line : source.getLines()) { + try { + if (line.getSize() != 0) { + lower = Math.min(lower, line.getScope().getAddress()); + higher = Math.max(higher, line.getScope().getAddress() + line.getSize() - 1); + } + } catch (Exception e) { + // Ignore, not important + } + } + System.out.println(); + System.out.println(String.format("Compiled %d lines from %04XH to %04XH (%d bytes)", source.getLines().size(), lower, higher, higher - lower + 1)); + } + + void build(Source source) { + for (Line line : source.getLines()) { + try { + if (line.getDirective() instanceof If) { + if (line.getInstructionObject() != null) { + nl.grauw.glass.instructions.If ins = (nl.grauw.glass.instructions.If) line.getInstruction(); + build(ins.getThenSource()); + if (ins.getElseSource() != null) { + build(ins.getElseSource()); + } + } + else { + If ins = (If) line.getDirective(); + build(ins.getThenSource()); + if (ins.getElseSource() != null) { + build(ins.getElseSource()); + } + } + } + else if (line.getDirective() instanceof Section) { + if (line.getInstructionObject() != null) { + nl.grauw.glass.instructions.Section ins = (nl.grauw.glass.instructions.Section) line.getInstruction(); + build(ins.getSource()); + } + else { + Section ins = (Section) line.getDirective(); + build(ins.getSource()); + } + } + else { + Scope scope = line.getScope(); + if (scope.isAddressSet()) { + byte[] code = line.getBytes(); + for (int i = 0; i < code.length; i++) { + ram.put(scope.getAddress() + i, code[i]); + } + } + } + } catch (AssemblyException e) { + e.addContext(line); + throw e; + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + void writeObject(File objectPath) { + try { + if (objectPath.getName().toLowerCase().endsWith(".hex")) { + OutputStreamWriter os = new OutputStreamWriter(new FileOutputStream(objectPath)); + os.write(getHex().toString()); + os.write(":00000001FF\r\n"); + os.close(); + } + else { + OutputStream output = new FileOutputStream(objectPath); + output.write(getBinary()); + output.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + byte[] getBinary() { + int from, to; + + from = 0; + while (from < RAM_SIZE && !ram.containsKey(from)) { + from++; + } + to = RAM_SIZE - 1; + while (to > from && !ram.containsKey(to)) { + to--; + } + to++; + + byte[] data = new byte[to - from]; + for (int i = 0; i < data.length; i++) { + if (ram.containsKey(from + i)) { + data[i] = ram.get(from + i); + } + } + + return data; + } + + StringBuilder getHex() { + int from, to; + StringBuilder sb = new StringBuilder(); + + from = 0; + while (from < RAM_SIZE) { + if (ram.containsKey(from)) { + to = from + 1; + while (to < RAM_SIZE && ram.containsKey(to)) { + to++; + } + byte[] data = new byte[to - from]; + for (int i = 0; i < data.length; i++) { + data[i] = ram.get(from + i); + } + sb.append(toHexString(from, data)); + from = to - 1; + } + from++; + } + + return sb; + } + + String toHexString(int addr, byte[] data) { + StringBuilder sb = new StringBuilder(); + + int i = 0; + + while ((data.length - i) > 0) { + int l = data.length - i; + if (l > 24) { + l = 24; + } + sb.append(String.format(":%02X%04X%02X", l, addr, 0)); + + int checksum = l + (addr & 0xFF) + ((addr >> 8) & 0xFF) + 0; + for (int n = 0; n < l; n++, i++, addr++) { + sb.append(String.format("%02X", data[i])); + checksum += data[i]; + } + + sb.append(String.format("%02X\r\n", (-checksum) & 0xFF)); + } + + return sb.toString(); + } + +}