// insert byte until PC fits condition static enum eos_t PO_align(void) { intval_t and, equal, fill, test = CPU_pc.intval; // make sure PC is defined. if ((CPU_pc.flags & MVALUE_DEFINED) == 0) { Throw_error(exception_pc_undefined); CPU_pc.flags |= MVALUE_DEFINED; // do not complain again return SKIP_REMAINDER; } and = ALU_defined_int(); if (!Input_accept_comma()) Throw_error(exception_syntax); equal = ALU_defined_int(); if (Input_accept_comma()) fill = ALU_any_int(); else fill = CPU_now->default_align_value; while ((test++ & and) != equal) Output_8b(fill); return ENSURE_EOS; }
// try to load encoding table from given file void encoding_load(char target[256], const char *filename) { FILE *fd = fopen(filename, FILE_READBINARY); if (fd) { if (fread(target, sizeof(char), 256, fd) != 256) Throw_error("Conversion table incomplete."); fclose(fd); } else { Throw_error(exception_cannot_open_input_file); } }
// Try to read a file name. If "allow_library" is TRUE, library access by using // <...> quoting is possible as well. The file name given in the assembler // source code is converted from UNIX style to platform style. // Returns whether error occurred (TRUE on error). Filename in GlobalDynaBuf. // Errors are handled and reported, but caller should call // Input_skip_remainder() then. bool Input_read_filename(bool allow_library) { char *lib_prefix, end_quote; DYNABUF_CLEAR(GlobalDynaBuf); SKIPSPACE(); // check for library access if(GotByte == '<') { // if library access forbidden, complain if(allow_library == FALSE) { Throw_error("Writing to library not supported."); return(TRUE); } // read platform's lib prefix lib_prefix = PLATFORM_LIBPREFIX; #ifndef NO_NEED_FOR_ENV_VAR // if lib prefix not set, complain if(lib_prefix == NULL) { Throw_error("\"ACME\" environment variable not found."); return(TRUE); } #endif // copy lib path and set quoting char DynaBuf_add_string(GlobalDynaBuf, lib_prefix); end_quote = '>'; } else { if(GotByte == '"') end_quote = '"'; else { Throw_error("File name quotes not found (\"\" or <>)."); return(TRUE); } } // read first character, complain if closing quote if(GetQuotedByte() == end_quote) { Throw_error("No file name given."); return(TRUE); } // read characters until closing quote (or EOS) is reached // append platform-converted characters to current string while((GotByte != CHAR_EOS) && (GotByte != end_quote)) { DYNABUF_APPEND(GlobalDynaBuf, PLATFORM_CONVERTPATHCHAR(GotByte)); GetQuotedByte(); } // on error, return if(GotByte == CHAR_EOS) return(TRUE); GetByte(); // fetch next to forget closing quote // terminate string DynaBuf_append(GlobalDynaBuf, '\0'); // add terminator return(FALSE); // no error }
// (Re)set label static enum eos_t PO_set(void) // Now GotByte = illegal char { struct result_t result; int force_bit; struct label_t *label; zone_t zone; if (Input_read_zone_and_keyword(&zone) == 0) // skips spaces before // Now GotByte = illegal char return SKIP_REMAINDER; force_bit = Input_get_force_bit(); // skips spaces after label = Label_find(zone, force_bit); if (GotByte != '=') { Throw_error(exception_syntax); return SKIP_REMAINDER; } // label = parsed value GetByte(); // proceed with next char ALU_any_result(&result); // clear label's force bits and set new ones label->result.flags &= ~(MVALUE_FORCEBITS | MVALUE_ISBYTE); if (force_bit) { label->result.flags |= force_bit; result.flags &= ~(MVALUE_FORCEBITS | MVALUE_ISBYTE); } Label_set_value(label, &result, TRUE); return ENSURE_EOS; }
// if cpu type and value match, set register length variable to value. // if cpu type and value don't match, complain instead. static void check_and_set_reg_length(int *var, int make_long) { if (((CPU_now->flags & CPUFLAG_SUPPORTSLONGREGS) == 0) && make_long) Throw_error("Chosen CPU does not support long registers."); else *var = make_long; }
// Search for label. Create if nonexistant. If created, give it flags "Flags". // The label name must be held in GlobalDynaBuf. struct label_t *Label_find(zone_t zone, int flags) { struct node_ra_t *node; struct label_t *label; int node_created, force_bits = flags & MVALUE_FORCEBITS; node_created = Tree_hard_scan(&node, Label_forest, zone, TRUE); // if node has just been created, create label as well if (node_created) { // Create new label structure label = safe_malloc(sizeof(*label)); // Finish empty label item label->result.flags = flags; if (flags & MVALUE_IS_FP) label->result.val.fpval = 0; else label->result.val.intval = 0; label->usage = 0; // usage count label->pass = pass_count; node->body = label; } else { label = node->body; } // make sure the force bits don't clash if ((node_created == FALSE) && force_bits) if ((label->result.flags & MVALUE_FORCEBITS) != force_bits) Throw_error("Too late for postfix."); return label; }
// Include source file ("!source" or "!src"). Has to be re-entrant. static enum eos_t PO_source(void) {// Now GotByte = illegal char FILE* fd; char local_gotbyte; input_t new_input, *outer_input; // Enter new nesting level. // Quit program if recursion too deep. if(--source_recursions_left < 0) Throw_serious_error("Too deeply nested. Recursive \"!source\"?"); // Read file name. Quit function on error. if(Input_read_filename(TRUE)) return(SKIP_REMAINDER); // If file could be opened, parse it. Otherwise, complain. if((fd = fopen(GLOBALDYNABUF_CURRENT, FILE_READBINARY))) { char *filename = (char *) safe_malloc(GlobalDynaBuf->size); // MPi: Arrays must be a constant expression strcpy(filename, GLOBALDYNABUF_CURRENT); outer_input = Input_now;// remember old input local_gotbyte = GotByte;// CAUTION - ugly kluge Input_now = &new_input;// activate new input Parse_and_close_file(fd, filename); Input_now = outer_input;// restore previous input GotByte = local_gotbyte;// CAUTION - ugly kluge free(filename); } else Throw_error(exception_cannot_open_input_file); // Leave nesting level source_recursions_left++; return(ENSURE_EOS); }
// Parse {block} [else {block}] static void parse_block_else_block(bool parse_first) { // Parse first block. // If it's not correctly terminated, return immediately (because // in that case, there's no use in checking for an "else" part). if(skip_or_parse_block(parse_first)) return; // now GotByte = '}'. Check for "else" part. // If end of statement, return immediately. NEXTANDSKIPSPACE(); if(GotByte == CHAR_EOS) return; // read keyword and check whether really "else" if(Input_read_and_lower_keyword()) { if(strcmp(GlobalDynaBuf->buffer, "else")) Throw_error(exception_syntax); else { SKIPSPACE(); if(GotByte != CHAR_SOB) Throw_serious_error(exception_no_left_brace); skip_or_parse_block(!parse_first); // now GotByte = '}' GetByte(); } } Input_ensure_EOS(); }
// Ensure that the remainder of the current statement is empty, for example // after mnemonics using implied addressing. void Input_ensure_EOS(void) {// Now GotByte = first char to test SKIPSPACE(); if(GotByte) { Throw_error("Garbage data at end of statement."); Input_skip_remainder(); } }
// Assign value to label. The function acts upon the label's flag bits and // produces an error if needed. void Label_set_value(struct label_t *label, struct result_t *newvalue, int change_allowed) { int oldflags = label->result.flags; // value stuff if ((oldflags & MVALUE_DEFINED) && (change_allowed == FALSE)) { // Label is already defined, so compare new and old values // if different type OR same type but different value, complain if (((oldflags ^ newvalue->flags) & MVALUE_IS_FP) || ((oldflags & MVALUE_IS_FP) ? (label->result.val.fpval != newvalue->val.fpval) : (label->result.val.intval != newvalue->val.intval))) Throw_error("Label already defined."); } else { // Label is not defined yet OR redefinitions are allowed label->result = *newvalue; } // flags stuff // Ensure that "unsure" labels without "isByte" state don't get that if ((oldflags & (MVALUE_UNSURE | MVALUE_ISBYTE)) == MVALUE_UNSURE) newvalue->flags &= ~MVALUE_ISBYTE; if (change_allowed) { oldflags = (oldflags & MVALUE_UNSURE) | newvalue->flags; } else { if ((oldflags & MVALUE_FORCEBITS) == 0) if ((oldflags & (MVALUE_UNSURE | MVALUE_DEFINED)) == 0) oldflags |= newvalue->flags & MVALUE_FORCEBITS; oldflags |= newvalue->flags & ~MVALUE_FORCEBITS; } label->result.flags = oldflags; }
// Try to read a condition into DynaBuf and store copy pointer in // given loopcond_t structure. // If no condition given, NULL is written to structure. // Call with GotByte = first interesting character static void store_condition(loopcond_t* condition, char terminator) { void* node_body; // write line number condition->line = Input_now->line_number; // Check for empty condition if(GotByte == terminator) { // Write NULL condition, then return condition->body = NULL; return; } // Seems as if there really *is* a condition. // Read UNTIL/WHILE keyword if(Input_read_and_lower_keyword()) { // Search for new tree item if(!Tree_easy_scan(condkey_tree, &node_body, GlobalDynaBuf)) { Throw_error(exception_syntax); condition->body = NULL; return; } condition->type = (enum cond_key_t) node_body; // Write given condition into buffer SKIPSPACE(); DYNABUF_CLEAR(GlobalDynaBuf); Input_until_terminator(terminator); DynaBuf_append(GlobalDynaBuf, CHAR_EOS);// ensure terminator condition->body = DynaBuf_get_copy(GlobalDynaBuf); } return; }
// Looping assembly ("!for"). Has to be re-entrant. static enum eos_t PO_for(void) {// Now GotByte = illegal char input_t loop_input, *outer_input; result_t loop_counter; intval_t maximum; char* loop_body;// pointer to loop's body block label_t* label; zone_t zone; int force_bit, loop_start;// line number of "!for" pseudo opcode if(Input_read_zone_and_keyword(&zone) == 0) // skips spaces before return(SKIP_REMAINDER); // Now GotByte = illegal char force_bit = Input_get_force_bit(); // skips spaces after label = Label_find(zone, force_bit); if(Input_accept_comma() == FALSE) { Throw_error(exception_syntax); return(SKIP_REMAINDER); } maximum = ALU_defined_int(); if(maximum < 0) Throw_serious_error("Loop count is negative."); if(GotByte != CHAR_SOB) Throw_serious_error(exception_no_left_brace); // remember line number of loop pseudo opcode loop_start = Input_now->line_number; // read loop body into DynaBuf and get copy loop_body = Input_skip_or_store_block(TRUE); // changes line number! // switching input makes us lose GotByte. But we know it's '}' anyway! // set up new input loop_input = *Input_now;// copy current input structure into new loop_input.source_is_ram = TRUE; // set new byte source // remember old input outer_input = Input_now; // activate new input // (not yet useable; pointer and line number are still missing) Input_now = &loop_input; // init counter loop_counter.flags = MVALUE_DEFINED | MVALUE_EXISTS; loop_counter.val.intval = 0; // if count == 0, skip loop if(maximum) { do { loop_counter.val.intval++;// increment counter Label_set_value(label, &loop_counter, TRUE); parse_ram_block(loop_start, loop_body); } while(loop_counter.val.intval < maximum); } else Label_set_value(label, &loop_counter, TRUE); // Free memory free(loop_body); // restore previous input: Input_now = outer_input; // GotByte of OuterInput would be '}' (if it would still exist) GetByte(); // fetch next byte return(ENSURE_EOS); }
// Select CPU ("!cpu" pseudo opcode) static enum eos_t PO_cpu(void) { struct cpu_t* cpu_buffer = CPU_now; // remember current cpu if(Input_read_and_lower_keyword()) if(!CPU_find_cpu_struct(&CPU_now)) Throw_error("Unknown processor."); // If there's a block, parse that and then restore old value! if(Parse_optional_block()) CPU_now = cpu_buffer; return(ENSURE_EOS); }
// Append to GlobalDynaBuf while characters are legal for keywords. // Throws "missing string" error if none. // Returns number of characters added. int Input_append_keyword_to_global_dynabuf(void) { int length = 0; // add characters to buffer until an illegal one comes along while(BYTEFLAGS(GotByte) & CONTS_KEYWORD) { DYNABUF_APPEND(GlobalDynaBuf, GotByte); length++; GetByte(); } if(length == 0) Throw_error(exception_missing_string); return(length); }
// Parse a whole source code file void Parse_and_close_file(FILE* fd, const char* filename) { // be verbose if(Process_verbosity > 2) printf("Parsing source file '%s'\n", filename); // set up new input Input_new_file(filename, fd); // Parse block and check end reason Parse_until_eob_or_eof(); if(GotByte != CHAR_EOF) Throw_error("Found '}' instead of end-of-file."); // close sublevel src fclose(Input_now->src.fd); }
// lookup encoder held in DynaBuf and return its struct pointer (or NULL on failure) const struct encoder *encoding_find(void) { void *node_body; // make sure tree is initialised if (encoder_tree == NULL) Tree_add_table(&encoder_tree, encoder_list); // perform lookup if (!Tree_easy_scan(encoder_tree, &node_body, GlobalDynaBuf)) { Throw_error("Unknown encoding."); return NULL; } return node_body; }
// read optional info about parameter length int Input_get_force_bit(void) { char byte; int force_bit = 0; if(GotByte == '+') { byte = GetByte(); if(byte == '1') force_bit = MVALUE_FORCE08; else if(byte == '2') force_bit = MVALUE_FORCE16; else if(byte == '3') force_bit = MVALUE_FORCE24; if(force_bit) GetByte(); else Throw_error("Illegal postfix."); } SKIPSPACE(); return(force_bit); }
// This function delivers the next byte from the currently active byte source // in un-shortened high-level format. // This function complains if CHAR_EOS (end of statement) is read. char GetQuotedByte(void) { int from_file; // must be an int to catch EOF // If byte source is RAM, then no conversion is necessary, // because in RAM the source already has high-level format if(Input_now->source_is_ram) GotByte = *(Input_now->src.ram_ptr++); // Otherwise, the source is a file. else { // fetch a fresh byte from the current source file from_file = hacked_getc(Input_now); switch(from_file) { case EOF: // remember to send an end-of-file Input_now->state = INPUTSTATE_EOF; GotByte = CHAR_EOS; // end of statement break; case CHAR_LF:// LF character // remember to send a start-of-line Input_now->state = INPUTSTATE_LF; GotByte = CHAR_EOS; // end of statement break; case CHAR_CR:// CR character // remember to check for CRLF + send a start-of-line Input_now->state = INPUTSTATE_CR; GotByte = CHAR_EOS; // end of statement break; default: GotByte = from_file; } } // now check for end of statement if(GotByte == CHAR_EOS) Throw_error("Quotes still open at end of line."); return(GotByte); }
// Include binary file static enum eos_t PO_binary(void) { FILE* fd; int byte; intval_t size = -1, // means "not given" => "until EOF" skip = 0, interleave = 1; int skipByte; // if file name is missing, don't bother continuing if(Input_read_filename(TRUE)) return(SKIP_REMAINDER); // try to open file fd = fopen(GLOBALDYNABUF_CURRENT, FILE_READBINARY); if(fd == NULL) { Throw_error(exception_cannot_open_input_file); return(SKIP_REMAINDER); } // read optional arguments if(Input_accept_comma()) { if(ALU_optional_defined_int(&size) && (size <0)) Throw_serious_error("Negative size argument."); if(Input_accept_comma()) { ALU_optional_defined_int(&skip);// read skip if(Input_accept_comma()) { ALU_optional_defined_int(&interleave);// read interleave if (interleave <= 1) { Throw_serious_error("Negative size argument."); } } } } skipByte = interleave; // check whether including is a waste of time if((size >= 0) && (pass_undefined_count || pass_real_errors)) Output_fake(size); // really including is useless anyway else { // really insert file fseek(fd, skip, SEEK_SET); // set read pointer // if "size" non-negative, read "size" bytes. // otherwise, read until EOF. while(size != 0) { byte = getc(fd); if(byte == EOF) break; skipByte++; if (skipByte >= interleave) { Output_byte(byte); size--; skipByte = 0; } } // if more should have been read, warn and add padding if(size > 0) { Throw_warning("Padding with zeroes."); do Output_byte(0); while(--size); } } fclose(fd); // if verbose, produce some output if((pass_count == 0) && (Process_verbosity > 1)) printf("Loaded %d ($%x) bytes from file offset %ld ($%lx).\n", CPU_2add, CPU_2add, skip, skip); return(ENSURE_EOS); }
// If cpu type and value match, set register length variable to value. // If cpu type and value don't match, complain instead. static void check_and_set_reg_length(bool *var, bool long_reg) { if(long_reg && ((CPU_now->long_regs) == NULL)) Throw_error("Chosen CPU does not support long registers."); else *var = long_reg; }
// Deliver source code from current file (!) in shortened high-level format static char get_processed_from_file(void) { int from_file; do { switch(Input_now->state) { case INPUTSTATE_NORMAL: // fetch a fresh byte from the current source file //from_file = getc(Input_now->src.fd); from_file = hacked_getc(Input_now); // now process it /*FALLTHROUGH*/ case INPUTSTATE_AGAIN: // Process the latest byte again. Of course, this only // makes sense if the loop has executed at least once, // otherwise the contents of from_file are undefined. // If the source is changed so there is a possibility // to enter INPUTSTATE_AGAIN mode without first having // defined "from_file", trouble may arise... Input_now->state = INPUTSTATE_NORMAL; // EOF must be checked first because it cannot be used // as an index into Byte_flags[] if(from_file == EOF) { // remember to send an end-of-file Input_now->state = INPUTSTATE_EOF; return(CHAR_EOS);// end of statement } // check whether character is special one // if not, everything's cool and froody, so return it if((BYTEFLAGS(from_file) & BYTEIS_SYNTAX) == 0) return((char) from_file); // check special characters ("0x00 TAB LF CR SPC :;}") switch(from_file) { case CHAR_TAB:// TAB character case ' ': // remember to skip all following blanks Input_now->state = INPUTSTATE_SKIPBLANKS; return(' '); case CHAR_LF:// LF character // remember to send a start-of-line Input_now->state = INPUTSTATE_LF; return(CHAR_EOS);// end of statement case CHAR_CR:// CR character // remember to check CRLF + send start-of-line Input_now->state = INPUTSTATE_CR; return(CHAR_EOS);// end of statement case CHAR_EOB: // remember to send an end-of-block Input_now->state = INPUTSTATE_EOB; return(CHAR_EOS);// end of statement case CHAR_STATEMENT_DELIMITER: // just deliver an EOS instead return(CHAR_EOS);// end of statement case CHAR_COMMENT_SEPARATOR: // remember to skip remainder of line Input_now->state = INPUTSTATE_COMMENT; return(CHAR_EOS);// end of statement default: // complain if byte is 0 Throw_error("Source file contains illegal character."); return((char) from_file); } case INPUTSTATE_SKIPBLANKS: // read until non-blank, then deliver that do { // from_file = getc(Input_now->src.fd); from_file = hacked_getc(Input_now); } while((from_file == CHAR_TAB) || (from_file == ' ')); // re-process last byte Input_now->state = INPUTSTATE_AGAIN; break; case INPUTSTATE_LF: // return start-of-line, then continue in normal mode Input_now->state = INPUTSTATE_NORMAL; return(CHAR_SOL);// new line case INPUTSTATE_CR: // return start-of-line, remember to check for LF Input_now->state = INPUTSTATE_SKIPLF; return(CHAR_SOL);// new line case INPUTSTATE_SKIPLF: from_file = hacked_getc(Input_now); // if LF, ignore it and fetch another byte // otherwise, process current byte if(from_file == CHAR_LF) Input_now->state = INPUTSTATE_NORMAL; else Input_now->state = INPUTSTATE_AGAIN; break; case INPUTSTATE_COMMENT: // read until end-of-line or end-of-file do from_file = hacked_getc(Input_now); while((from_file != EOF) && (from_file != CHAR_CR) && (from_file != CHAR_LF)); // re-process last byte Input_now->state = INPUTSTATE_AGAIN; break; case INPUTSTATE_EOB: // deliver EOB Input_now->state = INPUTSTATE_NORMAL; return(CHAR_EOB);// end of block case INPUTSTATE_EOF: // deliver EOF Input_now->state = INPUTSTATE_NORMAL; return(CHAR_EOF);// end of file default: Bug_found("StrangeInputMode", Input_now->state); } } while(TRUE); }
// Parse macro call ("+MACROTITLE"). Has to be re-entrant. void Macro_parse_call(void) // Now GotByte = dot or first char of macro name { char local_gotbyte; struct symbol *symbol; struct section new_section, *outer_section; struct input new_input, *outer_input; struct macro *actual_macro; struct rwnode *macro_node, *symbol_node; zone_t macro_zone, symbol_zone; int arg_count = 0; // Enter deeper nesting level // Quit program if recursion too deep. if (--macro_recursions_left < 0) Throw_serious_error("Too deeply nested. Recursive macro calls?"); macro_zone = get_zone_and_title(); // now GotByte = first non-space after title // internal_name = MacroTitle ARG_SEPARATOR (grows to signature) // Accept n>=0 comma-separated arguments before CHAR_EOS. // Valid argument formats are: // EXPRESSION (everything that does NOT start with '~' // ~.LOCAL_LABEL_BY_REFERENCE // ~GLOBAL_LABEL_BY_REFERENCE // now GotByte = non-space if (GotByte != CHAR_EOS) { // any at all? do { // if arg table cannot take another element, enlarge if (argtable_size <= arg_count) enlarge_arg_table(); // Decide whether call-by-reference or call-by-value // In both cases, GlobalDynaBuf may be used. if (GotByte == REFERENCE_CHAR) { // read call-by-reference arg DynaBuf_append(internal_name, ARGTYPE_NUM_REF); GetByte(); // skip '~' character Input_read_zone_and_keyword(&symbol_zone); // GotByte = illegal char arg_table[arg_count].symbol = symbol_find(symbol_zone, 0); } else { // read call-by-value arg DynaBuf_append(internal_name, ARGTYPE_NUM_VAL); ALU_any_result(&(arg_table[arg_count].result)); } ++arg_count; } while (Input_accept_comma()); } // now arg_table contains the arguments // now GlobalDynaBuf = unused // check for "unknown macro" // Search for macro. Do not create if not found. search_for_macro(¯o_node, macro_zone, FALSE); if (macro_node == NULL) { Throw_error("Macro not defined (or wrong signature)."); Input_skip_remainder(); } else { // make macro_node point to the macro struct actual_macro = macro_node->body; local_gotbyte = GotByte; // CAUTION - ugly kluge // set up new input new_input.original_filename = actual_macro->def_filename; new_input.line_number = actual_macro->def_line_number; new_input.source_is_ram = TRUE; new_input.state = INPUTSTATE_NORMAL; // FIXME - fix others! new_input.src.ram_ptr = actual_macro->parameter_list; // remember old input outer_input = Input_now; // activate new input Input_now = &new_input; // remember old section outer_section = Section_now; // start new section (with new zone) // FALSE = title mustn't be freed Section_new_zone(&new_section, "Macro", actual_macro->original_name, FALSE); GetByte(); // fetch first byte of parameter list // assign arguments if (GotByte != CHAR_EOS) { // any at all? arg_count = 0; do { // Decide whether call-by-reference // or call-by-value // In both cases, GlobalDynaBuf may be used. if (GotByte == REFERENCE_CHAR) { // assign call-by-reference arg GetByte(); // skip '~' character Input_read_zone_and_keyword(&symbol_zone); if ((Tree_hard_scan(&symbol_node, symbols_forest, symbol_zone, TRUE) == FALSE) && (pass_count == 0)) Throw_error("Macro parameter twice."); symbol_node->body = arg_table[arg_count].symbol; } else { // assign call-by-value arg Input_read_zone_and_keyword(&symbol_zone); symbol = symbol_find(symbol_zone, 0); // FIXME - add a possibility to symbol_find to make it possible to find out // whether symbol was just created. Then check for the same error message here // as above ("Macro parameter twice."). symbol->result = arg_table[arg_count].result; } ++arg_count; } while (Input_accept_comma()); } // and now, finally, parse the actual macro body Input_now->state = INPUTSTATE_NORMAL; // FIXME - fix others! // maybe call parse_ram_block(actual_macro->def_line_number, actual_macro->body) Input_now->src.ram_ptr = actual_macro->body; Parse_until_eob_or_eof(); if (GotByte != CHAR_EOB) Bug_found("IllegalBlockTerminator", GotByte); // end section (free title memory, if needed) Section_finalize(&new_section); // restore previous section Section_now = outer_section; // restore previous input: Input_now = outer_input; // restore old Gotbyte context GotByte = local_gotbyte; // CAUTION - ugly kluge Input_ensure_EOS(); } ++macro_recursions_left; // leave this nesting level }