(* Copyright 2021-2024 Sebastian Lederer. See the file LICENSE.md for details *)
{$MODE objfpc}
{$H600}
{$S4}
program sasm;
{$!}{$ifdef FPC}uses math,crt;{$endif}
{$R+}
type 	TokenType = (
		PlusToken, MinusToken, AsteriskToken, SlashToken,
		SemicolonToken, EOFToken, EOLToken,
		NumberToken, KeywordToken, LabelToken, DirectiveToken,
		StringLitToken, CharLitToken, MetaKeywordToken,
		CommaToken, DotToken, ColonToken, PercentToken, TildeToken,
		AndToken, OrToken, XorToken,
		AtToken, UnknownToken
	);

	IdentString = string[120];
	KeywordString = string[255];
	InsString = string[24];
	AddrString = string[8];

	Token = record
		tokenText: string[255];
		tokenKind: TokenType;
	end;

	OperandType = (
		NoOprnd,
		U13WOprnd, (* unsigned 13 bit word-aligned operand e.g. LOAD and STORE *)
		S13Oprnd,  (* signed 13 bit operand e.g. LOADC *)
		RelWOprnd, (* signed 13 bit word-aligned operand, PC-relative, e.g. BRANCH/CBRANCH *)
		S10Oprnd,  (* signed 10 bit operand, e.g. FPADJ *)
		U4Oprnd,   (* unsigned 4 bit operand, would used for modifiers but is actually unused *)
		CmpOprnd,  (* comparison operand, unsigned 4 bit, actually unused *)
		RegOprnd,  (* register id operand for LOADREG, 4 bit unsigned *)
		RelU10Oprnd, (* PC-relative 10 bit unsigned, for LOADREL *)
		OptOprnd   (* optional 4 bit operand for ALU *)
	);

	EncodingEntry = record
		mask: integer;
		value: integer;
	end;

	InstructionWord = integer;
	MachineWord = integer;
	OutputWord = record
		intvalue:integer;
	end;

	ModifierEntry = record
		keyword: string[24];
		encoding: EncodingEntry;
		next: ^ModifierEntry;
	end;

	OpcodeData = record	(* the key is stored in the tree as metadata *)
		encoding: EncodingEntry;
		modifiers: ^ModifierEntry;
		operand: OperandType;
		id: integer; (* unique id for this opcode or alias *)
	end;

	SymbolType = (ConstSymbol, LabelSymbol, SpecialSymbol, SkippedSymbol);

	Symbol = record
		value:integer;
		typ:SymbolType;
		aligned:boolean;
		padded:boolean;
		exported:boolean;
	end;

	SymbolRef = ^Symbol;

	CPoolEntry = record
		labelname: string[40];
		value: KeywordString;
		offset: integer;
		symbolic: boolean;
		sym: SymbolRef;
		next: ^CPoolEntry;
		prev: ^CPoolEntry;
	end;

	CPoolRef = ^CPoolEntry;

	LabelList = record
		name: IdentString;
		next: ^LabelList;
		prev: ^LabelList;
	end;

	LabelListRef = ^LabelList;

	UnresolvedBranch = record
		target:IdentString;
		origin:integer;
		maxDistance:integer;
		shrinkage:integer;
		labels:LabelListRef;
		next:^UnresolvedBranch;
	end;

	UnresBranchRef = ^UnresolvedBranch;

{$I 'platform-types+.pas'} (* defines OutputFileType, InputFileType, SymFileType *)

	InputFileState = record
		name: string;
		filevar: InputFileType;
		line: integer;
	end;

	TreeDataType = (TDString, TDInteger, TDSymbol, TDOpcode);
	Treedata = record
		case typ:Treedatatype of
		TDString:(stringdata:string);
		TDInteger:(intdata:integer);
		TDSymbol:(symboldata:Symbol);
		TDOpcode:(opcodedata:OpcodeData);
	end;

{$I 'treetypes.pas'}

const	insSize = 2;
	wordSize = 4;
	wordSizeMask = 3;
	MaxUShortOffset = 8191;
	MaxShortOffset = 4095;
	MaxShorterOffset = 511;
	MaxTinyOffset  = 15;
	Unresolved = 2147483647; (* max integer - 1 *)
        MaxIntegerDigits = 24;
	wordBits = 32;
        MaxIncludes = 4;
        FilenameSuffix = '.s';
        OutfileSuffix = '.prog';
	SymFileSuffix = '.sym';
	AsciifileSuffix = '.mem';

	progressSteps = 511;

	shortcutChar = '`';
	firstShCChar = 'A';
	lastShCChar = 'i';

var
	curToken, nextToken, lastToken: Token;

	infileOpened: boolean;
	outputEnabled: boolean;
	asciiOutput: boolean;
	lastOpcode: TreeDataRef;

	bufferedChar: char;
	buffered: boolean;
	infile: InputFileType;
	outfile: OutputFileType;
	filename: string;
	outfilename: string;
	editOnError, runOnSuccess: boolean;
	lineno: integer;
	prevFiles: array[1..MaxIncludes] of InputFileState;
	includeLevel: integer;
	paramPos: integer;

	pc: integer;
	pass:integer;

	bytesCount:integer;

	symbolTable: TreeRef;
	opcodeTable: TreeRef;
	nextOpcodeId: integer;
	constantPool: CPoolRef;
	cPoolCount: integer;
	nextConstId: integer;

	firstUnresBranch: ^UnresolvedBranch;

	LOADCPId: integer;

	outputPrefix: string;
	includePrefix: string;

	shortcuts: array[firstShCChar..lastShCChar] of OpcodeData;

procedure errorExit2(message1, message2: string); forward;

{$I 'platform+.pas'}
{$I 'treeimpl.pas'}


procedure verifyTree(node:TreeRef); forward;

procedure dumpSymbolTable; forward;


procedure cleanup;
begin
	if infileOpened then
		close(infile);
	if outputEnabled then
		close(outfile);
end;


procedure errorExit;
begin
	cleanup;
	(* dumpSymbolTable; *)
	halt;
end;

procedure errorLine(line:integer);
begin
	if curToken.tokenKind = EOLToken then
		lineno := lineno - 1;
	writeln('at line ',lineno, ' in ', filename);
end;

procedure errorExit2(message1, message2: string);
var errormsg:string[128];
begin
	errormsg := message1 + ' ' + message2;
	writeln;
	writeln('Error: ', errormsg);
	errorLine(lineno);
	cleanup;
	if editOnError then
		ExecEditor(filename, lineno, errormsg)
	else
		halt;
end;

function descToken(kind:tokenType):string;
begin
	case kind of
	PlusToken, MinusToken, AsteriskToken, SlashToken, AndToken, OrToken, XorToken:
		descToken := 'one of + - * / & | ^';
	SemicolonToken:
		descToken := '";"';
	EOFToken:
		descToken := 'end-of-file';
	EOLToken:
		descToken := 'end-of-line';
	NumberToken:
		descToken := 'number';
	KeywordToken:
		descToken := 'keyword';
	LabelToken:
		descToken := 'label';
	DirectiveToken:
		descToken := 'directive';
	StringLitToken:
		descToken := 'string literal';
	CharLitToken:
		descToken := 'char literal';
	MetaKeywordToken:
		descToken := 'meta directive';
	CommaToken:
		descToken := '","';
	DotToken:
		descToken := '"."';
	ColonToken:
		descToken := '":"';
	PercentToken:
		descToken := '"."';
	AtToken:
		descToken := '"@"'
	else
		descToken := '<unknown>';
	end;
end;

procedure makeCPoolLabel(var labelname:IdentString);
var digits:string[16];
begin
	str(nextConstId, digits);
	labelname := '_CP_' + digits;
end;

procedure hexstr(value:integer;var output:string); forward;

procedure putCPoolEntry(var constant:KeywordString; offset:integer; symbolic:boolean;var labelname:IdentString);
var newEntry:CPoolRef;
begin
	makeCPoolLabel(labelname);

	new(newEntry);

	newEntry^.labelname := labelname;
	newEntry^.value := constant;
	newEntry^.offset := offset;
	newEntry^.symbolic := symbolic;
	newEntry^.next := constantPool;
	newEntry^.prev := nil;

	if constantPool <> nil then
		constantPool^.prev := newEntry;
	constantPool := newEntry;

	nextConstId := nextConstId + 1;
end;

function isNumber(var s:string):boolean;
begin
	if isdigit(s[1]) then
		isNumber := true
	else
		isNumber := s[1] in [ '-', '$', '%' ];
end;

function convertNumber(digits:KeywordString):integer; forward;
function findSymbol(var keyword:KeywordString):TreeDataRef; forward;

procedure addCPoolEntry(var constant:KeywordString; offset:integer; var labelname:IdentString);
begin
	putCPoolEntry(constant, offset, not isNumber(constant), labelname);
	cPoolCount := cPoolCount + 1;
end;

function getSymbolValue(var keyword:KeywordString):integer; forward;

