/*

  SjASMPlus Z80 Cross Compiler

  This is modified sources of SjASM by Aprisobal - aprisobal@tut.by

  Copyright (c) 2006 Sjoerd Mastijn

  This software is provided 'as-is', without any express or implied warranty.
  In no event will the authors be held liable for any damages arising from the
  use of this software.

  Permission is granted to anyone to use this software for any purpose,
  including commercial applications, and to alter it and redistribute it freely,
  subject to the following restrictions:

  1. The origin of this software must not be misrepresented; you must not claim
	 that you wrote the original software. If you use this software in a product,
	 an acknowledgment in the product documentation would be appreciated but is
	 not required.

  2. Altered source versions must be plainly marked as such, and must not be
	 misrepresented as being the original software.

  3. This notice may not be removed or altered from any source distribution.

*/

// direct.cpp

#include "sjdefs.h"

CFunctionTable DirectivesTable;
CFunctionTable DirectivesTable_dup;

int ParseDirective(bool beginningOfLine)
{
	char* olp = lp;

	// if END/.END directive is at the beginning of line = ignore them (even with "--dirbol")
	if (beginningOfLine && (cmphstr(lp, "end") || cmphstr(lp, ".end"))) {
		lp = olp;
		return 0;
	}

	bp = lp;
	char* n;
	aint val;
	if (!(n = getinstr(lp))) {	// will also reject any instruction followed by colon char (label)
		lp = olp;
		return 0;
	}

	if (DirectivesTable.zoek(n)) return 1;

	// Only "." repeat directive remains, but that one can't start at beginning of line (without --dirbol)
	const bool isDigitDot = ('.' == *n) && isdigit((byte)n[1]);
	const bool isExprDot = ('.' == *n) && (0 == n[1]) && ('(' == *lp);
	if ((beginningOfLine && !Options::syx.IsPseudoOpBOF) || (!isDigitDot && !isExprDot)) {
		lp = olp;		// alone "." must be followed by digit, or math expression in parentheses
		return 0;		// otherwise just return
	}

	// parse repeat-count either from n+1 (digits) or lp (parentheses) (if syntax is valid)
	if ((isDigitDot && !White(*lp)) || !ParseExpression(isDigitDot ? ++n : ++lp, val) || (isExprDot && ')' != *lp++)) {
		lp = olp; Error("Dot-repeater must be followed by number or parentheses", olp, SUPPRESS);
		return 0;
	}
	if (val < 1) {
		lp = olp; ErrorInt(".N must be positive integer", val, SUPPRESS);
		return 0;
	}

	// preserve original line buffer, and also the line to be repeated (at `lp`)
	char* ml = STRDUP(line);
	SkipBlanks();
	char* pp = STRDUP(lp);
	// create new copy of eolComment because original "line" content will be destroyed
	char* eolCommCopy = eolComment ? STRDUP(eolComment) : nullptr;
	eolComment = eolCommCopy;
	if (NULL == ml || NULL == pp) ErrorOOM();
	++listmacro;
	do {
		line[0] = ' ';
		STRCPY(line+1, LINEMAX-1, pp);	// reset `line` to the content which should be repeated
		ParseLineSafe();			// and parse it
		eolComment = NULL;			// switch OFF EOL-comment after first line
	} while (--val);
	// restore everything
	STRCPY(line, LINEMAX, ml);
	--listmacro;
	donotlist = 1;
	if (eolCommCopy) free(eolCommCopy);
	free(pp);
	free(ml);
	// make lp point at \0, as the repeating line was processed fully
	lp = sline;
	*sline = 0;
	return 1;
}

int ParseDirective_REPT() {
	char* olp = bp = lp, * n;
	if ((n = getinstr(lp)) && DirectivesTable_dup.zoek(n)) return 1;
	lp = olp;
	return 0;
}


static void getBytesWithCheck(int add = 0, int dc = 0, bool dz = false) {
	check8(add); add &= 255;
	int dirDx[130];
	if (GetBytes(lp, dirDx, add, dc)) {
		EmitBytes(dirDx);
		if (dz) EmitByte(0);
	} else {
		Error("no arguments");
	}
}

static void dirBYTE() {
	getBytesWithCheck();
}

static void dirDC() {
	getBytesWithCheck(0, 1);
}

static void dirDZ() {
	getBytesWithCheck(0, 0, true);
}

static void dirDP() {
	// calculate size of the string by subtracting current address + 1 from value of helper temporary label
	const auto endLabel = TemporaryLabelTable.seekForward(DP_HELPER_LABEL);
	const aint dpSize = (nullptr != endLabel) ? (endLabel->value - (CurAddress + 1)) : 0;
	if ((LASTPASS == pass) && (nullptr == endLabel)) Error("dirDP(): please, contact the author of this program.", nullptr, FATAL);
	EmitByte(dpSize);											// emit the size byte
	getBytesWithCheck();										// use regular DB-like parsing for string body
	TemporaryLabelTable.InsertRefresh(DP_HELPER_LABEL);			// insert the forward helper temporary label after it
}

static void dirABYTE() {
	aint add;
	if (ParseExpressionNoSyntaxError(lp, add)) {
		Relocation::checkAndWarn();
		getBytesWithCheck(add);
	} else {
		Error("ABYTE <offset> <bytes>: parsing <offset> failed", bp, SUPPRESS);
	}
}

static void dirABYTEC() {
	aint add;
	if (ParseExpressionNoSyntaxError(lp, add)) {
		Relocation::checkAndWarn();
		getBytesWithCheck(add, 1);
	} else {
		Error("ABYTEC <offset> <bytes>: parsing <offset> failed", bp, SUPPRESS);
	}
}

static void dirABYTEZ() {
	aint add;
	if (ParseExpressionNoSyntaxError(lp, add)) {
		Relocation::checkAndWarn();
		getBytesWithCheck(add, 0, true);
	} else {
		Error("ABYTEZ <offset> <bytes>: parsing <offset> failed", bp, SUPPRESS);
	}
}

static void dirWORD() {
	aint val;
	int teller = 0, e[130];
	do {
		// reset alternate result flag in ParseExpression part of code
		Relocation::isResultAffected = false;
		if (SkipBlanks()) {
			Error("Expression expected", NULL, SUPPRESS);
		} else if (ParseExpressionNoSyntaxError(lp, val)) {
			check16(val);
			e[teller] = val & 65535;
			Relocation::resolveRelocationAffected(teller * 2);
			++teller;
		} else {
			Error("[DW/DEFW/WORD] Syntax error", lp, SUPPRESS);
			break;
		}
	} while (comma(lp) && teller < 128);
	e[teller] = -1;
	if (teller == 128 && *lp) Error("Over 128 values in DW/DEFW/WORD. Values over", lp, SUPPRESS);
	if (teller) EmitWords(e);
	else		Error("DW/DEFW/WORD with no arguments");
}

static void dirDWORD() {
	aint val;
	int teller = 0, e[130 * 2];
	do {
		if (SkipBlanks()) {
			Error("Expression expected", NULL, SUPPRESS);
		} else if (ParseExpressionNoSyntaxError(lp, val)) {
			e[teller * 2] = val & 65535; e[teller * 2 + 1] = (val >> 16) & 0xFFFF; ++teller;
		} else {
			Error("[DWORD] Syntax error", lp, SUPPRESS);
			break;
		}
	} while (comma(lp) && teller < 128);
	e[teller * 2] = -1;
	if (teller == 128 && *lp) Error("Over 128 values in DWORD. Values over", lp, SUPPRESS);
	if (teller) EmitWords(e);
	else		Error("DWORD with no arguments");
}

static void dirD24() {
	aint val;
	int teller = 0, e[130 * 3];
	do {
		if (SkipBlanks()) {
			Error("Expression expected", NULL, SUPPRESS);
		} else if (ParseExpressionNoSyntaxError(lp, val)) {
			check24(val);
			e[teller++] = val & 255; e[teller++] = (val >> 8) & 255; e[teller++] = (val >> 16) & 255;
		} else {
			Error("[D24] Syntax error", lp, SUPPRESS);
			break;
		}
	} while (comma(lp) && teller < 128*3);
	e[teller] = -1;
	if (teller == 128*3 && *lp) Error("Over 128 values in D24. Values over", lp, SUPPRESS);
	if (teller) EmitBytes(e);
	else		Error("D24 with no arguments");
}

static void dirDG() {
	int dirDx[130];
	if (GetBits(lp, dirDx)) {
		EmitBytes(dirDx);
	} else {
		Error("no arguments");
	}
}

static void dirDH() {
	int dirDx[130];
	if (GetBytesHexaText(lp, dirDx)) {
		EmitBytes(dirDx);
	} else {
		Error("no arguments");
	}
}

static void dirBLOCK() {
	int dirDx[130];
	aint teller = 0, val = 0, initByteCount = 0, emitMaxToListing = 4;
	if (ParseExpressionNoSyntaxError(lp, teller)) {
		if (teller < 0) {
			Warning("Negative BLOCK moves PC backwards");
		}
		if (comma(lp)) {
			// Parse operand list using GetBytes (like DEFB does)
			initByteCount = GetBytes(lp, dirDx, 0, 0);
			if (teller < initByteCount) {		// Operands exceed DEFS size - truncate and error
				WarningById(W_SHORT_BLOCK, teller);
				initByteCount = teller;
				if (initByteCount < 0) initByteCount = 0;
				dirDx[initByteCount] = -1;		// truncate init data
			}
			if (0 < initByteCount) {			// emit the explicit init bytes first (if any are available)
				EmitBytes(dirDx);
				val = dirDx[initByteCount - 1];	// last explicit byte is new filler value
				emitMaxToListing -= (initByteCount & 3);
			}
		}
		// emit remaining bytes set with filler
		EmitBlock(val, teller - initByteCount, false, emitMaxToListing);
	} else {
		Error("[BLOCK] Syntax Error in <length>", lp, SUPPRESS);
	}
}

bool dirPageImpl(const char* const dirName, int pageNumber) {
	if (!Device) return false;
	if (pageNumber < 0 || Device->PagesCount <= pageNumber) {
		char buf[LINEMAX];
		SPRINTF2(buf, LINEMAX, "[%s] Page number must be in range 0..%d", dirName, Device->PagesCount - 1);
		ErrorInt(buf, pageNumber);
		return false;
	}
	Device->GetCurrentSlot()->Page = Device->GetPage(pageNumber);
	Device->CheckPage(CDevice::CHECK_RESET);
	return true;
}

static void dirPageImpl(const char* const dirName) {
	aint pageNum;
	if (ParseExpressionNoSyntaxError(lp, pageNum)) {
		dirPageImpl(dirName, pageNum);
	} else {
		Error("Syntax error in <page_number>", lp, SUPPRESS);
	}
}

static void dirORG() {
	if (DISP_NONE == PseudoORG) LabelTable.SizeBoundary(CLabelTable::BOUNDARY_ALL);	// ORG is hard SIZEOF-boundary (even for same address)
	aint val;
	if (!ParseExpressionNoSyntaxError(lp, val)) {
		Error("[ORG] Syntax error in <address>", lp, SUPPRESS);
		return;
	}
	// crop (with warning) address in device or non-longptr mode to 16bit address range
	if ((DeviceID || !Options::IsLongPtr) && !check16u(val)) val &= 0xFFFF;
	// warn about jump in address in raw/output mode
	if (DidWriteOutputByte() && (CurAddress != val)) {
		WarningById(W_FILE_ORG, val - CurAddress);
	}
	CurAddress = val;
	if (DISP_NONE != PseudoORG) WarningById(W_DISPLACED_ORG);
	if (!DeviceID) return;
	if (!comma(lp)) {
		Device->CheckPage(CDevice::CHECK_RESET);
		return;
	}
	// emit warning when current slot does not cover address used for ORG
	auto slot = Device->GetCurrentSlot();
	if ((CurAddress < slot->Address || slot->Address + slot->Size <= CurAddress)) {
		char warnTxt[LINEMAX];
		SPRINTF4(warnTxt, LINEMAX,
					"address 0x%04X vs slot %d range 0x%04X..0x%04X",
					CurAddress, Device->GetCurrentSlotNum(), slot->Address, slot->Address + slot->Size - 1);
		WarningById(W_ORG_PAGE, warnTxt);
	}
	dirPageImpl("ORG");
}

