#include "mxvm/valid.hpp" #include "mxvm/instruct.hpp" #include "scanner/exception.hpp" #include <algorithm> #include <climits> #include <sstream> #include <unordered_map> #include <unordered_set> namespace mxvm { static inline bool isImmediate(const OpKind k) { return k == OpKind::Num || k == OpKind::Hex || k == OpKind::Str; } static bool has_semicolon(const std::string &s) { bool in_str = false, escaped = false; for (size_t i = 0; i < s.size(); ++i) { char c = s[i]; if (!in_str) { if (c == '#') break; if (c == '/' && i + 1 < s.size() && s[i + 1] == '/') break; if (c == '"') { in_str = true; continue; } if (c == ';') return true; } else { if (escaped) { escaped = false; continue; } if (c == '\\') { escaped = true; continue; } if (c == '"') { in_str = false; continue; } } } return false; } void Validator::collect_objects(std::unordered_set<std::string> &objects, size_t start_index, size_t end_index) { bool in_object_section = false; int brace_depth = 0; for (size_t i = start_index; i <= end_index && i < scanner.size(); ++i) { const auto &tok = scanner[i]; if (!in_object_section) { // Look for "section object {" if (tok.getTokenType() == types::TokenType::TT_ID && tok.getTokenValue() == "section" && i + 2 < scanner.size() && scanner[i + 1].getTokenValue() == "object" && scanner[i + 2].getTokenValue() == "{") { in_object_section = true; brace_depth = 1; i += 2; // Skip "object" and "{" continue; } } else { // Inside object section if (tok.getTokenValue() == "{") { ++brace_depth; } else if (tok.getTokenValue() == "}") { --brace_depth; if (brace_depth == 0) { in_object_section = false; } } else if (tok.getTokenType() == types::TokenType::TT_ID && brace_depth == 1) { // This is an object name at the top level of the object section objects.insert(tok.getTokenValue()); } } } } static const std::unordered_map<std::string, OpSpec> kOpSpecs = { {"mov", {"mov", {OpKind::Id, OpKind::Any}}}, {"load", {"load", {OpKind::Id, OpKind::Id, OpKind::Any}, VArity::None, 3, 4}}, {"store", {"store", {OpKind::Any, OpKind::Id, OpKind::Any}, VArity::None, 3, 4}}, {"add", {"add", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"sub", {"sub", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"mul", {"mul", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"div", {"div", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"or", {"or", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"and", {"and", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"xor", {"xor", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"not", {"not", {OpKind::Id}}}, {"neg", {"neg", {OpKind::Id}}}, {"mod", {"mod", {OpKind::Id, OpKind::Any}, VArity::AnyTail, 2, 3}}, {"cmp", {"cmp", {OpKind::Any, OpKind::Any}}}, {"fcmp", {"fcmp", {OpKind::Any, OpKind::Any}}}, {"jmp", {"jmp", {OpKind::Label}}}, {"je", {"je", {OpKind::Label}}}, {"jne", {"jne", {OpKind::Label}}}, {"jl", {"jl", {OpKind::Label}}}, {"jle", {"jle", {OpKind::Label}}}, {"jg", {"jg", {OpKind::Label}}}, {"jge", {"jge", {OpKind::Label}}}, {"jz", {"jz", {OpKind::Label}}}, {"jnz", {"jnz", {OpKind::Label}}}, {"ja", {"ja", {OpKind::Label}}}, {"jb", {"jb", {OpKind::Label}}}, {"print", {"print", {OpKind::Any}, VArity::AnyTail, 1, -1}}, {"string_print", {"string_print", {OpKind::Any}}}, {"exit", {"exit", {}, VArity::AnyTail, 0, 1}}, {"alloc", {"alloc", {OpKind::Id, OpKind::Any, OpKind::Any}}}, {"free", {"free", {OpKind::Id}}}, {"getline", {"getline", {OpKind::Id}}}, {"push", {"push", {OpKind::Any}}}, {"pop", {"pop", {OpKind::Id}}}, {"stack_load", {"stack_load", {OpKind::Id, OpKind::Any}}}, {"stack_store", {"stack_store", {OpKind::Any, OpKind::Any}}}, {"stack_sub", {"stack_sub", {OpKind::Any}}}, {"call", {"call", {OpKind::Label}}}, {"ret", {"ret", {}}}, {"done", {"done", {}}}, {"to_int", {"to_int", {OpKind::Id, OpKind::Any}}}, {"to_float", {"to_float", {OpKind::Id, OpKind::Any}}}, {"invoke", {"invoke", {OpKind::Id}, VArity::ArgsTail}}, {"return", {"return", {OpKind::Id}}}}; static bool isIdLike(OpKind k) { return k == OpKind::Id || k == OpKind::Member; } ParsedOp Validator::parseOperand() { if (!token) throw mx::Exception("Syntax Error in '" + filename + "': Unexpected EOF parsing operand"); ParsedOp n; if (match(types::TokenType::TT_ID)) { std::string a = token->getTokenValue(); const scan::TToken *at = token; next(); if (match(".")) { next(); require(types::TokenType::TT_ID); a += "." + token->getTokenValue(); n.kind = OpKind::Member; n.text = a; n.at = at; next(); return n; } n.kind = OpKind::Id; n.text = a; n.at = at; return n; } if (match("-") && peekIs(types::TokenType::TT_NUM)) { next(); } if (match(types::TokenType::TT_NUM)) { std::string num_str = token->getTokenValue(); if (num_str.find('.') != std::string::npos) { throw mx::Exception("Syntax Error in '" + filename + "': Floating point constants must be declared as variables, not used directly in instructions at line " + std::to_string(token->getLine())); } n.kind = OpKind::Num; n.text = token->getTokenValue(); n.at = token; next(); return n; } if (match(types::TokenType::TT_HEX)) { n.kind = OpKind::Hex; n.text = token->getTokenValue(); n.at = token; next(); return n; } if (match(types::TokenType::TT_STR)) { n.kind = OpKind::Str; n.text = token->getTokenValue(); n.at = token; next(); return n; } throw mx::Exception("Syntax Error in '" + filename + "': invalid operand '" + token->getTokenValue() + "' at line " + std::to_string(token->getLine())); } std::vector<ParsedOp> Validator::parseOperandList() { std::vector<ParsedOp> out; while (token && !match("}")) { size_t operand_line = token->getLine(); out.push_back(parseOperand()); if (match(",")) { next(); if (token && token->getLine() != operand_line) { throw mx::Exception("Syntax Error in file '" + filename + "': Operands must be on the same line"); } continue; } if (token && token->getLine() == operand_line && !token->getTokenValue().empty()) { throw mx::Exception("Syntax Error in file '" + filename + "': Multiple items or missing comma between operands on line " + std::to_string(operand_line)); } break; } return out; } void Validator::collect_labels(std::unordered_map<std::string, std::string> &labels) { for (size_t i = 0; i < scanner.size(); ++i) { const auto &tok = scanner[i]; if (tok.getTokenType() == types::TokenType::TT_ID) { if (i + 1 < scanner.size() && scanner[i + 1].getTokenValue() == ":") { labels[tok.getTokenValue()] = tok.getTokenValue(); } } } } void Validator::validateAgainstSpec( const std::string &op, const std::vector<ParsedOp> &ops, const std::unordered_map<std::string, Variable> &vars, const std::unordered_map<std::string, std::string> &labels, const std::unordered_set<std::string> &objects, std::vector<UseVar> &usedVarsRef, std::vector<UseLabel> &usedLabelsRef) { auto it = kOpSpecs.find(op); if (it == kOpSpecs.end()) { throw mx::Exception("Syntax Error in '" + filename + "': Unknown instruction '" + op + "'"); } const OpSpec &spec = it->second; int minArgs = (spec.minArgs >= 0) ? spec.minArgs : (int)spec.fixed.size(); int maxArgs = (spec.maxArgs >= 0) ? spec.maxArgs : (spec.varPolicy == VArity::None ? (int)spec.fixed.size() : INT32_MAX); if ((int)ops.size() < minArgs || (int)ops.size() > maxArgs) { throw mx::Exception("Syntax Error in '" + filename + "': '" + op + "' expects " + std::to_string(minArgs) + ((maxArgs == INT32_MAX) ? "..∞" : ".." + std::to_string(maxArgs)) + " operands; found " + std::to_string(ops.size())); } auto kindOk = [&](OpKind want, OpKind got) { if (want == OpKind::Any) return true; if (want == OpKind::Label) return got == OpKind::Id || got == OpKind::Member || got == OpKind::Label; if (want == OpKind::Id) return got == OpKind::Id || got == OpKind::Member; return want == got; }; for (size_t i = 0; i < spec.fixed.size() && i < ops.size(); ++i) { if (!kindOk(spec.fixed[i], ops[i].kind)) { throw mx::Exception("Syntax Error in '" + filename + "': '" + op + "' operand " + std::to_string((int)i + 1) + " has wrong kind"); } } if (spec.varPolicy == VArity::ArgsTail) { for (size_t i = spec.fixed.size(); i < ops.size(); ++i) { if (!(isImmediate(ops[i].kind) || isIdLike(ops[i].kind))) { throw mx::Exception("Syntax Error in '" + filename + "': '" + op + "' extra args must be Id/Imm"); } } } auto pushVar = [&](const ParsedOp &p) { if (isIdLike(p.kind)) { if (p.kind == OpKind::Id && p.text.find('.') == std::string::npos) { if (!vars.count(p.text)) { std::string msg = "Syntax Error in '" + filename + "': Undefined variable '" + p.text + "'"; if (p.at) { msg += " at line " + std::to_string(p.at->getLine()); } throw mx::Exception(msg); } } // NEW: Validate object references else if (p.kind == OpKind::Member && p.text.find('.') != std::string::npos) { std::string objectName = p.text.substr(0, p.text.find('.')); if (!objects.count(objectName)) { std::string msg = "Syntax Error in '" + filename + "': Undefined object '" + objectName + "'"; if (p.at) { msg += " at line " + std::to_string(p.at->getLine()); } throw mx::Exception(msg); } } usedVarsRef.push_back({p.text, p.at}); } }; auto pushLabel = [&](const ParsedOp &p) { if (p.text.find('.') == std::string::npos) { usedLabelsRef.push_back({p.text, p.at}); } }; if (op == "jmp" || op == "je" || op == "jne" || op == "jl" || op == "jle" || op == "jg" || op == "jge" || op == "jz" || op == "jnz" || op == "ja" || op == "jb") { if (!ops.empty()) pushLabel(ops[0]); } if (op == "call") { if (!ops.empty()) pushLabel(ops[0]); } if (op == "mov" || op == "pop" || op == "stack_load" || op == "alloc" || op == "getline" || op == "return" || op == "not" || op == "neg" || op == "to_int" || op == "to_float") { if (!ops.empty() && isIdLike(ops[0].kind)) pushVar(ops[0]); } if (op == "add" || op == "sub" || op == "mul" || op == "div" || op == "or" || op == "and" || op == "xor" || op == "mod" || op == "cmp") { if (ops.size() >= 1 && isIdLike(ops[0].kind)) pushVar(ops[0]); if (ops.size() >= 2 && isIdLike(ops[1].kind)) pushVar(ops[1]); if (ops.size() >= 3 && isIdLike(ops[2].kind)) pushVar(ops[2]); } if (op == "load") { if (ops.size() >= 1 && isIdLike(ops[0].kind)) pushVar(ops[0]); if (ops.size() >= 2 && isIdLike(ops[1].kind)) pushVar(ops[1]); if (ops.size() >= 3 && isIdLike(ops[2].kind)) pushVar(ops[2]); } if (op == "store") { if (ops.size() >= 1 && isIdLike(ops[0].kind)) pushVar(ops[0]); if (ops.size() >= 2 && isIdLike(ops[1].kind)) pushVar(ops[1]); if (ops.size() >= 3 && isIdLike(ops[2].kind)) pushVar(ops[2]); } if (op == "free") { if (!ops.empty() && isIdLike(ops[0].kind)) pushVar(ops[0]); } if (op == "invoke") { for (size_t i = 1; i < ops.size(); ++i) { if (isIdLike(ops[i].kind)) pushVar(ops[i]); } } if (op == "print") { for (size_t i = 0; i < ops.size(); ++i) { if (isIdLike(ops[i].kind)) pushVar(ops[i]); } } } bool Validator::validate(const std::string &name) { filename = name; scanner.scan(); next(); while (token) { std::unordered_map<std::string, std::string> labels; std::unordered_set<std::string> objects; std::vector<UseVar> usedVars; std::vector<UseLabel> usedLabels; size_t block_start = index - 1; size_t block_end = block_start; int brace_count = 0; bool found_opening = false; for (size_t i = block_start; i < scanner.size(); ++i) { if (scanner[i].getTokenValue() == "{") { found_opening = true; brace_count++; } else if (scanner[i].getTokenValue() == "}") { brace_count--; if (found_opening && brace_count == 0) { block_end = i; break; } } } collect_labels(labels); collect_objects(objects, block_start, block_end); std::vector<std::string> lines; { std::istringstream code_info(source); std::string l; while (std::getline(code_info, l)) { lines.push_back(l); } } std::vector<std::pair<int, int>> code_ranges; { bool seen_section = false; bool section_is_code = false; bool in_code = false; int brace_depth = 0; int range_start = -1; for (size_t i = 0; i < scanner.size(); ++i) { const auto &t = scanner[i]; if (!in_code) { if (t.getTokenType() == types::TokenType::TT_ID && t.getTokenValue() == "section") { seen_section = true; section_is_code = false; continue; } if (seen_section && t.getTokenType() == types::TokenType::TT_ID) { section_is_code = (t.getTokenValue() == "code"); continue; } if (section_is_code && t.getTokenValue() == "{") { in_code = true; brace_depth = 1; range_start = t.getLine(); seen_section = false; section_is_code = false; continue; } } else { if (t.getTokenValue() == "{") { ++brace_depth; } else if (t.getTokenValue() == "}") { --brace_depth; if (brace_depth == 0) { int range_end = t.getLine(); code_ranges.emplace_back(range_start, range_end); in_code = false; } } } } } for (const auto &pr : code_ranges) { const int start = std::max(1, pr.first); const int end = pr.second; for (int line = start; line <= end; ++line) { const int idx = line - 1; if (idx < 0 || idx >= static_cast<int>(lines.size())) continue; const std::string &text = lines[idx]; if (has_semicolon(text)) { throw mx::Exception( "Syntax Error in file '" + filename + "': Semicolons are not allowed in code section at line " + std::to_string(line) + ": '" + text + "'"); } } } std::unordered_map<int, int> line_instruction_count; auto is_in_code = [&](int line) -> bool { for (const auto &pr : code_ranges) { if (line >= pr.first && line <= pr.second) return true; } return false; }; for (size_t i = 0; i < scanner.size(); ++i) { const auto &tok = scanner[i]; const int line = tok.getLine(); if (!is_in_code(line)) continue; if (tok.getTokenType() != types::TokenType::TT_ID) continue; if (i + 1 < scanner.size() && scanner[i + 1].getTokenValue() == ":") { continue; } if (tok.getTokenValue() == "function") { continue; } if (std::find(IncType.begin(), IncType.end(), tok.getTokenValue()) != IncType.end()) { line_instruction_count[line]++; } } for (const auto &kv : line_instruction_count) { const int line_num = kv.first; const int count = kv.second; if (count > 1) { const int idx = line_num - 1; const std::string line_text = (idx >= 0 && idx < static_cast<int>(lines.size())) ? lines[idx] : std::string("<unknown>"); throw mx::Exception("Syntax Error in file '" + filename + "': Multiple instructions on same line at line " + std::to_string(line_num) + ": '" + line_text + "'"); } } auto skipSeparators = [&]() {}; skipSeparators(); if (!token) break; if (match("program")) { next(); } else { require("object"); next(); } skipSeparators(); require(types::TokenType::TT_ID); next(); skipSeparators(); require("{"); next(); std::unordered_map<std::string, Variable> vars; for (auto &n : {"stdout", "stdin", "stderr"}) { vars[n] = Variable(); vars[n].var_name = n; } skipSeparators(); while (token && !match("}")) { skipSeparators(); require("section"); next(); skipSeparators(); require(types::TokenType::TT_ID); std::string sectionName = token->getTokenValue(); next(); skipSeparators(); require("{"); next(); if (sectionName == "module" || sectionName == "object") { skipSeparators(); while (token && !match("}")) { skipSeparators(); if (match(types::TokenType::TT_ID)) { next(); skipSeparators(); if (match(",")) { next(); continue; } continue; } break; } skipSeparators(); require("}"); next(); } else if (sectionName == "data") { skipSeparators(); while (token && !match("}")) { skipSeparators(); if (match(types::TokenType::TT_ID) && (token->getTokenValue() == "int" || token->getTokenValue() == "string" || token->getTokenValue() == "float" || token->getTokenValue() == "ptr" || token->getTokenValue() == "byte" || token->getTokenValue() == "export")) { if (token->getTokenValue() == "export") next(); std::string vtype = token->getTokenValue(); next(); skipSeparators(); require(types::TokenType::TT_ID); std::string vname = token->getTokenValue(); vars[vname].var_name = vname; next(); skipSeparators(); if (match(",") && vtype == "string") { next(); skipSeparators(); if (match(types::TokenType::TT_NUM) || match(types::TokenType::TT_HEX)) { next(); skipSeparators(); continue; } else { throw mx::Exception("Syntax Error in file '" + filename + "': string buffer requires number on line " + std::to_string(token->getLine())); } } require("="); next(); skipSeparators(); if (match("-")) { next(); skipSeparators(); } if (vtype == "byte") { if (!(match(types::TokenType::TT_NUM) || match(types::TokenType::TT_HEX))) { throw mx::Exception("Syntax Error in file '" + filename + "': byte must be a valid byte value integer 0-255 on line " + std::to_string(token->getLine())); } int64_t value = std::stoll(token->getTokenValue(), nullptr, 0); if (value < 0 || value > 0xFF) { throw mx::Exception("Syntax Error in file '" + filename + "': byte out of range 0-255 on line: " + std::to_string(token->getLine())); } next(); skipSeparators(); } else if (vtype == "string") { require(types::TokenType::TT_STR); next(); skipSeparators(); } else if (token->getTokenValue() == "null" || match(types::TokenType::TT_NUM) || match(types::TokenType::TT_HEX) || match(types::TokenType::TT_STR)) { next(); skipSeparators(); } else { throw mx::Exception("Syntax Error in file '" + filename + "': Expected value for variable, found: " + token->getTokenValue() + " at line " + std::to_string(token->getLine())); } } else { throw mx::Exception("Syntax Error in file '" + filename + "': Expected variable declaration, found: " + token->getTokenValue() + " at line " + std::to_string(token->getLine())); } } skipSeparators(); require("}"); next(); } else if (sectionName == "code") { skipSeparators(); while (token && !match("}")) { size_t old_index = index; skipSeparators(); if (token && token->getTokenValue() == ";") { throw mx::Exception("Syntax Error in file '" + filename + "': Semicolons not allowed in code section at line " + std::to_string(token->getLine())); } if (match(types::TokenType::TT_ID) && token->getTokenValue() == "function") { next(); skipSeparators(); require(types::TokenType::TT_ID); next(); skipSeparators(); require(":"); next(); continue; } if (match(types::TokenType::TT_ID) && peekIs(":")) { next(); next(); continue; } if (match(types::TokenType::TT_ID)) { std::string op = token->getTokenValue(); if (std::find(IncType.begin(), IncType.end(), op) == IncType.end()) { throw mx::Exception("Syntax Error in file '" + filename + "': Unknown instruction '" + op + "' at line " + std::to_string(token->getLine())); } next(); skipSeparators(); if (op == "ret" || op == "done") { std::vector<ParsedOp> emptyOps; validateAgainstSpec(op, emptyOps, vars, labels, objects, usedVars, usedLabels); // Add objects continue; } auto ops = parseOperandList(); validateAgainstSpec(op, ops, vars, labels, objects, usedVars, usedLabels); // Add objects continue; } else { throw mx::Exception("Syntax Error in file '" + filename + "': Unexpected token '" + token->getTokenValue() + "' in code section at line " + std::to_string(token->getLine())); } if (old_index == index) { if (!next()) break; } } skipSeparators(); require("}"); next(); } else { throw mx::Exception("Syntax Error in file '" + filename + "': Unknown section: " + sectionName + " at line " + std::to_string(token->getLine())); } skipSeparators(); } skipSeparators(); require("}"); next(); // Consume the closing brace of the object/program for (auto &u : usedLabels) { if (!labels.count(u.name)) { throw mx::Exception("Syntax Error in '" + filename + "': Undefined label '" + u.name + "' at line " + std::to_string(u.at->getLine())); } } skipSeparators(); // Prepare for the next object/program or EOF } return true; } bool Validator::match(const std::string &m) { if (!token) return false; if (token->getTokenValue() != m) return false; return true; } void Validator::require(const std::string &r) { if (!token) { throw mx::Exception("Syntax Error in '" + filename + "': Required: " + r + " but reached end of file"); } if (r != token->getTokenValue()) throw mx::Exception( "Syntax Error in '" + filename + "': Required: " + r + " Found: " + token->getTokenValue() + " at line " + std::to_string(token->getLine())); } bool Validator::match(const types::TokenType &t) { if (!token || index >= scanner.size()) return false; if (t != token->getTokenType()) return false; return true; } void Validator::require(const types::TokenType &t) { if (!token || index >= scanner.size()) { throw mx::Exception("Syntax Error in '" + filename + "': unexpected EOF"); } if (t != token->getTokenType()) throw mx::Exception( "Syntax Error in '" + filename + "': Required: " + tokenTypeToString(t) + " instead found: " + token->getTokenValue() + ":" + tokenTypeToString(token->getTokenType()) + " at line " + std::to_string(token->getLine())); } bool Validator::next() { while (index < scanner.size() && scanner[index].getTokenValue() == "\n" && scanner[index].getTokenType() == types::TokenType::TT_SYM) { index++; } if (index < scanner.size()) { token = &scanner[index++]; return true; } token = nullptr; return false; } bool Validator::peekIs(const std::string &s) { return index < scanner.size() && scanner[index].getTokenValue() == s; } bool Validator::peekIs(const types::TokenType &t) { return index < scanner.size() && scanner[index].getTokenType() == t; } std::string Validator::tokenTypeToString(types::TokenType t) { switch (t) { case types::TokenType::TT_ID: return "Identifier"; case types::TokenType::TT_NUM: return "Number"; case types::TokenType::TT_HEX: return "Hex"; case types::TokenType::TT_STR: return "String"; case types::TokenType::TT_SYM: return "Symbol"; default: return ""; } } Validator::Validator(const std::string &source_) : scanner(source_), source(source_) { } } // namespace mxvm