// SPDX-License-Identifier: Apache-2.0
/*
 * Copyright 2025-2026 Richard Lesh
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package cloud.lesh.CPUSim64;

import org.antlr.v4.runtime.*;
import org.antlr.v4.runtime.tree.ParseTree;

import org.apache.commons.lang3.tuple.Pair;

import java.util.*;
import static cloud.lesh.CPUSim64.Simulator.LabelType;

public class AssemblerVisitor extends CPUSim64BaseVisitor<Void> implements HasLocation {
	private Map<String, Long> labelMap;
	private Map<Long, String> reverseLabelMap;
	private Map<String, LabelType> labelTypes = new HashMap<>();
	private final Stack<String> blockNames = new Stack<>();
	private String filename = null;
	int lineNum = 1;
	boolean pauseLineIncrement = false;
	private long blockCount = 0;
	private boolean hasErrors = false;
	private Map<Integer, String> originalSourceLocations;

	public String getLocation(int offendingLine) {
		return (filename == null ? "" : filename + ":") + offendingLine;
	}
	public boolean hasErrors() { return hasErrors; }

	public class AssemblerException extends RuntimeException {
		public AssemblerException(String msg) {
			super(getLocation(lineNum) + ":ERROR:" + msg);
		}
	}

	public AssemblerVisitor(Map<String, Long> labelMap, Map<Long, String> reverseLabelMap) {
		this.labelMap = labelMap;
		this.reverseLabelMap = reverseLabelMap;
	}

	// Result: one 64-bit word per assembled instruction
	private final List<Long> out = new ArrayList<>();

	public List<Long> result() {
		return out;
	}

	// ---- Encoding helpers (64-bit only) ----
	// Type field: 2 bits (tt)
	private static final int TT_STD = 0;  // Type 0 (standard: up to 4 short operands)
	private static final int TT_C1 = 1;  // Type 1 (single long constant C1 = 56-bit signed)
	private static final int TT_YC2 = 2;   // Type 2 ([YZ]C2, 42-bit signed)
	private static final int TT_YYC3 = 3;  // Type 3 ([YZ][YZ]C3, 28-bit signed)

	private static final int C0_BITS = 12; // bits for each of op0..op3 in Type-0
	private static final int C1_BITS = 56; // bits for imm56 in Type-1
	private static final int C2_BITS = 42; // bits for imm42 in Type-2
	private static final int C3_BITS = 28; // bits for imm28 in Type-3

	// Operand type codes for aa/bb/cc/dd fields (2 bits each)
	private static final int OT_NONE = 0;
	private static final int OT_CONST = 1;
	private static final int OT_REG = 2;
	private static final int OT_FP = 3;

	void addWord(LabelType type, long word) {
		if (type != LabelType.CODE) {
			String lastLabel = Simulator.Decoded.findNearestLabel(reverseLabelMap, out.size());
			if (lastLabel != null) {
				labelTypes.put(lastLabel, type);
			}
		}
		out.add(word);
	}

	public Map<String, LabelType> getLabelTypes() { return labelTypes; }

	// ---- packers for each format (bit layout chosen to match your 64-bit diagrams) ----
	// 64-bit Type-0:
	//   [63:62] tt
	//   [61:56] opcode
	//   [55:48] aabbccdd  (2b each)
	//   [47:36] op0 (12b)
	//   [35:24] op1 (12b)
	//   [23:12] op2 (12b)
	//   [11: 0] op3 (12b)
	//
	// NOTE: Your diagram labels bytes as 00/11/22/33; using 12-bit operand slots fits the 0..4095 (12-bit) range you gave for 64-bit.
	private long encType0(int opcode, int a, int b, int c, int d, int v0, int v1, int v2, int v3) {
		long w = 0;
		v0 = (int) fitSigned(v0, 12);
		v1 = (int) fitSigned(v1, 12);
		v2 = (int) fitSigned(v2, 12);
		v3 = (int) fitSigned(v3, 12);
		w |= ((long) TT_STD & 0x3L) << 62;
		w |= ((long) opcode & 0x3FL) << 56;
		long types = ((a & 3) << 6) | ((b & 3) << 4) | ((c & 3) << 2) | (d & 3);
		w |= (types & 0xFFL) << 48;
		w |= ((long) (v0 & 0xFFF)) << 36;
		w |= ((long) (v1 & 0xFFF)) << 24;
		w |= ((long) (v2 & 0xFFF)) << 12;
		w |= ((long) (v3 & 0xFFF));
		return w;
	}

	// 64-bit Type-1 (C1 = 56-bit signed):
	//   [63:62] tt=1
	//   [61:56] opcode
	//   [55: 0] imm56 (two’s complement)
	private long encType1C1(int opcode, long imm56) {
		long w = 0;
		imm56 = fitSigned(imm56, 56);
		w |= ((long) TT_C1 & 0x3L) << 62;
		w |= ((long) opcode & 0x3FL) << 56;
		long masked = imm56 & ((1L << 56) - 1);
		w |= masked;
		return w;
	}

	// 64-bit Type-2 ([YZ]C2, 42-bit signed):
	//   [63:62] tt=2
	//   [61:56] opcode
	//   [55:54] aa (type of operand 0: REG/FP/CONST per table; here [YZ] means REG/FP or A/FP → we encode as REG or FP)
	//   [53:42] op0 (12b)
	//   [41: 0] imm42 (two’s complement)
	private long encType2RC2(int opcode, int aType, int op0, long imm42) {
		long w = 0;
		op0 = (int) fitSigned(op0, 12);
		imm42 = fitSigned(imm42, 42);
		w |= ((long) TT_YC2 & 0x3L) << 62;
		w |= ((long) opcode & 0x3FL) << 56;
		w |= ((long) (aType & 0x3)) << 54;
		w |= ((long) (op0 & 0xFFF)) << 42;
		w |= (imm42 & ((1L << 42) - 1));
		return w;
	}

	// 64-bit Type-3 ([YZ][YZ]C3, 28-bit signed):
	//   [63:62] tt=3
	//   [61:56] opcode
	//   [55:54] aa (op0 type)
	//   [53:52] bb (op1 type)
	//   [51:40] op0 (12b)
	//   [39:28] op1 (12b)
	//   [27: 0] imm28 (two’s complement)
	private long encType3ZZC3(int opcode, int aType, int op0, int bType, int op1, int imm28) {
		long w = 0;
		op0 = (int) fitSigned(op0, 12);
		op1 = (int) fitSigned(op1, 12);
		imm28 = (int) fitSigned(imm28, 28);
		w |= ((long) TT_YYC3 & 0x3L) << 62;
		w |= ((long) opcode & 0x3FL) << 56;
		w |= ((long) (aType & 0x3)) << 54;
		w |= ((long) (bType & 0x3)) << 52;
		w |= ((long) (op0 & 0xFFF)) << 40;
		w |= ((long) (op1 & 0xFFF)) << 28;
		w |= (imm28 & ((1 << 28) - 1));
		return w;
	}

	// ---- Small helpers to classify operands and pull numeric values ----

	// Operand type codes
	private static int typeCodeForReg() {
		return OT_REG;
	}

	private static int typeCodeForFp() {
		return OT_FP;
	}

	private static int typeCodeForConst() {
		return OT_CONST;
	}

	private static int typeCodeNone() {
		return OT_NONE;
	}

	// R registers: r0..r28 (we let semantic pass restrict; here value is numeric index)
	private int regIndex(String rText) {
		// r or R followed by decimal
		int idx = Integer.parseInt(rText.substring(1));
		if (idx < 0 || idx > 28)
			throw new AssemblerException("Register index out of range (0..28): " + rText);
		return idx;
	}

	// F registers: f0..f31
	private int fpIndex(String fText) {
		int idx = Integer.parseInt(fText.substring(1));
		if (idx < 0 || idx > 31)
			throw new AssemblerException("FP index out of range (0..31): " + fText);
		return idx;
	}

	private int aIndexFromToken(Token t) {
		String s = t.getText().toUpperCase();
		if (s.equals("SF")) return 29;
		else if (s.equals("SP")) return 30;
		else if (s.equals("PC")) return 31;
		else if (s.startsWith("R")) return regIndex(s);
		throw new AssemblerException("Not an address-capable register: " + s);
	}

	private long parseIntLike(String text) {
		if (text.startsWith("0x") || text.startsWith("0X")) {
			return Long.parseUnsignedLong(text.substring(2), 16);
		} else if (text.startsWith("-0x") || text.startsWith("-0X")) {
			return -Long.parseUnsignedLong(text.substring(3), 16);
		} else if ((text.charAt(0) == '-') ||
				(text.charAt(0) >= '0' && text.charAt(0) <= '9')) {
			return Long.parseLong(text);
		} else if (text.charAt(0) == '\'') {
			return Utils.parseCharLiteral(text);
		} else {
			if (text.charAt(0) == '@')
				text = text.substring(1);
			if (text.charAt(0) == '$') {
				if (text.charAt(1) == '$') {
					text = getLoopScopeName() + text.substring(1);
				} else {
					text = getScopeName() + text;
				}
			}
			var v = labelMap.get(text.toUpperCase());
			if (v != null)
				return Long.valueOf(v);
			else
				throw new AssemblerException("Invalid integer/label: " + text);
		}
	}

	private Pair<Integer, Long> parseOOperand(CPUSim64Parser.OOperandContext ctx) {
		int d;
		long v;
		if (ctx == null) {
			d = OT_CONST;
			v = 0;
		} else if (ctx.rOperand() != null) {
			d = OT_REG;
			v = regIndex(ctx.rOperand().REG_R().getText());
		} else {
			d = OT_CONST;
			v = parseIntLike(ctx.cLiteral().getText());
		}
		return Pair.of(d, v);
	}

	private Pair<Pair<Integer, Long>, Pair<Integer, Long>> parseMemRef(CPUSim64Parser.MemRefContext ctx) {
		int a = OT_NONE, b;
		long v0 = 0, v1;

		if (ctx.aOperand() != null) {
			a = OT_REG;
			v0 = aIndexFromToken(ctx.aOperand().start);
		} else if (ctx.aLiteral() != null) {
			a = OT_CONST;
			v0 = parseIntLike(ctx.aLiteral().getText());
		}

		if (ctx.rOperand() != null) {
			b = OT_REG;
			v1 = regIndex(ctx.rOperand().REG_R().getText());
		} else if (ctx.cLiteral() != null){
			b = OT_CONST;
			v1 = parseIntLike(ctx.cLiteral().getText());
		} else {
			b = OT_NONE;
			v1 = 0L;
		}
		Pair<Integer, Long> addr = Pair.of(a, v0);
		Pair<Integer, Long> offset = Pair.of(b, v1);
		return Pair.of(addr, offset);
	}

	// zPort or zCond (0..15)
	private int parseZSmall(String text) {
		int v = 0;
		switch (text.toLowerCase()) {
			case "u":
				return 0;
			case "z":
			case "eq":
				return 1;
			case "nz":
			case "ne":
				return 2;
			case "n":
			case "lt":
				return 3;
			case "p":
			case "gt":
				return 4;
			case "nn":
			case "ge":
				return 5;
			case "np":
			case "le":
				return 6;
			case "o":
			case "inf":
				return 7;
			case "no":
			case "ninf":
				return 8;
			case "pe":
				return 9;
			case "po":
				return 10;
			default:
				v = (int) parseIntLike(text);
		}
		if (v < 0 || v > 15) throw new AssemblerException("Z out of range (0..15): " + text);
		return (int) v;
	}

	// sign-range checks for immediates (throw for now; you can downgrade to errors list)
	private long fitSigned(long val, int bits) {
		long min = -(1L << (bits - 1));
		long max = (1L << (bits - 1)) - 1;
		if (val < min || val > max) {
			throw new AssemblerException("Immediate " + val + " does not fit in " + bits + " signed bits");
		}
		// stored as two's complement masked to width:
		long mask = (bits == 64) ? -1L : ((1L << bits) - 1);
		return val & mask;
	}

	private int fitUnsigned8(long v) {
		if (v < 0 || v > 0xFF) throw new AssemblerException("Field needs 8-bit unsigned (0..255): " + v);
		return (int) v;
	}

	private boolean fitsIn(long val, int bits, boolean isSigned) {
		if (isSigned) {
			long min = -(1L << (bits - 1));
			long max = (1L << (bits - 1)) - 1;
			return (val >= min && val <= max);
		} else {
			return (val >= 0 && val <= ((1L << bits) - 1));
		}
	}

	private String getScopeName() {
		return String.join("$", blockNames);
	}

	private String getLoopScopeName() {
		int i = blockNames.size() - 1;
		for (; i >= 0; --i) {
			String name = blockNames.get(i).toUpperCase();
			if (name.startsWith("FOR_") || name.startsWith("WHILE_") || name.startsWith("DO_WHILE_"))
				break;
		}
		return i >= 0 ? String.join("$", blockNames.subList(0, i + 1)) : "";
	}

	@Override
	public Void visitProgram(CPUSim64Parser.ProgramContext ctx) {
		for (var child : ctx.children) {
			try {
				visit(child);
			} catch (AssemblerException ex) {
				System.err.println(ex.getMessage());
				hasErrors = true;
			}
			if (!pauseLineIncrement) ++lineNum;
		}
		return null;
	}

	// --------------------------------------------------------------------
	// VISITORS: each instruction rule chooses a format, gathers operands,
	//           and emits one encoded 64-bit word into `out`.
	// --------------------------------------------------------------------

	@Override
	public Void visitInstrNOP(CPUSim64Parser.InstrNOPContext ctx) {
		long w = encType0(Opcode.NOP.code, OT_NONE, OT_NONE, OT_NONE, OT_NONE, 0, 0, 0, 0);
		addWord(LabelType.CODE, w);
		return null;
	}

	@Override
	public Void visitInstrDEBUG(CPUSim64Parser.InstrDEBUGContext ctx) {
		if (ctx.children.size() == 1) {
			// No operands: just DEBUG
			addWord(LabelType.CODE, encType1C1(Opcode.DEBUG.code, -1));
		} else if (ctx.y1to4() != null) {
			var ys = ctx.y1to4().yOperand();
			int a = OT_NONE, b = OT_NONE, c = OT_NONE, d = OT_NONE;
			int v0 = 0, v1 = 0, v2 = 0, v3 = 0;
			for (int i = 0; i < ys.size(); i++) {
				var y = ys.get(i);
				boolean isFp = (y.fOperand() != null);
				int t = isFp ? OT_FP : OT_REG;   // Y = A or F; encode A (R/SF/SP/PC) as REG here with A-index
				int v = isFp ? fpIndex(y.fOperand().REG_F().getText())
						: aIndexFromToken(y.aOperand().start);
				if (i == 0) {
					a = t;
					v0 = v;
				} else if (i == 1) {
					b = t;
					v1 = v;
				} else if (i == 2) {
					c = t;
					v2 = v;
				} else {
					d = t;
					v3 = v;
				}
			}
			addWord(LabelType.CODE, encType0(Opcode.DEBUG.code, a, b, c, d, v0, v1, v2, v3));
		} else {
			// TODO Is this really the best way to do this?
			// AC form: treat as Type-3: op0=addr (A), op1 unused, imm28=count
			int aType = OT_REG; // A encoded via REG code + A-index
			long cnt = parseIntLike(ctx.cLiteral().getText());
			int op0;
			long k;
			if (ctx.aOperand() != null) {
				op0 = aIndexFromToken(ctx.aOperand().start);
				addWord(LabelType.CODE, encType0(Opcode.DEBUG.code, aType, OT_CONST, OT_NONE, OT_NONE,
						op0, (int)cnt, 0, 0));
			} else if (ctx.aLiteral() != null) {
				k = parseIntLike(ctx.aLiteral().getText());
				addWord(LabelType.CODE, encType2RC2(Opcode.DEBUG.code, OT_CONST, (int)cnt, k));
			} else {
				throw new AssemblerException("DEBUG with C literal needs A or R operand");
			}
		}
		return null;
	}

	@Override
	public Void visitInstrCLEAR(CPUSim64Parser.InstrCLEARContext ctx) {
		// N or X..XXXX → always Type-0
		// We’ll flatten y1to4/x1to4 counts into up to four REG/FP fields; unused are NONE/0
		List<CPUSim64Parser.XOperandContext> xs = ctx.x1to4() == null ? List.of() : ctx.x1to4().xOperand();
		int a = OT_NONE, b = OT_NONE, c = OT_NONE, d = OT_NONE;
		int v0 = 0, v1 = 0, v2 = 0, v3 = 0;

		for (int i = 0; i < xs.size(); i++) {
			var x = xs.get(i);
			boolean isFp = x.fOperand() != null;
			int t = isFp ? OT_FP : OT_REG;
			int v = isFp ? fpIndex(x.fOperand().REG_F().getText())
					: regIndex(x.rOperand().REG_R().getText());
			if (i == 0) {
				a = t;
				v0 = v;
			} else if (i == 1) {
				b = t;
				v1 = v;
			} else if (i == 2) {
				c = t;
				v2 = v;
			} else {
				d = t;
				v3 = v;
			}
		}
		long w = encType0(Opcode.CLEAR.code, a, b, c, d, v0, v1, v2, v3);
		addWord(LabelType.CODE, w);
		return null;
	}

	@Override
	public Void visitInstrMOVE(CPUSim64Parser.InstrMOVEContext ctx) {
		int a = OT_NONE, b = OT_NONE, c = OT_NONE, d = OT_NONE;
		int v0 = 0, v1 = 0, v2 = 0, v3 = 0;
		long k;
		if (ctx.zCond() != null) {
			// ZYQQ
			int z = parseZSmall(ctx.zCond().getText());
			a = OT_CONST;
			v0 = z;

			CPUSim64Parser.YOperandContext y = ctx.yOperand(0);
			boolean yIsFp = (y.fOperand() != null);
			b = yIsFp ? OT_FP : OT_REG;
			v1 = yIsFp
					? fpIndex(y.fOperand().REG_F().getText())
					: aIndexFromToken(y.aOperand().start);

			CPUSim64Parser.QOperandContext q1 = ctx.qOperand(0);
			if (q1.fOperand() != null) {
				c = OT_FP;
				v2 = fpIndex(q1.fOperand().REG_F().getText());
			} else if (q1.aOperand() != null) {
				c = OT_REG;
				v2 = aIndexFromToken(q1.aOperand().start);
			} else {
				c = OT_CONST;
				v2 = (int)parseIntLike(q1.cLiteral().getText());
			}

			CPUSim64Parser.QOperandContext q2 = ctx.qOperand(1);
			if (q2.fOperand() != null) {
				d = OT_FP;
				v3 = fpIndex(q2.fOperand().REG_F().getText());
			} else if (q2.aOperand() != null) {
				d = OT_REG;
				v3 = aIndexFromToken(q2.aOperand().start);
			} else {
				d = OT_CONST;
				v3 = (int)parseIntLike(q2.cLiteral().getText());
			}
			addWord(LabelType.CODE, encType0(Opcode.MOVE.code, a, b, c, d, v0, v1, v2, v3));
		} else if (ctx.yOperand().size() == 2) {
			// YY
			CPUSim64Parser.YOperandContext y0 = ctx.yOperand(0);
			CPUSim64Parser.YOperandContext y1 = ctx.yOperand(1);

			a = (y0.fOperand() != null) ? OT_FP : OT_REG;
			v0 = (y0.fOperand() != null) ? fpIndex(y0.fOperand().REG_F().getText())
					: aIndexFromToken(y0.aOperand().start);

			b = (y1.fOperand() != null) ? OT_FP : OT_REG;
			v1 = (y1.fOperand() != null) ? fpIndex(y1.fOperand().REG_F().getText())
					: aIndexFromToken(y1.aOperand().start);
			addWord(LabelType.CODE, encType0(Opcode.MOVE.code, a, b, c, d, v0, v1, v2, v3));
		} else if (ctx.yOperand().size() == 1 && ctx.cLiteral() != null) {
			// YC
			CPUSim64Parser.YOperandContext y = ctx.yOperand(0);
			a = (y.fOperand() != null) ? OT_FP : OT_REG;
			v0 = (y.fOperand() != null) ? fpIndex(y.fOperand().REG_F().getText())
					: aIndexFromToken(y.aOperand().start);
			b = OT_CONST;
			k =parseIntLike(ctx.cLiteral().getText());
			addWord(LabelType.CODE, encType2RC2(Opcode.MOVE.code, a, v0, k));
		} else if (ctx.aOperand(0) != null && ctx.aOperand(1) != null && ctx.rOperand() != null) {
			// AAR
			a = OT_REG;
			v0 = aIndexFromToken(ctx.aOperand(0).start);
			b = OT_REG;
			v1 = aIndexFromToken(ctx.aOperand(1).start);
			c = OT_REG;
			v2 = regIndex(ctx.rOperand().REG_R().getText());
			addWord(LabelType.CODE, encType0(Opcode.MOVE.code, a, b, c, d, v0, v1, v2, v3));
		} else if (ctx.aOperand(0) != null && ctx.aOperand(1) != null && ctx.aLiteral() != null) {
			// AAC
			a = OT_REG;
			v0 = aIndexFromToken(ctx.aOperand(0).start);
			b = OT_REG;
			v1 = aIndexFromToken(ctx.aOperand(1).start);
			c = OT_CONST;
			v2 = (int)parseIntLike(ctx.aLiteral().getText());
			addWord(LabelType.CODE, encType3ZZC3(Opcode.MOVE.code, a, v0, b, v1, v2));
		} else if (ctx.aOperand().size() == 2 && ctx.aLiteral() != null) {
			// ACA
			a = OT_REG;
			v0 = aIndexFromToken(ctx.aOperand(0).start);
			c = OT_CONST;
			v2 = (int)parseIntLike(ctx.aLiteral().getText());
			b = OT_REG;
			v1 = aIndexFromToken(ctx.aOperand(1).start);
			addWord(LabelType.CODE, encType3ZZC3(Opcode.MOVE.code, a, v0, b, v1, v2));
		} else {
			throw new AssemblerException("Unhandled MOVE form");
		}
		return null;
	}

	@Override
	public Void visitInstrLOAD(CPUSim64Parser.InstrLOADContext ctx) {
		var y = ctx.yOperand();
		boolean yIsFp = y.fOperand() != null;
		int a = yIsFp ? OT_FP : OT_REG;
		int v0 = yIsFp ? fpIndex(y.fOperand().REG_F().getText())
				: aIndexFromToken(y.aOperand().start);

		int b = OT_NONE, c = OT_NONE, d = OT_NONE;
		long v1 = 0, v2 = 0, v3 = 0;

		var memRef = parseMemRef(ctx.memRef());
		b = memRef.getLeft().getLeft();
		c = memRef.getRight().getLeft();
		v1 = memRef.getLeft().getRight();
		v2 = memRef.getRight().getRight();

		if (b == OT_CONST && c == OT_NONE) {     			// Y[C]
			addWord(LabelType.CODE, encType2RC2(Opcode.LOAD.code, a, v0, v1));
		} else if (b == OT_REG && c == OT_NONE) {           // Y[A]
			addWord(LabelType.CODE, encType0(Opcode.LOAD.code, a, b, c, d, v0, (int)v1, (int)v2, (int)v3));
		} else if (b == OT_REG && c == OT_CONST) {      	// Y[A+C]
			addWord(LabelType.CODE, encType3ZZC3(Opcode.LOAD.code, a, v0, b, (int)v1, (int)v2));
		} else if (b == OT_CONST && c == OT_REG) {			// Y[C+A]
			addWord(LabelType.CODE, encType3ZZC3(Opcode.LOAD.code, a, v0, c, (int)v2, (int)v1));
		} else if (b == OT_CONST && c == OT_CONST) {  		// Y[C+C]
			if (fitsIn(v2, C0_BITS, true)) {
				addWord(LabelType.CODE, encType3ZZC3(Opcode.LOAD.code, a, v0, c, (int)v2, (int)v1));
			} else if (fitsIn(v1, C0_BITS, true)) {
				addWord(LabelType.CODE, encType3ZZC3(Opcode.LOAD.code, a, v0, b, (int)v1, (int)v2));
			} else {
				throw new AssemblerException("LOAD [C+C] requires one constant to fit in 12 bits");
			}
		} else if (b == OT_REG && c == OT_REG) {      		// Y[A+R]
			addWord(LabelType.CODE, encType0(Opcode.LOAD.code, a, b, c, d, v0, (int)v1, (int)v2, (int)v3));
		} else {
			throw new AssemblerException("Unhandled LOAD form");
		}
		return null;
	}

	@Override
	public Void visitInstrSTORE(CPUSim64Parser.InstrSTOREContext ctx) {
		// Q , [addr] → Type-0
		int a, v0;
		var q = ctx.qOperand();
		if (q.fOperand() != null) {
			a = OT_FP;
			v0 = fpIndex(q.fOperand().REG_F().getText());
		} else if (q.aOperand() != null) {
			a = OT_REG;
			v0 = aIndexFromToken(q.aOperand().start);
		} else {
			a = OT_CONST;
			v0 = (int)parseIntLike(q.cLiteral().getText());
		}

		int b = OT_NONE, c = OT_NONE, d = OT_NONE;
		long v1 = 0, v2 = 0, v3 = 0;

		var memRef = parseMemRef(ctx.memRef());
		b = memRef.getLeft().getLeft();
		c = memRef.getRight().getLeft();
		v1 = memRef.getLeft().getRight();
		v2 = memRef.getRight().getRight();

		if (b == OT_CONST && c == OT_NONE) {       			// [C]
			addWord(LabelType.CODE, encType2RC2(Opcode.STORE.code, a, v0, v1));
		} else if (b == OT_REG && c == OT_NONE) {           // [A]
			addWord(LabelType.CODE, encType0(Opcode.STORE.code, a, b, c, d, v0, (int)v1, (int)v2, (int)v3));
		} else if (b == OT_REG && c == OT_CONST) {      	// [A+C]
			addWord(LabelType.CODE, encType3ZZC3(Opcode.STORE.code, a, v0, b, (int)v1, (int)v2));
		} else if (b == OT_CONST && c == OT_REG) {      	// [C+A]
			addWord(LabelType.CODE, encType3ZZC3(Opcode.STORE.code, a, v0, c, (int)v2, (int)v1));
		} else if (b == OT_CONST && c == OT_CONST) {      	// [C+C]
			if (fitsIn(v2, C0_BITS, true)) {
				addWord(LabelType.CODE, encType3ZZC3(Opcode.STORE.code, a, v0, b, (int)v2, (int)v1));
			} else if (fitsIn(v1, C0_BITS, true)) {
				addWord(LabelType.CODE, encType3ZZC3(Opcode.STORE.code, a, v0, b, (int)v1, (int)v2));
			} else {
				throw new AssemblerException("LOAD [C+C] requires one constant to fit in 12 bits");
			}
		} else if (b == OT_REG && c == OT_REG) {      		// [A+R]
			addWord(LabelType.CODE, encType0(Opcode.STORE.code, a, b, c, d, v0, (int)v1, (int)v2, (int)v3));
		} else {
			throw new AssemblerException("Unhandled LOAD form");
		}
		return null;
	}

	@Override
	public Void visitInstrPOP(CPUSim64Parser.InstrPOPContext ctx) {
		if (ctx.yOperand() == null) {
			addWord(LabelType.CODE, encType0(Opcode.POP.code, OT_NONE, OT_NONE, OT_NONE, OT_NONE, 0, 0, 0, 0));
		} else {
			var y = ctx.yOperand();
			boolean isFp = y.fOperand() != null;
			int t = isFp ? OT_FP : OT_REG;
			int v = isFp ? fpIndex(y.fOperand().REG_F().getText())
					: aIndexFromToken(y.aOperand().start);
			addWord(LabelType.CODE, encType0(Opcode.POP.code, t, OT_NONE, OT_NONE, OT_NONE, v, 0, 0, 0));
		}
		return null;
	}

	@Override
	public Void visitInstrPUSH(CPUSim64Parser.InstrPUSHContext ctx) {
		int aType, v0;
		if (ctx.yOperand() != null) {
			var y = ctx.yOperand();
			boolean isFp = y.fOperand() != null;
			aType = isFp ? OT_FP : OT_REG;
			v0 = isFp ? fpIndex(y.fOperand().REG_F().getText())
					: aIndexFromToken(y.aOperand().start);
			addWord(LabelType.CODE, encType0(Opcode.PUSH.code, aType, OT_NONE, OT_NONE, OT_NONE, v0, 0, 0, 0));
		} else {
			long k = parseIntLike(ctx.cLiteral().getText());
			addWord(LabelType.CODE, encType1C1(Opcode.PUSH.code, k));
		}
		return null;
	}

	@Override
	public Void visitInstrJUMP(CPUSim64Parser.InstrJUMPContext ctx) {
		visitInstrBRANCH(Opcode.JUMP, ctx.branchModes());
		return null;
	}

	@Override
	public Void visitInstrCALL(CPUSim64Parser.InstrCALLContext ctx) {
		visitInstrBRANCH(Opcode.CALL, ctx.branchModes());
		return null;
	}

	private Void visitInstrBRANCH(Opcode opc, CPUSim64Parser.BranchModesContext ctx) {
		int a = OT_NONE, b = OT_NONE, c = OT_NONE, d = OT_NONE;
		long v0 = 0, v1 = 0, v2 = 0, v3 = 0;

		var memRef = parseMemRef(ctx.memRef());

		// Non-conditional: JUMP A | JUMP C
		if (ctx.zCond() == null) {
			a = memRef.getLeft().getLeft();
			b = memRef.getRight().getLeft();
			v0 = memRef.getLeft().getRight();
			v1 = memRef.getRight().getRight();
			if (a == OT_CONST && b == OT_NONE) {     				// C
				addWord(LabelType.CODE, encType1C1(opc.code, v0));
			} else if (a == OT_REG && b == OT_NONE) {          		// A
				addWord(LabelType.CODE, encType0(opc.code, a, b, c, d, (int)v0, (int)v1, (int)v2, (int)v3));
			} else if (a == OT_REG && b == OT_CONST) {      		// A[C]
				addWord(LabelType.CODE, encType2RC2(opc.code, a, (int)v0, v1));
			} else if (a == OT_CONST && b == OT_REG) {				// C[A]
				addWord(LabelType.CODE, encType2RC2(opc.code, b, (int)v1, v0));
			} else if (a == OT_CONST && b == OT_CONST) {  			// C[C]
				if (fitsIn(v0, C0_BITS, true)) {
					addWord(LabelType.CODE, encType3ZZC3(opc.code, OT_CONST, Condition.U.ordinal(), a, (int)v0, (int)v1));
				} else if (fitsIn(v1, C0_BITS, true)) {
					addWord(LabelType.CODE, encType3ZZC3(opc.code, OT_CONST, Condition.U.ordinal(), b, (int)v1, (int)v0));
				} else {
					throw new AssemblerException("BRANCH [C+C] requires one constant to fit in 12 bits");
				}
			} else if (a == OT_REG && b == OT_REG) {      			// A[R]
				addWord(LabelType.CODE, encType0(opc.code, a, b, c, d, (int)v0, (int)v1, (int)v2, (int)v3));
			} else {
				throw new AssemblerException("Unhandled BRANCH form");
			}
		} else {
			// Conditional forms: Z + (A | C | AC | CA | CC | AR)
			final int z = parseZSmall(ctx.zCond().getText());
			final int zt = OT_CONST; // op0 = Z (small)

			a = memRef.getLeft().getLeft();
			b = memRef.getRight().getLeft();
			v0 = memRef.getLeft().getRight();
			v1 = memRef.getRight().getRight();
			if (a == OT_CONST && b == OT_NONE) {                	// Z,C
				addWord(LabelType.CODE, encType2RC2(opc.code, zt, z, v0));
			} else if (a == OT_REG && b == OT_NONE) {           	// Z,A
				addWord(LabelType.CODE, encType0(opc.code, zt, a, b, c, z, (int) v0, (int) v1, (int) v2));
			} else if (a == OT_REG && b == OT_CONST) {        		// Z,A[C]
				addWord(LabelType.CODE, encType3ZZC3(opc.code, zt, z, a, (int) v0, (int)v1));
			} else if (a == OT_CONST && b == OT_REG) {            	// Z,C[A]
				addWord(LabelType.CODE, encType3ZZC3(opc.code, zt, z, b, (int) v1, (int)v0));
			} else if (a == OT_CONST && b == OT_CONST) {        	// Z,C[C]
				if (fitsIn(v0, C0_BITS, true)) {
					addWord(LabelType.CODE, encType3ZZC3(opc.code, zt, z, a, (int) v0, (int)v1));
				} else if (fitsIn(v1, C0_BITS, true)) {
					addWord(LabelType.CODE, encType3ZZC3(opc.code, zt, z, b, (int) v1, (int)v0));
				} else {
					throw new AssemblerException("BRANCH [C+C] requires one constant to fit in 12 bits");
				}
			} else if (a == OT_REG && b == OT_REG) {            // Y[A+R]
				addWord(LabelType.CODE, encType0(opc.code, zt, a, b, c, z, (int) v0, (int) v1, (int) v2));
			} else {
				throw new AssemblerException("Unhandled BRANCH form");
			}
		}
		return null;
	}

	@Override
	public Void visitInstrRETURN(CPUSim64Parser.InstrRETURNContext ctx) {
		long w = encType0(Opcode.RETURN.code, OT_NONE, OT_NONE, OT_NONE, OT_NONE, 0, 0, 0, 0);
		addWord(LabelType.CODE, w);
		return null;
	}

	@Override
	public Void visitInstrINTERRUPT(CPUSim64Parser.InstrINTERRUPTContext ctx) {
		// Type-0: single short operand (R or C)
		int aType;
		long v0;
		if (ctx.rOperand() != null) {
			aType = OT_REG;
			v0 = regIndex(ctx.rOperand().REG_R().getText());
			addWord(LabelType.CODE, encType0(Opcode.INTERRUPT.code, aType, OT_NONE, OT_NONE, OT_NONE, (int) v0, 0, 0, 0));
		} else {
			v0 = parseIntLike(ctx.cLiteral().getText());
			if (fitsIn(v0, C0_BITS, false)) {
				aType = OT_CONST;
				v0 = v0;
				addWord(LabelType.CODE, encType0(Opcode.INTERRUPT.code, aType, OT_NONE, OT_NONE, OT_NONE, (int) v0, 0, 0, 0));
			} else {
				addWord(LabelType.CODE, encType1C1(Opcode.INTERRUPT.code, v0));
			}
		}
		return null;
	}

	@Override
	public Void visitInstrSTOP(CPUSim64Parser.InstrSTOPContext ctx) {
		long w = encType0(Opcode.STOP.code, OT_NONE, OT_NONE, OT_NONE, OT_NONE, 0, 0, 0, 0);
		addWord(LabelType.CODE, w);
		return null;
	}

	@Override
	public Void visitInstrNEG(CPUSim64Parser.InstrNEGContext ctx) {
		// instrNEG : NEG xOperand ;
		// xOperand : rOperand | fOperand ;

		int aType;
		int v0;

		// Prefer the explicit branch the parser chose
		if (ctx.xOperand().fOperand() != null) {
			aType = OT_FP;
			v0 = fpIndex(ctx.xOperand().fOperand().REG_F().getText());
		} else if (ctx.xOperand().rOperand() != null) {
			aType = OT_REG;
			v0 = regIndex(ctx.xOperand().rOperand().REG_R().getText());
		} else {
			// This should be unreachable with your xOperand rule
			throw new IllegalStateException("NEG: xOperand is neither rOperand nor fOperand");
		}

		addWord(LabelType.CODE, encType0(Opcode.NEGATE.code, aType, OT_NONE, OT_NONE, OT_NONE, v0, 0, 0, 0
		));
		return null;
	}

/* ============================
   ARITHMETIC (ADD / SUB / MULT)
   ============================ */

	public Void visitArithmeticModes(Opcode opc, CPUSim64Parser.ArithmeticModesContext ctx) {
		int a = OT_NONE, b = OT_NONE, c = OT_NONE, d = OT_NONE;
		int v0 = 0, v1 = 0, v2 = 0, v3 = 0;
		long k = 0;

		// How many of each did this alt produce?
		final int nA = (ctx.aOperand() == null) ? 0 : ctx.aOperand().size();
		final int nF = (ctx.fOperand() == null) ? 0 : ctx.fOperand().size();
		final int nR = (ctx.rOperand() == null) ? 0 : 1;
		final int nX = (ctx.xOperand() == null) ? 0 : 1;
		final int nY = (ctx.yOperand() == null) ? 0 : 1;
		final int nC = (ctx.cLiteral() == null) ? 0 : 1;

		// AR
		if (nA == 1 && nR == 1 && nF == 0 && nX == 0 && nY == 0 && nC == 0) {
			a = OT_REG;
			v0 = aIndexFromToken(ctx.aOperand(0).start);
			b = OT_REG;
			v1 = regIndex(ctx.rOperand().getStart().getText());
			addWord(LabelType.CODE, encType0(opc.code, a, b, c, d, v0, v1, v2, v3));
		}
		// FX  (F , X)  where X is R|F
		else if (nF == 1 && nX == 1) {
			a = OT_FP;
			v0 = fpIndex(ctx.fOperand(0).getStart().getText());
			if (ctx.xOperand().fOperand() != null) {
				b = OT_FP;
				v1 = fpIndex(ctx.xOperand().fOperand().getStart().getText());
			} else {
				b = OT_REG;
				v1 = regIndex(ctx.xOperand().rOperand().getStart().getText());
			}
			addWord(LabelType.CODE, encType0(opc.code, a, b, c, d, v0, v1, v2, v3));
		}
		// YC  (Y , C)  where Y is A|F
		else if (nY == 1 && nC == 1) {
			boolean yIsFp = (ctx.yOperand().fOperand() != null);
			a = yIsFp ? OT_FP : OT_REG;
			v0 = yIsFp
					? fpIndex(ctx.yOperand().fOperand().getStart().getText())
					: aIndexFromToken(ctx.yOperand().aOperand().start);
			k =parseIntLike(ctx.cLiteral().getText());
			addWord(LabelType.CODE, encType2RC2(opc.code, a, v0, k));
		}
		// AAR (A, A, R)
		else if (nA == 2 && nR == 1 && nC == 0 && nF <= 1 && nX == 0) {
			a = OT_REG;
			v0 = aIndexFromToken(ctx.aOperand(0).start);
			b = OT_REG;
			v1 = aIndexFromToken(ctx.aOperand(1).start);
			c = OT_REG;
			v2 = regIndex(ctx.rOperand().getStart().getText());
			addWord(LabelType.CODE, encType0(opc.code, a, b, c, d, v0, v1, v2, v3));
		}
		// FFX (F, F, X)
		else if (nF == 2 && nX == 1) {
			a = OT_FP;
			v0 = fpIndex(ctx.fOperand(0).getStart().getText());
			b = OT_FP;
			v1 = fpIndex(ctx.fOperand(1).getStart().getText());
			if (ctx.xOperand().fOperand() != null) {
				c = OT_FP;
				v2 = fpIndex(ctx.xOperand().fOperand().getStart().getText());
			} else {
				c = OT_REG;
				v2 = regIndex(ctx.xOperand().rOperand().getStart().getText());
			}
			addWord(LabelType.CODE, encType0(opc.code, a, b, c, d, v0, v1, v2, v3));
		}
		// AAC vs ACA  (both have nA==2 and nC==1) → decide by token order
		else if (nA == 2 && nC == 1 && nF == 0 && nX == 0) {
			a = OT_REG;
			v0 = aIndexFromToken(ctx.aOperand(0).start);
			b = OT_REG;
			v1 = aIndexFromToken(ctx.aOperand(1).start);
			v2 = (int)parseIntLike(ctx.cLiteral().getText());
			addWord(LabelType.CODE, encType3ZZC3(opc.code, a, v0, b, v1, v2));
		}
		// FFC vs FCF (nF==2, nC==1)
		else if (nF == 2 && nC == 1 && nX == 0) {
			a = OT_FP;
			v0 = fpIndex(ctx.fOperand(0).getStart().getText());
			b = OT_FP;
			v1 = fpIndex(ctx.fOperand(1).getStart().getText());
			v2 = (int)parseIntLike(ctx.cLiteral().getText());
			addWord(LabelType.CODE, encType3ZZC3(opc.code, a, v0, b, v1, v2));
		} else {
			throw new AssemblerException("Unhandled " + opc.name() + " form");
		}
		return null;
	}

	public Void visitInstrADD(CPUSim64Parser.InstrADDContext ctx) {
		visitArithmeticModes(Opcode.ADD, ctx.arithmeticModes());
		return null;
	}

	public Void visitInstrSUB(CPUSim64Parser.InstrSUBContext ctx) {
		visitArithmeticModes(Opcode.SUBTRACT, ctx.arithmeticModes());
		return null;
	}

	public Void visitInstrMULT(CPUSim64Parser.InstrMULTContext ctx) {
		visitArithmeticModes(Opcode.MULTIPLY, ctx.arithmeticModes());
		return null;
	}

	@Override
	public Void visitInstrDIV(CPUSim64Parser.InstrDIVContext ctx) {
		int a = OT_NONE, b = OT_NONE, c = OT_NONE, d = OT_NONE;
		int v0 = 0, v1 = 0, v2 = 0, v3 = 0;

		final int nR = (ctx.rOperand() == null) ? 0 : ctx.rOperand().size();
		final int nC = (ctx.cLiteral() == null) ? 0 : 1;

		// 10) RRRR
		if (nR == 4 && nC == 0) {
			a = OT_REG;
			v0 = regIndex(ctx.rOperand(0).getStart().getText());
			b = OT_REG;
			v1 = regIndex(ctx.rOperand(1).getStart().getText());
			c = OT_REG;
			v2 = regIndex(ctx.rOperand(2).getStart().getText());
			d = OT_REG;
			v3 = regIndex(ctx.rOperand(3).getStart().getText());
			addWord(LabelType.CODE, encType0(Opcode.DIVIDE.code, a, b, c, d, v0, v1, v2, v3)); // use Opcode.DIV if that's your enum
		}
		// 11) RRRC (R, R, R, C)
		else if (nR == 3 && nC == 1) {
			a = OT_REG;
			v0 = regIndex(ctx.rOperand(0).getStart().getText());
			b = OT_REG;
			v1 = regIndex(ctx.rOperand(1).getStart().getText());
			c = OT_REG;
			v2 = regIndex(ctx.rOperand(2).getStart().getText());
			d = OT_CONST;
			v3 = (int)parseIntLike(ctx.cLiteral().getText());
			addWord(LabelType.CODE, encType0(Opcode.DIVIDE.code, a, b, c, d, v0, v1, v2, v3)); // use Opcode.DIV if that's your enum
		} else {
			visitArithmeticModes(Opcode.DIVIDE, ctx.arithmeticModes());
		}
		return null;
	}

	@Override
	public Void visitInstrRECIP(CPUSim64Parser.InstrRECIPContext ctx) {
		// RECIP R
		int v0 = fpIndex(ctx.fOperand().REG_F().getText());
		addWord(LabelType.CODE, encType0(Opcode.RECIP.code, OT_FP, OT_NONE, OT_NONE, OT_NONE,
				v0, 0, 0, 0));
		return null;
	}

	/* ============================
   LOGICAL (COMPL / AND / OR / XOR / TEST / CMP)
   ============================ */

	@Override
	public Void visitInstrCOMPL(CPUSim64Parser.InstrCOMPLContext ctx) {
		// COMPL R
		int v0 = regIndex(ctx.rOperand().REG_R().getText());
		addWord(LabelType.CODE, encType0(Opcode.COMPL.code, OT_REG, OT_NONE, OT_NONE, OT_NONE,
				v0, 0, 0, 0));
		return null;
	}

	public Void visitLogicModes(Opcode opc, CPUSim64Parser.LogicModesContext ctx) {
		// RR | RC | RRR | RRC | RCR | RCC
		int a = OT_REG, v0 = regIndex(ctx.rOperand(0).REG_R().getText());
		int b, v1, c = OT_NONE, v2 = 0;
		long k;

		if (ctx.rOperand().size() == 2 && ctx.cLiteral().size() == 0) {
			// RR
			b = OT_REG;
			v1 = regIndex(ctx.rOperand(1).REG_R().getText());
			addWord(LabelType.CODE, encType0(opc.code, a, b, c, OT_NONE, v0, v1, v2, 0));
		} else if (ctx.rOperand().size() == 1 && ctx.cLiteral().size() == 1) {
			// RC
			b = OT_CONST;
			k =parseIntLike(ctx.cLiteral(0).getText());
			addWord(LabelType.CODE, encType2RC2(opc.code, a, v0, k));
		} else if (ctx.rOperand().size() == 3 && ctx.cLiteral().size() == 0) {
			// RRR
			b = OT_REG;
			v1 = regIndex(ctx.rOperand(1).REG_R().getText());
			c = OT_REG;
			v2 = regIndex(ctx.rOperand(2).REG_R().getText());
			addWord(LabelType.CODE, encType0(opc.code, a, b, c, OT_NONE, v0, v1, v2, 0));
		} else if (ctx.rOperand().size() == 2 && ctx.cLiteral().size() == 1) {
			// RRC or RCR
			b = OT_REG;
			v1 = regIndex(ctx.rOperand(1).REG_R().getText());
			if (ctx.rightC != null) {
				v2 = (int) parseIntLike(ctx.rightC.getText());
				addWord(LabelType.CODE, encType3ZZC3(opc.code, a, v0, b, v1, v2));
			} else {
				v2 = (int) parseIntLike(ctx.leftC.getText());
				addWord(LabelType.CODE, encType0(opc.code, a, OT_CONST, b, OT_NONE, v0, v2, v1, 0));
			}
		} else if (ctx.rOperand().size() == 1 && ctx.cLiteral().size() == 2) {
			// RCC
			b = OT_CONST;
			v1 = (int) parseIntLike(ctx.cLiteral(0).getText());
			v2 = (int) parseIntLike(ctx.cLiteral(1).getText());
			addWord(LabelType.CODE, encType0(opc.code, a, b, b, OT_NONE, v0, v1, v2, 0));
		}
		return null;
	}

	@Override
	public Void visitInstrAND(CPUSim64Parser.InstrANDContext ctx) {
		visitLogicModes(Opcode.AND, ctx.logicModes());
		return null;
	}

	@Override
	public Void visitInstrOR(CPUSim64Parser.InstrORContext ctx) {
		visitLogicModes(Opcode.OR, ctx.logicModes());
		return null;
	}

	@Override
	public Void visitInstrXOR(CPUSim64Parser.InstrXORContext ctx) {
		visitLogicModes(Opcode.XOR, ctx.logicModes());
		return null;
	}

	@Override
	public Void visitInstrTEST(CPUSim64Parser.InstrTESTContext ctx) {
		// TEST X
		int a = 0, v0 = 0;
		var x = ctx.xOperand();
		if (x.fOperand()!=null) { a=OT_FP; v0=fpIndex(x.fOperand().REG_F().getText()); }
		else if (x.rOperand()!=null) { a=OT_REG; v0=regIndex(x.rOperand().REG_R().getText()); }
		addWord(LabelType.CODE, encType0(Opcode.TEST.code, a, OT_NONE, OT_NONE, OT_NONE,
				v0, 0, 0, 0));
		return null;
	}

	@Override
	public Void visitInstrCMP(CPUSim64Parser.InstrCMPContext ctx) {
		// AA | AC | CA | FF
		int a,b, v0, v1;
		long k;

		if (ctx.aOperand(0)!=null && ctx.aOperand(1)!=null) {
			// AA
			a=OT_REG; v0=aIndexFromToken(ctx.aOperand(0).start);
			b=OT_REG; v1=aIndexFromToken(ctx.aOperand(1).start);
			addWord(LabelType.CODE, encType0(Opcode.CMP.code, a,b, OT_NONE, OT_NONE, v0, v1, 0, 0));
		} else if (ctx.aOperand()!=null && ctx.cLiteral()!=null) {
			// AC, CA
			if (ctx.rightC != null) {
				// AC
				a = OT_REG; v0 = aIndexFromToken(ctx.aOperand(0).start);
				k = parseIntLike(ctx.cLiteral().getText());
				addWord(LabelType.CODE, encType2RC2(Opcode.CMP.code, a, v0, k));
			} else {
				// CA
				a = OT_CONST; v0 = (int)parseIntLike(ctx.cLiteral().getText());
				b = OT_REG; v1 = aIndexFromToken(ctx.aOperand(0).start);
				addWord(LabelType.CODE, encType0(Opcode.CMP.code, a,b, OT_NONE, OT_NONE, v0, v1, 0, 0));
			}
		} else {
			// FF
			a=OT_FP; v0=fpIndex(ctx.fOperand(0).REG_F().getText());
			b=OT_FP; v1=fpIndex(ctx.fOperand(1).REG_F().getText());
			addWord(LabelType.CODE, encType0(Opcode.CMP.code, a,b, OT_NONE, OT_NONE, v0, v1, 0, 0));
		}

		return null;
	}

