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.
- package/README.md +71 -13
- package/package.json +1 -1
- 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
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
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
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 {
|
|
1876
|
+
return {
|
|
1877
|
+
label,
|
|
1878
|
+
mnemonic: stripDirectiveDot(first),
|
|
1879
|
+
operands: rest ? splitOperands(rest) : []
|
|
1880
|
+
};
|
|
1779
1881
|
}
|
|
1780
|
-
function
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
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
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
-
|
|
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 [
|
|
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 [
|
|
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
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
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.
|
|
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
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
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
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
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(
|
|
4190
|
-
|
|
4191
|
-
|
|
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
|
-
},
|
|
4539
|
+
}, armDelayMs);
|
|
4205
4540
|
}
|
|
4206
|
-
|
|
4207
|
-
|
|
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();
|