(*
   Get the address of a cpool entry for a constant value.
   If the pool does not contain the constant, create
   a new entry.
   The constant needs to be a string because it might
   be a symbol. In pass 1, the value of the symbol is not
   known, so to reuse a value we need to use the symbolic name.

   The offset is an optional numerical value or symbol that gets
   added to the constant before it is put into the pool.
   This is used by the compiler for global variables
   (arrays, record fields). Can be zero.
 *)

function getCPoolAddr(var constant:KeywordString;offset:integer):integer;
var labelname:IdentString;
    current:CPoolRef;
    found:CPoolRef;
begin
	getCPoolAddr := pc;

	found := nil;
	current := constantPool;
	while (current <> nil) and (found = nil) do
	begin
		if (constant = current^.value) and (offset = current^.offset) then
			found := current;
		current := current^.next;
	end;

	if found <> nil then
	begin
		(* value already exists in pool *)
		labelname := found^.labelname;
	end
	else
		(* value not found, add it to the pool and
		   set the label name for the new entry *)
		addCPoolEntry(constant,offset,labelname);

	(* writeln(' [P', pass, ' cpool ', constant, ' -> ', labelname, '] '); *)
	getCPoolAddr := getSymbolValue(labelname);
end;

procedure printPassNo;
begin
	write('P', pass, ' ');
end;

