| #!/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<T> name;
|
| * fixed arrays T name[N]; -> MqlFixed<T, (N)> name;
|
| * array parameters [const] T &name[] -> [const] MqlArray<T> &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()
|