#!/usr/bin/env python3 """Translate the MQL5 sources of this project into C++ so that the exact same code can be compiled and tested without MetaTrader. Only the constrained MQL5 subset used in this repository is supported: * dynamic arrays T name[]; -> MqlArray name; * fixed arrays T name[N]; -> MqlFixed name; * array parameters [const] T &name[] -> [const] MqlArray &name * #property lines are dropped. * #include "path/File.mqh" is mapped to the translated header "File.h". The path must be quoted and relative to the including file, which is how MetaEditor resolves it; <...> searches MQL5/Include only, and the project is not there when it is cloned into MQL5/Shared Projects. A path that does not point to an existing file is an error. * datetime literals D'YYYY.MM.DD [HH:MM[:SS]]' become integer seconds (UTC). Everything else must already be valid C++ (see tests/mql5_shim.h). usage: mql2cpp.py input.mqh output.h """ import calendar import os import re import sys KEYWORDS = {'return', 'delete', 'new', 'else', 'case', 'goto', 'throw', 'typedef', 'using'} RE_PARAM = re.compile(r'(\bconst\s+)?\b([A-Za-z_]\w*)\s*&\s*([A-Za-z_]\w*)\s*\[\s*\]') RE_DYN = re.compile(r'^(\s*)(static\s+)?([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\[\s*\]\s*;') RE_FIX = re.compile(r'^(\s*)(static\s+)?([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*\[\s*([^\]]+?)\s*\]\s*;') RE_INCLUDE = re.compile(r'^\s*#include\s*(<[^>]*>|"[^"]*")') RE_DATE = re.compile(r"D'(\d{4})\.(\d{2})\.(\d{2})(?:\s+(\d{2}):(\d{2})(?::(\d{2}))?)?'") def date_literal(m): y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3)) hh = int(m.group(4) or 0) mm = int(m.group(5) or 0) ss = int(m.group(6) or 0) return '((datetime)%dL)' % calendar.timegm((y, mo, d, hh, mm, ss, 0, 0, 0)) def include(spec, src_path): """Return the C++ #include for the MQL5 include spec ("..." or <...>).""" if not spec.startswith('"'): sys.exit('%s: #include %s: use a quoted path relative to this file' % (src_path, spec)) path = spec[1:-1].replace('\\', '/') if not os.path.isfile(os.path.join(os.path.dirname(src_path), path)): sys.exit('%s: #include %s: no such file relative to this file' % (src_path, spec)) return '#include "%s.h"' % os.path.splitext(os.path.basename(path))[0] def translate(src, src_path): out = [] for line in src.splitlines(): if line.lstrip().startswith('#property'): out.append('// ' + line) continue m = RE_INCLUDE.match(line) if m: out.append(include(m.group(1), src_path)) continue line = RE_DATE.sub(date_literal, line) line = RE_PARAM.sub(lambda mm: '%sMqlArray<%s> &%s' % (mm.group(1) or '', mm.group(2), mm.group(3)), line) m = RE_DYN.match(line) if m and m.group(3) not in KEYWORDS: line = '%s%sMqlArray<%s> %s;%s' % (m.group(1), m.group(2) or '', m.group(3), m.group(4), line[m.end():]) else: m = RE_FIX.match(line) if m and m.group(3) not in KEYWORDS: line = '%s%sMqlFixed<%s, (%s)> %s;%s' % (m.group(1), m.group(2) or '', m.group(3), m.group(5), m.group(4), line[m.end():]) out.append(line) return '\n'.join(out) + '\n' def main(): src = open(sys.argv[1], encoding='utf-8').read() res = '// Generated by tests/mql2cpp.py from %s - do not edit.\n' % sys.argv[1].split('/')[-1] res += translate(src, sys.argv[1]) open(sys.argv[2], 'w', encoding='utf-8').write(res) if __name__ == '__main__': main()