procedure printCurrentLineno;
begin
	write(#13);
	printPassNo;
	write(filename, '    ', lineno);
	ClrEol;
end;

procedure printLastLineno;
begin
	printCurrentLineno;
end;

procedure beginInclude(var newname: string);
var newfile: InputFileType;
begin
	if includeLevel = MaxIncludes then
		errorExit2('Too many nested includes', '');

	includeLevel := includeLevel + 1;

	prevFiles[includeLevel].filevar := infile;
	prevFiles[includeLevel].name := filename;
	prevFiles[includeLevel].line := lineno;

	openFileWithDefault(newfile, newname);

	infile := newfile;
	filename := newname;
	lineno := 1;
	buffered := false;
end;

procedure endInclude;
begin
	if includeLevel = 0 then
		errorExit2('Internal error in', 'endInclude');

	close(infile);

	infile := prevFiles[includeLevel].filevar;
	filename := prevFiles[includeLevel].name;
	lineno := prevFiles[includeLevel].line;

	buffered := false;

	includeLevel := includeLevel - 1;
end;

function includeIsActive:boolean;
begin
	includeIsActive := includeLevel > 0;
end;

function nextChar: char;
var ch: char;
begin
	if buffered then
	begin
		ch := bufferedChar;
		buffered := false;
	end
	else
	begin
		if not eof(infile) then
		begin
			read(infile, ch);
		end
		else
		begin
			(* we reached end-of-file, was this
				the end of an include file? *)
			if includeIsActive then
			begin
				 (* if yes, switch back to previous file *)
				endInclude;
				ch := ' '; (* return a space which will get skipped *)
			end
			else
				(* no, return null character which becomes an EOFToken *)
				ch := #0;
		end
	end;
	if ch = #10 then lineno := lineno + 1;
	nextChar := ch;
end;

function peekChar: char;
var tmpChar: char;
begin
	if buffered then
	begin
		peekChar := bufferedChar;
	end
	else
	begin
		if not eof(infile) then
		begin
			read(infile, tmpChar);
			peekChar := tmpChar;
			bufferedChar := tmpChar;
			buffered := true;
		end
		else
		begin
			(* at the eof of an include,
				just return an extra space and let nextChar
				do the work *)
			if includeIsActive then
			begin
				peekChar := ' ';
				buffered := false; (* force nextChar to do real I/O *)
			end
			else
				peekChar := #0;
		end
	end
end;

procedure skipWhitespace;
var c:char;
begin
	while peekChar() in [ #13, #32, #9 ] do
		c := nextChar;
end;

function integerFromString(digits:KeywordString):integer;
var value,error:integer;
begin
	val(digits, value, error);
	if error <> 0 then
		errorExit2('Invalid integer value', digits);
	integerFromString := value;
end;

function convertHex(var digits:KeywordString):integer;
var i,v,len:integer;
    c:char;
begin
	len := length(digits);

	i := 2;
	convertHex := 0;

	while i <= len do
	begin
		convertHex := convertHex shl 4;
		c := digits[i];
		if (c >= 'A') and (c <= 'F') then
			v := ord(c) - ord('A') + 10
		else
		if (c >= '0') and (c <= '9') then
			v := ord(c) - ord('0')
		else
			errorExit2('Invalid number',digits);
		convertHex := convertHex + v;
		i := i + 1;
	end;
end;

function convertBin(var digits:KeywordString):integer;
var i,v,len:integer;
    c:char;
begin
	len := length(digits);

	i := 2;
	convertBin := 0;

	while i <= len do
	begin
		c := digits[i];
		if c <> '_' then  (* ignore '_' for a syntax like 0000_0001 *)
		begin
			convertBin := convertBin shl 1;

			if (c >= '0') and (c <= '1') then
				v := ord(c) - ord('0')
			else
				errorExit2('Invalid number',digits);

			convertBin := convertBin + v;
		end;
		i := i + 1;
	end;
end;

function convertChar(digits:KeywordString):integer;
begin
	convertChar := ord(digits[1]);
end;

function convertNumber(digits:KeywordString):integer;
var negate:boolean;
begin
	negate := digits[1] = '-'; 
	(* we need to keep the sign for decimals
		because we cannot represent abs(-maxint)
		as a signed 32-bit integer and integerFromString
		uses val() *)
	if negate then
		if (digits[2] in [ '$', '%' ]) then
			delete(digits,1,1);

	if digits[1] = '$' then
		convertNumber := convertHex(digits)
	else
	if digits[1] = '%' then
		convertNumber := convertBin(digits)
	else
	begin
		negate := false;
		convertNumber := integerFromString(digits);
	end;
	if negate then
		convertNumber := -convertNumber;
end;

function getCharLitValue(tokenText:string):integer;
begin
	(* is is a one-character-string-literal like 'A' ? *)
	if length(tokenText) = 1 then
		getCharLitValue := ord(tokenText[1])
	else
		errorExit2('Cannot use string as char here', tokenText);
end;

(* scan for an integer number. the first digit is already in curChar.
   digits are written to keyword. *)
procedure getDigits(curChar: char; var keyword: KeywordString);
begin
	keyword := keyword + curChar;
	while peekChar in [ '0'..'9' ] do
	begin
		keyword := keyword + nextChar;
	end;
end;

procedure hexstr(value:integer;var output:string);
var i:integer;
    nibble:integer;
    c:char;
begin
	output := '00000000';

	for i := 8 downto 1 do
	begin
		nibble := value and $F;
		if nibble > 9 then
			c := chr( ord('A') + nibble - 10)
		else
			c := chr( ord('0') + nibble);

		output[i] := c;
		value := value shr 4;
		if value = 0 then break;
	end;
end;

(* Scan for an integer number in hexadecimal format.
   The hex marker '$' is already in curChar.
   Digits are written to keyword. *)
procedure getHexDigits(curChar: char; var keyword: KeywordString);
begin
	keyword := keyword + curChar;
	while peekChar in [ '0'..'9', 'A'..'F' ] do
	begin
		keyword := keyword + nextChar;
	end;
end;

procedure getToken(var tokenReturn:Token;stringTokens:boolean);
var curChar,pkChar: char;
    keyword: KeywordString;
	startLine: string[12];

function isKeywordChar(ch:char):boolean;
begin
	isKeywordChar := (ch >= 'A') and (ch <= 'Z') or
			(ch >= 'a') and (ch <= 'z') or
			(ch >= '0') and (ch <= '9') or
			(ch = '_') or (ch = '.');
end;

function isAlpha(ch:char):boolean;
begin
	isAlpha := ((ch >= 'A') and (ch <= 'Z')) or
		((ch >= 'a') and (ch <= 'z'));
end;

function isKeywordStart(ch:char):boolean;
begin
	isKeywordStart := ((ch >= 'A') and (ch <= 'Z')) or
		((ch >= 'a') and (ch <= 'z')) or
		(ch = '_');
end;

begin
	curChar := nextChar;

	tokenReturn.tokenText := curChar;

	if curChar = shortcutChar then (* two character instruction shortcut *)
	begin
		keyword := curChar + nextChar;

		(* shortcuts can have modifiers *)
		while isKeywordChar(peekChar) do
		begin
			curChar := Upcase(nextChar);
			if (length(keyword) < 80) and (curChar <> #0) then keyword := keyword + curChar;
		end;

		tokenReturn.tokenKind := KeywordToken;
		tokenReturn.tokenText := keyword;
	end
	else
	if curChar = #0 then
		tokenReturn.tokenKind := EOFToken
	else
	if curChar = #10 then
		tokenReturn.tokenKind := EOLToken
	else
	if curChar = '+' then
		tokenReturn.tokenKind := PlusToken
	else
	if curChar = '-' then
		tokenReturn.tokenKind := MinusToken
	else
	if curChar = '*' then
			tokenReturn.tokenKind := AsteriskToken
	else
	if curChar = '/' then
		tokenReturn.tokenKind := SlashToken
	else
	if curChar = '~' then
		tokenReturn.tokenKind := TildeToken
	else
	if curChar = '@' then
		tokenReturn.tokenKind := AtToken
	else
	if curChar = ',' then
		tokenReturn.tokenKind := CommaToken
	else
	if curChar = '&' then
		tokenReturn.tokenKind := AndToken
	else
	if curChar = '|' then
		tokenReturn.tokenKind := OrToken
	else
	if curChar = '^' then
		tokenReturn.tokenKind := XorToken
	else
	if curChar = '.' then
	begin
		pkChar := peekChar;
		if isAlpha(pkChar) then
		begin
			keyword := Upcase(curChar);
			while isKeywordChar(peekChar) do
			begin
				curChar := Upcase(nextChar);
				if (length(keyword) < 80) and (curChar <> #0) then keyword := keyword + curChar;
			end;
			tokenReturn.tokenText := keyword;
			tokenReturn.tokenKind := DirectiveToken;
		end
		else
			tokenReturn.tokenKind := DotToken;
	end
	else
	if curChar = '%' then
		(* percent sign can be the start of a binary number or
			an include directive *)
	begin
		pkChar := peekChar;
		if pkChar in ['A'..'Z', 'a'..'z' ] then (* is it a meta directive? *)
		begin
			keyword := Upcase(curChar);
			while peekChar in ['A'..'Z', 'a'..'z', '0'..'9', '_'] do
			begin
				curChar := Upcase(nextChar);
				if (length(keyword) < 80) and (curChar <> #0) then keyword := keyword + curChar;
			end;
			tokenReturn.tokenText := keyword;
			tokenReturn.tokenKind := MetaKeywordToken;
		end
		else
		if pkChar in ['0'..'1' ] then	(* is it a binary number? *)
		begin
			keyword := curChar;
			while peekChar in ['0','1','_' ] do
			begin
				curChar := nextChar;
				if (length(keyword) < 80) and (curChar <> #0) then keyword := keyword + curChar;
			end;
			tokenReturn.tokenText := keyword;
			tokenReturn.tokenKind := NumberToken;
		end
		else
			tokenReturn.tokenKind := PercentToken;  (* this is most likely unusable *)
	end
	else
	if curChar = ';' then
		tokenReturn.tokenKind := SemicolonToken
	else
	if isKeywordStart(curChar) then
	begin
		keyword := Upcase(curChar);
		while isKeywordChar(peekChar) do
		begin
			curChar := Upcase(nextChar);
			if (length(keyword) < 80) and (curChar <> #0) then keyword := keyword + curChar;
		end;

		tokenReturn.tokenText := keyword;
		if peekChar = ':' then
		begin
			tokenReturn.tokenKind := LabelToken;
			curChar := nextChar;
		end
		else
			tokenReturn.tokenKind := KeywordToken;
	end
	else
	if isdigit(curChar) then
	begin
		keyword := '';
		getDigits(curChar, keyword);
		tokenReturn.tokenText := keyword;
		tokenReturn.tokenKind := NumberToken;
	end
	else
	if curChar = '$' then
	begin
		keyword := '';
		getHexDigits(curChar, keyword);
		tokenReturn.tokenText := keyword;
		tokenReturn.tokenKind := NumberToken;
	end
	else
	if (curChar = '''') and stringTokens then
	begin
		keyword := nextChar;
		curChar := nextChar;
		tokenReturn.tokenKind := CharLitToken;
		tokenReturn.tokenText := keyword;

		if curChar <> '''' then
			errorExit2('Invalid character literal, missing ''', keyword);
	end
	else
	if (curChar = '"') and stringTokens then
	begin
		str(lineno, startLine);
		keyword := '';
		curChar := nextChar;
		(* add characters as long as the current char is not '
		  (or if it is a double ') and not EOF *)
		while (not ((curChar = '"') and (peekChar <> '"'))) and (curChar <> #0 ) do
		begin
			if (curChar = '"') and (peekChar = '"') then
			begin
				keyword := keyword + curChar;
				curChar := nextChar;
			end
			else
				keyword := keyword + curChar;
			curChar := nextChar;
		end;
		if curChar = #0 then
			errorExit2('Unterminated string constant starting at line', startLine);
		tokenReturn.tokenText := keyword;
		(* string literals with a length of 1 are char literals
			which may be converted into string constants later  *)
		if length(keyword) = 1 then
			tokenReturn.tokenKind := CharLitToken
		else
			tokenReturn.tokenKind := StringLitToken;
	end
	else
		tokenReturn.tokenKind := UnknownToken;
end;

(* check for (and do not consume) a specific token, returns true on match *)
function checkToken(kind: TokenType): boolean;
begin
	checkToken := curToken.tokenKind = kind;
end;

(*  move to next token without any processing.
	sets curToken global variable. *)
procedure skipToNextToken;
begin
	getToken(nextToken, true);
	curToken := nextToken;
end;

(* read the next token into the global variable curToken.
   skips whitespace and comments.
*)
procedure readNextToken;
var c:char;
begin
	skipWhitespace;

	lastToken := curToken;
	getToken(nextToken, true);

	curToken := nextToken;

	if curToken.tokenKind = SemicolonToken then
	begin
		repeat
			c := nextChar;
		until c = #10;
		curToken.tokenKind := EOLToken;
	end;
end;

(* match (and consume) a token or exit with error *)
procedure matchToken(kind: TokenType);
begin
	if curToken.tokenKind <> kind then
		errorExit2('Expected ' + descToken(kind) + ', found', curToken.tokenText);
	readNextToken;
end;

(* match (and consume) a token, returning true, or if no match, do not
	consume token and return false *)
function matchTokenOrNot(wantedToken: TokenType): boolean;
begin
	if checkToken(wantedToken) then
	begin
		matchTokenOrNot := true;
		readNextToken;
	end
	else
		matchTokenOrNot := false;
end;

procedure emitInstructionWord(value:InstructionWord); forward;
procedure fixupLabel(oldPc, newPc:integer); forward;

procedure alignOutput(amount:integer);
var mask,o:integer;
    oldPc:integer;
begin
	oldPc := pc;
	mask := amount - 1;

	o := pc and mask;
	if o = 2 then
	begin
		emitInstructionWord(0);
		(* if there was a label, we need to fix the address *)
		fixupLabel(oldPc, pc);
	end
	else
	if o = 0 then
	begin (* do nothing *) end
	else
		errorExit2('internal error: bad alignment', '');
end;

procedure emitBinByte(value:integer);
var c:char;
begin
	c := chr(value and $FF);
	write(outfile,c);
end;

procedure emitBin16(value:InstructionWord);
var hi,lo:integer;
begin
	hi := value and $FF00 shr 8;
	lo := value and $00FF;
	emitBinByte(hi);
	emitBinByte(lo);
end;

procedure emitBin32(value:MachineWord);
var b3,b2,b1,b0:integer;
begin
	b0 := value and $FF;
	value := value shr 8;
	b1 := value and $FF;
	value := value shr 8;
	b2 := value and $FF;
	value := value shr 8;
	b3 := value and $FF;
	emitBinByte(b3);
	emitBinByte(b2);
	emitBinByte(b1);
	emitBinByte(b0);
end;

procedure emitAsciiBin(encoded:InstructionWord);
var i:integer;
   digit:char;
begin
	for i := 1 to 16 do
	begin
		if (encoded and $8000) <> 0 then
			digit := '1'
		else
			digit := '0';

		write(outfile, digit);
		encoded := encoded shl 1;
	end;
	if((pc and 3) = 2) then
		writeln(outfile);
end;

(* assumes 32-bit alignment *)
procedure emitAsciiBin32(value:MachineWord);
var i:integer;
   digit:char;
begin
	for i := 1 to 32 do
	begin
		if (value and $80000000) <> 0 then
			digit := '1'
		else
			digit := '0';

		write(outfile, digit);
		value := value shl 1;
	end;
	writeln(outfile);
end;

procedure emitAsciiByte(value:integer);
var i:integer;
   digit:char;
begin
	for i := 1 to 8 do
	begin
		if (value and $80) <> 0 then
			digit := '1'
		else
			digit := '0';

		write(outfile, digit);
		value := value shl 1;
	end;

	if (pc and wordSizeMask) = 3 then
		writeln(outfile);
end;

(*
  Emit a single byte.
  Make sure to do proper alignment afterwards.
  Mainly used for .BYTE directive.
*)
procedure emitByte(value:integer);
begin
	if outputEnabled then
	begin
		if asciiOutput then
			emitAsciiByte(value)
		else
			emitBinByte(value);
	end;
	bytesCount := bytesCount + 1;
	pc := pc + 1;
end;

(* assumes aligned output *)
procedure emitWord(value:MachineWord);
begin
	if outputEnabled then
	begin
		if asciiOutput then
			emitAsciiBin32(value)
		else
			emitBin32(value);
	end;
	bytesCount := bytesCount + 4;
	pc := pc + 4;
end;

procedure emitInstructionWord(value:InstructionWord);
begin
	if outputEnabled then
	begin
		if asciiOutput then
			emitAsciiBin(value)
		else
			emitBin16(value);
	end;
	bytesCount := bytesCount + 2;
	pc := pc + 2;
end;

procedure emitInstruction(var opcode:OpcodeData; encoded:InstructionWord);
begin
	emitInstructionWord(encoded);
end;

procedure emitBlock(count:integer; value:integer);
var i:integer;
begin
	alignOutput(wordSize);
	for i := 1 to count do
		emitWord(value);
end;

procedure encodeOperand(value:integer; var op:OpcodeData; var encoded:InstructionWord); forward;
procedure getBaseAndModifiers(var ins:KeywordString; var opcode:OpcodeData; var encoded:InstructionWord); forward;

procedure encodeInstruction(ins:string; operand:integer; var encoded:InstructionWord);
var opcode:OpcodeData;
begin
	getBaseAndModifiers(ins, opcode, encoded);
	if opcode.id = LOADCPId then
		errorExit2('internal error in encodeInstruction', curToken.tokenText);
	encodeOperand(operand, opcode, encoded);
end;

procedure ClearTree(var root:TreeRef);
begin
	while root <> nil do
	begin
		(* delete subtrees first to reduce reshuffling of nodes *)
		if root^.left <> nil then TreeDelete(root^.left, root^.left^.key^);
		if root^.right <> nil then TreeDelete(root^.right, root^.right^.key^);
		TreeDelete(root, root^.key^);
	end;
	verifyTree(root);
end;

procedure ClearCPool;
var current:CPoolRef;
    next:CPoolRef;
begin
	current := constantPool;

	while current <> nil do
	begin
		next := current^.next;
		dispose(current);
		current := next;
	end;
	constantPool := nil;
end;

procedure createSymbol(var keyword:IdentString; typ:SymbolType; value:integer; aligned,padded:boolean);
	forward;
procedure dumpCPool; forward;

procedure emitConstantPool(branch:boolean);
var current:CPoolRef;
    labelname:IdentString;
    value:KeywordString;
    intValue:integer;
    size:integer;
    encoded:InstructionWord;
    padded:boolean;
begin
	(* writeln('*** emitConstantPool at ', pc, ' count ', cPoolCount); *)
	if branch then
	begin
		(* calculate size of all constants in bytes *)
		size := cPoolCount * wordSize;

		(*
			Add padding if alignment is needed.
			This happens when the pc is at a word boundary (pc and wordSize = 0).
			Then after the branch instruction, the pc is at a half-word boundary
			and we need a padding half-word to get the correct alignment for the
			word-sized constants.
		*)
		if (pc and wordSizeMask) = 0 then size := size + insSize;

		(* encode the instruction, adjust operand for the size of the branch instruction*)
		encodeInstruction('BRANCH', pc + size + insSize, encoded);
		emitInstructionWord(encoded);
	end;


	current := constantPool;
	padded := false;

	(* if the constant pool is empty, do no alignment *)
	if current <> nil then
	begin
		(* remember if we needed alignment padding *)
		padded := (pc and wordSizeMask) = insSize;
		alignOutput(wordSize);
	end;

	(* the cpool list has the latest entries at the front,
		so go to the tail first and then go backward *)
	if current <> nil then
		while current^.next <> nil do
			current := current^.next;

	while current <> nil do
	begin
		labelname := current^.labelname;
		value := current^.value;

		createSymbol(labelname, LabelSymbol, pc, true, padded);
		(* only the first entry is marked as padded *)
		if padded then padded := false;
		if current^.symbolic then
			intValue := getSymbolValue(value)
		else
			intValue := convertNumber(value);
		emitWord(intValue + current^.offset);

		current := current^.prev;
	end;

	(* writeln('*** emitConstantPool new pc ', pc); *)
	(* dumpCPool; *)
	ClearCPool;
	cPoolCount := 0;
end;

procedure encodeOperand(value:integer; var op:OpcodeData; var encoded:InstructionWord);
var mask, negativeMask:integer;
    valueStr:string;
    isSigned:boolean;
    min, max: integer;
begin
	case op.operand of
	NoOprnd:     begin mask := 0;     negativeMask := 0;     end;
	U13WOprnd:   begin mask := $1FFE; negativeMask := $0000; end;
	S13Oprnd:    begin mask := $1FFF; negativeMask := $1000; end;
	RelWOprnd:   begin mask := $1FFE; negativeMask := $1000; value := value - pc; end;
	S10Oprnd:    begin mask := $03FF; negativeMask := $0200; end;
	U4Oprnd:     begin mask := $000F; negativeMask := $0000; end;
	CmpOprnd:    begin mask := $000F; negativeMask := $0000; end;
	RegOprnd:    begin mask := $000F; negativeMask := $0000; end;
	RelU10Oprnd: begin mask := $03FF; negativeMask := $0000; value := value - pc; end;
	OptOprnd:    begin mask := $000F; negativeMask := $0000; end;
	end;

	isSigned := negativeMask <> 0;

	if isSigned then
	begin
		min := -negativeMask;
		max := -min + 1;
	end
	else
	begin
		min := 0;
		max := mask;
	end;

	if (value < min) or (value > max) then
	begin
		(* if not on the last pass (generating output),
			we ignore values which are out of range,
			because they might change after the 1st pass *)

		if not outputEnabled then
			value := 0
		else
		begin
			if op.operand = RelU10Oprnd then
			begin
				writeln;
				writeln('bad RelU10Oprnd:', value, ' pc:', pc, ' pass:', pass);
				dumpCPool;
			end;
			str(value, valueStr);
			errorExit2('Invalid operand value', valueStr);
		end;
	end;

	encoded := (encoded and not mask) or (value and mask);
end;

function getSymbol(var keyword:KeywordString):TreeDataRef;
begin
	getSymbol := TreeSearch(symbolTable, keyword);
	if (getSymbol = nil) and (pass > 1) then
			errorExit2('Undeclared symbol', keyword);
end;

function getLabelAddr(var keyword:KeywordString):integer;
var sym:TreeDataRef;
begin
	sym := getSymbol(curToken.tokenText);

	if sym = nil then
		getLabelAddr := Unresolved
	else
		getLabelAddr := sym^.symboldata.value;
end;

function findSymbol(var keyword:KeywordString):TreeDataRef;
begin
	findSymbol := TreeSearch(symbolTable, keyword);
end;

function getSymbolValue(var keyword:KeywordString):integer;
var data:TreeDataRef;
begin
	data := TreeSearch(symbolTable, keyword);

	if (data = nil) then
	begin
		if pass = 1 then
			(* in pass 1, we do not care about undefined symbols *)
			getSymbolValue := Unresolved
		else
			errorExit2('Undeclared symbol', keyword);
	end
	else
		getSymbolValue := data^.symboldata.value;
end;

procedure addUncertainLabel(var name:IdentString);
var current:UnresBranchRef;
    newLListEntry:LabelListRef;
begin
	current := firstUnresBranch;

	while (current <> nil) do
	begin
		(* writeln('**addUncertainLabel ',name, '  for unresBranch ', current^.target); *)
		(* put new label list entry at head of the list *)
		new(newLListEntry);
		newLListEntry^.next := current^.labels;
		newLListEntry^.prev := nil;
		newLListEntry^.name := name;
		if current^.labels <> nil then
			current^.labels^.prev := newLListEntry;
		current^.labels := newLListEntry;

		current := current^.next;
	end;
end;

procedure addUnresBranch(var name:IdentString; origin:integer; maxDistance,shrink:integer);
var newUnresBranch:^UnresolvedBranch;
begin
	new(newUnresBranch);
	newUnresBranch^.target := name;
	newUnresBranch^.origin := origin;
	newUnresBranch^.maxDistance := maxDistance;
	newUnresBranch^.shrinkage := shrink;
	newUnresBranch^.labels := nil;
	newUnresBranch^.next := firstUnresBranch;
	firstUnresBranch := newUnresBranch;

	(* writeln('** addUnresBranch ', name, ' at ', origin); *)
end;

(* Delete unresolved branch entry with target *name*.
   Will delete multiple occurrences of the same name. *)
procedure deleteUnresBranch(var name:IdentString);
var last,current,temp:^UnresolvedBranch;
begin
	current := firstUnresBranch;
	last := nil;

	while (current <> nil) do
	begin
		if current^.target = name then
		begin
			if last = nil then
				firstUnresBranch := current^.next
			else
				last^.next := current^.next;
			temp := current^.next;
			dispose(current);
			current := temp;
		end
		else
		begin
			last := current;
			current := current^.next;
		end;
	end;
end;

procedure addUnresBranch2(var name:IdentString; o:integer; max,shrink:integer);
var newUnresBranch:^UnresolvedBranch;
begin
	new(newUnresBranch);
	with newUnresBranch^ do
	begin
		target := name;
		origin := o;
		maxDistance := max;
		shrinkage := shrink;
		labels := nil;
		next := firstUnresBranch;
	end;
	firstUnresBranch := newUnresBranch;
end;

(* Check if a label declaration resolves an unresolved branch
   (.LBRANCH/.LCBRANCH). If it does, apply the code size correction
   to all labels we encountered since the branch. *)
procedure checkUnresBranches(var name:IdentString);
var r:UnresBranchRef;
    current,last,next:LabelListRef;
    sym:TreeDataRef;
    distance:integer;
    shrink:boolean;
    adjustment:integer;
begin
	r := firstUnresBranch;

	while r <> nil do
	begin
		if r^.target = name then
		begin
			distance := pc - r^.origin;
			shrink := distance <= r^.maxDistance;
{			writeln('** checkUnresBranches found ', name, ' at ', pc, ' distance ', pc - r^.origin, ' line ', lineno);
			writeln('   ', r^.origin, ' ', r^.maxDistance, ' ', r^.shrinkage); }
			current := r^.labels;
			last := nil;

			if shrink then
				pc := pc - r^.shrinkage;
			(* writeln('    short:', shrink); *)

			while current <> nil do
			begin
				(* go through all labels we encountered since the LBRANCH/LCBRANCH,
					process and dispose of the list entries *)

				(* writeln('    ', current^.name); *)

				if shrink then
				begin
					sym := findSymbol(current^.name);
					with sym^.symboldata do
					begin
						(* writeln('        ', value, ' -', r^.shrinkage, ' ', aligned, ' ', padded); *)
						value := value - r^.shrinkage;
						(* writeln('           ', value); *)
					end;
				end;
				last := current;
				current := current^.next;
			end;

			(* walk through list backwards to check alignment,
				that is in order of occurrence *)
			adjustment := 0;
			current := last;
			while current <> nil do
			begin
				if shrink then
				begin
					sym := findSymbol(current^.name);
					with sym^.symboldata do
					begin
						if aligned then
						begin
							(* if this label needs alignment, shift all labels
								after this one by one instruction word (2 bytes) *)
							if ((value + adjustment) and wordSizeMask) = insSize then
							begin
								(* if a constant pool entry was already padded,
								   do not add second padding but remove first
								   padding instead *)
								if not padded then
								begin
									padded := true;
									pc := pc + insSize;
									adjustment := adjustment + insSize;
								end
								else
								begin
									padded := false;
									pc := pc - insSize;
									adjustment := adjustment - insSize;
								end;
							end;
						end;
						value := value + adjustment;
						(* if adjustment <> 0 then
							writeln('  ', current^.name, ' adjusted to ', value, ' by ', adjustment); *)
					end;
				end;
{$ifndef FPC}
				{writeln('   disposing ', current^.name, ' ', current);}
{$endif}
				next := current^.prev;
				dispose(current);
				current := next;
			end;
			r^.labels := nil;
		end;
		r := r^.next;
	end;
	deleteUnresBranch(name);
end;

(* Create a symbol table entry.
   On passes other than 1, symbol must exist and the value is updated. *)
procedure createSymbol(var keyword:IdentString; typ:SymbolType; value:integer; aligned, padded:boolean);
var d:TreeData;
    dref:TreeDataRef;
begin
	dref := TreeSearch(symbolTable, keyword);

	if pass = 1 then
	begin
		if dref <> nil then
			errorExit2('Duplicate label', keyword);
		d.typ := TDSymbol;
		d.symboldata.typ := typ;
		d.symboldata.value := value;
		d.symboldata.aligned := aligned;
		d.symboldata.padded := padded;
		d.symboldata.exported := false;
		TreeInsert(symbolTable, keyword, d);

		addUncertainLabel(keyword);
		checkUnresBranches(keyword);
	end
	else
	begin
		if dref = nil then
		begin
			dumpCPool;
			ErrorExit2('internal error in createSymbol', keyword);
		end;

		if dref^.symboldata.value <> value then
			(* writeln('////// label changed value ', keyword, ' ',
				dref^.symboldata.value, ' -> ', value); *)
		dref^.symboldata.value := value;
	end;
end;

(* Change the address of a label after it has been
   created. This happens if an alignment is required
   e.g. for a .WORD directive which has a label *)
procedure fixupLabel(oldPc, newPc:integer);
var current:TreeRef;
    walkState:TreeWalkState;
begin
	TreeWalkFirst(symbolTable, walkState, current);
	while current <> nil do
	begin
		if current^.data^.symboldata.value = oldPc then
		begin
			current^.data^.symboldata.aligned := true;
			current^.data^.symboldata.value := newPc;
			current^.data^.symboldata.padded := (newPc <> oldPc);

			(* if newPc <> oldPc then
				writeln('     aligning ', current^.key^, ' ', oldPc, ' -> ', newPc); *)
		end;
		TreeWalkNext(walkState, current);
	end;
end;

function parsePrimary:integer;
var applyNot:boolean;
    negate:boolean;
    value:integer;
begin
	if checkToken(TildeToken) then
	begin
		readNextToken;
		applyNot := true;
	end
	else
		applyNot := false;

	if checkToken(MinusToken) then
	begin
		readNextToken;
		negate := true;
	end
	else
		negate := false;

	if checkToken(NumberToken) then
	begin
		(* let convertNumber handle negative numbers
			because the statement
			"value :=  -value" below would
			not work for -maxint.
			abs(-maxint) cannot be represented
			as a signed 32-bit integer  *)
		if negate then
		begin
			value := convertNumber('-' + curToken.tokenText);
			negate := false;
		end
		else
			value := convertNumber(curToken.tokenText);
	end
	else
	if checkToken(KeywordToken) then
		value := getSymbolValue(curToken.tokenText)
	else
	if checkToken(CharLitToken) then
		value := convertChar(curToken.tokenText)
	else
	if checkToken(AtToken) then
		value := pc
	else
		errorExit2('number or symbol expected, got', descToken(curToken.tokenKind));

	if applyNot then
		value := not value;

	if negate then
		value := - value;

	readNextToken;

	parsePrimary := value;
end;

function parseExpression:integer;
var value:integer;

function parseNextPrimary:integer;
begin
	readNextToken;
	parseNextPrimary := parsePrimary();
end;

begin
	value := parsePrimary;

	while not (curToken.tokenKind in [ EOLToken, CommaToken ]) do
	begin
		if checkToken(PlusToken) then
			value := value + parseNextPrimary
		else
		if checkToken(MinusToken) then
			value := value - parseNextPrimary
		else
		if checkToken(AsteriskToken) then
			value := value * parseNextPrimary
		else
		if checkToken(AndToken) then
			value := value and parseNextPrimary
		else
		if checkToken(OrToken) then
			value := value or parseNextPrimary
		else
		if checkToken(XorToken) then
			value := value xor parseNextPrimary
		else
		if checkToken(SlashToken) then
			value := value  div parseNextPrimary
		else
			errorExit2('Expected one of + - * / & | ^ but got', curToken.tokenText);
	end;

	parseExpression := value;
end;

procedure parseLabel;
begin
	(* writeln(':::: parseLabel ', curToken.tokenText, ' at ', pc); *)
	createSymbol(curToken.tokenText, LabelSymbol, pc, false, false);

	readNextToken;
end;

procedure parseOneWordArg;
var operandValue:integer;
begin
	operandValue := parseExpression;
	emitWord(operandValue);
end;

procedure parseWordArgs;
begin
	alignOutput(wordSize);
	parseOneWordArg;
	while checkToken(CommaToken) do
	begin
		readNextToken;
		(* if there is a comma at the end of the line,
			continue to the next line *)
		if checkToken(EOLToken) then
			readNextToken;

		parseOneWordArg;
	end;
end;

procedure parseOneByteArg;
var bytevalue:integer;
    c:char;
begin
	if checkToken(StringLitToken) then
	begin
		for c in curToken.tokenText do
			emitByte(ord(c));
		readNextToken;
	end
	else
	begin
		bytevalue := parseExpression;
		emitByte(byteValue);
	end;
end;

procedure parseByteArgs;
begin
	alignOutput(wordSize);
	parseOneByteArg;
	while checkToken(CommaToken) do
	begin
		readNextToken;
		(* if there is a comma at the end of the line,
			continue to the next line *)
		if checkToken(EOLToken) then
			readNextToken;

		parseOneByteArg;
	end;

	(* align to word *)
	while (pc and wordsizeMask) <> 0 do
		emitByte(0);
end;

procedure dumpOpcodeTable;
var walkState:TreeWalkState;
    walkRes:TreeRef;
begin
	writeln('Opcode Table:');
	TreeWalkStart(opcodeTable, walkState);
	repeat
		TreeWalkNext(walkState, walkRes);
		if walkRes <> nil then
			writeln(walkRes^.key^);
	until walkRes = nil;
end;

function changeFileSuffix(filename: string; suffix:string): string; forward;

procedure writeSymbolTable;
var walkState:TreeWalkState;
    walkRes:TreeRef;
    h:string;
    f:SymFileType;
    fname:string;
    c:char;
begin
	fname := changeFileSuffix(filename, SymFileSuffix);
	overwriteFile(f, fname);

	TreeWalkStart(symbolTable, walkState);
	repeat
		TreeWalkNext(walkState, walkRes);
		if walkRes <> nil then
		begin
			if walkRes^.data^.symboldata.typ in [ LabelSymbol, ConstSymbol ] then
			begin
				hexstr(walkRes^.data^.symboldata.value, h);
				if walkRes^.data^.symboldata.typ = ConstSymbol then
					c := '='
				else
				if walkRes^.data^.symboldata.exported then
					c := '!'
				else
					c := ' ';
				writeln(f, h, ' ', c, walkRes^.key^);
			end;
		end;
	until walkRes = nil;
	close(f);
end;

procedure dumpSymbolTable;
var walkState:TreeWalkState;
    walkRes:TreeRef;
    h:string;
begin
	writeln('Symbol Table:');
	TreeWalkStart(symbolTable, walkState);
	repeat
		TreeWalkNext(walkState, walkRes);
		if walkRes <> nil then
		begin
			if walkRes^.data^.symboldata.typ = LabelSymbol then
			begin
				hexstr(walkRes^.data^.symboldata.value, h);
				writeln(h, ' ', walkRes^.key^);
			end;
		end;
	until walkRes = nil;
end;

procedure dumpCPool;
var current:CPoolRef;
begin
	writeln('dump constant pool ', cPoolCount, ' ', nextConstId);

	current := constantPool;

	while current <> nil do
	begin
		writeln(current^.labelname, ': ', current^.value, ' ',
			getSymbolValue(current^.labelname), ' ');
		current := current^.next;
	end;
end;

procedure emitLoadcp(var operand:KeywordString; numOffset:integer);
var cpooladdr:integer;
    encoded:InstructionWord;
begin
	cpooladdr := getCPoolAddr(operand, numOffset);
	(* writeln('** emitLoadcp for ',operand, '/', numOffset, ' ', cpooladdr); *)
	encodeInstruction('LOADREL', cpooladdr, encoded);
	emitInstructionWord(encoded);
end;

procedure parseLoadcp;
var opstr:KeywordString;
    data:TreeDataRef;
    numOffset:integer;
begin
	if not (curToken.tokenKind in [ KeywordToken, NumberToken, MinusToken ]) then
		errorExit2('Identifier or number expected, got', curToken.tokenText);

	if checkToken(MinusToken) then
	begin
		opstr := '-';
		readNextToken;
		if not checkToken(NumberToken) then
			errorExit2('Invalid number', curToken.tokenText);
		opstr := opstr + curToken.tokenText;
	end
	else
		opstr := curToken.tokenText;

	readNextToken;

	(* check for optional offset *)
	if checkToken(CommaToken) then
	begin
		readNextToken;
		(* offset can be either a number literal *)
		if checkToken(NumberToken) then
			numOffset := convertNumber(curToken.tokenText)
		else
		(* or a symbol *)
		if checkToken(KeywordToken) then
		begin
			data := findSymbol(curToken.tokenText);
			if data = nil then
				errorExit2('Cannot use unresolved symbol for LOADCP offset:',
					curToken.tokenText);
			numOffset := data^.symboldata.value;
		end
		else
			errorExit2('Number or symbol required, got', curToken.tokenText);
		readNextToken;
	end
	else
		numOffset := 0;

	emitLoadcp(opstr, numOffset);
end;

procedure parseLbranch;
var value:integer;
    distance:integer;
    offset,shrinkage:integer;
    pad:boolean;
    encoded:InstructionWord;
begin
	if not checkToken(KeywordToken) then
		errorExit2('identifier expected, got', curToken.tokenText);

	value := getLabelAddr(curToken.tokenText);

	distance := value - pc;
	if (value = Unresolved) or (distance > 4095) or (distance < -4096) then
	begin
		if (pc and 3) = 0 then	(* no padding *)
		begin
			pad := false;
			(* total size 8 bytes *)
			offset := 4;	(* offset for LOADREL *)
			shrinkage := 6; (* difference to short form size *)
		end
		else
		begin
			pad := true;
			(* total size 10 bytes *)
			offset := 6;	(* offset for LOADREL with padding *)
			shrinkage := 8; (* difference to short form size *)
		end;
		if value = Unresolved then
			addUnresBranch(curToken.tokenText, pc, 4095, shrinkage);

		encodeInstruction('LOADREL', pc + offset, encoded);
		emitInstructionWord(encoded);

		encodeInstruction('JUMP', 0, encoded);
		emitInstructionWord(encoded);

		if pad then
			emitInstructionWord(0);
		emitWord(value);
	end
	else
	begin
		encodeInstruction('BRANCH', value, encoded);
		emitInstructionWord(encoded);
	end;

	(*
	if value = Unresolved then
		writeln('** parseLbranch ', curToken.tokenText, ' unresolved')
	else
	if (distance > 4095) then
		writeln('** parseLbranch ', curToken.tokenText, ' long ', distance)
	else
		writeln('** parseLbranch ', curToken.tokenText, ' short ', distance);
	*)

	readNextToken;
end;

procedure parseLcbranch(negate:boolean);
var value:integer;
    relValue:integer;
    offset,shrinkage:integer;
    encoded:InstructionWord;
    modifier:string[4];
    pad:boolean;
begin
	if not checkToken(KeywordToken) then
		errorExit2('identifier expected, got', curToken.tokenText);

	value := getLabelAddr(curToken.tokenText);

	relValue := value - pc;

	modifier := '';

	if (value = Unresolved) or (relValue > 4095) or (relValue < -4096) then
	begin
		if (pc and 3) = 2 then (* no padding *)
		begin
			pad := false;
			(* total size 10 bytes *)
			offset := 4;	(* offset for LOADREL *)
			shrinkage := 8; (* difference to short form size *)
		end
		else
		begin
			pad := true;
			(* total size 12 bytes *)
			offset := 6;	 (* offset for LOADREL with padding *)
			shrinkage := 10; (* difference to short form size *)
		end;

		if value = Unresolved then
			addUnresBranch(curToken.tokenText, pc, 4095, shrinkage);

		(* writeln('*** long cbranch triggered:', value, ' ', relValue, ' pass:',pass);
		writeln('***   pc ', pc, ' offset ', offset, ' insSize ', insSize);
		writeln('***   ', pc + offset + insSize); *)
		if not negate then
			modifier := '.Z';
		(* branch over CBRANCH, LOADREL, padding and literal value *)
		encodeInstruction('CBRANCH' + modifier, pc + insSize + offset + wordSize, encoded);
		emitInstructionWord(encoded);

		encodeInstruction('LOADREL', pc + offset, encoded);
		emitInstructionWord(encoded);

		encodeInstruction('JUMP', 0, encoded);
		emitInstructionWord(encoded);

		if pad then
			emitInstructionWord(0);
		emitWord(value);
	end
	else
	begin
		if negate then
			modifier := '.Z';
		encodeInstruction('CBRANCH' + modifier, value, encoded);
		emitInstructionWord(encoded);
	end;

	readNextToken;
end;

procedure parseDirective;
var operandValue:integer;
    count:integer;
    name:IdentString;
    oldsym:TreeDataRef;
begin
	readNextToken;

	if lastToken.tokenText = '.LBRANCH' then
		parseLbranch
	else
	if lastToken.tokenText = '.LCBRANCH' then
		parseLcbranch(false)
	else
	if lastToken.tokenText = '.LCBRANCHZ' then
		parseLcbranch(true)
	else
	if lastToken.tokenText = '.ORG' then
	begin
		operandValue := parseExpression;
		pc := operandValue;
	end
	else
	if lastToken.tokenText = '.EQU' then
	begin
		matchToken(KeywordToken);
		name := lastToken.tokenText;
		operandValue := parseExpression;

		oldsym := findSymbol(name);
		if oldsym <> nil then
		begin
			if oldsym^.symboldata.value <> operandValue then
				errorExit2('Symbol already declared:', name);
		end
		else
			createSymbol(name, ConstSymbol, operandValue, false, false);
	end
	else
	if lastToken.tokenText = '.WORD' then
		parseWordArgs
	else
	if lastToken.tokenText = '.BYTE' then
		parseByteArgs
	else
	if lastToken.tokenText = '.CPOOL' then
		emitConstantPool(false)
	else
	if lastToken.tokenText = '.CPOOLNOP' then
		emitConstantPool(true)
	else
	if lastToken.tokenText = '.BLOCK' then
	begin
		count := parseExpression;
		if matchTokenOrNot(CommaToken) then
			operandValue := parseExpression
		else
			operandValue := 0;
		emitBlock(count, operandValue);
	end
	else
		errorExit2('Unrecognized directive', lastToken.tokenText);
end;

procedure parseMetaDirective;
var filename:string;
    sym:TreeDataRef;
begin
	readNextToken;
	if lastToken.tokenText = '%INCLUDE' then
	begin
		if curToken.tokenKind in [StringLitToken, KeywordToken] then
		begin
			filename := curToken.tokenText;
			readNextToken;
			beginInclude(filename);
		end
		else
			errorExit2('Filename expected', '');
	end
	else
	if lastToken.tokenText = '%EXPORT' then
	begin
		if curToken.tokenKind = KeywordToken then
		begin
			sym := findSymbol(curToken.tokenText);
			if sym = nil then
				errorExit2('Undeclared symbol', curToken.tokenText);
			sym^.symboldata.exported := true;
		end
		else
			errorExit2('Symbol expected', '');
		readNextToken;
	end
	else
		errorExit2('Invalid meta directive', lastToken.tokenText);
end;

procedure encode(var entry:EncodingEntry; var value:InstructionWord);
begin
	value := (value and not entry.mask) or entry.value;
end;

procedure getModifier(var m:KeywordString; var opcode:OpcodeData; var encoded:InstructionWord);
var cur:^ModifierEntry;
begin
	if opcode.id <> 0 then
	begin
		cur := opcode.modifiers;
		while cur <> nil do
		begin
			if m = cur^.keyword then
			begin
				encode(cur^.encoding, encoded);
				break;
			end;
			cur := cur^.next;
		end;
		if cur = nil then
			errorExit2('Invalid modifier', m);
	end;
end;

procedure getMnemonic(var m:KeywordString; var opcode:OpcodeData; var encoded:InstructionWord);
var data:TreeDataRef;
    ch:char;
begin
	if m[1] = shortcutChar then
	begin
		ch := m[2];
		if (ch < firstShCChar)	or (ch > lastShCChar) then
			errorExit2('invalid shortcut', m);
		opcode := shortcuts[ch];
		if opcode.id = -1 then
			errorExit2('invalid shortcut', m);
		encode(opcode.encoding, encoded);
	end
	else
	begin
		data := TreeSearch(opcodeTable, m);
		if data = nil then
		begin
			errorExit2('Unrecognized instruction', m);
			opcode.id := 0;
		end
		else
		begin
			opcode := data^.opcodedata;
			encode(opcode.encoding, encoded);
		end;
	end;
end;

procedure getBaseAndModifiers(var ins:KeywordString; var opcode:OpcodeData; var encoded:InstructionWord);
var i:integer;
    insLength:integer;
    slice:string;
    startPos:integer;

function scanchar(c:char; curPos:integer):integer;
begin
	scanchar := 0;

	while curPos <= insLength do
	begin
		if ins[curPos] = c then
		begin
			scanchar := curPos;
			break;
		end
		else
			curPos := curPos + 1;
	end;
end;

begin
	encoded := 0;
	insLength := length(ins);

	i := pos('.',ins);

	{ writeln('** getModifiers ',i); }

	if i > 1 then
	begin
		slice := copy(ins,1,i-1);
		getMnemonic(slice, opcode, encoded);
		repeat
			startPos := i + 1;
			i := scanchar('.', startPos);
			if i > 0 then
			begin
				slice := copy(ins,startPos,i-startPos);
				getModifier(slice, opcode, encoded);
			end;
		until i < 1;

		(* last slice *)
		slice := copy(ins, startPos, insLength-startPos+1);
		getModifier(slice, opcode, encoded);
	end
	else
		getMnemonic(ins, opcode, encoded);
end;

procedure parseInstruction;
var operandValue:integer;
    opcode:OpcodeData;
    encodedIns:InstructionWord;
begin
	getBaseAndModifiers(curToken.tokenText, opcode, encodedIns);

	readNextToken;

	if opcode.id = LOADCPId then
		parseLoadcp
	else
	begin
		if opcode.operand <> NoOprnd then
		begin
			if not checkToken(EOLToken) then
				operandValue := parseExpression
			else
			begin
				if opcode.operand = OptOprnd then
					operandValue := 0
				else
					errorExit2('Missing operand', lastToken.tokenText)
			end
		end
		else
			operandValue := 0;

		encodeOperand(operandValue, opcode, encodedIns);
		emitInstruction(opcode, encodedIns);
	end;
end;

procedure parseLine;
begin
	(* writeln('## P', pass, ' line ', lineno:4, ' pc ', pc:8); *)

	if checkToken(LabelToken) then
		parseLabel;

	if checkToken(DirectiveToken) then
		parseDirective
	else
	if checkToken(KeywordToken) then
		parseInstruction
	else
	if checkToken(MetaKeywordToken) then
		parseMetaDirective
	else
	if checkToken(EOLToken) then
		begin end (* empty line *)
	else
	begin
		(* writeln(curToken.tokenKind); *)
		errorExit2('Invalid syntax', curToken.tokenText);
	end;

	matchToken(EOLToken);
end;

procedure parseFile;
begin
	readNextToken;
	repeat
		parseLine;
		if (lineno and progressSteps) = 0 then
			printCurrentLineno;
	until checkToken(EOFToken);
	emitConstantPool(false);
end;

(* Add an instruction to the opcodeTable (which is a tree).
   mask specifies the bits which are used by the opcode.
*)
procedure addOpcode(mnemonic:InsString; value, mask:integer; oprnd: OperandType);
var data:TreeData;

begin
	data.typ := TDOpcode;
	data.opcodedata.encoding.value := value;
	data.opcodedata.encoding.mask  := mask;
	data.opcodedata.modifiers := nil;
	data.opcodedata.operand := oprnd;
	data.opcodedata.id := nextOpcodeId;

	nextOpcodeId := nextOpcodeId + 1;

	TreeInsert(opcodeTable, mnemonic, data);

	lastOpcode := TreeSearch(opcodeTable, mnemonic);
end;

(* Add a modifier to the instruction that was last added by addOpcode.
   mask specifies the bits used by the modifier.
*)
procedure addModifier(key:InsString; value, mask:integer);
var newModifier:^ModifierEntry;
    cur:^ModifierEntry;
begin
	if lastOpcode = nil then
		errorExit2('internal error in addModifier', key);

	new(newModifier);
	newModifier^.keyword := key;
	newModifier^.encoding.value := value;
	newModifier^.encoding.mask  := mask;
	newModifier^.next := nil;


	cur := lastOpcode^.opcodedata.modifiers;
	if cur = nil then
	begin
		lastOpcode^.opcodedata.modifiers := newModifier
	end
	else
	begin
		while cur^.next <> nil do cur := cur^.next;
		cur^.next := newModifier;
	end;
end;

procedure addAlias(key:InsString; dest:InsString);
var opcode:OpcodeData;
    encoded:InstructionWord;
begin
	(* encode the instruction and modifiers *)
	getBaseAndModifiers(dest, opcode, encoded);
	(* add a new opcode entry with the alias name and
		the values we just calculated *)
	addOpcode(key, encoded, opcode.encoding.mask, opcode.operand);
	(* copy the modifier list (list of valid modifiers)
		from the original instruction *)
	lastOpcode^.opcodedata.modifiers := opcode.modifiers;
end;

procedure addShortcut(ch:char; dest:InsString);
var opcode:OpcodeData;
    encoded:InstructionWord;
begin
	if shortcuts[ch].id <> -1 then
		errorExit2('internal error in addShortcut for', dest);

	getBaseAndModifiers(dest, opcode, encoded);
	shortcuts[ch] := opcode;
end;

procedure addSpecialOperand(key:IdentString; value:integer);
begin
	createSymbol(key, SpecialSymbol, value, false, false)
end;

procedure initSpecialOperands;
begin
	pass := 1; (* createSymbol only creates symbols on pass 1 *)

	(* create special operand symbols *)
	addSpecialOperand('FP', 0);
	addSpecialOperand('BP', 1);
	addSpecialOperand('RP', 2);
	addSpecialOperand('IV', 3);
	addSpecialOperand('IR', 4);
	addSpecialOperand('ESP', 5);
	addSpecialOperand('EQ', 2);
	addSpecialOperand('LT', 1);
	addSpecialOperand('NE', 6);
	addSpecialOperand('LE', 3);
	addSpecialOperand('GE', 5);
	addSpecialOperand('GT', 7);
end;

procedure initOpcodes;
begin
	addOpcode('BRANCH', $0000, $E000, RelWOprnd);

	addOpcode('LOADC',  $C000, $E000, S13Oprnd);

	addOpcode('LOAD',   $8000, $E000, U13WOprnd);
	addModifier('B',    $0001, $0001);

	addOpcode('STORE',  $4000, $E000, U13WOprnd);
	addModifier('B',    $0001, $0001);

	addOpcode('CBRANCH',$A001, $E001, RelWOprnd);
	addModifier('N',    $0001, $0001);
	addModifier('NZ',   $0001, $0001);
	addModifier('Z',    $0000, $0001);

	addOpcode('XFER',   $6000, $E000, NoOprnd);
	addModifier('RSM1', $0300, $0300);
	addModifier('RS0',  $0000, $0300);
	addModifier('RS1',  $0100, $0300);
	addModifier('R2P',  $0080, $0080);
	addModifier('P2R',  $0040, $0040);
	addModifier('SM1',  $0030, $0030);
	addModifier('S0',   $0000, $0030);
	addModifier('S1',   $0010, $0030);
	addModifier('X2P',  $0001, $0001);

	addOpcode('ALU',    $2000, $E000, OptOprnd);
	addModifier('SM1',  $0030, $0030);
	addModifier('S0',   $0000, $0030);
	addModifier('S1',   $0010, $0030);
	addModifier('X2Y',  $0040, $0040);
	addModifier('NX2Y', $0000, $0040);
	addModifier('XT',   $0080, $0080);
	addModifier('ADD',  $0000, $1e00);
	addModifier('SUB',  $0200, $1e00);
	addModifier('NOT',  $0400, $1e00);
	addModifier('AND',  $0600, $1e00);
	addModifier('OR',   $0800, $1e00);
	addModifier('XOR',  $0a00, $1e00);
	addModifier('CMP',  $0c00, $1e00);
	addModifier('Y',    $0e00, $1e00);
	addModifier('SHR',  $1000, $1e00);
	addModifier('SHL',  $1200, $1e00);
	addModifier('INC',  $1400, $1e00);
	addModifier('DEC',  $1600, $1e00);
	addModifier('BPLC', $1a00, $1e00);
	addModifier('BROT', $1c00, $1e00);
	addModifier('BSEL', $1e00, $1e00);
	addModifier('CMPU', $1800, $1e00);

	(* addOpcode('EXT',    $E000, $E000); *)

	addOpcode('MEM',    $E400, $FFF0, OptOprnd);
	addModifier('W',    $0200, $0200);
	addModifier('SM1',  $0030, $0030);
	addModifier('S0',   $0000, $0030);
	addModifier('S1',   $0010, $0030);
	addModifier('X2Y',  $0040, $0040);
	addModifier('NX2Y', $0000, $0040);

	addOpcode('LOADREL',$F400, $FC00, RelU10Oprnd);

	LOADCPId := nextOpcodeId;
	addOpcode('LOADCP', $F400, $FC00, RelU10Oprnd);

	addOpcode('REG',    $E000, $FFF0, RegOprnd);
	addModifier('W',    $0200, $0200);

	addOpcode('FPADJ',  $EC00, $FC00, S10Oprnd);

	addAlias('JUMP', 'XFER.SM1.X2P');
	addAlias('CALL', 'XFER.RS1.SM1.P2R.X2P');
	addAlias('RET', 'XFER.RSM1.R2P');
	addAlias('ADD', 'ALU.ADD.SM1');
	addAlias('SUB', 'ALU.SUB.SM1');
	addAlias('NOT', 'ALU.NOT.S0');
	addAlias('AND', 'ALU.AND.SM1');
	addAlias('OR', 'ALU.OR.SM1');
	addAlias('XOR', 'ALU.XOR.SM1');
	addAlias('CMP', 'ALU.CMP.SM1');
	addAlias('SHR', 'ALU.SHR.S0');
	addAlias('SHL', 'ALU.SHL.S0');
	addAlias('DUP', 'ALU.INC.S1.X2Y');
	addAlias('NIP', 'ALU.INC.SM1');
	addAlias('INC', 'ALU.INC.S0');
	addAlias('DEC', 'ALU.DEC.S0');
	addAlias('CMPU', 'ALU.CMPU.SM1');
	addAlias('BPLC', 'ALU.BPLC.SM1');
	addAlias('BROT', 'ALU.BROT.S0');
	addAlias('BSEL', 'ALU.BSEL.SM1');
	addAlias('Y', 'ALU.Y.S1.X2Y');
	addAlias('DROP', 'ALU.Y.SM1');
	addAlias('SWAP', 'ALU.Y.S0.X2Y');
	addAlias('OVER', 'ALU.Y.S1.X2Y');
	addAlias('LOADI', 'MEM');
	addAlias('STOREI', 'MEM.W.SM1');
	addAlias('LOADREG', 'REG');
	addAlias('STOREREG', 'REG.W');
end;

procedure initShortcuts;
var ch:char;
begin
	for ch := firstShCChar to lastShCChar do
		shortcuts[ch].id := -1;

	addShortcut('A', 'ADD');
	addShortcut('B', 'BRANCH');
	addShortcut('C', 'CALL');
	addShortcut('D', 'DUP');
	addShortcut('E', 'LOADREL');
	addShortcut('F', 'LOAD');
	addShortcut('G', 'LOADREG');
	addShortcut('H', 'SHL');
	addShortcut('I', 'LOADI');
	addShortcut('J', 'JUMP');
	addShortcut('K', 'LOADC');
	addShortcut('L', 'LOADCP');
	addShortcut('M', 'STORE');
	addShortcut('N', 'NIP');
	addShortcut('O', 'OR');
	addShortcut('P', 'DROP');
	(* Q is unused *)
	addShortcut('R', 'RET');
	addShortcut('S', 'STOREI');
	addShortcut('T', 'NOT');
	addShortcut('U', 'CMPU');
	addShortcut('V', 'OVER');
	addShortcut('W', 'SWAP');
	addShortcut('X', 'XOR');
	(* Y is ununsed *)
	addShortcut('Z', 'SUB');
	addShortcut('a', 'AND');
	addShortcut('b', 'CBRANCH');
	addShortcut('c', 'CMP');
	addShortcut('d', 'DEC');
	(* e is unused *)
	addShortcut('f', 'FPADJ');
	addShortcut('g', 'STOREREG');
	addShortcut('h', 'SHR');
	addShortcut('i', 'INC');
end;

function changeFileSuffix(filename: string; suffix:string): string;
var suffixPos:integer;
begin
	suffixPos := pos(filenameSuffix, filename);
	if suffixPos > 0 then
		setlength(filename, suffixPos-1);
	filename := filename + suffix;
	changeFileSuffix := filename;
end;

procedure performPass(passNo:integer);
begin
	lineno := 1;
	nextConstId := 0;
	cPoolCount := 0;
	pc := 0;
	bytesCount := 0;
	pass := passNo;

	outputEnabled := pass = 2;

	openFileWithDefault(infile, filename);
	infileOpened := true;

	if outputEnabled then
		overwriteFile(outfile, outfilename);

	parseFile;
	printLastLineno;

	close(infile);

	if outputEnabled then
		close(outfile);

	(* dumpSymbolTable; *)
end;

procedure verifyNodeKey(node:TreeRef);
var c:integer;
begin
	if node = nil then
		errorExit2('verifyNodeKey FAIL node is nil', '');
	if node^.key = nil then
		errorExit2('verifyNodeKey FAIL key is nil', '');

	if length(node^.key^) < 1 then
		errorExit2('verifyNodeKey FAIL key has zero length', '');

	c := ord(node^.key^[1]);

	if not (
			((c >= ord('0')) and (c <= ord('9')))
		or
			((c >= ord('A')) and (c <= ord('F')))
	) then
	begin
		writeln('verifyNodeKey FAIL at ', node^.key^, ' ', c);
		if node^.parent <> nil then
			writeln('  parent:', node^.parent^.key^);
		errorExit;
	end;
end;

procedure verifyTree(node:TreeRef);
begin
	if node <> nil then
	begin
		verifyNodeKey(node);

		if node^.right <> nil then
		begin
			if node^.right^.parent <> node then
				errorExit2('verifyTree FAIL parent check right at', node^.key^);
			verifyTree(node^.right);
		end;

		if node^.left <> nil then
		begin
			if node^.left^.parent <> node then
				errorExit2('verifyTree FAIL parent check left at', node^.key^);
			verifyTree(node^.left);
		end;
	end;
end;

begin
	infileOpened := false;
	outputEnabled := false;
	asciiOutput := false;
	editOnError := false;
	runOnSuccess := false;
	buffered := false;
	includeLevel := 0;
	symbolTable := nil;
	opcodeTable := nil;
	lastOpcode := nil;
	firstUnresBranch := nil;

	nextOpcodeId := 1;

	if ParamCount < 1 then halt;

	paramPos := 1;
	filename := '';
	outfilename := '';

	while paramPos <= ParamCount do
	begin
		if paramStr(paramPos) = '-e' then
			editOnError := true
		else
		if paramStr(paramPos) = '-R' then
			runOnSuccess := true
		else
		if paramStr(paramPos) = '-A' then
			asciiOutput := true
		else
		begin
			if length(filename) = 0 then
				filename := ParamStr(paramPos)
			else
				outfilename := ParamStr(paramPos);
		end;
		paramPos := paramPos + 1;
	end;

	initPlatform;
	initOpcodes;
	initSpecialOperands;
	initShortcuts;

	if length(outfilename) = 0 then
	begin
		if asciiOutput then
			outfilename := changeFileSuffix(filename, asciifileSuffix)
		else
			outfilename := changeFileSuffix(filename, outfileSuffix)
	end;

	writeln('Assembling ', filename, ' to ', outfilename);

	performPass(1);
	performPass(2);

	writeln(#13, lineno - 1, ' lines, program size ', bytesCount, ' bytes.');

	(* dumpOpcodeTable; *)
	(* dumpSymbolTable; *)
	writeSymbolTable;

	if runOnSuccess then
		ExecProgram(outfilename);
end.