/* ============================
   SHIFTS / ROTATES (LSHIFT / RSHIFT / ARSHIFT / LROTATE / RROTATE)
   ============================ */

	@Override
	public Void visitInstrLSHIFT(CPUSim64Parser.InstrLSHIFTContext ctx) {
		visitLogicModes(Opcode.LSHIFT, ctx.logicModes());
		return null;
	}

	@Override
	public Void visitInstrRSHIFT(CPUSim64Parser.InstrRSHIFTContext ctx) {
		visitLogicModes(Opcode.RSHIFT, ctx.logicModes());
		return null;
	}

	@Override
	public Void visitInstrARSHIFT(CPUSim64Parser.InstrARSHIFTContext ctx) {
		visitLogicModes(Opcode.ARSHIFT, ctx.logicModes());
		return null;
	}

	@Override
	public Void visitInstrLROTATE(CPUSim64Parser.InstrLROTATEContext ctx) {
		visitLogicModes(Opcode.LROTATE, ctx.logicModes());
		return null;
	}

	@Override
	public Void visitInstrRROTATE(CPUSim64Parser.InstrRROTATEContext ctx) {
		visitLogicModes(Opcode.RROTATE, ctx.logicModes());
		return null;
	}

   /* ============================
   PACK / PACK64 / UNPACK / UNPACK64
   ============================ */

	@Override
	public Void visitInstrPACK(CPUSim64Parser.InstrPACKContext ctx) {
		// RR | RRR | RRRR    (all R-only)
		int a=OT_REG, v0=regIndex(ctx.rOperand(0).REG_R().getText());
		int b=OT_NONE,c=OT_NONE,d=OT_NONE, v1=0,v2=0,v3=0;
		if (ctx.rOperand().size() >= 2) { b=OT_REG; v1=regIndex(ctx.rOperand(1).REG_R().getText()); }
		if (ctx.rOperand().size() >= 3) { c=OT_REG; v2=regIndex(ctx.rOperand(2).REG_R().getText()); }
		if (ctx.rOperand().size() >= 4) { d=OT_REG; v3=regIndex(ctx.rOperand(3).REG_R().getText()); }
		addWord(LabelType.CODE, encType0(Opcode.PACK.code, a,b,c,d, v0, v1, v2, v3));
		return null;
	}

	@Override
	public Void visitInstrPACK64(CPUSim64Parser.InstrPACK64Context ctx) {
		int a=OT_REG, v0=regIndex(ctx.rOperand(0).REG_R().getText());
		int b=OT_NONE,c=OT_NONE,d=OT_NONE, v1=0,v2=0,v3=0;
		if (ctx.rOperand().size() >= 2) { b=OT_REG; v1=regIndex(ctx.rOperand(1).REG_R().getText()); }
		if (ctx.rOperand().size() >= 3) { c=OT_REG; v2=regIndex(ctx.rOperand(2).REG_R().getText()); }
		if (ctx.rOperand().size() >= 4) { d=OT_REG; v3=regIndex(ctx.rOperand(3).REG_R().getText()); }
		addWord(LabelType.CODE, encType0(Opcode.PACK64.code, a,b,c,d, v0, v1, v2, v3));
		return null;
	}

	@Override
	public Void visitInstrUNPACK(CPUSim64Parser.InstrUNPACKContext ctx) {
		// RR | RRRR
		int a=OT_REG, v0=regIndex(ctx.rOperand(0).REG_R().getText());
		int b=OT_NONE,c=OT_NONE,d=OT_NONE, v1=0,v2=0,v3=0;
		if (ctx.rOperand().size() >= 2) { b=OT_REG; v1=regIndex(ctx.rOperand(1).REG_R().getText()); }
		if (ctx.rOperand().size() >= 3) { c=OT_REG; v2=regIndex(ctx.rOperand(2).REG_R().getText()); }
		if (ctx.rOperand().size() >= 4) { d=OT_REG; v3=regIndex(ctx.rOperand(3).REG_R().getText()); }
		addWord(LabelType.CODE, encType0(Opcode.UNPACK.code, a,b,c,d, v0, v1, v2, v3));
		return null;
	}

	@Override
	public Void visitInstrUNPACK64(CPUSim64Parser.InstrUNPACK64Context ctx) {
		// RR | RRRR
		int a=OT_REG, v0=regIndex(ctx.rOperand(0).REG_R().getText());
		int b=OT_NONE,c=OT_NONE,d=OT_NONE, v1=0,v2=0,v3=0;
		if (ctx.rOperand().size() >= 2) { b=OT_REG; v1=regIndex(ctx.rOperand(1).REG_R().getText()); }
		if (ctx.rOperand().size() >= 3) { c=OT_REG; v2=regIndex(ctx.rOperand(2).REG_R().getText()); }
		if (ctx.rOperand().size() >= 4) { d=OT_REG; v3=regIndex(ctx.rOperand(3).REG_R().getText()); }
		addWord(LabelType.CODE, encType0(Opcode.UNPACK64.code, a,b,c,d, v0, v1, v2, v3));
		return null;
	}

	/* ============================
   CAS (RRAO / CCAO / RCAO / CRAO)
   ============================ */

	@Override
	public Void visitInstrCAS(CPUSim64Parser.InstrCASContext ctx) {
		int a=OT_NONE,b=OT_NONE,c=OT_NONE,d=OT_NONE;
		int v0=0,v1=0,v2=0,v3=0;

//		ctx.children.forEach(ch -> System.out.println("  child: " + ch.getText() + " (" + ch.getClass().getSimpleName() + ")"));
		var oldVal = parseOOperand(ctx.oOperand(0));
		var newVal = parseOOperand(ctx.oOperand(1));
		var memRef = parseMemRef(ctx.memRef());
		addWord(LabelType.CODE, encType0(Opcode.CAS.code,
				oldVal.getLeft(), newVal.getLeft(), memRef.getLeft().getLeft(), memRef.getRight().getLeft(),
				oldVal.getRight().intValue(), newVal.getRight().intValue(),
				memRef.getLeft().getRight().intValue(), memRef.getRight().getRight().intValue()));
		return null;
	}

	/* ============================
   ENDIAN (RR / RC / CR / CC)
   ============================ */

	@Override
	public Void visitInstrENDIAN(CPUSim64Parser.InstrENDIANContext ctx) {
		int a=OT_NONE,b=OT_NONE;
		int v0=0,v1=0;

		if (ctx.children.size()<2) throw new AssemblerException("ENDIAN missing operands");
		if (ctx.getChild(1) instanceof CPUSim64Parser.ROperandContext &&
				ctx.getChild(3) instanceof CPUSim64Parser.ROperandContext) {
			// RR
			a = OT_REG; b = OT_REG;
			v0 = regIndex(ctx.rOperand(0).REG_R().getText());
			v1 = regIndex(ctx.rOperand(1).REG_R().getText());
		} else if (ctx.getChild(1) instanceof CPUSim64Parser.ZPortContext &&
				ctx.getChild(3) instanceof CPUSim64Parser.ZPortContext) {
			// ZZ
			a = OT_CONST; b = OT_CONST;
			v0 = (int)parseIntLike(ctx.zPort(0).getText());
			v1 = (int)parseIntLike(ctx.zPort(1).getText());
		} else if (ctx.getChild(1) instanceof CPUSim64Parser.ROperandContext &&
				ctx.getChild(3) instanceof CPUSim64Parser.ZPortContext) {
			// RZ
			a = OT_REG; b = OT_CONST;
			v0 = (int)regIndex(ctx.rOperand(0).REG_R().getText());
			v1 = (int)parseIntLike(ctx.zPort(0).getText());
		} else if (ctx.getChild(1) instanceof CPUSim64Parser.ZPortContext &&
				ctx.getChild(3) instanceof CPUSim64Parser.ROperandContext) {
			// ZR
			a = OT_CONST; b = OT_REG;
			v0 = (int)parseIntLike(ctx.zPort(0).getText());
			v1 = regIndex(ctx.rOperand(0).REG_R().getText());
		} else {
			throw new AssemblerException("ENDIAN invalid operand types");
		}

		addWord(LabelType.CODE, encType0(Opcode.ENDIAN.code, a,b,OT_NONE,OT_NONE, v0, v1, 0, 0));
		return null;
	}

	// IN / OUT (I/O)
	@Override
	public Void visitInstrIN(CPUSim64Parser.InstrINContext ctx) {
		int a=OT_NONE,b=OT_NONE,c=OT_NONE,d=OT_NONE;
		int v0=0,v1=0,v2=0,v3=0;

		// X
		boolean isFP = (ctx.xOperand().fOperand()!=null);
		if (isFP) { a=OT_FP; v0=fpIndex(ctx.xOperand().fOperand().REG_F().getText()); }
		else      { a=OT_REG; v0=regIndex(ctx.xOperand().rOperand().REG_R().getText()); }

		if (ctx.children.size()<3) throw new AssemblerException("IN missing operands");
		if (ctx.getChild(3) instanceof CPUSim64Parser.ROperandContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ROperandContext) {
			// XRR
			b = OT_REG; c = OT_REG;
			v1 = regIndex(ctx.rOperand(0).REG_R().getText());
			v2 = regIndex(ctx.rOperand(1).REG_R().getText());
		} else if (ctx.getChild(3) instanceof CPUSim64Parser.ZPortContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ZPortContext) {
			// XZZ
			b = OT_CONST; c = OT_CONST;
			v1 = (int)parseIntLike(ctx.zPort(0).getText());
			v2 = (int)parseIntLike(ctx.zPort(1).getText());
		} else if (ctx.getChild(3) instanceof CPUSim64Parser.ROperandContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ZPortContext) {
			// XRZ
			b = OT_REG; c = OT_CONST;
			v1 = regIndex(ctx.rOperand(0).REG_R().getText());
			v2 = (int)parseIntLike(ctx.zPort(0).getText());
		} else if (ctx.getChild(3) instanceof CPUSim64Parser.ZPortContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ROperandContext) {
			// XZR
			b = OT_CONST; c = OT_REG;
			v1 = (int)parseIntLike(ctx.zPort(0).getText());
			v2 = regIndex(ctx.rOperand(0).REG_R().getText());
		} else {
			throw new AssemblerException("IN invalid operand types");
		}

		addWord(LabelType.CODE, encType0(Opcode.IN.code, a,b,c,d, v0, v1, v2, v3));
		return null;
	}

	@Override
	public Void visitInstrOUT(CPUSim64Parser.InstrOUTContext ctx) {
		int a=OT_NONE,b=OT_NONE,c=OT_NONE,d=OT_NONE;
		int v0=0,v1=0,v2=0,v3=0;

		// Q
		boolean isFP = (ctx.qOperand().fOperand()!=null);
		boolean isREG = (ctx.qOperand().aOperand()!=null);
		if (isFP) { a=OT_FP; v0=fpIndex(ctx.qOperand().fOperand().REG_F().getText()); }
		else if (isREG) {
			a = OT_REG;
			v0 = aIndexFromToken(ctx.qOperand().aOperand().start);
		} else {
			a = OT_CONST;
			v0 = (int)parseIntLike(ctx.qOperand().cLiteral().getText());
		}

		if (ctx.children.size()<3) throw new AssemblerException("IN missing operands");
		if (ctx.getChild(3) instanceof CPUSim64Parser.ROperandContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ROperandContext) {
			// XRR
			b = OT_REG; c = OT_REG;
			v1 = regIndex(ctx.rOperand(0).REG_R().getText());
			v2 = regIndex(ctx.rOperand(1).REG_R().getText());
		} else if (ctx.getChild(3) instanceof CPUSim64Parser.ZPortContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ZPortContext) {
			// XZZ
			b = OT_CONST; c = OT_CONST;
			v1 = (int)parseIntLike(ctx.zPort(0).getText());
			v2 = (int)parseIntLike(ctx.zPort(1).getText());
		} else if (ctx.getChild(3) instanceof CPUSim64Parser.ROperandContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ZPortContext) {
			// XRZ
			b = OT_REG; c = OT_CONST;
			v1 = regIndex(ctx.rOperand(0).REG_R().getText());
			v2 = (int)parseIntLike(ctx.zPort(0).getText());
		} else if (ctx.getChild(3) instanceof CPUSim64Parser.ZPortContext &&
				ctx.getChild(5) instanceof CPUSim64Parser.ROperandContext) {
			// XZR
			b = OT_CONST; c = OT_REG;
			v1 = (int)parseIntLike(ctx.zPort(0).getText());
			v2 = regIndex(ctx.rOperand(0).REG_R().getText());
		} else {
			throw new AssemblerException("OUT invalid operand types");
		}

		addWord(LabelType.CODE, encType0(Opcode.OUT.code, a,b,c,d, v0, v1, v2, v3));
		return null;
	}

	@Override
	public Void visitInstrSAVE(CPUSim64Parser.InstrSAVEContext ctx) {
		if (ctx.xOperand() != null) {
			boolean isFp = (ctx.xOperand(0).fOperand() != null);
			int a = isFp ? OT_FP : OT_REG;   // Y = A or F; encode A (R/SF/SP/PC) as REG here with A-index
			int v0 = isFp ? fpIndex(ctx.xOperand(0).fOperand().REG_F().getText())
					: regIndex(ctx.xOperand(0).rOperand().REG_R().getText());
			int b, v1;
			if (ctx.xOperand().size() >= 2) {
				if (isFp != (ctx.xOperand(1).fOperand() != null)) {
					throw new AssemblerException("SAVE operands must be the same type");
				}
				b = isFp ? OT_FP : OT_REG;   // Y = A or F; encode A (R/SF/SP/PC) as REG here with A-index
				v1 = isFp ? fpIndex(ctx.xOperand(1).fOperand().REG_F().getText())
						: regIndex(ctx.xOperand(1).rOperand().REG_R().getText());
			} else {
				b = a;
				v1 = isFp ? 31 : 28;
			}
			if (v0 > v1)
				throw new AssemblerException("SAVE first operand must be less than second");
			addWord(LabelType.CODE, encType0(Opcode.SAVE.code, a, b, 0, 0, v0, v1, 0, 0));
			return null;
		}
		throw new AssemblerException("SAVE invalid operand types");
	}

	@Override
	public Void visitInstrRESTORE(CPUSim64Parser.InstrRESTOREContext ctx) {
		if (ctx.xOperand() != null) {
			boolean isFp = (ctx.xOperand(0).fOperand() != null);
			int a = isFp ? OT_FP : OT_REG;   // Y = A or F; encode A (R/SF/SP/PC) as REG here with A-index
			int v0 = isFp ? fpIndex(ctx.xOperand(0).fOperand().REG_F().getText())
					: regIndex(ctx.xOperand(0).rOperand().REG_R().getText());
			int b, v1;
			if (ctx.xOperand().size() >= 2) {
				if (isFp != (ctx.xOperand(1).fOperand() != null)) {
					throw new AssemblerException("RESTORE operands must be the same type");
				}
				b = isFp ? OT_FP : OT_REG;   // Y = A or F; encode A (R/SF/SP/PC) as REG here with A-index
				v1 = isFp ? fpIndex(ctx.xOperand(1).fOperand().REG_F().getText())
						: regIndex(ctx.xOperand(1).rOperand().REG_R().getText());
			} else {
				b = a;
				v1 = isFp ? 31 : 28;
			}
			if (v0 > v1)
				throw new AssemblerException("RESTORE first operand must be less than second");
			addWord(LabelType.CODE, encType0(Opcode.RESTORE.code, a, b, 0, 0, v0, v1, 0, 0));
			return null;
		}
		throw new AssemblerException("RESTORE invalid operand types");
	}

	@Override
	public Void visitInstrREADONLY(CPUSim64Parser.InstrREADONLYContext ctx) {
		if (ctx.cLiteral() != null) {
			long k = parseIntLike(ctx.cLiteral().getText());
			addWord(LabelType.CODE, encType1C1(Opcode.READONLY.code, k));
		} else {
			throw new AssemblerException("READONLY invalid operand types");
		}
		return null;
	}

	public Void visitData_Directive(CPUSim64Parser.Data_DirectiveContext ddctx) {
		CPUSim64Parser.DataDirectiveContext ctx = ddctx.dataDirective();
		if (ctx.DCI() != null) {
			if (ctx.INTLIT() != null) {
				addWord(LabelType.INT, parseIntLike(ctx.INTLIT().getText()));
			} else if (ctx.HEXLIT() != null) {
				addWord(LabelType.HEX, parseIntLike(ctx.HEXLIT().getText()));
			} else {
				throw new IllegalStateException("Invalid DCI literal");
			}
		} else if (ctx.DCF() != null) {
			if (ctx.FLOATLIT() != null) {
				addWord(LabelType.FLOAT, Double.doubleToRawLongBits(Double.parseDouble(ctx.FLOATLIT().getText())));
			} else {
				throw new IllegalStateException("Invalid DCF literal");
			}
		} else if (ctx.DCA() != null) {
			long b = 0;
			if (ctx.INTLIT() != null) {
				b = parseIntLike(ctx.INTLIT().getText());
			} else if (ctx.HEXLIT() != null) {
				b = parseIntLike(ctx.HEXLIT().getText());
			}
			if (b != 0) {
				addWord(LabelType.INT, b);
				for (int i = 0; i < b; ++i)
					addWord(LabelType.INT, 0L);
			}
		} else if (ctx.DCB() != null) {
			if (ctx.byteList() != null) {
				addWord(LabelType.INT, (long) ctx.byteList().bLiteral().size());
				long buffer = 0;
				int index = 7;
				for (var bctx : ctx.byteList().bLiteral()) {
					long b = 0;
					if (bctx.INTLIT() != null) {
						b = parseIntLike(bctx.INTLIT().getText());
					} else if (bctx.HEXLIT() != null) {
						b = parseIntLike(bctx.HEXLIT().getText());
					} else if (bctx.CHARLIT() != null) {
						String s = bctx.CHARLIT().getText();
						b = parseIntLike(bctx.HEXLIT().getText());
					} else {
						throw new IllegalStateException("Invalid DCB byte literal");
					}
					buffer |= (b & 0xFFL) << (index * 8);
					--index;
					if (index < 0) {
						addWord(LabelType.HEX, buffer);
						buffer = 0;
						index = 7;
					}
				}
				if (index != 7) {
					addWord(LabelType.HEX, buffer);
				}
			} else {
				throw new IllegalStateException("Invalid DCB directive");
			}
		} else if (ctx.DCC() != null) {
			if (ctx.byteList() != null) {
				addWord(LabelType.INT, (long) ctx.byteList().bLiteral().size());
				long buffer = 0;
				int index = 3;
				for (var bctx : ctx.byteList().bLiteral()) {
					long b = 0;
					if (bctx.INTLIT() != null) {
						b = parseIntLike(bctx.INTLIT().getText());
					} else if (bctx.HEXLIT() != null) {
						b = parseIntLike(bctx.HEXLIT().getText());
					} else if (bctx.CHARLIT() != null) {
						String s = bctx.CHARLIT().getText();
						b = Utils.parseCharLiteral(s);
					} else {
						throw new IllegalStateException("Invalid DCC char literal");
					}
					buffer |= (b & 0xFFFFL) << (index * 16);
					--index;
					if (index < 0) {
						addWord(LabelType.HEX, buffer);
						buffer = 0;
						index = 3;
					}
				}
				if (index != 3) {
					addWord(LabelType.HEX, buffer);
				}
			} else {
				throw new IllegalStateException("Invalid DCC directive");
			}
		} else if (ctx.DCW() != null) {
			if (ctx.intList() != null) {
				addWord(LabelType.INT, (long) ctx.intList().kLiteral().size());
				LabelType type;
				for (var kctx : ctx.intList().kLiteral()) {
					long k = 0;
					if (kctx.INTLIT() != null) {
						type = LabelType.INT;
						k = parseIntLike(kctx.INTLIT().getText());
					} else if (kctx.HEXLIT() != null) {
						type = LabelType.HEX;
						k = parseIntLike(kctx.HEXLIT().getText());
					} else {
						throw new IllegalStateException("Invalid DCW literal");
					}
					addWord(type, k);
				}
			} else if (ctx.floatList() != null) {
				addWord(LabelType.INT, (long) ctx.floatList().FLOATLIT().size());
				for (var fctx : ctx.floatList().FLOATLIT()) {
					addWord(LabelType.FLOAT, Double.doubleToRawLongBits(Double.parseDouble(fctx.getText())));
				}
			} else if (ctx.charList() != null) {
				addWord(LabelType.INT, (long) ctx.charList().CHARLIT().size());
				for (var kctx : ctx.charList().CHARLIT()) {
					String s = kctx.getText();
					long k = Utils.parseCharLiteral(s);
					addWord(LabelType.CHAR, k);
				}
			} else {
				throw new IllegalStateException("Invalid DCW directive");
			}
		} else if (ctx.DCS() != null) {
			if (ctx.STRINGLIT() == null) throw new IllegalStateException("Invalid DCS directive");
			String s = ctx.STRINGLIT().getText();
			if (s != null && s.length() >= 2) {
				s = s.substring(1, s.length() - 1);
				byte[] utf8 = Utils.parseStringLiteral(s);
				addWord(LabelType.STRING, (long)utf8.length);
				long buffer = 0;
				int index = 7;
				for (var c : utf8) {
					buffer |= (c & 0xFFL) << (index * 8);
					--index;
					if (index < 0) {
						addWord(LabelType.STRING, buffer);
						buffer = 0;
						index = 7;
					}
				}
				if (index != 7) {
					addWord(LabelType.STRING, buffer);
				}
			} else {
				throw new IllegalStateException("Invalid DCS directive");
			}
		}
		return null;
	}

	@Override
	public Void visitORG_Directive(CPUSim64Parser.ORG_DirectiveContext ctx) {
		long currentAddress = 0;
		if (ctx.INTLIT() != null) {
			currentAddress = Long.parseLong(ctx.INTLIT().getText());
		} else if (ctx.HEXLIT() != null) {
			currentAddress = Long.parseLong(ctx.HEXLIT().getText().substring(2), 16);
		} else {
			throw new AssemblerException(getLocation(lineNum) + ":ERROR:Missing integer literal for .ORG directive");
		}
		currentAddress = Math.max(out.size(), currentAddress); // prevent negative addresses
		for (int i = out.size(); i < currentAddress; ++i) {
			addWord(LabelType.INT, 0L);
		}
		return null;
	}

	@Override
	public Void visitLINE_Directive(CPUSim64Parser.LINE_DirectiveContext ctx) {
		filename = ctx.FILENAMELIT().getText();
		lineNum = ctx.INTLIT() != null ? Integer.parseInt(ctx.INTLIT().getText()) : 1;
		pauseLineIncrement = false;
		return null;
	}

	@Override
	public Void visitLINE_BEGIN_Directive(CPUSim64Parser.LINE_BEGIN_DirectiveContext ctx) {
		filename = ctx.FILENAMELIT().getText();
		lineNum = ctx.INTLIT() != null ? Integer.parseInt(ctx.INTLIT().getText()) : 1;
		pauseLineIncrement = true;
		return null;
	}

	@Override
	public Void visitLINE_END_Directive(CPUSim64Parser.LINE_END_DirectiveContext ctx) {
		pauseLineIncrement = false;
		return null;
	}

	@Override
	public Void visitBLOCK_BEGIN_Directive(CPUSim64Parser.BLOCK_BEGIN_DirectiveContext ctx) {
		String blockname = null;
		if (ctx.IDENT() != null) {
			blockname = ctx.IDENT().getText();
			if (blockname.contains("$"))
				throw new AssemblerException("Blocknames can not contain $: " + blockname);
		}
		if (blockname == null)
			throw new AssemblerException(".block directive must have an argument@");
		if (blockname.contains("{}") || blockname.contains("%d") || blockname.contains("%x"))
			blockname = String.format(blockname.replace("{}", "%04x"), ++blockCount);
		blockNames.push(blockname);
		return null;
	}

	@Override
	public Void visitBLOCK_END_Directive(CPUSim64Parser.BLOCK_END_DirectiveContext ctx) {
		String name = blockNames.pop();
		return null;
	}

	public void assemble(String src) {
		originalSourceLocations = Utils.readLineDirectives(src);
		CharStream input = CharStreams.fromString(src);
		var lex = new cloud.lesh.CPUSim64.CPUSim64Lexer(input);
		lex.removeErrorListeners();                				// remove ConsoleErrorListener
		lex.addErrorListener(this.new LabelErrorListener());     // collect lexer errors
		CommonTokenStream toks = new CommonTokenStream(lex);
		toks.fill();

		var parser = new cloud.lesh.CPUSim64.CPUSim64Parser(toks);
		parser.removeErrorListeners();             				// remove ConsoleErrorListener
		parser.addErrorListener(this.new LabelErrorListener());  // collect parser errors
		ParseTree tree = parser.program();
		visit(tree);
	}

	final class LabelErrorListener extends BaseErrorListener {
		public LabelErrorListener() {}

		@Override
		public void syntaxError(Recognizer<?, ?> recognizer,
								Object offendingSymbol,
								int line,
								int charPositionInLine,
								String msg,
								RecognitionException e) {

			String where = getLocation(line);

			String tokenText = "";
			if (offendingSymbol instanceof Token t) {
				tokenText = " near '" + t.getText() + "'";
			}
			System.err.println(where + ":ASMERROR:" + msg);
			hasErrors = true;
		}
	}
}
