diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a226a6b..74d1f71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -123,10 +123,10 @@ jobs: - name: Write Julia data for MATLAB run: julia --color=yes tests/julia/test_matlab_interop.jl write - - name: Run MATLAB interop tests + - name: Run MATLAB interop and wire-format tests uses: matlab-actions/run-command@v3 with: - command: addpath(pwd); addpath(fullfile(pwd, 'tests', 'matlab')); test_julia_interop + command: addpath(pwd); addpath(fullfile(pwd, 'tests', 'matlab')); test_julia_interop; test_literal_eval - name: Read MATLAB data from Julia run: julia --color=yes tests/julia/test_matlab_interop.jl read diff --git a/README.md b/README.md index 270d25c..1d59a86 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ _concore_ enables composing studies from programs developed in different languag ## Wire Format -Concore payloads follow Python literal syntax compatible with `ast.literal_eval()`. The Python, C++, and Java implementations parse this shared format; the MATLAB and Verilog implementations currently support only flat numeric arrays derived from it. Supported value types include: +Concore payloads follow Python literal syntax compatible with `ast.literal_eval()`. The Python, C++, Java, Julia, and MATLAB/Octave implementations parse and serialize this shared format; the Verilog implementation currently supports flat numeric arrays derived from it. Supported value types include: * **Numbers** — integers and floats, including scientific notation (e.g., `1e3`, `-2.5`) * **Booleans** — `True` / `False` (converted to `1.0` / `0.0` in numeric contexts) diff --git a/TestLiteralEvalMatlab.m b/TestLiteralEvalMatlab.m new file mode 100644 index 0000000..5dd0e8a --- /dev/null +++ b/TestLiteralEvalMatlab.m @@ -0,0 +1,146 @@ +% TestLiteralEvalMatlab.m +% Test suite for MATLAB/Octave Python-literal parser and serializer. +% +% Run in Octave: octave-cli TestLiteralEvalMatlab.m +% Run in MATLAB: matlab -batch "TestLiteralEvalMatlab" + +passed = 0; +failed = 0; + +function check(testName, condition) + global passed failed; + if condition + fprintf('PASS: %s\n', testName); + passed = passed + 1; + else + fprintf('FAIL: %s\n', testName); + failed = failed + 1; + end +end + +function eq = approx(a, b) + eq = abs(a - b) < 1e-9; +end + +global passed failed; +passed = 0; +failed = 0; + +fprintf('===== MATLAB/Octave Literal Parser & Serializer Tests =====\n\n'); + +% 1. Flat numeric list +v = concore_literal_eval('[10.0, 0.5, 2.3]'); +check('flat_numeric size==3', numel(v) == 3); +check('flat_numeric[1]==10.0', approx(v(1), 10.0)); +check('flat_numeric[2]==0.5', approx(v(2), 0.5)); +check('flat_numeric[3]==2.3', approx(v(3), 2.3)); + +% 2. Empty list +v = concore_literal_eval('[]'); +check('empty_list isempty', isempty(v)); + +% 3. Single element +v = concore_literal_eval('[42.0]'); +check('single_element len==1', numel(v) == 1); +check('single_element[1]==42', approx(v(1), 42.0)); + +% 4. Negative numbers +v = concore_literal_eval('[-1.5, -3.0, 2.0]'); +check('negative size==3', numel(v) == 3); +check('negative[1]==-1.5', approx(v(1), -1.5)); +check('negative[2]==-3.0', approx(v(2), -3.0)); + +% 5. Scientific notation +v = concore_literal_eval('[1e3, 2.5E-2, -1.0e+1]'); +check('sci size==3', numel(v) == 3); +check('sci[1]==1000', approx(v(1), 1000.0)); +check('sci[2]==0.025', approx(v(2), 0.025)); +check('sci[3]==-10', approx(v(3), -10.0)); + +% 6. Integer values +v = concore_literal_eval('[1, 2, 3]'); +check('int size==3', numel(v) == 3); +check('int[1]==1', approx(v(1), 1.0)); +check('int[3]==3', approx(v(3), 3.0)); + +% 7. String elements +v = concore_literal_eval('[10.0, "start", 0.5]'); +check('string_elem iscell', iscell(v)); +check('string_elem len==3', numel(v) == 3); +check('string_elem[1]==10.0', approx(v{1}, 10.0)); +check('string_elem[2]=="start"', strcmp(v{2}, 'start')); +check('string_elem[3]==0.5', approx(v{3}, 0.5)); + +% 8. Boolean elements +v = concore_literal_eval('[True, False]'); +check('bool iscell or array', numel(v) == 2); +if iscell(v) + check('bool[1]==true', v{1} == true); + check('bool[2]==false', v{2} == false); +else + check('bool[1]==true', v(1) == true); + check('bool[2]==false', v(2) == false); +end + +% 9. Nested list +v = concore_literal_eval('[10.0, [0.5, 0.3], 0.1]'); +check('nested iscell', iscell(v)); +check('nested len==3', numel(v) == 3); +check('nested[1]==10.0', approx(v{1}, 10.0)); +check('nested[2] is list', numel(v{2}) == 2); +if iscell(v{2}) + check('nested[2][1]==0.5', approx(v{2}{1}, 0.5)); + check('nested[2][2]==0.3', approx(v{2}{2}, 0.3)); +else + check('nested[2][1]==0.5', approx(v{2}(1), 0.5)); + check('nested[2][2]==0.3', approx(v{2}(2), 0.3)); +end + +% 10. Tuple payload +v = concore_literal_eval('(10.0, 0.3)'); +check('tuple len==2', numel(v) == 2); +if iscell(v) + check('tuple[1]==10.0', approx(v{1}, 10.0)); + check('tuple[2]==0.3', approx(v{2}, 0.3)); +else + check('tuple[1]==10.0', approx(v(1), 10.0)); + check('tuple[2]==0.3', approx(v(2), 0.3)); +end + +% 11. Escape sequences in string +v = concore_literal_eval('["line\none", "tab\ttwo"]'); +check('escape newline', iscell(v) && strcmp(v{1}, sprintf('line\none'))); +check('escape tab', iscell(v) && strcmp(v{2}, sprintf('tab\ttwo'))); + +% 12. Single-quoted strings +v = concore_literal_eval('[\''single_quote\'', 42]'); +check('single_quote string', iscell(v) && strcmp(v{1}, 'single_quote')); + +% 13. None literal +v = concore_literal_eval('[None, 1]'); +check('none is empty', iscell(v) && isempty(v{1})); +check('none second elem is 1', iscell(v) && (v{2} == 1)); + +% 14. Dictionary parsing +v = concore_literal_eval('{"a": 1, "b": 2.5}'); +check('dict is map', isa(v, 'containers.Map')); +check('dict has a', v('a') == 1); +check('dict has b', approx(v('b'), 2.5)); + +% 15. Serialization tests +s_num = concore_literal_serialize([1.0, 2.0, 3.0]); +check('serialize numeric contains 1', ~isempty(strfind(s_num, '1'))); +check('serialize numeric contains 3', ~isempty(strfind(s_num, '3'))); + +s_bool = concore_literal_serialize(true); +check('serialize bool True', strcmp(s_bool, 'True')); + +s_str = concore_literal_serialize('hello'); +check('serialize string quotes', strcmp(s_str, '"hello"')); + +s_cell = concore_literal_serialize({10.0, 'start', true, [1, 2]}); +check('serialize cell starts with [', s_cell(1) == '['); +check('serialize cell contains start', ~isempty(strfind(s_cell, '"start"'))); +check('serialize cell contains True', ~isempty(strfind(s_cell, 'True'))); + +fprintf('\n=== Results: %d passed, %d failed out of %d tests ===\n', passed, failed, passed + failed); diff --git a/concore_default_maxtime.m b/concore_default_maxtime.m index 489187d..2530920 100644 --- a/concore_default_maxtime.m +++ b/concore_default_maxtime.m @@ -3,14 +3,9 @@ function concore_default_maxtime(default) try maxfile = fopen(strcat(concore.inpath,'1/concore.maxtime')); instr = fscanf(maxfile,'%c'); - % Safe numeric parsing (replaces unsafe eval) - clean_str = strtrim(instr); - clean_str = regexprep(clean_str, '[\[\]]', ''); - % Normalize commas to whitespace so sscanf can parse all tokens - clean_str = strrep(clean_str, ',', ' '); - parsed_values = sscanf(clean_str, '%f'); - if numel(parsed_values) == 1 - concore.maxtime = parsed_values; + parsed_val = concore_literal_eval(instr); + if isnumeric(parsed_val) && numel(parsed_val) == 1 + concore.maxtime = parsed_val; else concore.maxtime = default; end @@ -19,4 +14,3 @@ function concore_default_maxtime(default) concore.maxtime = default; end end - diff --git a/concore_initval.m b/concore_initval.m index 4b92b31..5c51e6b 100644 --- a/concore_initval.m +++ b/concore_initval.m @@ -1,20 +1,56 @@ function [result] = concore_initval(simtime_val) global concore; - % Safe numeric parsing (replaces unsafe eval) - clean_str = strtrim(simtime_val); - clean_str = regexprep(clean_str, '[\[\]]', ''); - clean_str = strrep(clean_str, ',', ' '); - result = sscanf(clean_str, '%f').'; - % Guard against empty or invalid numeric input - if isempty(result) + try + parsed = concore_literal_eval(simtime_val); + catch exc concore.simtime = 0; result = []; return; end - concore.simtime = result(1); - if numel(result) >= 2 - result = result(2:end); - else + + if isempty(parsed) + concore.simtime = 0; result = []; + return; + end + + if iscell(parsed) + if isempty(parsed) + concore.simtime = 0; + result = []; + return; + end + first_elem = parsed{1}; + if isnumeric(first_elem) || islogical(first_elem) + concore.simtime = double(first_elem); + else + concore.simtime = 0; + end + if numel(parsed) >= 2 + all_num_scalar = true; + for k = 2:numel(parsed) + if ~isnumeric(parsed{k}) || numel(parsed{k}) ~= 1 + all_num_scalar = false; + break; + end + end + if all_num_scalar + result = cell2mat(parsed(2:end)); + else + result = parsed(2:end); + end + else + result = []; + end + elseif isnumeric(parsed) + concore.simtime = parsed(1); + if numel(parsed) >= 2 + result = parsed(2:end); + else + result = []; + end + else + concore.simtime = 0; + result = parsed; end end diff --git a/concore_literal_eval.m b/concore_literal_eval.m new file mode 100644 index 0000000..03300ba --- /dev/null +++ b/concore_literal_eval.m @@ -0,0 +1,236 @@ +function [val] = concore_literal_eval(str) +% CONCORE_LITERAL_EVAL Parses a Python-literal-formatted payload into MATLAB types. +% +% Supported grammar: +% - Numbers: integers, floating point, scientific notation (e.g. 10.0, -1.5, 1e3, 2.5E-2) +% - Booleans: True -> true (logical), False -> false (logical) +% - None: None -> [] +% - Strings: '...' or "..." with escape sequence support (\n, \t, \r, \\, \', \") +% - Lists: [...] -> cell array or numeric array +% - Tuples: (...) -> cell array or numeric array (identical to lists) +% - Dictionaries: {...} -> containers.Map +% +% Examples: +% concore_literal_eval('[10.0, 0.5, 2.3]') -> [10.0, 0.5, 2.3] +% concore_literal_eval('[10.0, "start", True, [1, 2]]') -> {10.0, 'start', true, [1, 2]} + + if ~ischar(str) + if isstring(str) + str = char(str); + else + val = str; + return; + end + end + + str = strtrim(str); + if isempty(str) + val = []; + return; + end + + [val, pos] = parse_value(str, 1); +end + +function [val, p] = parse_value(s, p) + p = skip_ws(s, p); + n = length(s); + if p > n + val = []; + return; + end + + c = s(p); + if c == '[' || c == '(' + [val, p] = parse_array(s, p); + elseif c == '{' + [val, p] = parse_dict(s, p); + elseif c == '''' || c == '"' + [val, p] = parse_string(s, p); + else + [val, p] = parse_atom(s, p); + end +end + +function [res, p] = parse_array(s, p) + open_char = s(p); + if open_char == '[' + close_char = ']'; + else + close_char = ')'; + end + p = p + 1; + n = length(s); + elements = {}; + p = skip_ws(s, p); + + if p <= n && s(p) == close_char + p = p + 1; + res = {}; + return; + end + + while p <= n + [elem, p] = parse_value(s, p); + elements{end+1} = elem; + p = skip_ws(s, p); + + if p <= n && s(p) == ',' + p = p + 1; + p = skip_ws(s, p); + end + + if p <= n && s(p) == close_char + p = p + 1; + % If all elements are numeric scalars, simplify to standard numeric array + all_num_scalar = true; + for k = 1:numel(elements) + if ~isnumeric(elements{k}) || numel(elements{k}) ~= 1 + all_num_scalar = false; + break; + end + end + if all_num_scalar && ~isempty(elements) + res = cell2mat(elements); + else + res = elements; + end + return; + end + end + + error('concore:parse_error', 'Unterminated array or tuple'); +end + +function [res, p] = parse_dict(s, p) + p = p + 1; % Skip '{' + n = length(s); + res = containers.Map(); + p = skip_ws(s, p); + + if p <= n && s(p) == '}' + p = p + 1; + return; + end + + while p <= n + [k_val, p] = parse_value(s, p); + p = skip_ws(s, p); + if p > n || s(p) ~= ':' + error('concore:parse_error', 'Expected ":" in dictionary'); + end + p = p + 1; % Skip ':' + [v_val, p] = parse_value(s, p); + + if ischar(k_val) + key_str = k_val; + else + key_str = num2str(k_val); + end + res(key_str) = v_val; + + p = skip_ws(s, p); + if p <= n && s(p) == ',' + p = p + 1; + p = skip_ws(s, p); + end + + if p <= n && s(p) == '}' + p = p + 1; + return; + end + end + + error('concore:parse_error', 'Unterminated dictionary'); +end + +function [res, p] = parse_string(s, p) + quote = s(p); + p = p + 1; + n = length(s); + buf = ''; + + while p <= n && s(p) ~= quote + if s(p) == '\' && p < n + p = p + 1; + esc = s(p); + switch esc + case 'n' + buf = [buf, sprintf('\n')]; + case 't' + buf = [buf, sprintf('\t')]; + case 'r' + buf = [buf, sprintf('\r')]; + case '\' + buf = [buf, '\']; + case '''' + buf = [buf, '''']; + case '"' + buf = [buf, '"']; + otherwise + buf = [buf, '\', esc]; + end + else + buf = [buf, s(p)]; + end + p = p + 1; + end + + if p > n + error('concore:parse_error', 'Unterminated string literal'); + end + + p = p + 1; % Skip closing quote + res = buf; +end + +function [res, p] = parse_atom(s, p) + n = length(s); + + % Boolean: True + if p + 3 <= n && strcmp(s(p:p+3), 'True') && (p + 4 > n || ~is_id_char(s(p+4))) + res = true; + p = p + 4; + return; + end + + % Boolean: False + if p + 4 <= n && strcmp(s(p:p+4), 'False') && (p + 5 > n || ~is_id_char(s(p+5))) + res = false; + p = p + 5; + return; + end + + % None + if p + 3 <= n && strcmp(s(p:p+3), 'None') && (p + 4 > n || ~is_id_char(s(p+4))) + res = []; + p = p + 4; + return; + end + + % Number parsing + start_p = p; + while p <= n && s(p) ~= ' ' && s(p) ~= sprintf('\t') && s(p) ~= sprintf('\n') && ... + s(p) ~= sprintf('\r') && s(p) ~= ',' && s(p) ~= ']' && s(p) ~= ')' && ... + s(p) ~= '}' && s(p) ~= ':' + p = p + 1; + end + + num_str = s(start_p:p-1); + num_val = str2double(num_str); + if isnan(num_val) && ~strcmpi(num_str, 'nan') + error('concore:parse_error', sprintf('Invalid token: %s', num_str)); + end + res = num_val; +end + +function p = skip_ws(s, p) + n = length(s); + while p <= n && (s(p) == ' ' || s(p) == sprintf('\t') || s(p) == sprintf('\n') || s(p) == sprintf('\r')) + p = p + 1; + end +end + +function b = is_id_char(c) + b = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'; +end diff --git a/concore_literal_serialize.m b/concore_literal_serialize.m new file mode 100644 index 0000000..0503bf0 --- /dev/null +++ b/concore_literal_serialize.m @@ -0,0 +1,102 @@ +function [str] = concore_literal_serialize(val) +% CONCORE_LITERAL_SERIALIZE Serializes MATLAB data types into a Python-literal string. +% +% Type mappings: +% - double / single / int* (scalar) -> formatted numeric string (e.g. 1.5, -2) +% - double / numeric vector/matrix -> [1.0, 2.0, 3.0] or nested [[...], [...]] +% - logical (scalar) -> True / False +% - logical array -> [True, False, ...] +% - char / string -> "..." with escaped quotes and control chars +% - cell array -> [elem1, elem2, ...] +% - containers.Map / struct -> {'key1': val1, 'key2': val2} +% - empty [] -> [] + + if isempty(val) + str = '[]'; + return; + end + + if islogical(val) + if numel(val) == 1 + if val + str = 'True'; + else + str = 'False'; + end + else + items = cell(1, numel(val)); + for k = 1:numel(val) + items{k} = concore_literal_serialize(val(k)); + end + str = ['[', strjoin(items, ', '), ']']; + end + elseif isnumeric(val) + if numel(val) == 1 + str = sprintf('%.17g', val); + else + sz = size(val); + if sz(1) == 1 || sz(2) == 1 + % 1D vector + items = cell(1, numel(val)); + for k = 1:numel(val) + items{k} = sprintf('%.17g', val(k)); + end + str = ['[', strjoin(items, ', '), ']']; + else + % 2D matrix -> nested list of rows + rows = cell(1, sz(1)); + for r = 1:sz(1) + row_items = cell(1, sz(2)); + for c = 1:sz(2) + row_items{c} = sprintf('%.17g', val(r, c)); + end + rows{r} = ['[', strjoin(row_items, ', '), ']']; + end + str = ['[', strjoin(rows, ', '), ']']; + end + end + elseif ischar(val) + escaped = strrep(val, '\', '\\'); + escaped = strrep(escaped, '"', '\"'); + escaped = strrep(escaped, sprintf('\n'), '\n'); + escaped = strrep(escaped, sprintf('\t'), '\t'); + escaped = strrep(escaped, sprintf('\r'), '\r'); + str = ['"', escaped, '"']; + elseif isstring(val) + if numel(val) == 1 + str = concore_literal_serialize(char(val)); + else + items = cell(1, numel(val)); + for k = 1:numel(val) + items{k} = concore_literal_serialize(char(val(k))); + end + str = ['[', strjoin(items, ', '), ']']; + end + elseif iscell(val) + items = cell(1, numel(val)); + for k = 1:numel(val) + items{k} = concore_literal_serialize(val{k}); + end + str = ['[', strjoin(items, ', '), ']']; + elseif isa(val, 'containers.Map') + keys = val.keys(); + items = cell(1, numel(keys)); + for k = 1:numel(keys) + k_str = concore_literal_serialize(keys{k}); + v_str = concore_literal_serialize(val(keys{k})); + items{k} = [k_str, ': ', v_str]; + end + str = ['{', strjoin(items, ', '), '}']; + elseif isstruct(val) + flds = fieldnames(val); + items = cell(1, numel(flds)); + for k = 1:numel(flds) + k_str = ['"', flds{k}, '"']; + v_str = concore_literal_serialize(val.(flds{k})); + items{k} = [k_str, ': ', v_str]; + end + str = ['{', strjoin(items, ', '), '}']; + else + str = ['"', char(val), '"']; + end +end diff --git a/concore_read.m b/concore_read.m index b4ed1bc..9e03214 100644 --- a/concore_read.m +++ b/concore_read.m @@ -25,21 +25,57 @@ ins = inistr; end concore.s = strcat(concore.s, ins); - % Safe numeric parsing (replaces unsafe eval) - clean_str = strtrim(ins); - clean_str = regexprep(clean_str, '[\[\]]', ''); - % Normalize comma delimiters to whitespace so sscanf parses all values - clean_str = strrep(clean_str, ',', ' '); - result = sscanf(clean_str, '%f').'; - % Guard against empty parse result to avoid indexing errors - if isempty(result) + + % Python-literal-compatible parsing + try + parsed = concore_literal_eval(ins); + catch exc + try + parsed = concore_literal_eval(inistr); + catch exc2 + parsed = []; + end + end + + if isempty(parsed) result = []; return; end - concore.simtime = max(concore.simtime, result(1)); - if numel(result) > 1 - result = result(2:end); + + if iscell(parsed) + if isempty(parsed) + result = []; + return; + end + first_elem = parsed{1}; + if isnumeric(first_elem) || islogical(first_elem) + concore.simtime = max(concore.simtime, double(first_elem)); + end + if numel(parsed) > 1 + % If all remaining elements are numeric scalars, return numeric row vector + all_num_scalar = true; + for k = 2:numel(parsed) + if ~isnumeric(parsed{k}) || numel(parsed{k}) ~= 1 + all_num_scalar = false; + break; + end + end + if all_num_scalar + result = cell2mat(parsed(2:end)); + else + result = parsed(2:end); + end + else + result = []; + end + elseif isnumeric(parsed) + concore.simtime = max(concore.simtime, parsed(1)); + if numel(parsed) > 1 + result = parsed(2:end); + else + result = []; + end else - result = []; + result = parsed; end end diff --git a/concore_write.m b/concore_write.m index 29b2095..f026e8e 100644 --- a/concore_write.m +++ b/concore_write.m @@ -1,9 +1,56 @@ function concore_write(port, name, val, delta) global concore; try - output1 = fopen(cat(2,concore.outpath, num2str(port), '/', name),"w"); - outstr = cat(2,"[",num2str(concore.simtime+delta),num2str(val,",%e"),"]"); - fprintf(output1,'%s',outstr); + out_dir = cat(2, concore.outpath, num2str(port), '/'); + output1 = fopen(cat(2, out_dir, name), "w"); + st_val = concore.simtime + delta; + st_str = sprintf('%.17g', st_val); + + if isempty(val) + outstr = ['[', st_str, ']']; + elseif iscell(val) + items = cell(1, numel(val) + 1); + items{1} = st_str; + for k = 1:numel(val) + items{k+1} = concore_literal_serialize(val{k}); + end + outstr = ['[', strjoin(items, ', '), ']']; + elseif ischar(val) + outstr = ['[', st_str, ', ', concore_literal_serialize(val), ']']; + elseif isstring(val) && numel(val) == 1 + outstr = ['[', st_str, ', ', concore_literal_serialize(char(val)), ']']; + elseif islogical(val) + if numel(val) == 1 + outstr = ['[', st_str, ', ', concore_literal_serialize(val), ']']; + else + items = cell(1, numel(val) + 1); + items{1} = st_str; + for k = 1:numel(val) + items{k+1} = concore_literal_serialize(val(k)); + end + outstr = ['[', strjoin(items, ', '), ']']; + end + elseif isnumeric(val) + if numel(val) == 1 + outstr = ['[', st_str, ', ', sprintf('%.17g', val), ']']; + else + sz = size(val); + if sz(1) == 1 || sz(2) == 1 + items = cell(1, numel(val) + 1); + items{1} = st_str; + for k = 1:numel(val) + items{k+1} = sprintf('%.17g', val(k)); + end + outstr = ['[', strjoin(items, ', '), ']']; + else + outstr = ['[', st_str, ', ', concore_literal_serialize(val), ']']; + end + end + else + outstr = ['[', st_str, ', ', concore_literal_serialize(val), ']']; + end + + fprintf(output1, '%s', outstr); fclose(output1); % simtime must not be mutated here (issue #385). catch exc diff --git a/tests/matlab/test_literal_eval.m b/tests/matlab/test_literal_eval.m new file mode 100644 index 0000000..8923f10 --- /dev/null +++ b/tests/matlab/test_literal_eval.m @@ -0,0 +1,71 @@ +function test_literal_eval + global concore; + import_concore; + + % 1. Flat numeric list + v = concore_literal_eval('[10.0, 0.5, 2.3]'); + assert(numel(v) == 3); + assert(abs(v(1) - 10.0) < 1e-9); + assert(abs(v(2) - 0.5) < 1e-9); + assert(abs(v(3) - 2.3) < 1e-9); + + % 2. Empty list + v = concore_literal_eval('[]'); + assert(isempty(v)); + + % 3. Negative and scientific notation + v = concore_literal_eval('[-1.5e2, 2.5E-2, -1.0e+1]'); + assert(numel(v) == 3); + assert(abs(v(1) - (-150.0)) < 1e-9); + assert(abs(v(2) - 0.025) < 1e-9); + assert(abs(v(3) - (-10.0)) < 1e-9); + + % 4. Strings + v = concore_literal_eval('[10.0, "start", ''sensor_a'']'); + assert(iscell(v)); + assert(numel(v) == 3); + assert(abs(v{1} - 10.0) < 1e-9); + assert(strcmp(v{2}, 'start')); + assert(strcmp(v{3}, 'sensor_a')); + + % 5. Booleans + v = concore_literal_eval('[True, False]'); + assert(numel(v) == 2); + if iscell(v) + assert(v{1} == true); + assert(v{2} == false); + else + assert(v(1) == true); + assert(v(2) == false); + end + + % 6. Nested list + v = concore_literal_eval('[10.0, [0.5, 0.3], 0.1]'); + assert(iscell(v)); + assert(numel(v) == 3); + assert(abs(v{1} - 10.0) < 1e-9); + if iscell(v{2}) + assert(abs(v{2}{1} - 0.5) < 1e-9); + assert(abs(v{2}{2} - 0.3) < 1e-9); + else + assert(abs(v{2}(1) - 0.5) < 1e-9); + assert(abs(v{2}(2) - 0.3) < 1e-9); + end + + % 7. Serialization round-trip + s_cell = concore_literal_serialize({10.0, 'start', true, [1.0, 2.0]}); + v_back = concore_literal_eval(s_cell); + assert(iscell(v_back)); + assert(abs(v_back{1} - 10.0) < 1e-9); + assert(strcmp(v_back{2}, 'start')); + assert(v_back{3} == true); + + % 8. concore_initval test + res = concore_initval('[5.0, 1.2, 3.4]'); + assert(abs(concore.simtime - 5.0) < 1e-9); + assert(numel(res) == 2); + assert(abs(res(1) - 1.2) < 1e-9); + assert(abs(res(2) - 3.4) < 1e-9); + + disp('All MATLAB literal eval and wire-format tests passed successfully!'); +end