// public API for lua setter
void dirOrgOnlyAddr(aint address) {
	if (DISP_NONE == PseudoORG) LabelTable.SizeBoundary(CLabelTable::BOUNDARY_ALL);	// ORG is hard SIZEOF-boundary (even for same address)
	// crop (with warning) address in device or non-longptr mode to 16bit address range
	if ((DeviceID || !Options::IsLongPtr) && !check16u(address)) address &= 0xFFFF;
	CurAddress = address;
	if (DISP_NONE != PseudoORG) WarningById(W_DISPLACED_ORG);
	if (DeviceID) Device->CheckPage(CDevice::CHECK_RESET);
}

static void dirDISP() {
	if (DISP_NONE != PseudoORG) {
		Warning("[DISP] displacement inside another displacement block, ignoring it.");
		SkipToEol(lp);
		return;
	}
	aint valAdr, valPageNum;
	// parse+validate values first, don't even switch into DISP mode in case of any error
	Relocation::isResultAffected = false;
	if (!ParseExpressionNoSyntaxError(lp, valAdr)) {
		Error("[DISP] Syntax error in <address>", lp, SUPPRESS);
		return;
	}
	// the expression of the DISP shouldn't be affected by relocation (even when starting inside relocation block)
	if (Relocation::checkAndWarn(true)) {
		SkipToEol(lp);
		return;		// report it as error and exit early
	}
	if (comma(lp)) {
		if (!ParseExpressionNoSyntaxError(lp, valPageNum)) {
			Error("[DISP] Syntax error in <page number>", lp);
			return;
		}
		if (!DeviceID) {
			Error("[DISP] <page number> is accepted only in device mode", line);
			return;
		}
		if (valPageNum < 0 || Device->PagesCount <= valPageNum) {
			ErrorInt("[DISP] <page number> is out of range", valPageNum);
			return;
		}
		dispPageNum = valPageNum;
	} else {
		dispPageNum = LABEL_PAGE_UNDEFINED;
	}
	// crop (with warning) address in device or non-longptr mode to 16bit address range
	if ((DeviceID || !Options::IsLongPtr) && !check16u(valAdr)) valAdr &= 0xFFFF;
	// everything is valid, switch to DISP mode (dispPageNum is already set above)
	adrdisp = CurAddress;
	CurAddress = valAdr;
	PseudoORG = Relocation::type ? DISP_INSIDE_RELOCATE : DISP_ACTIVE;
}

static void dirENT() {
	if (DISP_NONE == PseudoORG) {
		Error("ENT should be after DISP");
		return;
	}
	// check if the DISP..ENT block is either fully inside relocation block, or engulfing it fully.
	if (DISP_ACTIVE == PseudoORG && Relocation::type) {
		Error("The DISP block did start outside of relocation block, can't end inside it");
		return;
	}
	if (DISP_INSIDE_RELOCATE == PseudoORG && !Relocation::type) {
		Error("The DISP block did start inside of relocation block, can't end outside of it");
		return;
	}
	CurAddress = adrdisp;
	PseudoORG = DISP_NONE;
	dispPageNum = LABEL_PAGE_UNDEFINED;
}

static void dirPAGE() {
	if (!DeviceID) {
		Warning("PAGE only allowed in real device emulation mode (See DEVICE)");
		SkipParam(lp);
	} else {
		dirPageImpl("PAGE");
	}
}

