rk86 2.0.15 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +71 -13
  2. package/package.json +1 -1
  3. package/rk86.js +486 -112
package/README.md CHANGED
@@ -5,24 +5,33 @@
5
5
  ## Запуск
6
6
 
7
7
  ```bash
8
- bunx rk86
8
+ bunx rk86 # встроенный монитор mon32
9
+ bunx rk86 CHESS.GAM # загрузить и запустить файл из текущей директории
10
+ bunx rk86 -p CHESS.GAM # загрузить без запуска
11
+ bunx rk86 -g 0x100 prog.bin # запуск с адреса 0x100
12
+ bunx rk86 -m mon16.bin # другой монитор
13
+ bunx rk86 prog.asm # собрать и запустить .asm (i8080)
14
+ bunx rk86 -l # список файлов встроенного каталога
9
15
  ```
10
16
 
11
- ## С программой
12
-
13
- ```bash
14
- bunx rk86 CHESS.GAM
15
- ```
16
-
17
- Файл программы загружается из текущей директории.
18
-
19
17
  ## Опции
20
18
 
21
19
  ```
22
- -h справка
23
- -l список файлов из каталога
24
- -m <файл> монитор (по умолчанию: встроенный mon32.bin)
25
- -p загрузить файл без запуска
20
+ -v версия
21
+ -h справка
22
+ -l список файлов из каталога
23
+ -m <файл> монитор (по умолчанию: встроенный mon32.bin)
24
+ -p загрузить файл без запуска
25
+ -g <адрес> адрес запуска (несовместим с -p)
26
+ --exit-halt выход при выполнении HLT
27
+ --exit-address [адрес] выход при переходе на адрес (по умолчанию: 0xFFFE)
28
+ --headless без отображения экрана (для автотестов)
29
+ --timeout <сек> выход по таймауту
30
+ --memory <файл> сохранить память в файл при выходе
31
+ --memory-from <адрес> начало области дампа памяти (по умолчанию: 0x0000)
32
+ --memory-to <адрес> конец области дампа памяти включительно (по умолчанию: 0xFFFF)
33
+ --screen <файл> сохранить экран 78x30 как текст при выходе
34
+ --input <seq> инъекция клавиш (через запятую): KeyA,Digit1,Enter,...
26
35
  ```
27
36
 
28
37
  ## Управление
@@ -30,6 +39,55 @@ bunx rk86 CHESS.GAM
30
39
  - Клавиатура работает через stdin (raw mode)
31
40
  - `Ctrl+C` — выход
32
41
 
42
+ ## Безголовый режим (headless) и автотесты
43
+
44
+ Флаг `--headless` отключает вывод на терминал и настройку stdin — удобно для
45
+ автоматизированных e2e-проверок. Обычно комбинируется с `--input`, `--timeout`,
46
+ `--exit-halt` и `--screen`/`--memory` для анализа результата.
47
+
48
+ Формат `--input`: через запятую указываются WebKit-коды клавиш
49
+ (`KeyA`..`KeyZ`, `Digit0`..`Digit9`, `Enter`, `Space`, `Comma`, `Period`,
50
+ `Backspace`, `ArrowLeft`, `F1`..`F10`, и т.д.). Инъекция начинается после того,
51
+ как эмулятор инициализируется (как при подгрузке файла — с небольшой задержкой),
52
+ клавиши нажимаются по одной с минимальной паузой.
53
+
54
+ Формат файла `--screen`: 30 строк по 78 символов, разделитель `\r\n`.
55
+ Байты `\0`, `\t`, `\n`, `\r` заменяются на `.`, остальные символы с кодами <0x20
56
+ рисуются псевдографикой (как на реальном РК-86).
57
+
58
+ ### Пример 1. Дамп памяти через команду монитора `D`
59
+
60
+ Монитор `mon32` печатает дамп памяти с адреса 0 до FFh, через 10 секунд
61
+ эмулятор завершается по таймауту и содержимое экрана сохраняется в файл:
62
+
63
+ ```bash
64
+ bunx rk86 --headless \
65
+ --input "KeyD,Digit0,Comma,KeyF,KeyF,Enter" \
66
+ --timeout 10 \
67
+ --screen out.txt
68
+ ```
69
+
70
+ После выхода `out.txt` содержит приглашение `-->D0,FF` и 16 строк шестнадцатеричного
71
+ дампа `0000..00F0`.
72
+
73
+ ### Пример 2. Запись HLT и запуск через команды `M` / `G`
74
+
75
+ Командой `M` мы помещаем байт `76h` (опкод HLT) в ячейку `0000`, выходим из режима
76
+ редактирования (`.`), затем командой `G 0` запускаем с адреса 0. На первой же
77
+ инструкции срабатывает HLT и `--exit-halt` завершает эмулятор:
78
+
79
+ ```bash
80
+ bunx rk86 --headless \
81
+ --exit-halt \
82
+ --input "KeyM,Enter,Digit7,Digit6,Enter,Period,KeyG,Digit0,Enter" \
83
+ --timeout 15 \
84
+ --screen out.txt \
85
+ --memory mem.bin --memory-from 0x0000 --memory-to 0x0000
86
+ ```
87
+
88
+ После выхода `mem.bin` содержит один байт `76h`, а `out.txt` — скриншот с
89
+ приглашениями `-->M` и `-->G0`.
90
+
33
91
  ## Требования
