diff --git a/concore.hpp b/concore.hpp index e109a5d..da2c792 100644 --- a/concore.hpp +++ b/concore.hpp @@ -24,6 +24,8 @@ #include #include +#include "concore_base.hpp" + using namespace std; /** @@ -219,48 +221,13 @@ class Concore{ */ map mapParser(string filename){ map ans; - - ifstream portfile; - string portstr; - portfile.open(filename); - if(portfile){ - ostringstream ss; - ss << portfile.rdbuf(); - portstr = ss.str(); - portfile.close(); - } - - if (portstr.empty()) { - return ans; - } - - portstr[portstr.size()-1]=','; - portstr+='}'; - int i=0; - string portname=""; - string portnum=""; - - while(portstr[i]!='}'){ - if(portstr[i]=='\''){ - i++; - while(portstr[i]!='\''){ - portname+=portstr[i]; - i++; - } - ans.insert({portname,0}); + auto str_map = concore_base::safe_literal_eval_dict(filename, {}); + for (const auto& kv : str_map) { + try { + ans[kv.first] = std::stoi(kv.second); + } catch (...) { + ans[kv.first] = 0; } - - if(portstr[i]==':'){ - i++; - while(portstr[i]!=','){ - portnum+=portstr[i]; - i++; - } - ans[portname]=stoi(portnum); - portnum=""; - portname=""; - } - i++; } return ans; } @@ -286,25 +253,7 @@ class Concore{ * @return A vector of double values extracted from the input string. */ vector parser(string f){ - vector temp; - if(f.empty()) return temp; - string value = ""; - - //Changing last bracket to comma to use comma as a delimiter - f[f.length()-1]=','; - - for(int i=1;i= 2 && ((str.front() == '\'' && str.back() == '\'') || (str.front() == '"' && str.back() == '"'))) - return str.substr(1, str.size() - 2); - return str; + return concore_base::stripquotes(str); } /** @@ -629,21 +573,7 @@ class Concore{ * @return A map of key-value string pairs. */ map parsedict(string str){ - map result; - string trimmed = stripstr(str); - if (trimmed.size() < 2 || trimmed.front() != '{' || trimmed.back() != '}') - return result; - string inner = trimmed.substr(1, trimmed.size() - 2); - stringstream ss(inner); - string token; - while (getline(ss, token, ',')) { - size_t colon = token.find(':'); - if (colon == string::npos) continue; - string key = stripquotes(stripstr(token.substr(0, colon))); - string val = stripquotes(stripstr(token.substr(colon + 1))); - if (!key.empty()) result[key] = val; - } - return result; + return concore_base::parsedict(str); } /** @@ -651,33 +581,15 @@ class Concore{ * @param defaultValue The fallback value if the file is missing. */ void default_maxtime(int defaultValue){ - maxtime = defaultValue; - ifstream file(inpath + "/1/concore.maxtime"); - if (file) { - file >> maxtime; - } + maxtime = (int)concore_base::load_maxtime( + inpath + "/1/concore.maxtime", (double)defaultValue); } /** * @brief Loads simulation parameters from concore.params into the params map. */ void load_params(){ - ifstream file(inpath + "/1/concore.params"); - if (!file) return; - stringstream buffer; - buffer << file.rdbuf(); - string sparams = buffer.str(); - - if (!sparams.empty() && sparams[0] == '"') { - sparams = sparams.substr(1, sparams.find('"', 1) - 1); - } - - if (!sparams.empty() && sparams[0] != '{') { - sparams = "{\"" + regex_replace(regex_replace(regex_replace(sparams, regex(","), ",\""), regex("="), "\":"), regex(" "), "") + "}"; - } - try { - params = parsedict(sparams); - } catch (...) {} + params = concore_base::load_params(inpath + "/1/concore.params"); } /** @@ -687,7 +599,7 @@ class Concore{ * @return The parameter value or the default. */ string tryparam(string n, string i){ - return params.count(n) ? params[n] : i; + return concore_base::tryparam(params, n, i); } /** diff --git a/concore_base.hpp b/concore_base.hpp new file mode 100644 index 0000000..6479942 --- /dev/null +++ b/concore_base.hpp @@ -0,0 +1,183 @@ +// concore_base.hpp -- shared utilities for concore.hpp and concoredocker.hpp +// Extracted to eliminate drift between local and Docker C++ implementations. +#ifndef CONCORE_BASE_HPP +#define CONCORE_BASE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace concore_base { + +// =================================================================== +// String Helpers +// =================================================================== +inline std::string stripstr(const std::string& str) { + size_t start = str.find_first_not_of(" \t\n\r"); + if (start == std::string::npos) return ""; + size_t end = str.find_last_not_of(" \t\n\r"); + return str.substr(start, end - start + 1); +} + +inline std::string stripquotes(const std::string& str) { + if (str.size() >= 2 && + ((str.front() == '\'' && str.back() == '\'') || + (str.front() == '"' && str.back() == '"'))) + return str.substr(1, str.size() - 2); + return str; +} + +// =================================================================== +// Parsing Utilities +// =================================================================== + +/** + * Parses a Python-style dict string into a string→string map. + * Format: {'key1': val1, 'key2': val2} + * Handles both quoted and unquoted keys/values. + */ +inline std::map parsedict(const std::string& str) { + std::map result; + std::string trimmed = stripstr(str); + if (trimmed.size() < 2 || trimmed.front() != '{' || trimmed.back() != '}') + return result; + std::string inner = trimmed.substr(1, trimmed.size() - 2); + std::stringstream ss(inner); + std::string token; + while (std::getline(ss, token, ',')) { + size_t colon = token.find(':'); + if (colon == std::string::npos) continue; + std::string key = stripquotes(stripstr(token.substr(0, colon))); + std::string val = stripquotes(stripstr(token.substr(colon + 1))); + if (!key.empty()) result[key] = val; + } + return result; +} + +/** + * Parses a Python-style list string into a vector of strings. + * Format: [val1, val2, val3] + */ +inline std::vector parselist(const std::string& str) { + std::vector result; + std::string trimmed = stripstr(str); + if (trimmed.size() < 2 || trimmed.front() != '[' || trimmed.back() != ']') + return result; + std::string inner = trimmed.substr(1, trimmed.size() - 2); + std::stringstream ss(inner); + std::string token; + while (std::getline(ss, token, ',')) { + std::string val = stripstr(token); + if (!val.empty()) result.push_back(val); + } + return result; +} + +/** + * Parses a double-valued list like "[0.0, 1.5, 2.3]" into a vector. + * Used by concore.hpp's read/write which work with numeric data. + */ +inline std::vector parselist_double(const std::string& str) { + std::vector result; + std::vector tokens = parselist(str); + for (const auto& tok : tokens) { + result.push_back(std::stod(tok)); + } + return result; +} + +/** + * Reads a file and parses its content as a dict. + * Returns defaultValue on any failure (matches Python safe_literal_eval). + */ +inline std::map safe_literal_eval_dict( + const std::string& filename, + const std::map& defaultValue) +{ + std::ifstream file(filename); + if (!file) return defaultValue; + std::stringstream buf; + buf << file.rdbuf(); + std::string content = buf.str(); + try { + return parsedict(content); + } catch (...) { + return defaultValue; + } +} + +/** + * Loads simulation parameters from a concore.params file. + * Handles Windows quote wrapping, semicolon-separated key=value, + * and dict-literal format. + */ +inline std::map load_params(const std::string& params_file) { + std::ifstream file(params_file); + if (!file) return {}; + std::stringstream buffer; + buffer << file.rdbuf(); + std::string sparams = buffer.str(); + + // Windows sometimes keeps surrounding quotes + if (!sparams.empty() && sparams[0] == '"') { + size_t closing = sparams.find('"', 1); + if (closing != std::string::npos) + sparams = sparams.substr(1, closing - 1); + } + + sparams = stripstr(sparams); + if (sparams.empty()) return {}; + + // If already a dict literal, parse directly + if (sparams.front() == '{') { + try { return parsedict(sparams); } catch (...) {} + } + + // Otherwise convert semicolon-separated key=value to dict format + // e.g. "a=1;b=2" -> {"a":"1","b":"2"} + std::string converted = "{\"" + + std::regex_replace( + std::regex_replace( + std::regex_replace(sparams, std::regex(","), ",\""), + std::regex("="), "\":"), + std::regex(" "), "") + + "}"; + try { return parsedict(converted); } catch (...) {} + + return {}; +} + +/** + * Reads maxtime from concore.maxtime file, falls back to defaultValue. + */ +inline double load_maxtime(const std::string& maxtime_file, double defaultValue) { + std::ifstream file(maxtime_file); + if (!file) return defaultValue; + double val; + if (file >> val) return val; + return defaultValue; +} + +/** + * Returns param value by name, or default if not found. + */ +inline std::string tryparam( + const std::map& params, + const std::string& name, + const std::string& defaultValue) +{ + auto it = params.find(name); + return (it != params.end()) ? it->second : defaultValue; +} + +} // namespace concore_base + +#endif // CONCORE_BASE_HPP diff --git a/concoredocker.hpp b/concoredocker.hpp index 88a7ab6..593da87 100644 --- a/concoredocker.hpp +++ b/concoredocker.hpp @@ -1,5 +1,5 @@ -#ifndef CONCORE_HPP -#define CONCORE_HPP +#ifndef CONCOREDOCKER_HPP +#define CONCOREDOCKER_HPP #include #include @@ -14,6 +14,8 @@ #include #include +#include "concore_base.hpp" + class Concore { public: std::unordered_map iport; @@ -28,49 +30,20 @@ class Concore { std::unordered_map params; std::string stripstr(const std::string& str) { - size_t start = str.find_first_not_of(" \t\n\r"); - if (start == std::string::npos) return ""; - size_t end = str.find_last_not_of(" \t\n\r"); - return str.substr(start, end - start + 1); + return concore_base::stripstr(str); } std::string stripquotes(const std::string& str) { - if (str.size() >= 2 && ((str.front() == '\'' && str.back() == '\'') || (str.front() == '"' && str.back() == '"'))) - return str.substr(1, str.size() - 2); - return str; + return concore_base::stripquotes(str); } std::unordered_map parsedict(const std::string& str) { - std::unordered_map result; - std::string trimmed = stripstr(str); - if (trimmed.size() < 2 || trimmed.front() != '{' || trimmed.back() != '}') - return result; - std::string inner = trimmed.substr(1, trimmed.size() - 2); - std::stringstream ss(inner); - std::string token; - while (std::getline(ss, token, ',')) { - size_t colon = token.find(':'); - if (colon == std::string::npos) continue; - std::string key = stripquotes(stripstr(token.substr(0, colon))); - std::string val = stripquotes(stripstr(token.substr(colon + 1))); - if (!key.empty()) result[key] = val; - } - return result; + auto ordered = concore_base::parsedict(str); + return std::unordered_map(ordered.begin(), ordered.end()); } std::vector parselist(const std::string& str) { - std::vector result; - std::string trimmed = stripstr(str); - if (trimmed.size() < 2 || trimmed.front() != '[' || trimmed.back() != ']') - return result; - std::string inner = trimmed.substr(1, trimmed.size() - 2); - std::stringstream ss(inner); - std::string token; - while (std::getline(ss, token, ',')) { - std::string val = stripstr(token); - if (!val.empty()) result.push_back(val); - } - return result; + return concore_base::parselist(str); } Concore() { @@ -82,37 +55,17 @@ class Concore { std::unordered_map safe_literal_eval(const std::string& filename, std::unordered_map defaultValue) { std::ifstream file(filename); - if (!file) { - std::cerr << "Error reading " << filename << "\n"; - return defaultValue; - } + if (!file) return defaultValue; std::stringstream buf; buf << file.rdbuf(); - std::string content = buf.str(); - try { - return parsedict(content); - } catch (...) { - return defaultValue; - } + auto result = concore_base::parsedict(buf.str()); + if (result.empty()) return defaultValue; + return std::unordered_map(result.begin(), result.end()); } void load_params() { - std::ifstream file(inpath + "/1/concore.params"); - if (!file) return; - std::stringstream buffer; - buffer << file.rdbuf(); - std::string sparams = buffer.str(); - - if (!sparams.empty() && sparams[0] == '"') { - sparams = sparams.substr(1, sparams.find('"') - 1); - } - - if (!sparams.empty() && sparams[0] != '{') { - sparams = "{\"" + std::regex_replace(std::regex_replace(std::regex_replace(sparams, std::regex(","), ",\""), std::regex("="), "\":"), std::regex(" "), "") + "}"; - } - try { - params = parsedict(sparams); - } catch (...) {} + auto ordered = concore_base::load_params(inpath + "/1/concore.params"); + params = std::unordered_map(ordered.begin(), ordered.end()); } std::string tryparam(const std::string& n, const std::string& i) { @@ -120,11 +73,8 @@ class Concore { } void default_maxtime(double defaultValue) { - maxtime = defaultValue; - std::ifstream file(inpath + "/1/concore.maxtime"); - if (file) { - file >> maxtime; - } + maxtime = concore_base::load_maxtime( + inpath + "/1/concore.maxtime", defaultValue); } bool unchanged() { @@ -188,7 +138,7 @@ class Concore { outfile << val[i] << (i + 1 < val.size() ? ", " : ""); } outfile << "]"; - simtime += delta; + // simtime must not be mutated here (issue #385). } } @@ -204,4 +154,4 @@ class Concore { } }; -#endif +#endif // CONCOREDOCKER_HPP diff --git a/concoredocker.java b/concoredocker.java index 2ac7cbd..58b68e7 100644 --- a/concoredocker.java +++ b/concoredocker.java @@ -39,7 +39,7 @@ public class concoredocker { } catch (IOException e) { } try { - String sparams = new String(Files.readAllBytes(Paths.get(inpath + "1/concore.params")), java.nio.charset.StandardCharsets.UTF_8); + String sparams = new String(Files.readAllBytes(Paths.get(inpath + "/1/concore.params")), java.nio.charset.StandardCharsets.UTF_8); if (sparams.length() > 0 && sparams.charAt(0) == '"') { // windows keeps "" need to remove sparams = sparams.substring(1); sparams = sparams.substring(0, sparams.indexOf('"')); @@ -114,7 +114,7 @@ private static Map parseFile(String filename) throws IOException */ private static void defaultMaxTime(double defaultValue) { try { - String content = new String(Files.readAllBytes(Paths.get(inpath + "1/concore.maxtime"))); + String content = new String(Files.readAllBytes(Paths.get(inpath + "/1/concore.maxtime"))); Object parsed = literalEval(content.trim()); if (parsed instanceof Number) { maxtime = ((Number) parsed).doubleValue(); @@ -161,7 +161,7 @@ private static List read(int port, String name, String initstr) { // initstr not parseable as list; defaultVal stays empty } - String filePath = inpath + port + "/" + name; + String filePath = inpath + "/" + port + "/" + name; try { Thread.sleep(delay); } catch (InterruptedException e) { @@ -277,7 +277,7 @@ private static String toPythonLiteral(Object obj) { */ private static void write(int port, String name, Object val, int delta) { try { - String path = outpath + port + "/" + name; + String path = outpath + "/" + port + "/" + name; StringBuilder content = new StringBuilder(); if (val instanceof String) { Thread.sleep(2 * delay); @@ -291,7 +291,8 @@ private static void write(int port, String name, Object val, int delta) { content.append(toPythonLiteral(listVal.get(i))); } content.append("]"); - simtime += delta; + // simtime must not be mutated here. + // Mutation breaks cross-language determinism (see issue #385). } else if (val instanceof Object[]) { // Legacy support for Object[] arguments Object[] arrayVal = (Object[]) val; @@ -302,7 +303,8 @@ private static void write(int port, String name, Object val, int delta) { content.append(toPythonLiteral(o)); } content.append("]"); - simtime += delta; + // simtime must not be mutated here. + // Mutation breaks cross-language determinism (see issue #385). } else { System.out.println("write must have list or str"); return; @@ -310,9 +312,9 @@ private static void write(int port, String name, Object val, int delta) { Files.write(Paths.get(path), content.toString().getBytes()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); - System.out.println("skipping " + outpath + port + "/" + name); + System.out.println("skipping " + outpath + "/" + port + "/" + name); } catch (IOException e) { - System.out.println("skipping " + outpath + port + "/" + name); + System.out.println("skipping " + outpath + "/" + port + "/" + name); } }