static void dirMMU() {
	if (!DeviceID) {
		Warning("MMU is allowed only in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}
	aint slot1, slot2, pageN = -1, address = -1;
	CDeviceSlot::ESlotOptions slotOpt = CDeviceSlot::O_NONE;
	if (!ParseExpression(lp, slot1)) {
		Error("[MMU] First slot number parsing failed", bp, SUPPRESS);
		return;
	}
	slot2 = slot1;
	if (!comma(lp)) {	// second slot or slot-option should follow (if not comma)
		// see if there is slot1-only with option-char (e/w/n options)
		const char slotOptChar = (*lp)|0x20;	// primitive ASCII tolower
		if ('a' <= slotOptChar && slotOptChar <= 'z' && (',' == lp[1] || White(lp[1]))) {
			if ('e' == slotOptChar) slotOpt = CDeviceSlot::O_ERROR;
			else if ('w' == slotOptChar) slotOpt = CDeviceSlot::O_WARNING;
			else if ('n' == slotOptChar) slotOpt = CDeviceSlot::O_NEXT;
			else {
				Warning("[MMU] Unknown slot option (legal: e, w, n)", lp);
			}
			++lp;
		} else {	// there was no option char, check if there was slot2 number to define range
			if (!ParseExpression(lp, slot2)) {
				Error("[MMU] Second slot number parsing failed", bp, SUPPRESS);
				return;
			}
		}
		if (!comma(lp)) {
			Error("[MMU] Comma and page number expected after slot info", bp, SUPPRESS);
			return;
		}
	}
	if (!ParseExpression(lp, pageN)) {
		Error("[MMU] Page number parsing failed", bp, SUPPRESS);
		return;
	}
	if (comma(lp)) {
		if (!ParseExpressionNoSyntaxError(lp, address)) {
			Error("[MMU] address parsing failed", bp, SUPPRESS);
			return;
		}
		check16(address);
		address &= 0xFFFF;
	}
	// convert slot entered as addresses into slot numbering (must be precise start address of slot)
	slot1 = Device->SlotNumberFromPreciseAddress(slot1);
	slot2 = Device->SlotNumberFromPreciseAddress(slot2);
	// validate argument values
	if (slot1 < 0 || slot2 < slot1 || Device->SlotsCount <= slot2) {
		char buf[LINEMAX];
		SPRINTF1(buf, LINEMAX, "[MMU] Slot number(s) must be in range 0..%u (or exact starting address of slot) and form a range",
				 Device->SlotsCount - 1);
		Error(buf, NULL, SUPPRESS);
		return;
	}
	if (pageN < 0 || Device->PagesCount <= pageN + (slot2 - slot1)) {
		char buf[LINEMAX];
		SPRINTF1(buf, LINEMAX, "[MMU] Requested page(s) must be in range 0..%u", Device->PagesCount - 1);
		Error(buf, NULL, SUPPRESS);
		return;
	}
	// all valid, set it up
	for (aint slotN = slot1; slotN <= slot2; ++slotN, ++pageN) {
		Device->GetSlot(slotN)->Page = Device->GetPage(pageN);
		// this ^ is also enough to keep global "Slot" up to date (it's a pointer)
		Device->GetSlot(slotN)->Option = slotOpt;	// resets whole range to NONE when range
	}
	// wrap output addresses back into 64ki address space, it's essential for MMU functionality
	if (DISP_NONE != PseudoORG) adrdisp &= 0xFFFF; else CurAddress &= 0xFFFF;
	// set explicit ORG address if the third argument was provided
	if (0 <= address) {
		LabelTable.SizeBoundary(CLabelTable::BOUNDARY_ALL);	// ORG is hard SIZEOF-boundary (even for same address)
		CurAddress = address;
		if (DISP_NONE != PseudoORG) {
			WarningById(W_DISPLACED_ORG);
		}
		// check if explicit ORG address is outside of the slots affected by MMU, warn about it
		const CDeviceSlot & check_s1 = *Device->GetSlot(slot1);
		const CDeviceSlot & check_s2 = *Device->GetSlot(slot2);
		if ((address < check_s1.Address) || (check_s2.Address + check_s2.Size <= address)) {
			char buf[LINEMAX];
			SPRINTF3(buf, LINEMAX, "[MMU] Requested ORG address 0x%04X is out of range 0x%04X..0x%04X",
					 address, check_s1.Address, check_s2.Address + check_s2.Size - 1);
			Warning(buf);
		}
	}
	Device->CheckPage(CDevice::CHECK_RESET);
}

static void dirSLOT() {
	aint val;
	if (!DeviceID) {
		Warning("SLOT only allowed in real device emulation mode (See DEVICE)");
		SkipParam(lp);
		return;
	}
	if (!ParseExpressionNoSyntaxError(lp, val)) {
		Error("[SLOT] Syntax error in <slot_number>", lp, SUPPRESS);
		return;
	}
	val = Device->SlotNumberFromPreciseAddress(val);
	if (!Device->SetSlot(val)) {
		char buf[LINEMAX];
		SPRINTF1(buf, LINEMAX, "[SLOT] Slot number must be in range 0..%u, or exact starting address of slot", Device->SlotsCount - 1);
		Error(buf, NULL, IF_FIRST);
	}
}

static void dirALIGN() {
	// default alignment is 4, default filler is "0/none" (if not specified in directive explicitly)
	aint val, fill;
	ParseAlignArguments(lp, val, fill);
	if (-1 == val) val = 4;
	// calculate how many bytes has to be filled to reach desired alignment
	aint len = (~CurAddress + 1) & (val - 1);
	if (len < 1) return;		// nothing to fill, already aligned
	if (-1 == fill) EmitBlock(0, len, true);
	else			EmitBlock(fill, len, false);
}

static void dirMODULE() {
	LabelTable.SizeBoundary(CLabelTable::BOUNDARY_ALL);	// start of module is hard SIZEOF-boundary
	char* n = GetID(lp);
	if (n && (nullptr == STRCHR(n, '.'))) {
		sourceBlockLevel += 2;
		if (*ModuleName) STRCAT(ModuleName, LINEMAX-1-strlen(ModuleName), ".");
		STRCAT(ModuleName, LINEMAX-1-strlen(ModuleName), n);
		InitVorlab();	// reset non-local label to default "<modules>._"
		if (IsSldExportActive()) {
			WriteToSldFile(-1, CurAddress, 'L', ExportModuleToSld());
		}
	} else {
		if (n) {
			Error("[MODULE] Dots not allowed in <module_name>", n, SUPPRESS);
		} else {
			Error("[MODULE] Syntax error in <name>", bp, SUPPRESS);
		}
	}
}

static void dirENDMODULE() {
	if (! *ModuleName) {
		Error("ENDMODULE without MODULE");
		return;
	}
	//SIZEOF-boundary(main) here, "all" shouldn't be needed if this is paired with MODULE correctly
	LabelTable.SizeBoundary(CLabelTable::BOUNDARY_MAIN);
	sourceBlockLevel -= 2;
	if (IsSldExportActive()) {
		WriteToSldFile(-1, CurAddress, 'L', ExportModuleToSld(true));
	}
	// remove last part of composite modules name
	char* lastDot = strrchr(ModuleName, '.');
	if (lastDot)	*lastDot = 0;
	else			*ModuleName = 0;
	InitVorlab();	// reset non-local label to default "<modules>._"
}

static void dirEND() {
	char* p = lp;
	aint val;
	if (ParseExpression(lp, val)) {
		if (val > 65535 || val < 0) ErrorInt("[END] Invalid address", IF_FIRST);
		else 						StartAddress = val;
	} else {
		lp = p;
	}

	IsRunning = 0;
}

static void dirSIZE() {
	aint val;
	if (!ParseExpressionNoSyntaxError(lp, val)) {
		Error("[SIZE] Syntax error in <filesize>", bp, SUPPRESS);
		return;
	}
	if (LASTPASS != pass) return;	// only active during final pass
	if (-1L == size) size = val;	// first time set
	else if (size != val) ErrorInt("[SIZE] Different size than previous", size);	// just check it's same
}

static void dirINCBIN() {
	int offset = 0, length = INT_MAX;
	fullpath_ref_t fnaam = GetInputFile(lp);
	if (anyComma(lp)) {
		aint val;
		if (!anyComma(lp)) {
			if (!ParseExpressionNoSyntaxError(lp, val)) {
				Error("[INCBIN] Syntax error in <offset>", bp, SUPPRESS);
				return;
			}
			offset = val;
		} else --lp;		// there was second comma right after, reread it
		if (anyComma(lp)) {
			if (!ParseExpressionNoSyntaxError(lp, val)) {
				Error("[INCBIN] Syntax error in <length>", bp, SUPPRESS);
				return;
			}
			length = val;
		}
	}
	BinIncFile(fnaam, offset, length);
}

static void dirINCHOB() {
	unsigned char len[2];
	int offset = 0,length = -1;
	fullpath_ref_t fnaam = GetInputFile(lp);
	if (anyComma(lp)) {
		aint val;
		if (!anyComma(lp)) {
			if (!ParseExpression(lp, val)) {
				Error("[INCHOB] Syntax error", bp, IF_FIRST); return;
			}
			if (val < 0) {
				Error("[INCHOB] Negative values are not allowed", bp); return;
			}
			offset += val;
		} else --lp;		// there was second comma right after, reread it
		if (anyComma(lp)) {
			if (!ParseExpression(lp, val)) {
				Error("[INCHOB] Syntax error", bp, IF_FIRST); return;
			}
			if (val < 0) {
				Error("[INCHOB] Negative values are not allowed", bp); return;
			}
			length = val;
		}
	}

	FILE* ff;
	if (!FOPEN_ISOK(ff, fnaam.full, "rb")) {
		Error("[INCHOB] Error opening file", fnaam.str.c_str(), FATAL);
	}
	if (fseek(ff, 0x0b, 0) || 2 != fread(len, 1, 2, ff)) {
		Error("[INCHOB] Hobeta file has wrong format", fnaam.str.c_str(), FATAL);
	}
	fclose(ff);
	if (length == -1) {
		// calculate remaining length of the file (from the specified offset)
		length = len[0] + (len[1] << 8) - offset;
	}
	offset += 17;		// adjust offset (skip HOB header)
	BinIncFile(fnaam, offset, length);
}

static void dirINCTRD() {
	aint val, offset = 0, length = INT_MAX;
	fullpath_ref_t trdfile = GetInputFile(lp);
	std::string filename {""};
	if (anyComma(lp) && !anyComma(lp)) filename = GetDelimitedString(lp);
	if (filename.empty()) {
		// file-in-disk syntax error
		Error("[INCTRD] Syntax error", bp, IF_FIRST);
		SkipToEol(lp);
		return;
	}
	if (anyComma(lp)) {
		if (!anyComma(lp)) {
			if (!ParseExpressionNoSyntaxError(lp, val)) {
				Error("[INCTRD] Syntax error", bp, IF_FIRST);
				SkipToEol(lp);
				return;
			}
			if (val < 0) {
				ErrorInt("[INCTRD] Negative offset value is not allowed", val);
				SkipToEol(lp);
				return;
			}
			offset = val;
		} else --lp;		// there was second comma right after, reread it
		if (anyComma(lp)) {
			if (!ParseExpressionNoSyntaxError(lp, val)) {
				Error("[INCTRD] Syntax error", bp, IF_FIRST);
				SkipToEol(lp);
				return;
			}
			if (val < 0) {
				ErrorInt("[INCTRD] Negative length value is not allowed", val);
				SkipToEol(lp);
				return;
			}
			length = val;
		}
	}
	if (TRD_PrepareIncFile(trdfile, filename.c_str(), offset, length)) {
		BinIncFile(trdfile, offset, length);
	}
}

static void dirSAVESNA() {
	if (pass != LASTPASS) return;		// syntax error is not visible in early passes

	if (!DeviceID) {
		Error("SAVESNA only allowed in real device emulation mode (See DEVICE)", nullptr, SUPPRESS);
		return;
	} else if (!IsZXSpectrumDevice(DeviceID)) {
		Error("[SAVESNA] Device must be ZXSPECTRUM48 or ZXSPECTRUM128.", nullptr, SUPPRESS);
		return;
	}

	const std::filesystem::path fnaam = GetOutputFileName(lp);
	int start = StartAddress;
	if (anyComma(lp)) {
		aint val;
		if (!ParseExpression(lp, val)) return;
		if (0 <= start) Warning("[SAVESNA] Start address was also defined by END, SAVESNA argument used instead");
		if (0 <= val) {
			start = val;
		} else {
			Error("[SAVESNA] Negative values are not allowed", bp, SUPPRESS);
			return;
		}
	}
	if (start < 0) {
		Error("[SAVESNA] No start address defined", bp, SUPPRESS);
		return;
	}

	if (!SaveSNA_ZX(fnaam, start)) Error("[SAVESNA] Error writing file (Disk full?)", bp, IF_FIRST);
}

static void dirEMPTYTAP() {
	if (pass != LASTPASS) {
		SkipParam(lp);
		return;
	}
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	if (!fnaam.has_filename()) {
		Error("[EMPTYTAP] Syntax error", bp, IF_FIRST); return;
	}
	TAP_SaveEmpty(fnaam);
}

static void dirSAVETAP() {
	if (pass != LASTPASS) {
		SkipParam(lp);
		return;
	}

	bool exec = true, realtapeMode = false;
	int headerType = -1;
	aint val;
	int start = -1, length = -1, param2 = -1, param3 = -1;

	if (!DeviceID) {
		Error("SAVETAP only allowed in real device emulation mode (See DEVICE)");
		exec = false;
	}

	const std::filesystem::path fnaam = GetOutputFileName(lp);
	std::string fnaamh {""};
	if (anyComma(lp)) {
		if (!anyComma(lp)) {
			char *tlp = lp;
			char *id;

			if ((id = GetID(lp)) && strlen(id) > 0) {
				if (cmphstr(id, "basic")) {
					headerType = BASIC;
					realtapeMode = true;
				} else if (cmphstr(id, "numbers")) {
					headerType = NUMBERS;
					realtapeMode = true;
				} else if (cmphstr(id, "chars")) {
					headerType = CHARS;
					realtapeMode = true;
				} else if (cmphstr(id, "code")) {
					headerType = CODE;
					realtapeMode = true;
				} else if (cmphstr(id, "headless")) {
					headerType = HEADLESS;
					realtapeMode = true;
				}
			}

			if (realtapeMode) {
				if (anyComma(lp)) {
					if (headerType == HEADLESS) {
						if (!anyComma(lp)) {
							if (!ParseExpression(lp, val)) {
								Error("[SAVETAP] Syntax error", bp, PASS3); return;
							}
							if (val < 0) {
								Error("[SAVETAP] Negative values are not allowed", bp, PASS3); return;
							} else if (val > 0xFFFF) {
								Error("[SAVETAP] Values higher than FFFFh are not allowed", bp, PASS3); return;
							}
							start = val;
						} else {
							Error("[SAVETAP] Syntax error. Missing start address", bp, PASS3); return;
						}
						if (anyComma(lp)) {
							if (!ParseExpression(lp, val)) {
								Error("[SAVETAP] Syntax error", bp, PASS3); return;
							}
							if (val < 0) {
								Error("[SAVETAP] Negative values are not allowed", bp, PASS3); return;
							} else if (val > 0xFFFF) {
								Error("[SAVETAP] Values higher than FFFFh are not allowed", bp, PASS3); return;
							}
							length = val;
						}
						if (anyComma(lp)) {
							if (!ParseExpression(lp, val)) {
								Error("[SAVETAP] Syntax error", bp, PASS3); return;
							}
							if (val < 0 || val > 255) {
								Error("[SAVETAP] Invalid flag byte", bp, PASS3); return;
							}
							param3 = val;
						}
					} else if (!anyComma(lp)) {
						fnaamh = GetDelimitedString(lp);
						if (fnaamh.empty()) {
							Error("[SAVETAP] Syntax error in tape file name", bp, PASS3);
							return;
						} else if (anyComma(lp) && !anyComma(lp) && ParseExpression(lp, val)) {
							if (val < 0) {
								Error("[SAVETAP] Negative values are not allowed", bp, PASS3); return;
							} else if (val > 0xFFFF) {
								Error("[SAVETAP] Values higher than FFFFh are not allowed", bp, PASS3); return;
							}
							start = val;

							if (anyComma(lp) && !anyComma(lp) && ParseExpression(lp, val)) {
								if (val < 0) {
									Error("[SAVETAP] Negative values are not allowed", bp, PASS3); return;
								} else if (val > 0xFFFF) {
									Error("[SAVETAP] Values higher than FFFFh are not allowed", bp, PASS3); return;
								}
								length = val;

								if (anyComma(lp)) {
									if (!ParseExpression(lp, val)) {
										Error("[SAVETAP] Syntax error", bp, IF_FIRST); return;
									}
									if (val < 0) {
										Error("[SAVETAP] Negative values are not allowed", bp, PASS3); return;
									} else if (val > 0xFFFF) {
										Error("[SAVETAP] Values more than FFFFh are not allowed", bp, PASS3); return;
									}
									param2 = val;
								}
								if (anyComma(lp)) {
									if (!ParseExpression(lp, val)) {
										Error("[SAVETAP] Syntax error", bp, IF_FIRST); return;
									}
									if (val < 0) {
										Error("[SAVETAP] Negative values are not allowed", bp, PASS3); return;
									} else if (val > 0xFFFF) {
										Error("[SAVETAP] Values more than FFFFh are not allowed", bp, PASS3); return;
									}
									param3 = val;
								}
							} else {
								Error("[SAVETAP] Syntax error. Missing block length", bp, PASS3); return;
							}
						} else {
							Error("[SAVETAP] Syntax error. Missing start address", bp, PASS3); return;
						}
					} else {
						Error("[SAVETAP] Syntax error. Missing tape block file name", bp, PASS3); return;
					}
				} else {
					realtapeMode = false;
				}
			}
			if (!realtapeMode) {
				lp = tlp;
				IsLabelNotFound = false;
				if (!ParseExpression(lp, val) || IsLabelNotFound) {
					Error("[SAVETAP] Syntax error", bp, PASS3); return;
				}
				if (val < 0) {
					Error("[SAVETAP] Negative values are not allowed", bp, PASS3); return;
				}
				start = val;
			}
		} else {
			Error("[SAVETAP] Syntax error. No parameters", bp, PASS3); return;
		}
	} else if (StartAddress < 0) {
		Error("[SAVETAP] Syntax error. No parameters", bp, PASS3); return;
	} else {
		start = StartAddress;
	}

	if (exec) {
		int done = 0;

		if (realtapeMode) {
			done = TAP_SaveBlock(fnaam, headerType, fnaamh.c_str(), start, length, param2, param3);
		} else {
			if (!IsZXSpectrumDevice(DeviceID)) {
				Error("[SAVETAP snapshot] Device is not of ZX Spectrum type.", Device->ID, SUPPRESS);
			} else {
				done = TAP_SaveSnapshot(fnaam, start);
			}
		}

		if (!done) {
			Error("[SAVETAP] Error writing file", bp, IF_FIRST);
		}
	}
}

static void dirSAVEBIN() {
	if (!DeviceID) {
		Error("SAVEBIN only allowed in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}
	bool exec = (LASTPASS == pass);
	aint val;
	int start = -1, length = -1;
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	if (anyComma(lp)) {
		if (!anyComma(lp)) {
			if (!ParseExpressionNoSyntaxError(lp, val)) {
				Error("[SAVEBIN] Syntax error", bp, SUPPRESS); return;
			}
			if (val < 0) {
				Error("[SAVEBIN] Values less than 0000h are not allowed", bp); return;
			} else if (val > 0xFFFF) {
			  	Error("[SAVEBIN] Values more than FFFFh are not allowed", bp); return;
			}
			start = val;
		} else {
		  	Error("[SAVEBIN] Syntax error. No parameters", bp, PASS3); return;
		}
		if (anyComma(lp)) {
			if (!ParseExpressionNoSyntaxError(lp, val)) {
				Error("[SAVEBIN] Syntax error", bp, SUPPRESS); return;
			}
			if (val < 0) {
				Error("[SAVEBIN] Negative values are not allowed", bp); return;
			}
			length = val;
		}
	} else {
		Error("[SAVEBIN] Syntax error. No parameters", bp); return;
	}

	if (exec && !SaveBinary(fnaam, start, length)) {
		Error("[SAVEBIN] Error writing file (Disk full?)", bp, IF_FIRST);
	}
}

static void dirSAVEDEV() {
	bool exec = DeviceID && LASTPASS == pass;
	if (!exec && LASTPASS == pass) Error("SAVEDEV only allowed in real device emulation mode (See DEVICE)");

	aint args[3]{-1, -1, -1};		// page, offset, length
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	for (auto & arg : args) {
		if (!comma(lp) || !ParseExpression(lp, arg)) {
			exec = false;
			Error("Expected syntax SAVEDEV <filename>,<startPage>,<startOffset>,<length>", bp, SUPPRESS);
		}
	}
	if (exec) {
		// validate arguments
		if (args[0] < 0 || Device->PagesCount <= args[0]) {
			exec = false; ErrorInt("[SAVEDEV] page number is out of range", args[0]);
		}
		const int32_t start = Device->GetMemoryOffset(args[0], args[1]);
		const int32_t totalRam = Device->GetMemoryOffset(Device->PagesCount, 0);
		if (exec && (start < 0 || totalRam <= start)) {
			exec = false; ErrorInt("[SAVEDEV] calculated start address is out of range", start);
		}
		if (exec && (args[2] <= 0 || totalRam < start + args[2])) {
			exec = false;
			if (args[2]) ErrorInt("[SAVEDEV] invalid end address (bad length?)", start + args[2]);
			else Warning("[SAVEDEV] zero length requested");
		}
		if (exec && !SaveDeviceMemory(fnaam, (size_t)start, (size_t)args[2])) {
			Error("[SAVEDEV] Error writing file (Disk full?)", bp, IF_FIRST);
		}
	}
}

static void dirSAVE3DOS() {
	if (!DeviceID) {
		Error("SAVE3DOS works in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}
	bool exec = (LASTPASS == pass);
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	aint args[5] = { -1, -1, 3, -1, -1 };	// address, size, type, w2_line, w3
	const bool optional[] = {false, false, true, true, true};
	if (!anyComma(lp) || !getIntArguments<5>(lp, args, optional)) {
		Error("[SAVE3DOS] expected syntax is <filename>,<address>,<size>[,<type>[,<w2_line>[,<w3>]]]", bp, SUPPRESS);
		return;
	}
	aint &address = args[0], &size = args[1], &type = args[2], &w2_line = args[3], &w3 = args[4];
	if (address < 0 || size < 1 || 0x10000 < address + size) {
		Error("[SAVE3DOS] [address, size] region outside of 64ki", bp);
		return;
	}
	if (-1 == w3) w3 = size;	// default for w3 is size for all types, unless overridden
	switch (type) {
	case 0:		// type Program: default w2 = 0x8000
		if (-1 == w2_line) w2_line = 0x8000;
	case 1:		// type Numeric array: no idea what w2 actually should be for these
	case 2:		// type Character array:
		break;
	case 3:		// type Code: default w2 = load address
		if (-1 == w2_line) w2_line = address;
		break;
	default:
		Error("[SAVE3DOS] expected type 0..3", bp);
		return;
	}
	if (exec && !SaveBinary3dos(fnaam, address, size, type, w2_line, w3)) {
		Error("[SAVE3DOS] Error writing file (Disk full?)", bp, IF_FIRST);
	}
}

static void dirSAVEAMSDOS() {
	if (!DeviceID) {
		Error("SAVEAMSDOS works in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}
	bool exec = (LASTPASS == pass);
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	aint args[] = { -1, -1, 0, 2 };	// address, size, start, type
	const bool optional[] = {false, false, true, true};
	if (!anyComma(lp) || !getIntArguments<4>(lp, args, optional)) {
		Error("[SAVEAMSDOS] expected syntax is <filename>,<address>,<size>[,<start = 0>[,<type = 2>]", bp, SUPPRESS);
		return;
	}
	aint &address = args[0], &size = args[1], &start = args[2], &type = args[3];
	if (address < 0 || size < 1 || 0x10000 < address + size) {
		Error("[SAVEAMSDOS] [address, size] region outside of 64ki", bp);
		return;
	}
	check16u(start);
	if (type < 0) type = -0x1000;		// check8 works for -256..+255 values, in this case just 0..255 is valid
	check8(type);
	if (exec && !SaveBinaryAmsdos(fnaam, address, size, start, type)) {
		Error("[SAVEAMSDOS] Error writing file (Disk full?)", bp, IF_FIRST);
	}
}

static void dirSAVEHEX() {
	if (!DeviceID) {
		Error("SAVEHEX works in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}
	bool exec = (LASTPASS == pass);
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	aint args[] = { -1, -1, -1 };	// address, size, start
	const bool optional[] = {false, false, true};
	if (!anyComma(lp) || !getIntArguments<3>(lp, args, optional) || !fnaam.has_filename()) {
		Error("[SAVEHEX] expected syntax is <filename>,<address>,<size>[,<start = -1>]", bp, SUPPRESS);
		return;
	}
	aint &address = args[0], &size = args[1], &start = args[2];
	if (address < 0 || size < 1 || 0x10000 < address + size) {
		Error("[SAVEHEX] [address, size] region outside of 64ki", bp);
		return;
	}
	if (start < -1 || 0xFFFF < start) {
		ErrorInt("[SAVEHEX] start should be -1 as OFF or uint16", start);
		start = -1;
	}
	if (exec && !SaveHex(fnaam, address, size, start)) {
		Error("[SAVEHEX] Error writing file (Disk full?)", bp, IF_FIRST);
	}
}

static void dirSAVEHOB() {
	if (!DeviceID || pass != LASTPASS) {
		if (!DeviceID) Error("SAVEHOB only allowed in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}
	aint val;
	int start = -1,length = -1;
	bool exec = true;

	const std::filesystem::path fnaam = GetOutputFileName(lp);
	std::string fnaamh {""};
	if (anyComma(lp)) {
		if (!anyComma(lp)) {
			fnaamh = GetDelimitedString(lp);
			if (fnaamh.empty()) {
				Error("[SAVEHOB] Syntax error", bp, PASS3); return;
			}
		} else {
		  	Error("[SAVEHOB] Syntax error. No parameters", bp, PASS3); return;
		}
	} else {
		Error("[SAVEHOB] Syntax error. No parameters", bp, PASS3); return; //is this ok?
	}

	if (anyComma(lp)) {
		if (!anyComma(lp)) {
			if (!ParseExpression(lp, val)) {
				Error("[SAVEHOB] Syntax error", bp, PASS3); return;
			}
			if (val < 0x4000) {
				Error("[SAVEHOB] Values less than 4000h are not allowed", bp, PASS3); return;
			} else if (val > 0xFFFF) {
			  	Error("[SAVEHOB] Values more than FFFFh are not allowed", bp, PASS3); return;
			}
			start = val;
		} else {
		  	Error("[SAVEHOB] Syntax error. No parameters", bp, PASS3); return;
		}
		if (anyComma(lp)) {
			if (!ParseExpression(lp, val)) {
				Error("[SAVEHOB] Syntax error", bp, PASS3); return;
			}
			if (val < 0) {
				Error("[SAVEHOB] Negative values are not allowed", bp, PASS3); return;
			}
			length = val;
		}
	} else {
		Error("[SAVEHOB] Syntax error. No parameters", bp, PASS3); return;
	}
	if (exec && !SaveHobeta(fnaam, fnaamh.c_str(), start, length)) {
		Error("[SAVEHOB] Error writing file (Disk full?)", bp, IF_FIRST); return;
	}
}

static void dirEMPTYTRD() {
	if (pass != LASTPASS) {
		SkipToEol(lp);
		return;
	}
	char diskLabel[9] = "        ";

	const std::filesystem::path fnaam = GetOutputFileName(lp);
	if (!fnaam.has_filename()) {
		Error("[EMPTYTRD] Syntax error", bp, IF_FIRST);
		return;
	}
	if (anyComma(lp)) {
		const std::string srcLabel = GetDelimitedString(lp);
		if (srcLabel.empty()) {
			Error("[EMPTYTRD] Syntax error, empty label", bp, IF_FIRST);
		} else {
			memcpy(diskLabel, srcLabel.data(), std::min(static_cast<std::string::size_type>(8), srcLabel.size()));
			if (8 < srcLabel.size()) {
				Warning("[EMPTYTRD] label will be truncated to 8 characters", diskLabel);
			}
		}
	}
	TRD_SaveEmpty(fnaam, diskLabel);
}

static void dirSAVETRD() {
	if (!DeviceID || pass != LASTPASS) {
		if (!DeviceID) Error("SAVETRD only allowed in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}

	bool exec = true, replace = false, addplace = false;
	aint val;
	int start = -1, length = -1, autostart = -1, lengthMinusVars = -1;

	const std::filesystem::path fnaam = GetOutputFileName(lp);

	std::string fnaamh {""};
	if (!anyComma(lp)) return Error("[SAVETRD] Syntax error. No parameters", bp, SUPPRESS);
	if (!anyComma(lp)) {
		if ((replace = ('|' == *lp))) SkipBlanks(++lp);	// detect "|" for "replace" feature
		else if ((addplace = ('&' == *lp))) SkipBlanks(++lp); // detect "&" for "addplace" feature
		fnaamh = GetDelimitedString(lp);
	}
	if (fnaamh.empty()) return Error("[SAVETRD] No filename", bp, SUPPRESS);

	if (!anyComma(lp)) return Error("[SAVETRD] Syntax error. No address", bp, SUPPRESS);
	if (ParseExpressionNoSyntaxError(lp, val) && 0 <= val && val <= 0xFFFF) {
		start = val;
	}	// else start == -1 (syntax error or invalid value)
	if (!anyComma(lp)) return Error("[SAVETRD] Syntax error. No length", bp, SUPPRESS);
	ParseExpressionNoSyntaxError(lp, length);	// parse error/invalid value will be reported later

	if (anyComma(lp)) {
		if (addplace) {
			Error("[SAVETRD] Autostart is not used here", bp, PASS3); return;
		} else {
			if (!ParseExpression(lp, val)) {
				Error("[SAVETRD] Syntax error", bp, PASS3); return;
			}
			if (val < -1) {						// -1 is allowed as "no autostart" special value
				Error("[SAVETRD] Negative values are not allowed", bp, SUPPRESS); return;
			}
			autostart = val;
			// optional length of BASIC without variables
			if (anyComma(lp)) {
				if (!ParseExpression(lp, val)) {
					Error("[SAVETRD] Syntax error", bp, PASS3); return;
				}
				lengthMinusVars = val;
			}
		}
	}

	if (exec) TRD_AddFile(fnaam, fnaamh.c_str(), start, length, autostart, replace, addplace, lengthMinusVars);
}

static void dirENCODING() {
	auto arg = GetDelimitedString(lp);
	char* aP = arg.data();
	if (cmphstr(aP, "dos")) {
		ConvertEncoding = ENCDOS;
	} else if (cmphstr(aP, "win")) {
		ConvertEncoding = ENCWIN;
	} else {
		Error("[ENCODING] Invalid argument (valid values: \"dos\" and \"win\")", aP, IF_FIRST);
	}
}

static void dirOPT() {
	// supported options: --zxnext[=cspect] --reversepop --dirbol --nofakes --syntax=<...> -W...
	// process OPT specific command keywords first: {push, pop, reset, listoff, liston, listall, listact, listmc}
	bool didReset = false, didList = Options::syx.IsListingSuspended;
	while (!SkipBlanks(lp) && '-' != *lp) {
		if (cmphstr(lp, "pop")) {	// "pop" previous syntax state
			if (!Options::SSyntax::popSyntax()) Warning("[OPT] no previous syntax found");
			return;
		} else if (cmphstr(lp, "push")) {	// "push" previous syntax state
			if (didReset) Warning("[OPT] pushing syntax status after reset");
			// preserve current syntax status, before using arguments of OPT
			Options::SSyntax::pushCurrentSyntax();
		} else if (cmphstr(lp, "reset")) {	// keep current syntax state
			Options::SSyntax::resetCurrentSyntax();
			didReset = true;
		} else if (cmphstr(lp, "listoff")) {
			if (!didList) {
				ListFile();		// *list* the OPT line suspending the listing
				// show in listing file that some part was suspended
				FILE* listFile = GetListingFile();
				if (LASTPASS == pass && listFile) fputs("# listing file suspended...\n", listFile);
			}
			donotlist = 1;
			Options::syx.IsListingSuspended = didList = true;
		} else if (cmphstr(lp, "liston")) {
			Options::syx.IsListingSuspended = false;
		} else if (cmphstr(lp, "listall")) {
			if (!didList) ListFile();		// *list* the OPT line changing the filtering
			didList = true;
			donotlist = 1;
			Options::syx.ListingType = Options::LST_T_ALL;
		} else if (cmphstr(lp, "listact")) {
			if (!didList) ListFile();		// *list* the OPT line changing the filtering
			didList = true;
			donotlist = 1;
			Options::syx.ListingType = Options::LST_T_ACTIVE;
		} else if (cmphstr(lp, "listmc")) {
			if (!didList) ListFile();		// *list* the OPT line changing the filtering
			didList = true;
			donotlist = 1;
			Options::syx.ListingType = Options::LST_T_MC_ONLY;
		} else {
			Error("[OPT] invalid command (valid commands: push, pop, reset, liston, listoff, listall, listact, listmc)", lp);
			SkipToEol(lp);
			return;
		}
	}
	// split user arguments into "argc, argv" like variables (by white-space)
	char parsedOpts[LINEMAX];
	std::vector<char*> parsedOptsArray;
	int charI = 0, errI;
	while (!SkipBlanks(lp)) {
		parsedOptsArray.push_back(parsedOpts + charI);
		while (*lp && !White()) parsedOpts[charI++] = *lp++;
		parsedOpts[charI++] = 0;
	}
	int optI = parsedOptsArray.size();
	parsedOptsArray.push_back(nullptr);
	// parse user arguments and adjust current syntax setup
	if (optI != (errI = Options::parseSyntaxOptions(optI, parsedOptsArray.data()))) {
		Error("[OPT] invalid/failed option", parsedOptsArray[errI]);
	}
	// init Z80N extensions if requested (the Init is safe to be called multiple times)
	if (Options::syx.IsNextEnabled) Z80::InitNextExtensions();
}

static void dirLABELSLIST() {
	if (pass != 1 || !DeviceID) {
		if (!DeviceID) Error("LABELSLIST only allowed in real device emulation mode (See DEVICE)");
		SkipToEol(lp);
		return;
	}
	const std::filesystem::path opt = GetOutputFileName(lp);
	if (opt.has_filename()) {
		Options::UnrealLabelListFName = opt;
		Options::EmitVirtualLabels = false;
		if (comma(lp)) {
			aint virtualLabelsArg;
			if (!ParseExpressionNoSyntaxError(lp, virtualLabelsArg)) {
				Error("[LABELSLIST] Syntax error in <virtual labels>", bp, EARLY);
				return;
			}
			Options::EmitVirtualLabels = (virtualLabelsArg != 0);
		}
	} else {
		Error("[LABELSLIST] No filename", bp, EARLY);	// pass == 1 -> EARLY
	}
}

static void dirCSPECTMAP() {
	if (LASTPASS != pass || !DeviceID) {
		if (!DeviceID) Error("CSPECTMAP only allowed in real device emulation mode (See DEVICE)");
		SkipParam(lp);
		return;
	}
	const std::filesystem::path fName = GetOutputFileName(lp);
	if (fName.has_filename()) {
		Options::CSpectMapFName = fName;
	} else {		// create default map file name from current source file name (appends ".map")
		assert(!sourcePosStack.empty());
		Options::CSpectMapFName = sourcePosStack.back().filename;
		Options::CSpectMapFName += ".map";
	}
	// remember page size of current device (in case the source is multi-device later)
	Options::CSpectMapPageSize = Device->GetPage(0)->Size;
}

static void dirBPLIST() {
	// breakpoint file is opened in second pass, and content is written through third pass
	// so position of `BPLIST` directive in source does not matter
	// TODO requires extra guard in case of N-pass assembling to open the file only once
	if (2 != pass || !DeviceID) {	// nothing to do in first or last pass, second will open the file
		if (2 == pass) {	// !Device is true -> no device in second pass -> error
			Error("BPLIST only allowed in real device emulation mode (See DEVICE)", nullptr, EARLY);
		}
		SkipToEol(lp);
		return;
	}
	const std::filesystem::path fName = GetOutputFileName(lp);
	EBreakpointsFile type = BPSF_UNREAL;
	if (cmphstr(lp, "unreal")) {
		type = BPSF_UNREAL;
	} else if (cmphstr(lp, "zesarux")) {
		type = BPSF_ZESARUX;
	} else if (cmphstr(lp, "mame")) {
		type = BPSF_MAME;
	} else if (cmphstr(lp, "fuse")) {
		type = BPSF_FUSE;
	} else if (!SkipBlanks()) {
		Warning("[BPLIST] invalid breakpoints file type (valid: \"unreal\", \"zesarux\", \"mame\", \"fuse\")", lp, W_EARLY);
	}
	OpenBreakpointsFile(fName, type);
}

static void dirSETBREAKPOINT() {
	if (LASTPASS != pass) {
		SkipToEol(lp);
		return;
	}
	aint val = CurAddress;
	delim_string_t delimTxt = {};
	if (!SkipBlanks(lp) && (DT_NONE == DelimiterAnyBegins(lp, false))) {	// not a condition, parse as address
		if (!ParseExpressionNoSyntaxError(lp, val)) {
			Error("[SETBREAKPOINT] Syntax error", bp, SUPPRESS);
			return;
		} else if (anyComma(lp) && !SkipBlanks(lp)) {						// condition can be second arg
			if (DT_NONE != DelimiterAnyBegins(lp, false)) delimTxt = GetDelimitedStringEx(lp);
		}
	} else {																// condition delimited string follows
		delimTxt = GetDelimitedStringEx(lp);
	}
	if (!SkipBlanks(lp)) {
		Error("[SETBREAKPOINT] Syntax error", lp, SUPPRESS);
		return;
	}
	WriteBreakpoint(val, (DT_NONE != delimTxt.second && !delimTxt.first.empty()) ? delimTxt.first.c_str() : nullptr);
}

/*void dirTEXTAREA() {

}*/

// error message templates for IF**some** directives
constexpr static size_t dirIfErrorsN = 2, dirIfErrorsSZ = 48;
const static char dirIfErrorsTxtSrc[dirIfErrorsN][dirIfErrorsSZ] = {
	{ "[%s] No ENDIF" },
	{ "[%s] one ELSE only expected" }
};

// IF and IFN internal helper, to evaluate expression
static bool dirIfIfn(aint & val) {
	IsLabelNotFound = false;
	if (!ParseExpression(lp, val)) {
		Error("[IF/IFN] Syntax error", lp, IF_FIRST);
		return false;
	}
	if (IsLabelNotFound) {
		WarningById(W_FWD_REF, bp, W_EARLY);
	}
	return true;
}

// main IF implementation parsing/skipping part of source depending on "val", handling ELSE/ENDIF
static void dirIfInternal(const char* dirName, aint val) {
	// set up error messages for the particular pseudo-op
	char errorsTxt[dirIfErrorsN][dirIfErrorsSZ];
	for (size_t i = 0; i < dirIfErrorsN; ++i) {
		SPRINTF1(errorsTxt[i], dirIfErrorsSZ, dirIfErrorsTxtSrc[i], dirName);
	}
	// do the IF**some** part
	ListFile();
	EReturn ret = END;
	aint elseCounter = 0;
	aint orVal = false;
	while (ENDIF != ret) {
		orVal |= val;
		switch (ret = val ? ReadFile() : SkipFile()) {
			case ELSE:
				if (elseCounter++) Error(errorsTxt[1]);
				val = !val && !orVal;
				break;
			case ELSEIF:
				val = !val && !orVal;
				if (val) {		// active ELSEIF, evaluate expression
					if (!dirIfIfn(val)) {
						val = false;		// syntax error in expression
						orVal = true;		// force remaining IF-blocks inactive
					}
				}
				break;
			case ENDIF:
				break;
			default:
				if (IsRunning) Error(errorsTxt[0]);
				donotlist=!IsRunning;		// do the listing only if still running
				return;
		}
	}
}

static void dirIF() {
	aint val;
	if (dirIfIfn(val)) dirIfInternal("IF", val);
}

static void dirIFN() {
	aint val;
	if (dirIfIfn(val)) dirIfInternal("IFN", !val);
}

// IFUSED and IFNUSED internal helper, to parse label
static bool dirIfusedIfnused(char* & id) {
	id = NULL;
	if (SkipBlanks()) {						// no argument (use last parsed label)
		if (LastParsedLabel) {
			id = STRDUP(LastParsedLabel);
		} else {
			Error("[IFUSED/IFNUSED] no label defined ahead");
			return false;
		}
	} else {
		std::unique_ptr<char[]> validLabel(ValidateLabel(lp, false, true));
		if (validLabel) {
			id = STRDUP(validLabel.get());
			while (islabchar(*lp)) ++lp;	// advance lp beyond parsed label (valid chars only)
		} else {
			SkipToEol(lp);					// ValidateLabel aready reported some error, skip rest
		}
	}
	return id && SkipBlanks();				// valid "id" and no extra characters = OK
}

static void dirIFUSED() {
	char* id;
	if (dirIfusedIfnused(id)) dirIfInternal("IFUSED", LabelTable.IsUsed(id));
	if (id) free(id);
}

static void dirIFNUSED() {
	char* id;
	if (dirIfusedIfnused(id)) dirIfInternal("IFNUSED", !LabelTable.IsUsed(id));
	if (id) free(id);
}

static void dirIFDEF() {
	char* id;
	if ((id = GetID(lp)) && *id) {
		dirIfInternal("IFDEF", DefineTable.FindDuplicate(id));
	} else {
		Error("[IFDEF] Illegal identifier", bp);
	}
}

static void dirIFNDEF() {
	char* id;
	if ((id = GetID(lp)) && *id) {
		dirIfInternal("IFNDEF", !DefineTable.FindDuplicate(id));
	} else {
		Error("[IFNDEF] Illegal identifier", bp);
	}
}

static void dirElseCheckLiveDup() {
	if (RepeatStack.empty()) return;
	if (!RepeatStack.top().IsInWork) return;

	// Seems some ELSE/ELSEIF/ENDIF was encountered inside DUP->EDUP without starting IF
	// -> probably IF was outside of DUP->EDUP block, which is not legal in sjasmplus
	// terminate the current DUP->EDUP macro early and report the open ELSE/ELSEIF/ENDIF
	Error("Conditional block must start and finish inside the repeat block, nested completely");
	lijstp = nullptr;
	RepeatStack.top().RepeatCount = 0;
}

static void dirELSE() {
	dirElseCheckLiveDup();
	Error("ELSE without IF/IFN/IFUSED/IFNUSED/IFDEF/IFNDEF");
}

static void dirELSEIF() {
	dirElseCheckLiveDup();
	Error("ELSEIF without IF/IFN");
}

static void dirENDIF() {
	dirElseCheckLiveDup();
	Error("ENDIF without IF/IFN/IFUSED/IFNUSED/IFDEF/IFNDEF");
}

/*void dirENDTEXTAREA() {
  Error("ENDT without TEXTAREA",0);
}*/

static void dirINCLUDE() {
	fullpath_ref_t fnaam = GetInputFile(lp);
	if (fnaam.full.has_filename()) {
		ListFile();
		IncludeFile(fnaam);
		donotlist = 1;
	} else {
		Error("[INCLUDE] empty filename", bp);
	}
}

static void dirHEXOUT() {
	if (LASTPASS != pass) {
		SkipToEol(lp);
	} else {
		const std::filesystem::path fnaam = GetOutputFileName(lp);
		if (fnaam.has_filename())	OpenHex(fnaam);
		else						Error("[HEXOUT] invalid filename", fnaam.string().c_str());
	}
}

static void dirHEXEND() {
	if (LASTPASS != pass) {
		SkipToEol(lp);
		return;
	}
	char* p = lp;
	aint start = StartAddress;				// global state StartAddress is default when no/invalid start is provided
	if (ParseExpression(lp, start)) {		// -1 is explicit "start address OFF"
		if (start < -1 || 0xFFFF < start) {
			ErrorInt("[HEXEND] Invalid address", start, IF_FIRST);
			start = StartAddress;
		}
	} else {
		lp = p;
	}
	if (!CloseHex(start)) {
		Error("[HEXEND] HEX output was not active");
	}
}

static void dirOUTPUT() {
	if (LASTPASS != pass) {
		SkipToEol(lp);
		return;
	}
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	char modechar = 0;
	int mode = OUTPUT_TRUNCATE;
	if (comma(lp)) {
		if (!SkipBlanks(lp)) modechar = (*lp++) | 0x20;
		switch (modechar) {
			case 't': mode = OUTPUT_TRUNCATE;	break;
			case 'r': mode = OUTPUT_REWIND;		break;
			case 'a': mode = OUTPUT_APPEND;		break;
			default:
				Error("[OUTPUT] Invalid <mode> (valid modes: t, a, r)", bp);
				return;
		}
	}
	//Options::NoDestinationFile = false;
	NewDest(fnaam, mode);
}

static void dirOUTEND()
{
	if (pass == LASTPASS) CloseDest();
}

static void dirTAPOUT()
{
	aint val;
	const std::filesystem::path fnaam = GetOutputFileName(lp);
	int tape_flag = 255;
	if (comma(lp))
	{
		if (!ParseExpression(lp, val))
		{
			Error("[TAPOUT] Missing flagbyte value", bp, PASS3); return;
		}
		tape_flag = val;
	}
	if (pass == LASTPASS) OpenTapFile(fnaam, tape_flag);
}

static void dirTAPEND()
{
	// if (!FP_tapout) {Error("TAPEND without TAPOUT", bp, PASS3); return;}
	if (pass == LASTPASS) CloseTapFile();
}

static void dirDEFINE() {
	bool replaceEnabled = ('+' == *lp) ? ++lp, true : false;
	char* id = GetID(lp);
	if (nullptr == id) {
		Error("[DEFINE] Illegal <id>", lp, SUPPRESS);
		return;
	}
	if (White(*lp)) ++lp;		// skip one whitespace (not considered part of value) (others are)
	// but trim trailing spaces of value, if there's eol-comment
	if (eolComment) {
		char *rtrim = lp + strlen(lp);
		while (lp < rtrim && ' ' == rtrim[-1]) --rtrim;
		*rtrim = 0;
	}

	if (replaceEnabled) {
		DefineTable.Replace(id, lp);
	} else {
		DefineTable.Add(id, lp, nullptr);
	}
	SkipToEol(lp);
	substitutedLine = line;		// override substituted listing for DEFINE
}

static void dirUNDEFINE() {
	char* id;

	if (!(id = GetID(lp)) && *lp != '*') {
		Error("[UNDEFINE] Illegal <id>", lp, SUPPRESS);
		return;
	}

	if (*lp == '*') {
		++lp;
		DefineTable.RemoveAll();
	} else if (DefineTable.FindDuplicate(id)) {
		DefineTable.Remove(id);
	} else {
		Warning("[UNDEFINE] Identifier not found", id);
	}
}

static void dirEXPORT() {
	aint val;
	char* n, * p;

	if (Options::ExportFName.empty()) {
		assert(!sourcePosStack.empty());
		Options::ExportFName = sourcePosStack.back().filename;
		Options::ExportFName.replace_extension(".exp");
		Warning("[EXPORT] Filename for exportfile was not indicated. Output will be in", Options::ExportFName.string().c_str(), W_EARLY);
	}
	if (!(n = p = GetID(lp))) {
		Error("[EXPORT] Syntax error", lp, SUPPRESS);
		return;
	}
	if (pass != LASTPASS) return;
	IsLabelNotFound = false;
	GetLabelValue(n, val);
	if (!IsLabelNotFound) WriteExp(p, val);
}

static void dirDISPLAY() {
	char decprint = 'H';
	char e[LINEMAX + 32], optionChar;		// put extra buffer at end for particular H/A/D number printout
	char* ep = e, * const endOfE = e + LINEMAX;
	aint val;
	do {
		if (SkipBlanks()) {
			Error("[DISPLAY] Expression expected");
			break;
		}
		if (*lp == '/') {
			switch (optionChar = toupper((byte)lp[1])) {
			case 'A': case 'D': case 'H': case 'B': case 'C':
				// known options, switching hex+dec / dec / hex / binary mode / char mode
				decprint = optionChar;
				break;
			case 'L': case 'T':				// silently ignored options (legacy compatibility)
				// in ALASM: 'L' is "concatenate to previous line" (as if there was no \r\n on it)
				// in ALASM: 'T' used ahead of expression will display first the expression itself, then value
				break ;
			default:
				Error("[DISPLAY] Syntax error, unknown option", lp, SUPPRESS);
				return;
			}
			lp += 2;
			continue;
		}
		// try to parse some string literal
		const int remainingBufferSize = endOfE - ep;
		if (remainingBufferSize <= 0) {
			Error("[DISPLAY] internal buffer overflow, resulting text is too long", line);
			return;
		}
		int ei = 0;
		val = GetCharConstAsString(lp, ep, ei, remainingBufferSize);
		if (-1 == val) {
			Error("[DISPLAY] Syntax error", line);
			return;
		} else if (val) {
			ep += ei;				// string literal successfuly parsed
		} else {
			// string literal was not there, how about expression?
			if (ParseExpressionNoSyntaxError(lp, val)) {
				if (decprint == 'B') {	// 8-bit binary (doesn't care about higher bits)
					*(ep++) = '%';
					aint bitMask = 0x80;
					while (bitMask) {
						*(ep++) = (val & bitMask) ? '1' : '0';
						if (0x10 == bitMask) *(ep++) = '\'';
						bitMask >>= 1;
					}
				}
				if (decprint == 'C') {
					val &= 0xFF;	// truncate to 8bit value
					if (' ' <= val && val < 127) {	// printable ASCII
						*ep++ = '\'';
						*ep++ = val;
						*ep++ = '\'';
					} else {		// non-printable char, do the \x?? form
						*ep++ = '\'';
						*ep++ = '\\';
						*ep++ = 'x';
						PrintHex(ep, val, 2);
						*ep++ = '\'';
					}
				}
				if (decprint == 'H' || decprint == 'A') {
					*(ep++) = '0';
					*(ep++) = 'x';
					PrintHexAlt(ep, val);
				}
				if (decprint == 'D' || decprint == 'A') {
					if (decprint == 'A') {
						*(ep++) = ','; *(ep++) = ' ';
					}
					int charsToPrint = SPRINTF1(ep, remainingBufferSize, "%u", val);
					if (remainingBufferSize <= charsToPrint) {
						Error("[DISPLAY] internal buffer overflow, resulting text is too long", line);
						return;
					}
					ep += charsToPrint;
				}
				decprint = 'H';
			} else {
				Error("[DISPLAY] Syntax error", line, SUPPRESS);
				return;
			}
		}
	} while(comma(lp));
	*ep = 0; // end line

	if (LASTPASS == pass && *e) {
		_CERR "> " _CMDL Options::tcols->display _CMDL e _CMDL Options::tcols->end _ENDL;
	}
}

static void dirMACRO() {
	if (lijst) {
		Error("[MACRO] No macro definitions allowed here", NULL, SUPPRESS);
		return;
	}
	// check if the name of macro is defined at beginning of the line ("label" name)
	const bool labelName = LastParsedLabelLine == CompiledCurrentLine;
	assert(!labelName || LastParsedLabel);
	char* lpLabel = labelName ? LastParsedLabel : lp;	// temporary pointer to advance by GetID
	char* n = GetID(lpLabel);							// get+validate macro name
	if (*lpLabel && !White(*lpLabel)) n = nullptr;		// if there's unexpected trailing char, report illegal name
	if (n) {
		if (!labelName) lp = lpLabel;					// name was after MACRO keyword, advance global `lp` (to parse arguments)
		MacroTable.Add(n, lp);
	} else {
		Error("[MACRO] Illegal macroname", labelName ? LastParsedLabel : lp);	// report what was fed into GetID
		SkipToEol(lp);
	}
}

static void dirENDS() {
	Error("[ENDS] End structure without structure");
}

static void dirASSERT() {
	char* p = lp;
	aint val;
	if (!ParseExpressionNoSyntaxError(lp, val)) {
		Error("[ASSERT] Syntax error", p, SUPPRESS);
		return;
	}
	if (pass == LASTPASS && !val) {
		Error("[ASSERT] Assertion failed", p);
	}
	if (comma(lp)) SkipToEol(lp);
}

static void dirSHELLEXEC() {
	//TODO for v2.x change the "SHELLEXEC <command>[, <params>]" syntax to "SHELLEXEC <whatever>"
	// (and add good examples how to deal with quotes/colons/long file names with spaces)
	std::string command = GetDelimitedString(lp);
	std::string parameters {""};
	if (comma(lp)) {
		parameters = GetDelimitedString(lp);
	}
	if (pass == LASTPASS) {
		if (!system(nullptr)) {
			Error("[SHELLEXEC] clib command processor is not available on this platform!");
		} else {
			if (!parameters.empty()) command += " " + parameters;
			if (Options::OutputVerbosity <= OV_ALL) {
				_CERR "Executing <" _CMDL command _CMDL ">" _ENDL;
			}
			// flush both stdout and stderr before trying to execute anything externally
			_COUT flush;
			_CERR flush;
			// execute the requested command
			int exitCode = system(command.c_str());
			if (exitCode) {
				ErrorInt("[SHELLEXEC] non-zero exit code", WEXITSTATUS(exitCode));
			}
		}
	}
}

static void dirSTRUCT() {
	CStructure* st;
	aint offset = 0;
	SkipBlanks();
	std::unique_ptr<char[]> validLabel(ValidateLabel(lp, false, true));
	if (!validLabel) {	// ValidateLabel() reports illegal name as "label" ... welp... good enough.
		SkipToEol(lp);
		return;
	}
	while (islabchar(*lp)) ++lp;	// advance lp beyond parsed label (valid chars only)
	if (comma(lp)) {
		IsLabelNotFound = false;
		if (!ParseExpressionNoSyntaxError(lp, offset)) {
			Error("[STRUCT] Offset syntax error", lp, SUPPRESS);
			return;
		}
		if (IsLabelNotFound) {
			Error("[STRUCT] Forward reference", NULL, EARLY);
		}
	}
	if (!SkipBlanks()) {
		Error("[STRUCT] syntax error, unexpected", lp);
	}
	st = StructureTable.Add(validLabel.get(), offset);	// create structure and start parsing members
	ListFile();
	while (ReadLine()) {
		lp = line;
		SkipBlanks(lp);
		if (*lp == '.') {
			++lp;
		}
		if (cmphstr(lp, "ends")) {
			++CompiledCurrentLine;
			if (st) st->deflab();		// create structure symbols from members and struct itself (size)
			lp = ReplaceDefine(lp);		// skip any empty substitutions and comments
			substitutedLine = line;		// override substituted listing for ENDS
			return;
		}
		if (st) ParseStructLine(st);
		ListFile(true);
	}
	Error("[STRUCT] Unexpected end of structure");
	if (st) st->deflab();
}

static void dirFPOS() {
	aint val;
	int method = SEEK_SET;
	SkipBlanks(lp);
	if ((*lp == '+') || (*lp == '-')) {
		method = SEEK_CUR;
	}
	if (!ParseExpressionNoSyntaxError(lp, val)) {
		Error("[FPOS] Syntax error", lp, SUPPRESS);
	} else if (pass == LASTPASS) {
		SeekDest(val, method);
	}
}

// isWhile == false: DUP/REPT parsing
// isWhile == true: WHILE parsing
static void DupWhileImplementation(bool isWhile) {
	aint val = 0;
	CStringsList* condition = nullptr;

	if (!RepeatStack.empty()) {
		SRepeatStack& dup = RepeatStack.top();
		if (!dup.IsInWork) {
			SkipToEol(lp);		// Just skip the expression to the end of line, don't evaluate yet
			++dup.Level;
			return;
		}
	}

	const char* indexVar = nullptr;
	if (isWhile) {
		condition = new CStringsList(lp);
		if (nullptr == condition) ErrorOOM();
		lp += strlen(condition->string);
		// scan condition string for extra guardian value, and split + parse it as needed
		char* expressionSource = condition->string;
		bool parseOk = ParseExpressionNoSyntaxError(expressionSource, val);
		if (parseOk && *expressionSource && comma(expressionSource)) {
			// comma found, try to parse explicit guardian value
			char* guardianSource = expressionSource;
			parseOk = parseOk && ParseExpressionNoSyntaxError(guardianSource, val);
			// overwrite the comma to keep only condition string without guardian argument
			if (parseOk) {
				assert(',' == expressionSource[-1]);
				expressionSource[-1] = 0;
				++val;		// +1 to explicit value to report error when WHILE does *over* that
			}
		} else {
			val = 100001;	// default guardian value is 100k
		}
		if (!parseOk) {
			Error("[WHILE] Syntax error in <expression>", condition->string, SUPPRESS);
			free(condition->string);			// release original string
			condition->string = STRDUP("0");	// force it to evaluate to zero
			val = 1;
		}
	} else {
		IsLabelNotFound = false;
		if (!ParseExpressionNoSyntaxError(lp, val)) {
			Error("[DUP/REPT] Syntax error in <count>", lp, SUPPRESS);
			return;
		}
		if (IsLabelNotFound) {
			Error("[DUP/REPT] Forward reference", NULL, ALL);
		}
		if ((int) val < 0) {
			ErrorInt("[DUP/REPT] Repeat value must be positive or zero", val, IF_FIRST); return;
		}
		if (comma(lp)) {
			indexVar = GetID(lp);
			if (nullptr == indexVar) {
				Error("[DUP/REPT] invalid index variable name", lp, IF_FIRST);
				SkipToEol(lp);
			}
		}
	}

	RepeatStack.emplace(val, condition, new CStringsList(indexVar ? indexVar : ""));
	if (!SkipBlanks()) Error("[DUP] unexpected chars", lp, SUPPRESS);
}

static void dirDUP() {
	DupWhileImplementation(false);
}

static void dirWHILE() {
	DupWhileImplementation(true);
}

static bool shouldRepeat(SRepeatStack& dup) {
	if (nullptr == dup.RepeatCondition) {
		return 0 <= --dup.RepeatCount;
	} else {
		if (!dup.RepeatCount--) {
			sourcePosStack.push_back(dup.RepeatCondition->source);
			Error("[WHILE] infinite loop? (reaching the guardian value, default 100k)");
			sourcePosStack.pop_back();
			return false;
		}
		aint val = 0;
		IsLabelNotFound = false;
		char* expressionSource = dup.RepeatCondition->string;
		if (!ParseExpressionNoSyntaxError(expressionSource, val) || *expressionSource) {
			const TextFilePos oSourcePos = sourcePosStack.back();
			sourcePosStack.back() = dup.RepeatCondition->source;
			Error("[WHILE] Syntax error in <expression>", dup.RepeatCondition->string, SUPPRESS);
			sourcePosStack.back() = oSourcePos;
			return false;
		}
		if (IsLabelNotFound) {
			WarningById(W_FWD_REF, dup.RepeatCondition->string, W_EARLY);
			return false;
		}
		return val;
	}
}

static void dirEDUP() {
	if (RepeatStack.empty() || RepeatStack.top().IsInWork) {
		Error("[EDUP/ENDR/ENDW] End repeat without repeat");
		return;
	}

	SRepeatStack& dup = RepeatStack.top();
	if (!dup.IsInWork && dup.Level) {
		--dup.Level;
		return;
	}
	dup.IsInWork = true;
	// kill the "EDUP" inside DUP-list (+ works as "while (IsRunning && lijstp && lijstp->string)" terminator)
	if (dup.Pointer->string) free(dup.Pointer->string);
	dup.Pointer->string = NULL;
	++listmacro;
	char* ml = STRDUP(line);	// copy the EDUP line for List purposes (after the DUP block emit)
	if (ml == NULL) ErrorOOM();

	CStringsList* olijstp = lijstp;
	++lijst;
	assert(!sourcePosStack.empty());
	const TextFilePos oSourcePos = sourcePosStack.back();
	aint currentRepeatIndex = 0;
	while (IsRunning && dup.Lines && shouldRepeat(dup)) {
		sourcePosStack.back() = dup.sourcePos;
		lijstp = dup.Lines;
		assert(lijstp);
		if (*lijstp->string) {
			// if the DUP has index variable, the first "line" is the variable name, set it up to current index
			std::unique_ptr<char[]> indexVar(ValidateLabel(lijstp->string,  false));
			if (indexVar.get() &&
				!LabelTable.Insert(indexVar.get(), currentRepeatIndex, LABEL_IS_DEFL) &&
				0 == currentRepeatIndex &&
				1 == pass)
			{
				Error("Duplicate label", indexVar.get(), EARLY);
			}
			++currentRepeatIndex;
		}
		lijstp = lijstp->next;	// skip first empty line / indexVar name
		while (IsRunning && lijstp && lijstp->string) {	// the EDUP/REPT/ENDM line has string=NULL => ends loop
			if (lijstp->source.line) sourcePosStack.back() = lijstp->source;
			STRCPY(line, LINEMAX, lijstp->string);
			substitutedLine = line;		// reset substituted listing
			eolComment = NULL;			// reset end of line comment
			lijstp = lijstp->next;
			ParseLineSafe();
			sourcePosStack.back().nextSegment();
		}
	}
	sourcePosStack.back() = oSourcePos;
	RepeatStack.pop();
	lijstp = olijstp;
	--lijst;
	--listmacro;
	STRCPY(line, LINEMAX,  ml);		// show EDUP line itself
	free(ml);
	++CompiledCurrentLine;
	substitutedLine = line;			// override substituted list line for EDUP
	ListFile();
}

static void dirENDM() {
	if (!RepeatStack.empty()) {
		Warning("ENDM used as DUP/REPT block terminator, this is deprecated (and bugged when used inside macro), change to EDUP or ENDR");
		dirEDUP();
	} else {
		Error("[ENDM] End macro without macro");
	}
}

static bool dirDEFARRAY_parseItems(CStringsList** nextPtr) {
	char ml[LINEMAX];
	do {
		const char* const itemLp = lp;
		char* n = ml;
		if (!GetMacroArgumentValue(lp, n)) {
			Error("[DEFARRAY] Syntax error", itemLp, SUPPRESS);
			return false;
		}
		*nextPtr = new CStringsList(ml);
		if ((*nextPtr)->string == NULL) ErrorOOM();
		nextPtr = &((*nextPtr)->next);
	} while (anyComma(lp));
	return SkipBlanks();
}

static void dirDEFARRAY_add(const char* id) {
	DefineTable.Get(id);
	if (NULL == DefineTable.DefArrayList) {
		Error("[DEFARRAY+] unknown array <id>", id);
		SkipToEol(lp);
		return;
	}
	// array was already defined, seek to the last item in the list
	while (DefineTable.DefArrayList->next) DefineTable.DefArrayList = DefineTable.DefArrayList->next;
	dirDEFARRAY_parseItems(&DefineTable.DefArrayList->next);
	return;
}

static void dirDEFARRAY() {
	bool plus = ('+' == *lp) ? ++lp, true : false;
	const char* id = White() ? GetID(lp) : nullptr;
	if (!id) {
		Error("[DEFARRAY] Syntax error in <id>", lp);
		SkipToEol(lp);
		return;
	}
	if (!White() || SkipBlanks()) {	// enforce whitespace between ID and first item and detect empty ones
		if (SkipBlanks()) Error("[DEFARRAY] must have at least one entry");
		else Error("[DEFARRAY] missing space between <id> and first <item>", lp);
		SkipToEol(lp);
		return;
	}
	if (plus) {
		dirDEFARRAY_add(id);
	} else {
		CStringsList* a = NULL;
		if (!dirDEFARRAY_parseItems(&a) || NULL == a) {
			if (a) delete a;	// release already parsed items, if there was syntax error
			return;
		}
		DefineTable.Add(id, "", a);
	}
}

static const char* DEFDEVICE_SYNTAX_ERR = "[DEFDEVICE] expected syntax is <deviceid>, <slot_size>, <page_count>[, <slot_0_initial_page>[, ...]]";

static void dirDEFDEVICE() {
	//DEFDEVICE <deviceid>, <slot_size>, <page_count>[, <slot_0_initial_page>[, ...]]
	const char* id = GetID(lp);
	if (!id) {
		Error(DEFDEVICE_SYNTAX_ERR, bp, SUPPRESS);
		return;
	}

	const bool is_defined = std::any_of(
		DefDevices.begin(), DefDevices.end(),
		[&](const CDeviceDef* el) { return 0 == strcasecmp(id, el->getID()); }
	);
	if (is_defined || 1 < pass) {
		// same id defined twice during first pass?
		if (pass <= 1) Error("[DEFDEVICE] device with such ID is already defined", id, EARLY);
		// in later passes ignore the line, DEFDEVICE works only in first pass
		SkipToEol(lp);
		return;
	}

	// add new definition if arguments are correct and this is first pass
	aint args[2 + CDeviceDef::MAX_SLOT_N] = {};	// slot_size, page_count, initial pages, ...
	bool optional[2 + CDeviceDef::MAX_SLOT_N] = {false, false};
	aint &slot_size = args[0], &page_count = args[1], *initial_pages = args + 2;
	for (size_t i = 2; i < CDeviceDef::MAX_SLOT_N; ++i) {
		args[i] = -1;
		optional[i] = true;
	}
	if (!anyComma(lp) || !getIntArguments<2 + CDeviceDef::MAX_SLOT_N>(lp, args, optional)) {
		Error(DEFDEVICE_SYNTAX_ERR, bp, EARLY);
		return;
	}
	if (slot_size < 256 || 0x10000 < slot_size || page_count <= 0 || LABEL_PAGE_MAX_RAM < page_count) {
		Error("[DEFDEVICE] valid slot_size: 256..64ki, page_count: 1..30000", bp, EARLY);
		return;
	}
	DefDevices.push_back(new CDeviceDef(id, slot_size, page_count));

	// init "initialPages array by going 0, 1, 2, ..., page_count-1, page_count-1, ... or parsed explicit values
	CDeviceDef & dev = *DefDevices.back();
	int previous_page = -1;
	for (int32_t i = 0; i < dev.SlotsCount; ++i) {
		if (0 <= initial_pages[i] && initial_pages[i] < dev.PagesCount) {
			previous_page = initial_pages[i];
		} else {
			if (-1 != initial_pages[i]) ErrorInt("[DEFDEVICE] invalid initial page", initial_pages[i], EARLY);
			if (previous_page < dev.PagesCount - 1) ++previous_page;
		}
		dev.initialPages[i] = previous_page;
	}
}

static void dirDEVICE() {
	// refresh source position of first DEVICE directive
	if (1 == ++deviceDirectivesCount) {
		assert(!sourcePosStack.empty());
		globalDeviceSourcePos = sourcePosStack.back();
	}

	char* id = GetID(lp);
	if (id) {
		aint ramtop = 0;
		if (anyComma(lp)) {
			if (!ParseExpressionNoSyntaxError(lp, ramtop)) {
				Error("[DEVICE] Syntax error", bp); return;
			}
			if ((ramtop < 0x5D00 || 0xFFFF < ramtop) && (-1 != ramtop)) {
				ErrorInt("[DEVICE] valid range for RAMTOP is $5D00..$FFFF", ramtop); return;
			}
		}
		// if (1 == deviceDirectivesCount && Device) -> device was already set globally, skip SetDevice
		if (1 < deviceDirectivesCount || !Devices) {
			if (!SetDevice(id, ramtop)) {
				Error("[DEVICE] Invalid parameter", id, IF_FIRST);
			}
		}
	} else {
		Error("[DEVICE] Syntax error in <deviceid>", lp, SUPPRESS);
	}
}

static void dirSLDOPT() {
	SkipBlanks(lp);
	if (cmphstr(lp, "comment")) {
		do {
			SldAddCommentKeyword(GetID(lp));
		} while (!SkipBlanks(lp) && anyComma(lp));
	} else if (cmphstr(lp, "swapon")) {
		++sldSwapSrcPos;
	} else if (cmphstr(lp, "swapoff")) {
		--sldSwapSrcPos;
	} else {
		Error("[SLDOPT] Syntax error in <type> (valid: COMMENT, SWAPON, SWAPOFF)", lp, SUPPRESS);
	}
}

void InsertDirectives() {
	DirectivesTable.insertd(".assert", dirASSERT);
	DirectivesTable.insertd(".byte", dirBYTE);
	DirectivesTable.insertd(".abyte", dirABYTE);
	DirectivesTable.insertd(".abytec", dirABYTEC);
	DirectivesTable.insertd(".abytez", dirABYTEZ);
	DirectivesTable.insertd(".word", dirWORD);
	DirectivesTable.insertd(".block", dirBLOCK);
	DirectivesTable.insertd(".dword", dirDWORD);
	DirectivesTable.insertd(".d24", dirD24);
	DirectivesTable.insertd(".dg", dirDG);
	DirectivesTable.insertd(".defg", dirDG);
	DirectivesTable.insertd(".dh", dirDH);
	DirectivesTable.insertd(".defh", dirDH);
	DirectivesTable.insertd(".hex", dirDH);
	DirectivesTable.insertd(".org", dirORG);
	DirectivesTable.insertd(".fpos",dirFPOS);
	DirectivesTable.insertd(".align", dirALIGN);
	DirectivesTable.insertd(".module", dirMODULE);
	DirectivesTable.insertd(".size", dirSIZE);
	//DirectivesTable.insertd(".textarea",dirTEXTAREA);
	DirectivesTable.insertd(".textarea", dirDISP);
	DirectivesTable.insertd(".else", dirELSE);
	DirectivesTable.insertd(".elseif", dirELSEIF);
	DirectivesTable.insertd(".export", dirEXPORT);
	DirectivesTable.insertd(".display", dirDISPLAY);
	DirectivesTable.insertd(".end", dirEND);
	DirectivesTable.insertd(".include", dirINCLUDE);
	DirectivesTable.insertd(".incbin", dirINCBIN);
	DirectivesTable.insertd(".binary", dirINCBIN);
	DirectivesTable.insertd(".inchob", dirINCHOB);
	DirectivesTable.insertd(".inctrd", dirINCTRD);
	DirectivesTable.insertd(".insert", dirINCBIN);
	DirectivesTable.insertd(".savenex", dirSAVENEX);
	DirectivesTable.insertd(".savesna", dirSAVESNA);
	DirectivesTable.insertd(".savehob", dirSAVEHOB);
	DirectivesTable.insertd(".savebin", dirSAVEBIN);
	DirectivesTable.insertd(".savedev", dirSAVEDEV);
	DirectivesTable.insertd(".emptytap", dirEMPTYTAP);
	DirectivesTable.insertd(".savetap", dirSAVETAP);
	DirectivesTable.insertd(".emptytrd", dirEMPTYTRD);
	DirectivesTable.insertd(".savetrd", dirSAVETRD);
	DirectivesTable.insertd(".savecpcsna", dirSAVECPCSNA);
	DirectivesTable.insertd(".savecdt", dirSAVECDT);
	DirectivesTable.insertd(".save3dos", dirSAVE3DOS);
	DirectivesTable.insertd(".saveamsdos", dirSAVEAMSDOS);
	DirectivesTable.insertd(".savecpr", dirSAVECPR);
	DirectivesTable.insertd(".savehex", dirSAVEHEX);
	DirectivesTable.insertd(".shellexec", dirSHELLEXEC);
/*#ifdef WIN32
	DirectivesTable.insertd(".winexec", dirWINEXEC);
#endif*/
	DirectivesTable.insertd(".if", dirIF);
	DirectivesTable.insertd(".ifn", dirIFN);
	DirectivesTable.insertd(".ifused", dirIFUSED);
	DirectivesTable.insertd(".ifnused", dirIFNUSED);
	DirectivesTable.insertd(".ifdef", dirIFDEF);
	DirectivesTable.insertd(".ifndef", dirIFNDEF);
	DirectivesTable.insertd(".hexout", dirHEXOUT);
	DirectivesTable.insertd(".hexend", dirHEXEND);
	DirectivesTable.insertd(".output", dirOUTPUT);
	DirectivesTable.insertd(".outend", dirOUTEND);
	DirectivesTable.insertd(".tapout", dirTAPOUT);
	DirectivesTable.insertd(".tapend", dirTAPEND);
	DirectivesTable.insertd(".define", dirDEFINE);
	DirectivesTable.insertd(".undefine", dirUNDEFINE);
	DirectivesTable.insertd(".defarray", dirDEFARRAY);
	DirectivesTable.insertd(".macro", dirMACRO);
	DirectivesTable.insertd(".struct", dirSTRUCT);
	DirectivesTable.insertd(".dc", dirDC);
	DirectivesTable.insertd(".dz", dirDZ);
	DirectivesTable.insertd(".db", dirBYTE);
	DirectivesTable.insertd(".dm", dirBYTE);
	DirectivesTable.insertd(".dp", dirDP);
	DirectivesTable.insertd(".dw", dirWORD);
	DirectivesTable.insertd(".ds", dirBLOCK);
	DirectivesTable.insertd(".dd", dirDWORD);
	DirectivesTable.insertd(".defb", dirBYTE);
	DirectivesTable.insertd(".defp", dirDP);
	DirectivesTable.insertd(".defw", dirWORD);
	DirectivesTable.insertd(".defs", dirBLOCK);
	DirectivesTable.insertd(".defd", dirDWORD);
	DirectivesTable.insertd(".defm", dirBYTE);
	DirectivesTable.insertd(".endmod", dirENDMODULE);
	DirectivesTable.insertd(".endmodule", dirENDMODULE);
	DirectivesTable.insertd(".rept", dirDUP);
	DirectivesTable.insertd(".dup", dirDUP);
	DirectivesTable.insertd(".while", dirWHILE);
	DirectivesTable.insertd(".disp", dirDISP);
	DirectivesTable.insertd(".phase", dirDISP);
	DirectivesTable.insertd(".ent", dirENT);
	DirectivesTable.insertd(".unphase", dirENT);
	DirectivesTable.insertd(".dephase", dirENT);
	DirectivesTable.insertd(".page", dirPAGE);
	DirectivesTable.insertd(".slot", dirSLOT);
	DirectivesTable.insertd(".mmu", dirMMU);
	DirectivesTable.insertd(".encoding", dirENCODING);
	DirectivesTable.insertd(".opt", dirOPT);
	DirectivesTable.insertd(".labelslist", dirLABELSLIST);
	DirectivesTable.insertd(".cspectmap", dirCSPECTMAP);
	DirectivesTable.insertd(".endif", dirENDIF);
	DirectivesTable.insertd(".endt", dirENT);
	DirectivesTable.insertd(".endm", dirENDM);
	DirectivesTable.insertd(".edup", dirEDUP);
	DirectivesTable.insertd(".endr", dirEDUP);
	DirectivesTable.insertd(".endw", dirEDUP);
	DirectivesTable.insertd(".ends", dirENDS);

	DirectivesTable.insertd(".device", dirDEVICE);
	DirectivesTable.insertd(".defdevice", dirDEFDEVICE);

	DirectivesTable.insertd(".bplist", dirBPLIST);
	DirectivesTable.insertd(".setbreakpoint", dirSETBREAKPOINT);
	DirectivesTable.insertd(".setbp", dirSETBREAKPOINT);

	DirectivesTable.insertd(".relocate_start", Relocation::dirRELOCATE_START);
	DirectivesTable.insertd(".relocate_end", Relocation::dirRELOCATE_END);
	DirectivesTable.insertd(".relocate_table", Relocation::dirRELOCATE_TABLE);

	DirectivesTable.insertd(".sldopt", dirSLDOPT);

#ifdef USE_LUA
	DirectivesTable.insertd(".lua", dirLUA);
	DirectivesTable.insertd(".endlua", dirENDLUA);
	DirectivesTable.insertd(".includelua", dirINCLUDELUA);
#endif //USE_LUA

	DirectivesTable_dup.insertd(".dup", dirDUP);
	DirectivesTable_dup.insertd(".edup", dirEDUP);
	DirectivesTable_dup.insertd(".endm", dirENDM);
	DirectivesTable_dup.insertd(".endr", dirEDUP);
	DirectivesTable_dup.insertd(".endw", dirEDUP);
	DirectivesTable_dup.insertd(".rept", dirDUP);
	DirectivesTable_dup.insertd(".while", dirWHILE);
}

//eof direct.cpp