34
92
 
35
93
  - [Bun](https://bun.sh) runtime
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rk86",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "description": "Эмулятор Радио-86РК (Intel 8080) для терминала",
5
5
  "bin": {
6
6
  "rk86": "rk86.js"
package/rk86.js CHANGED
@@ -1589,6 +1589,22 @@ var init_catalog_data = __esm(() => {
1589
1589
  });
1590
1590
 
1591
1591
  // node_modules/asm8080/dist/asm8.js
1592
+ class AsmError extends Error {
1593
+ line;
1594
+ column;
1595
+ source;
1596
+ constructor(message, line, source, column = 1) {
1597
+ super(message);
1598
+ this.name = "AsmError";
1599
+ this.line = line;
1600
+ this.source = source;
1601
+ this.column = column;
1602
+ }
1603
+ }
1604
+ function firstNonSpaceCol(s) {
1605
+ const m = s.match(/\S/);
1606
+ return m ? (m.index ?? 0) + 1 : 1;
1607
+ }
1592
1608
  var REG8 = {
1593
1609
  B: 0,
1594
1610
  C: 1,
@@ -1682,6 +1698,81 @@ var ADDR16 = {
1682
1698
  LHLD: 42,
1683
1699
  SHLD: 34
1684
1700
  };
1701
+ var ALL_MNEMONICS = new Set([
1702
+ ...Object.keys(IMPLIED),
1703
+ ...Object.keys(ALU_REG),
1704
+ ...Object.keys(ALU_IMM),
1705
+ ...Object.keys(ADDR16),
1706
+ "MOV",
1707
+ "MVI",
1708
+ "INR",
1709
+ "DCR",
1710
+ "LXI",
1711
+ "DAD",
1712
+ "INX",
1713
+ "DCX",
1714
+ "PUSH",
1715
+ "POP",
1716
+ "LDAX",
1717
+ "STAX",
1718
+ "IN",
1719
+ "OUT",
1720
+ "RST",
1721
+ "DB",
1722
+ "DW",
1723
+ "DS",
1724
+ "ORG",
1725
+ "SECTION",
1726
+ "END",
1727
+ "EQU"
1728
+ ]);
1729
+ var MAX_STATEMENTS_PER_LINE = 10;
1730
+ function splitStatements(line) {
1731
+ const src = stripComment(line);
1732
+ const out = [];
1733
+ let start = 0;
1734
+ let inQ = false;
1735
+ let qc = "";
1736
+ for (let i = 0;i + 2 < src.length; i++) {
1737
+ const c = src[i];
1738
+ if (inQ) {
1739
+ if (c === qc)
1740
+ inQ = false;
1741
+ continue;
1742
+ }
1743
+ if (c === '"' || c === "'") {
1744
+ inQ = true;
1745
+ qc = c;
1746
+ continue;
1747
+ }
1748
+ if (c !== " " || src[i + 1] !== "/" || src[i + 2] !== " ")
1749
+ continue;
1750
+ let j = i + 3;
1751
+ while (j < src.length && src[j] === " ")
1752
+ j++;
1753
+ let tokStart = j;
1754
+ if (src[j] === ".")
1755
+ j++;
1756
+ let tokEnd = j;
1757
+ while (tokEnd < src.length && /\w/.test(src[tokEnd]))
1758
+ tokEnd++;
1759
+ if (tokEnd === j)
1760
+ continue;
1761
+ let tok = src.slice(tokStart, tokEnd).toUpperCase();
1762
+ if (tok.startsWith("."))
1763
+ tok = tok.slice(1);
1764
+ if (!ALL_MNEMONICS.has(tok))
1765
+ continue;
1766
+ out.push(src.slice(start, i));
1767
+ start = i + 2;
1768
+ i += 2;
1769
+ }
1770
+ out.push(src.slice(start));
1771
+ if (out.length > MAX_STATEMENTS_PER_LINE) {
1772
+ throw new Error(`too many statements on one line (max ${MAX_STATEMENTS_PER_LINE})`);
1773
+ }
1774
+ return out;
1775
+ }
1685
1776
  function instrSize(m) {
1686
1777
  if (m in IMPLIED)
1687
1778
  return 1;
@@ -1749,6 +1840,13 @@ function splitOperands(s) {
1749
1840
  r.push(current.trim());
1750
1841
  return r;
1751
1842
  }
1843
+ var DIRECTIVES = new Set(["ORG", "SECTION", "END", "DB", "DW", "DS", "EQU"]);
1844
+ function stripDirectiveDot(s) {
1845
+ if (s.startsWith(".") && DIRECTIVES.has(s.slice(1).toUpperCase())) {
1846
+ return s.slice(1);
1847
+ }
1848
+ return s;
1849
+ }
1752
1850
  function parseLine(line) {
1753
1851
  let s = stripComment(line).trim();
1754
1852
  if (!s)
@@ -1766,7 +1864,7 @@ function parseLine(line) {
1766
1864
  const rest = si < 0 ? "" : s.slice(si).trim();
1767
1865
  if (!label && rest) {
1768
1866
  const parts = rest.split(/\s+/);
1769
- if (parts[0].toUpperCase() === "EQU") {
1867
+ if (stripDirectiveDot(parts[0]).toUpperCase() === "EQU") {
1770
1868
  return {
1771
1869
  label: first,
1772
1870
  mnemonic: "EQU",
@@ -1775,43 +1873,189 @@ function parseLine(line) {
1775
1873
  };
1776
1874
  }
1777
1875
  }
1778
- return { label, mnemonic: first, operands: rest ? splitOperands(rest) : [] };
1876
+ return {
1877
+ label,
1878
+ mnemonic: stripDirectiveDot(first),
1879
+ operands: rest ? splitOperands(rest) : []
1880
+ };
1779
1881
  }
1780
- function evalAtom(s, symbols) {
1781
- s = s.trim();
1782
- if (s.length === 3 && s[0] === "'" && s[2] === "'")
1783
- return s.charCodeAt(1);
1784
- if (/^[0-9][0-9A-Fa-f]*[hH]$/.test(s))
1785
- return parseInt(s.slice(0, -1), 16);
1786
- if (/^[0-9]+$/.test(s))
1787
- return parseInt(s, 10);
1788
- const k = s.toUpperCase();
1789
- if (symbols.has(k))
1790
- return symbols.get(k);
1791
- throw new Error(`unknown symbol: ${s}`);
1882
+ function tokenizeExpr(expr) {
1883
+ const tokens = [];
1884
+ let i = 0;
1885
+ while (i < expr.length) {
1886
+ let c = expr[i];
1887
+ if (/\s/.test(c)) {
1888
+ i++;
1889
+ continue;
1890
+ }
1891
+ if (c === "'" && i + 2 < expr.length && expr[i + 2] === "'") {
1892
+ tokens.push({ kind: "num", val: expr.charCodeAt(i + 1) });
1893
+ i += 3;
1894
+ continue;
1895
+ }
1896
+ if (/[0-9]/.test(c)) {
1897
+ let j = i;
1898
+ while (j < expr.length && /[0-9A-Fa-f]/.test(expr[j]))
1899
+ j++;
1900
+ if (j < expr.length && /[hH]/.test(expr[j])) {
1901
+ tokens.push({ kind: "num", val: parseInt(expr.slice(i, j), 16) });
1902
+ j++;
1903
+ } else {
1904
+ tokens.push({ kind: "num", val: parseInt(expr.slice(i, j), 10) });
1905
+ }
1906
+ i = j;
1907
+ continue;
1908
+ }
1909
+ if (/[A-Za-z_]/.test(c)) {
1910
+ let j = i;
1911
+ while (j < expr.length && /\w/.test(expr[j]))
1912
+ j++;
1913
+ tokens.push({ kind: "id", val: expr.slice(i, j) });
1914
+ i = j;
1915
+ continue;
1916
+ }
1917
+ if (c === "<" && expr[i + 1] === "<") {
1918
+ tokens.push({ kind: "op", val: "<<" });
1919
+ i += 2;
1920
+ continue;
1921
+ }
1922
+ if (c === ">" && expr[i + 1] === ">") {
1923
+ tokens.push({ kind: "op", val: ">>" });
1924
+ i += 2;
1925
+ continue;
1926
+ }
1927
+ if ("+-*/%&|^~()".includes(c)) {
1928
+ tokens.push({ kind: "op", val: c });
1929
+ i++;
1930
+ continue;
1931
+ }
1932
+ throw new Error(`unexpected character in expression: '${c}'`);
1933
+ }
1934
+ return tokens;
1792
1935
  }
1793
1936
  function evalExpr(expr, symbols) {
1794
- expr = expr.trim();
1795
- const tokens = [];
1796
- const ops = ["+"];
1797
- let current = "";
1798
- for (const c of expr) {
1799
- if ((c === "+" || c === "-") && current.trim()) {
1800
- tokens.push(current.trim());
1801
- ops.push(c);
1802
- current = "";
1803
- } else {
1804
- current += c;
1937
+ const tokens = tokenizeExpr(expr);
1938
+ let pos = 0;
1939
+ function peek() {
1940
+ return tokens[pos];
1941
+ }
1942
+ function next() {
1943
+ return tokens[pos++];
1944
+ }
1945
+ function isOp(val) {
1946
+ const t = peek();
1947
+ return t !== undefined && t.kind === "op" && t.val === val;
1948
+ }
1949
+ function atom() {
1950
+ const t = peek();
1951
+ if (!t)
1952
+ throw new Error("unexpected end of expression");
1953
+ if (t.kind === "num") {
1954
+ next();
1955
+ return t.val;
1956
+ }
1957
+ if (t.kind === "id") {
1958
+ next();
1959
+ const k = t.val.toUpperCase();
1960
+ if (k === "LOW" || k === "HIGH") {
1961
+ if (!isOp("("))
1962
+ throw new Error(`${k} requires parentheses`);
1963
+ next();
1964
+ const v = parseOr();
1965
+ if (!isOp(")"))
1966
+ throw new Error("expected ')'");
1967
+ next();
1968
+ return k === "LOW" ? v & 255 : v >> 8 & 255;
1969
+ }
1970
+ if (symbols.has(k))
1971
+ return symbols.get(k);
1972
+ throw new Error(`unknown symbol: ${t.val}`);
1973
+ }
1974
+ if (t.kind === "op" && t.val === "(") {
1975
+ next();
1976
+ const v = parseOr();
1977
+ if (!isOp(")"))
1978
+ throw new Error("expected ')'");
1979
+ next();
1980
+ return v;
1805
1981
  }
1982
+ throw new Error(`unexpected token: ${t.val}`);
1806
1983
  }
1807
- if (current.trim())
1808
- tokens.push(current.trim());
1809
- let r = 0;
1810
- for (let i = 0;i < tokens.length; i++) {
1811
- const v = evalAtom(tokens[i], symbols);
1812
- r = ops[i] === "+" ? r + v : r - v;
1984
+ function unary() {
1985
+ if (isOp("-")) {
1986
+ next();
1987
+ return -unary() & 65535;
1988
+ }
1989
+ if (isOp("+")) {
1990
+ next();
1991
+ return unary();
1992
+ }
1993
+ if (isOp("~")) {
1994
+ next();
1995
+ return ~unary() & 65535;
1996
+ }
1997
+ return atom();
1998
+ }
1999
+ function multiplicative() {
2000
+ let v = unary();
2001
+ while (isOp("*") || isOp("/") || isOp("%")) {
2002
+ const op = next().val;
2003
+ let r = unary();
2004
+ if (op === "*")
2005
+ v = v * r & 65535;
2006
+ else if (op === "/")
2007
+ v = Math.trunc(v / r) & 65535;
2008
+ else
2009
+ v = v % r & 65535;
2010
+ }
2011
+ return v;
2012
+ }
2013
+ function additive() {
2014
+ let v = multiplicative();
2015
+ while (isOp("+") || isOp("-")) {
2016
+ const op = next().val;
2017
+ let r = multiplicative();
2018
+ v = op === "+" ? v + r & 65535 : v - r & 65535;
2019
+ }
2020
+ return v;
2021
+ }
2022
+ function shift() {
2023
+ let v = additive();
2024
+ while (isOp("<<") || isOp(">>")) {
2025
+ const op = next().val;
2026
+ let r = additive();
2027
+ v = op === "<<" ? v << r & 65535 : v >>> r & 65535;
2028
+ }
2029
+ return v;
2030
+ }
2031
+ function parseAnd() {
2032
+ let v = shift();
2033
+ while (isOp("&")) {
2034
+ next();
2035
+ v = v & shift();
2036
+ }
2037
+ return v;
2038
+ }
2039
+ function parseXor() {
2040
+ let v = parseAnd();
2041
+ while (isOp("^")) {
2042
+ next();
2043
+ v = (v ^ parseAnd()) & 65535;
2044
+ }
2045
+ return v;
1813
2046
  }
1814
- return r & 65535;
2047
+ function parseOr() {
2048
+ let v = parseXor();
2049
+ while (isOp("|")) {
2050
+ next();
2051
+ v = (v | parseXor()) & 65535;
2052
+ }
2053
+ return v;
2054
+ }
2055
+ const result = parseOr();
2056
+ if (pos < tokens.length)
2057
+ throw new Error(`unexpected token: ${tokens[pos].val}`);
2058
+ return result;
1815
2059
  }
1816
2060
  function encode(m, ops, symbols) {
1817
2061
  if (m in IMPLIED)
@@ -1825,7 +2069,9 @@ function encode(m, ops, symbols) {
1825
2069
  return [ADDR16[m], v & 255, v >> 8 & 255];
1826
2070
  }
1827
2071
  if (m === "MOV")
1828
- return [64 | REG8[ops[0].toUpperCase()] << 3 | REG8[ops[1].toUpperCase()]];
2072
+ return [
2073
+ 64 | REG8[ops[0].toUpperCase()] << 3 | REG8[ops[1].toUpperCase()]
2074
+ ];
1829
2075
  if (m === "MVI") {
1830
2076
  const v = evalExpr(ops[1], symbols);
1831
2077
  return [6 | REG8[ops[0].toUpperCase()] << 3, v & 255];
@@ -1836,7 +2082,11 @@ function encode(m, ops, symbols) {
1836
2082
  return [5 | REG8[ops[0].toUpperCase()] << 3];
1837
2083
  if (m === "LXI") {
1838
2084
  const v = evalExpr(ops[1], symbols);
1839
- return [1 | REG_PAIR[ops[0].toUpperCase()] << 4, v & 255, v >> 8 & 255];
2085
+ return [
2086
+ 1 | REG_PAIR[ops[0].toUpperCase()] << 4,
2087
+ v & 255,
2088
+ v >> 8 & 255
2089
+ ];
1840
2090
  }
1841
2091
  if (m === "DAD")
1842
2092
  return [9 | REG_PAIR[ops[0].toUpperCase()] << 4];
@@ -1882,6 +2132,24 @@ function dwBytes(operands, symbols) {
1882
2132
  }
1883
2133
  return out;
1884
2134
  }
2135
+ function parseDs(operands) {
2136
+ if (operands.length !== 1)
2137
+ throw new Error("DS takes one operand: count [(fill)]");
2138
+ const m = operands[0].match(/^(.+?)\s+\((.+)\)\s*$/);
2139
+ if (m)
2140
+ return { count: m[1], fill: m[2] };
2141
+ return { count: operands[0], fill: "0" };
2142
+ }
2143
+ function dsBytes(operands, symbols) {
2144
+ const { count, fill } = parseDs(operands);
2145
+ const n = evalExpr(count, symbols);
2146
+ const f = evalExpr(fill, symbols) & 255;
2147
+ return new Array(n).fill(f);
2148
+ }
2149
+ function countDs(operands, symbols) {
2150
+ const { count } = parseDs(operands);
2151
+ return evalExpr(count, symbols);
2152
+ }
1885
2153
  function countDb(operands) {
1886
2154
  let n = 0;
1887
2155
  for (const op of operands) {
@@ -1897,75 +2165,103 @@ function asm(source) {
1897
2165
  `);
1898
2166
  const symbols = new Map;
1899
2167
  let pc = 0;
1900
- for (const line of lines) {
1901
- const parts = parseLine(line);
1902
- if (parts.label) {
1903
- if (parts.isEqu) {
1904
- symbols.set(parts.label.toUpperCase(), evalExpr(parts.operands[0], symbols));
1905
- continue;
2168
+ let ended = false;
2169
+ for (let idx = 0;idx < lines.length && !ended; idx++) {
2170
+ const line = lines[idx];
2171
+ try {
2172
+ for (const stmt of splitStatements(line)) {
2173
+ const parts = parseLine(stmt);
2174
+ if (parts.label) {
2175
+ if (parts.isEqu) {
2176
+ symbols.set(parts.label.toUpperCase(), evalExpr(parts.operands[0], symbols));
2177
+ continue;
2178
+ }
2179
+ symbols.set(parts.label.toUpperCase(), pc);
2180
+ }
2181
+ if (!parts.mnemonic)
2182
+ continue;
2183
+ const m = parts.mnemonic.toUpperCase();
2184
+ if (m === "EQU")
2185
+ continue;
2186
+ if (m === "ORG") {
2187
+ pc = evalExpr(parts.operands[0], symbols);
2188
+ continue;
2189
+ }
2190
+ if (m === "SECTION")
2191
+ continue;
2192
+ if (m === "END") {
2193
+ ended = true;
2194
+ break;
2195
+ }
2196
+ if (m === "DB") {
2197
+ pc += countDb(parts.operands);
2198
+ continue;
2199
+ }
2200
+ if (m === "DW") {
2201
+ pc += parts.operands.length * 2;
2202
+ continue;
2203
+ }
2204
+ if (m === "DS") {
2205
+ pc += countDs(parts.operands, symbols);
2206
+ continue;
2207
+ }
2208
+ pc += instrSize(m);
1906
2209
  }
1907
- symbols.set(parts.label.toUpperCase(), pc);
2210
+ } catch (e) {
2211
+ if (e instanceof AsmError)
2212
+ throw e;
2213
+ throw new AsmError(e.message, idx + 1, line, firstNonSpaceCol(line));
1908
2214
  }
1909
- if (!parts.mnemonic)
1910
- continue;
1911
- const m = parts.mnemonic.toUpperCase();
1912
- if (m === "EQU")
1913
- continue;
1914
- if (m === "ORG") {
1915
- pc = evalExpr(parts.operands[0], symbols);
1916
- continue;
1917
- }
1918
- if (m === "SECTION")
1919
- continue;
1920
- if (m === "END")
1921
- break;
1922
- if (m === "DB") {
1923
- pc += countDb(parts.operands);
1924
- continue;
1925
- }
1926
- if (m === "DW") {
1927
- pc += parts.operands.length * 2;
1928
- continue;
1929
- }
1930
- pc += instrSize(m);
1931
2215
  }
1932
2216
  const sections = [];
1933
2217
  let current = null;
1934
2218
  const sectionNames = new Set;
1935
- for (const line of lines) {
1936
- const parts = parseLine(line);
1937
- if (parts.isEqu || !parts.mnemonic)
1938
- continue;
1939
- const m = parts.mnemonic.toUpperCase();
1940
- if (m === "EQU")
1941
- continue;
1942
- if (m === "ORG") {
1943
- if (current && current.data.length) {
1944
- current.end = current.start + current.data.length - 1;
1945
- sections.push(current);
2219
+ let endedPass2 = false;
2220
+ for (let idx = 0;idx < lines.length && !endedPass2; idx++) {
2221
+ const line = lines[idx];
2222
+ try {
2223
+ for (const stmt of splitStatements(line)) {
2224
+ const parts = parseLine(stmt);
2225
+ if (parts.isEqu || !parts.mnemonic)
2226
+ continue;
2227
+ const m = parts.mnemonic.toUpperCase();
2228
+ if (m === "EQU")
2229
+ continue;
2230
+ if (m === "ORG") {
2231
+ if (current && current.data.length) {
2232
+ current.end = current.start + current.data.length - 1;
2233
+ sections.push(current);
2234
+ }
2235
+ const addr = evalExpr(parts.operands[0], symbols);
2236
+ current = { start: addr, end: addr, data: [] };
2237
+ continue;
2238
+ }
2239
+ if (m === "SECTION") {
2240
+ if (!current)
2241
+ throw new Error("SECTION before ORG");
2242
+ const name = parts.operands[0];
2243
+ if (!name)
2244
+ throw new Error("SECTION requires a name");
2245
+ if (sectionNames.has(name.toUpperCase()))
2246
+ throw new Error(`duplicate section name: ${name}`);
2247
+ sectionNames.add(name.toUpperCase());
2248
+ current.name = name;
2249
+ continue;
2250
+ }
2251
+ if (m === "END") {
2252
+ endedPass2 = true;
2253
+ break;
2254
+ }
2255
+ if (!current)
2256
+ throw new Error("code before ORG");
2257
+ const bytes = m === "DB" ? dbBytes(parts.operands, symbols) : m === "DW" ? dwBytes(parts.operands, symbols) : m === "DS" ? dsBytes(parts.operands, symbols) : encode(m, parts.operands, symbols);
2258
+ current.data.push(...bytes);
1946
2259
  }
1947
- const addr = evalExpr(parts.operands[0], symbols);
1948
- current = { start: addr, end: addr, data: [] };
1949
- continue;
1950
- }
1951
- if (m === "SECTION") {
1952
- if (!current)
1953
- throw new Error("SECTION before ORG");
1954
- const name = parts.operands[0];
1955
- if (!name)
1956
- throw new Error("SECTION requires a name");
1957
- if (sectionNames.has(name.toUpperCase()))
1958
- throw new Error(`duplicate section name: ${name}`);
1959
- sectionNames.add(name.toUpperCase());
1960
- current.name = name;
1961
- continue;
2260
+ } catch (e) {
2261
+ if (e instanceof AsmError)
2262
+ throw e;
2263
+ throw new AsmError(e.message, idx + 1, line, firstNonSpaceCol(line));
1962
2264
  }
1963
- if (m === "END")
1964
- break;
1965
- if (!current)
1966
- throw new Error("code before ORG");
1967
- const bytes = m === "DB" ? dbBytes(parts.operands, symbols) : m === "DW" ? dwBytes(parts.operands, symbols) : encode(m, parts.operands, symbols);
1968
- current.data.push(...bytes);
1969
2265
  }
1970
2266
  if (current && current.data.length) {
1971
2267
  current.end = current.start + current.data.length - 1;
@@ -1977,11 +2273,11 @@ if (false) {}
1977
2273
 
1978
2274
  // src/lib/terminal/rk86_terminal.ts
1979
2275
  import { existsSync } from "fs";
1980
- import { readFile } from "fs/promises";
2276
+ import { readFile, writeFile } from "fs/promises";
1981
2277
  // packages/rk86/package.json
1982
2278
  var package_default = {
1983
2279
  name: "rk86",
1984
- version: "2.0.14",
2280
+ version: "2.0.16",
1985
2281
  description: "\u042D\u043C\u0443\u043B\u044F\u0442\u043E\u0440 \u0420\u0430\u0434\u0438\u043E-86\u0420\u041A (Intel 8080) \u0434\u043B\u044F \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0430",
1986
2282
  bin: {
1987
2283
  rk86: "rk86.js"
@@ -3924,6 +4220,11 @@ class TerminalRenderer {
3924
4220
  process.stdout.write(output);
3925
4221
  }
3926
4222
  }
4223
+
4224
+ class HeadlessRenderer {
4225
+ connect(_machine) {}
4226
+ update() {}
4227
+ }
3927
4228
  var KEY_MAP = {
3928
4229
  a: "KeyA",
3929
4230
  b: "KeyB",
@@ -4045,6 +4346,13 @@ function printHelp() {
4045
4346
  -g <\u0430\u0434\u0440\u0435\u0441> \u0430\u0434\u0440\u0435\u0441 \u0437\u0430\u043F\u0443\u0441\u043A\u0430 (\u043D\u0435\u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C \u0441 -p)
4046
4347
  --exit-halt \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0438 HLT
4047
4348
  --exit-address [\u0430\u0434\u0440\u0435\u0441] \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0435 \u043D\u0430 \u0430\u0434\u0440\u0435\u0441 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0xFFFE)
4349
+ --headless \u0431\u0435\u0437 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u044D\u043A\u0440\u0430\u043D\u0430 (\u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0442\u0435\u0441\u0442\u043E\u0432)
4350
+ --timeout <\u0441\u0435\u043A> \u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443
4351
+ --memory <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0430\u043C\u044F\u0442\u044C \u0432 \u0444\u0430\u0439\u043B \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4352
+ --memory-from <\u0430\u0434\u0440\u0435\u0441> \u043D\u0430\u0447\u0430\u043B\u043E \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0434\u0430\u043C\u043F\u0430 \u043F\u0430\u043C\u044F\u0442\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0x0000)
4353
+ --memory-to <\u0430\u0434\u0440\u0435\u0441> \u043A\u043E\u043D\u0435\u0446 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0434\u0430\u043C\u043F\u0430 \u043F\u0430\u043C\u044F\u0442\u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435\u043B\u044C\u043D\u043E (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: 0xFFFF)
4354
+ --screen <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u044D\u043A\u0440\u0430\u043D 78x30 \u043A\u0430\u043A \u0442\u0435\u043A\u0441\u0442 \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4355
+ --input <seq> \u0438\u043D\u044A\u0435\u043A\u0446\u0438\u044F \u043A\u043B\u0430\u0432\u0438\u0448 (\u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043F\u044F\u0442\u0443\u044E): KeyA,Digit1,Enter,...
4048
4356
 
4049
4357
  \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
4050
4358
  bunx rk86 \u0437\u0430\u043F\u0443\u0441\u043A \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0430
@@ -4115,6 +4423,15 @@ async function main() {
4115
4423
  const exitAddrValue = arg(args, "--exit-address", "0xFFFE", /^0x[0-9a-fA-F]+$/i, (v) => parseInt(v, 16));
4116
4424
  const exitAddr = exitAddrValue !== undefined;
4117
4425
  const monitorFile_ = arg(args, "-m");
4426
+ const headless = flag(args, "--headless");
4427
+ const timeoutSec = arg(args, "--timeout", undefined, /^\d+(\.\d+)?$/, parseFloat);
4428
+ const memoryFile = arg(args, "--memory");
4429
+ const addrRe = /^(0x)?[0-9a-fA-F]+$/i;
4430
+ const parseAddr = (v) => parseInt(v.toLowerCase().startsWith("0x") ? v.slice(2) : v, 16) & 65535;
4431
+ const memoryFrom = arg(args, "--memory-from", undefined, addrRe, parseAddr) ?? 0;
4432
+ const memoryTo = arg(args, "--memory-to", undefined, addrRe, parseAddr) ?? 65535;
4433
+ const screenFile = arg(args, "--screen");
4434
+ const inputSeq = arg(args, "--input");
4118
4435
  const programFile = args[0];
4119
4436
  const keyboard = new Keyboard;
4120
4437
  const io = new IO;
@@ -4176,19 +4493,36 @@ async function main() {
4176
4493
  if (goAddr !== undefined)
4177
4494
  entryPoint = goAddr;
4178
4495
  }
4179
- process.stdout.write("\x1B[?25l");
4180
- process.stdout.write("\x1B[2J");
4181
- setupKeyboard(keyboard);
4182
- const renderer = new TerminalRenderer;
4183
- renderer.loadInfo = loadInfo;
4496
+ if (!headless) {
4497
+ process.stdout.write("\x1B[?25l");
4498
+ process.stdout.write("\x1B[2J");
4499
+ setupKeyboard(keyboard);
4500
+ } else {
4501
+ process.on("SIGINT", () => doExit(null));
4502
+ }
4503
+ const renderer = headless ? new HeadlessRenderer : Object.assign(new TerminalRenderer, { loadInfo });
4184
4504
  machine.screen.start(renderer);
4185
- const onTerminate = exitOnHalt || exitAddr ? () => {
4186
- renderer.update();
4187
- setTimeout(() => {
4505
+ let exiting = false;
4506
+ const doExit = async (message) => {
4507
+ if (exiting)
4508
+ return;
4509
+ exiting = true;
4510
+ if (screenFile)
4511
+ await writeFile(screenFile, dumpScreen(machine));
4512
+ if (memoryFile)
4513
+ await writeFile(memoryFile, new Uint8Array(machine.memory.buf.slice(memoryFrom, memoryTo + 1)));
4514
+ if (!headless)
4515
+ process.stdout.write("\x1B[?25h");
4516
+ if (message !== null && !headless) {
4188
4517
  console.log();
4189
- console.log("\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430", hex16(machine.cpu.pc));
4190
- process.exit(0);
4191
- }, 1000);
4518
+ console.log(message);
4519
+ }
4520
+ process.exit(0);
4521
+ };
4522
+ const onTerminate = exitOnHalt || exitAddr ? () => {
4523
+ if (!headless)
4524
+ renderer.update();
4525
+ setTimeout(() => doExit(`\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B\u0430 \u0440\u0430\u0431\u043E\u0442\u0443 \u043D\u0430 ${hex16(machine.cpu.pc)}`), headless ? 0 : 1000);
4192
4526
  } : undefined;
4193
4527
  const armed = { value: entryPoint === undefined };
4194
4528
  machine.runner.execute({
@@ -4197,14 +4531,54 @@ async function main() {
4197
4531
  on_terminate: onTerminate,
4198
4532
  armed
4199
4533
  });
4534
+ const armDelayMs = 500;
4200
4535
  if (entryPoint !== undefined && !loadOnly) {
4201
4536
  setTimeout(() => {
4202
4537
  machine.cpu.jump(entryPoint);
4203
4538
  armed.value = true;
4204
- }, 500);
4539
+ }, armDelayMs);
4205
4540
  }
4206
- process.on("exit", () => {
4207
- process.stdout.write("\x1B[?25h");
4208
- });
4541
+ if (inputSeq) {
4542
+ const keys = inputSeq.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4543
+ const settleMs = armDelayMs + 1000;
4544
+ const keyDownMs = 50;
4545
+ const keyGapMs = 50;
4546
+ setTimeout(() => {
4547
+ const pressNext = (i) => {
4548
+ if (i >= keys.length)
4549
+ return;
4550
+ const code = keys[i];
4551
+ keyboard.onkeydown(code);
4552
+ setTimeout(() => {
4553
+ keyboard.onkeyup(code);
4554
+ setTimeout(() => pressNext(i + 1), keyGapMs);
4555
+ }, keyDownMs);
4556
+ };
4557
+ pressNext(0);
4558
+ }, settleMs);
4559
+ }
4560
+ if (timeoutSec !== undefined) {
4561
+ setTimeout(() => doExit(`\u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443 ${timeoutSec}\u0441`), timeoutSec * 1000);
4562
+ }
4563
+ }
4564
+ function dumpScreen(machine) {
4565
+ const { memory, screen } = machine;
4566
+ const lines = [];
4567
+ let addr = screen.video_memory_base;
4568
+ for (let y = 0;y < screen.height; y++) {
4569
+ let line = "";
4570
+ for (let x = 0;x < screen.width; x++) {
4571
+ const byte = memory.read_raw(addr++) & 127;
4572
+ if (byte === 0 || byte === 9 || byte === 10 || byte === 13) {
4573
+ line += ".";
4574
+ } else {
4575
+ line += rk86char(byte);
4576
+ }
4577
+ }
4578
+ lines.push(line);
4579
+ }
4580
+ return lines.join(`\r
4581
+ `) + `\r
4582
+ `;
4209
4583
  }
4210
4584
  main();