rk86 2.0.17 → 2.0.20

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 +3 -3
  2. package/package.json +1 -1
  3. package/rk86.js +319 -90
package/README.md CHANGED
@@ -62,13 +62,13 @@ bunx rk86 -l # список файлов встроенн
62
62
 
63
63
  ```bash
64
64
  bunx rk86 --headless \
65
- --input "KeyD,Digit0,Comma,KeyF,KeyF,Enter" \
65
+ --input "KeyD,KeyF,Digit8,Digit0,Digit0,Comma,KeyF,Digit8,KeyF,KeyF,Enter" \
66
66
  --timeout 10 \
67
67
  --screen out.txt
68
68
  ```
69
69
 
70
- После выхода `out.txt` содержит приглашение `-->D0,FF` и 16 строк шестнадцатеричного
71
- дампа `0000..00F0`.
70
+ После выхода `out.txt` содержит приглашение `-->DF800,F8FF` и 16 строк шестнадцатеричного
71
+ дампа `F800..F8FF`.
72
72
 
73
73
  ### Пример 2. Запись HLT и запуск через команды `M` / `G`
74
74
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rk86",
3
- "version": "2.0.17",
3
+ "version": "2.0.20",
4
4
  "description": "Эмулятор Радио-86РК (Intel 8080) для терминала",
5
5
  "bin": {
6
6
  "rk86": "rk86.js"
package/rk86.js CHANGED
@@ -1726,6 +1726,127 @@ var ALL_MNEMONICS = new Set([
1726
1726
  "END",
1727
1727
  "EQU"
1728
1728
  ]);
1729
+ var INVERT_JUMP = {
1730
+ Z: "JNZ",
1731
+ NZ: "JZ",
1732
+ C: "JNC",
1733
+ NC: "JC",
1734
+ PO: "JPE",
1735
+ PE: "JPO",
1736
+ P: "JM",
1737
+ M: "JP",
1738
+ "==": "JNZ",
1739
+ "<>": "JZ"
1740
+ };
1741
+ var VALID_PROC_REGS = new Set(["PSW", "B", "D", "H"]);
1742
+ function popsAndRet(regs, orig) {
1743
+ const out = [];
1744
+ for (let k = regs.length - 1;k >= 0; k--) {
1745
+ out.push({ text: ` POP ${regs[k]}`, orig });
1746
+ }
1747
+ out.push({ text: ` RET`, orig });
1748
+ return out;
1749
+ }
1750
+ function preprocess(source) {
1751
+ const lines = source.split(`
1752
+ `);
1753
+ const out = [];
1754
+ const stack = [];
1755
+ let counter = 0;
1756
+ let proc = null;
1757
+ for (let i = 0;i < lines.length; i++) {
1758
+ const line = lines[i];
1759
+ const orig = i + 1;
1760
+ const bare = stripComment(line).trim();
1761
+ const ifMatch = bare.match(/^\.?if\s+(\S+)\s*$/i);
1762
+ if (ifMatch) {
1763
+ const cond = ifMatch[1].toUpperCase();
1764
+ const jmp = INVERT_JUMP[cond];
1765
+ if (!jmp) {
1766
+ throw new AsmError(`unknown .if condition: ${ifMatch[1]}`, orig, line, firstNonSpaceCol(line));
1767
+ }
1768
+ const id = counter++;
1769
+ stack.push({ id, sawElse: false, line: orig, source: line });
1770
+ out.push({ text: ` ${jmp} @_if_${id}_else`, orig });
1771
+ continue;
1772
+ }
1773
+ if (/^\.?else\s*$/i.test(bare)) {
1774
+ const top = stack[stack.length - 1];
1775
+ if (!top) {
1776
+ throw new AsmError(".else without .if", orig, line, firstNonSpaceCol(line));
1777
+ }
1778
+ if (top.sawElse) {
1779
+ throw new AsmError("duplicate .else", orig, line, firstNonSpaceCol(line));
1780
+ }
1781
+ top.sawElse = true;
1782
+ out.push({ text: ` JMP @_if_${top.id}_exit`, orig });
1783
+ out.push({ text: `@_if_${top.id}_else:`, orig });
1784
+ continue;
1785
+ }
1786
+ if (/^\.?endif\s*$/i.test(bare)) {
1787
+ const top = stack.pop();
1788
+ if (!top) {
1789
+ throw new AsmError(".endif without .if", orig, line, firstNonSpaceCol(line));
1790
+ }
1791
+ const suffix = top.sawElse ? "exit" : "else";
1792
+ out.push({ text: `@_if_${top.id}_${suffix}:`, orig });
1793
+ continue;
1794
+ }
1795
+ const procMatch = bare.match(/^([A-Za-z_]\w*):?\s+\.?proc\b\s*(.*)$/i);
1796
+ if (procMatch && !ALL_MNEMONICS.has(procMatch[1].toUpperCase())) {
1797
+ if (proc) {
1798
+ throw new AsmError("nested .proc not allowed", orig, line, firstNonSpaceCol(line));
1799
+ }
1800
+ const name = procMatch[1];
1801
+ const regsRaw = procMatch[2].trim();
1802
+ const regs = [];
1803
+ if (regsRaw) {
1804
+ for (const r of regsRaw.split(/[,\s]+/)) {
1805
+ if (!r)
1806
+ continue;
1807
+ const up = r.toUpperCase();
1808
+ if (!VALID_PROC_REGS.has(up)) {
1809
+ throw new AsmError(`invalid .proc register: ${r} (expected PSW, B, D, or H)`, orig, line, firstNonSpaceCol(line));
1810
+ }
1811
+ regs.push(up);
1812
+ }
1813
+ }
1814
+ proc = { regs, line: orig, source: line };
1815
+ out.push({ text: `${name}:`, orig });
1816
+ for (const r of regs) {
1817
+ out.push({ text: ` PUSH ${r}`, orig });
1818
+ }
1819
+ continue;
1820
+ }
1821
+ if (/^\.proc(\s|$)/i.test(bare) || /^proc\s+\S/i.test(bare)) {
1822
+ throw new AsmError(".proc requires a label", orig, line, firstNonSpaceCol(line));
1823
+ }
1824
+ if (/^\.?endp\s*$/i.test(bare)) {
1825
+ if (!proc) {
1826
+ throw new AsmError(".endp without .proc", orig, line, firstNonSpaceCol(line));
1827
+ }
1828
+ out.push(...popsAndRet(proc.regs, orig));
1829
+ proc = null;
1830
+ continue;
1831
+ }
1832
+ if (/^\.?return\s*$/i.test(bare)) {
1833
+ if (!proc) {
1834
+ throw new AsmError(".return outside .proc", orig, line, firstNonSpaceCol(line));
1835
+ }
1836
+ out.push(...popsAndRet(proc.regs, orig));
1837
+ continue;
1838
+ }
1839
+ out.push({ text: line, orig });
1840
+ }
1841
+ if (stack.length) {
1842
+ const top = stack[stack.length - 1];
1843
+ throw new AsmError(".if without .endif", top.line, top.source, firstNonSpaceCol(top.source));
1844
+ }
1845
+ if (proc) {
1846
+ throw new AsmError(".proc without .endp", proc.line, proc.source, firstNonSpaceCol(proc.source));
1847
+ }
1848
+ return out;
1849
+ }
1729
1850
  var MAX_STATEMENTS_PER_LINE = 10;
1730
1851
  function splitStatements(line) {
1731
1852
  const src = stripComment(line);
@@ -1847,35 +1968,46 @@ function stripDirectiveDot(s) {
1847
1968
  }
1848
1969
  return s;
1849
1970
  }
1971
+ var LABEL_RE = /^(?:[A-Za-z_]\w*|@\w+|\.\w+)$/;
1972
+ function isMnemonic(tok) {
1973
+ return ALL_MNEMONICS.has(stripDirectiveDot(tok).toUpperCase());
1974
+ }
1850
1975
  function parseLine(line) {
1851
1976
  let s = stripComment(line).trim();
1852
1977
  if (!s)
1853
1978
  return { operands: [] };
1854
1979
  let label;
1855
1980
  const ci = s.indexOf(":");
1856
- if (ci > 0 && /^[A-Za-z_]\w*$/.test(s.slice(0, ci).trim())) {
1981
+ if (ci > 0 && LABEL_RE.test(s.slice(0, ci).trim())) {
1857
1982
  label = s.slice(0, ci).trim();
1858
1983
  s = s.slice(ci + 1).trim();
1859
1984
  }
1860
1985
  if (!s)
1861
1986
  return { label, operands: [] };
1862
- const si = s.search(/\s/);
1863
- const first = si < 0 ? s : s.slice(0, si);
1864
- const rest = si < 0 ? "" : s.slice(si).trim();
1865
- if (!label && rest) {
1866
- const parts = rest.split(/\s+/);
1867
- if (stripDirectiveDot(parts[0]).toUpperCase() === "EQU") {
1868
- return {
1869
- label: first,
1870
- mnemonic: "EQU",
1871
- operands: [parts.slice(1).join(" ")],
1872
- isEqu: true
1873
- };
1987
+ let si = s.search(/\s/);
1988
+ let first = si < 0 ? s : s.slice(0, si);
1989
+ let rest = si < 0 ? "" : s.slice(si).trim();
1990
+ if (!label && rest && LABEL_RE.test(first) && !isMnemonic(first)) {
1991
+ const nextTok = rest.match(/^\S+/)?.[0] ?? "";
1992
+ if (isMnemonic(nextTok)) {
1993
+ label = first;
1994
+ si = rest.search(/\s/);
1995
+ first = si < 0 ? rest : rest.slice(0, si);
1996
+ rest = si < 0 ? "" : rest.slice(si).trim();
1874
1997
  }
1875
1998
  }
1999
+ const mnemonic = stripDirectiveDot(first);
2000
+ if (label && mnemonic.toUpperCase() === "EQU") {
2001
+ return {
2002
+ label,
2003
+ mnemonic: "EQU",
2004
+ operands: [rest],
2005
+ isEqu: true
2006
+ };
2007
+ }
1876
2008
  return {
1877
2009
  label,
1878
- mnemonic: stripDirectiveDot(first),
2010
+ mnemonic,
1879
2011
  operands: rest ? splitOperands(rest) : []
1880
2012
  };
1881
2013
  }
@@ -1893,6 +2025,31 @@ function tokenizeExpr(expr) {
1893
2025
  i += 3;
1894
2026
  continue;
1895
2027
  }
2028
+ if (c === "$") {
2029
+ tokens.push({ kind: "id", val: "$" });
2030
+ i++;
2031
+ continue;
2032
+ }
2033
+ if (c === "@") {
2034
+ let j = i + 1;
2035
+ while (j < expr.length && /\w/.test(expr[j]))
2036
+ j++;
2037
+ if (j === i + 1)
2038
+ throw new Error("expected identifier after '@'");
2039
+ tokens.push({ kind: "id", val: expr.slice(i, j) });
2040
+ i = j;
2041
+ continue;
2042
+ }
2043
+ if (c === ".") {
2044
+ let j = i + 1;
2045
+ while (j < expr.length && /\w/.test(expr[j]))
2046
+ j++;
2047
+ if (j === i + 1)
2048
+ throw new Error("expected identifier after '.'");
2049
+ tokens.push({ kind: "id", val: expr.slice(i, j) });
2050
+ i = j;
2051
+ continue;
2052
+ }
1896
2053
  if (/[0-9]/.test(c)) {
1897
2054
  let j = i;
1898
2055
  while (j < expr.length && /[0-9A-Fa-f]/.test(expr[j]))
@@ -1933,7 +2090,7 @@ function tokenizeExpr(expr) {
1933
2090
  }
1934
2091
  return tokens;
1935
2092
  }
1936
- function evalExpr(expr, symbols) {
2093
+ function evalExpr(expr, symbols, pc = 0, lastLabel = "") {
1937
2094
  const tokens = tokenizeExpr(expr);
1938
2095
  let pos = 0;
1939
2096
  function peek() {
@@ -1956,20 +2113,30 @@ function evalExpr(expr, symbols) {
1956
2113
  }
1957
2114
  if (t.kind === "id") {
1958
2115
  next();
1959
- const k = t.val.toUpperCase();
1960
- if (k === "LOW" || k === "HIGH") {
2116
+ const raw = t.val;
2117
+ if (raw === "$")
2118
+ return pc;
2119
+ const upper = raw.toUpperCase();
2120
+ if (upper === "LOW" || upper === "HIGH") {
1961
2121
  if (!isOp("("))
1962
- throw new Error(`${k} requires parentheses`);
2122
+ throw new Error(`${upper} requires parentheses`);
1963
2123
  next();
1964
2124
  const v = parseOr();
1965
2125
  if (!isOp(")"))
1966
2126
  throw new Error("expected ')'");
1967
2127
  next();
1968
- return k === "LOW" ? v & 255 : v >> 8 & 255;
2128
+ return upper === "LOW" ? v & 255 : v >> 8 & 255;
2129
+ }
2130
+ let name = raw;
2131
+ if (name.startsWith("@") || name.startsWith(".")) {
2132
+ if (!lastLabel)
2133
+ throw new Error(`local label without scope: ${raw}`);
2134
+ name = lastLabel + name;
1969
2135
  }
2136
+ const k = name.toUpperCase();
1970
2137
  if (symbols.has(k))
1971
2138
  return symbols.get(k);
1972
- throw new Error(`unknown symbol: ${t.val}`);
2139
+ throw new Error(`unknown symbol: ${raw}`);
1973
2140
  }
1974
2141
  if (t.kind === "op" && t.val === "(") {
1975
2142
  next();
@@ -2057,15 +2224,15 @@ function evalExpr(expr, symbols) {
2057
2224
  throw new Error(`unexpected token: ${tokens[pos].val}`);
2058
2225
  return result;
2059
2226
  }
2060
- function encode(m, ops, symbols) {
2227
+ function encode(m, ops, symbols, pc = 0, lastLabel = "") {
2061
2228
  if (m in IMPLIED)
2062
2229
  return [IMPLIED[m]];
2063
2230
  if (m in ALU_REG)
2064
2231
  return [ALU_REG[m] | REG8[ops[0].toUpperCase()]];
2065
2232
  if (m in ALU_IMM)
2066
- return [ALU_IMM[m], evalExpr(ops[0], symbols) & 255];
2233
+ return [ALU_IMM[m], evalExpr(ops[0], symbols, pc, lastLabel) & 255];
2067
2234
  if (m in ADDR16) {
2068
- const v = evalExpr(ops[0], symbols);
2235
+ const v = evalExpr(ops[0], symbols, pc, lastLabel);
2069
2236
  return [ADDR16[m], v & 255, v >> 8 & 255];
2070
2237
  }
2071
2238
  if (m === "MOV")
@@ -2073,7 +2240,7 @@ function encode(m, ops, symbols) {
2073
2240
  64 | REG8[ops[0].toUpperCase()] << 3 | REG8[ops[1].toUpperCase()]
2074
2241
  ];
2075
2242
  if (m === "MVI") {
2076
- const v = evalExpr(ops[1], symbols);
2243
+ const v = evalExpr(ops[1], symbols, pc, lastLabel);
2077
2244
  return [6 | REG8[ops[0].toUpperCase()] << 3, v & 255];
2078
2245
  }
2079
2246
  if (m === "INR")
@@ -2081,7 +2248,7 @@ function encode(m, ops, symbols) {
2081
2248
  if (m === "DCR")
2082
2249
  return [5 | REG8[ops[0].toUpperCase()] << 3];
2083
2250
  if (m === "LXI") {
2084
- const v = evalExpr(ops[1], symbols);
2251
+ const v = evalExpr(ops[1], symbols, pc, lastLabel);
2085
2252
  return [
2086
2253
  1 | REG_PAIR[ops[0].toUpperCase()] << 4,
2087
2254
  v & 255,
@@ -2103,31 +2270,31 @@ function encode(m, ops, symbols) {
2103
2270
  if (m === "STAX")
2104
2271
  return [2 | REG_PAIR[ops[0].toUpperCase()] << 4];
2105
2272
  if (m === "IN")
2106
- return [219, evalExpr(ops[0], symbols) & 255];
2273
+ return [219, evalExpr(ops[0], symbols, pc, lastLabel) & 255];
2107
2274
  if (m === "OUT")
2108
- return [211, evalExpr(ops[0], symbols) & 255];
2275
+ return [211, evalExpr(ops[0], symbols, pc, lastLabel) & 255];
2109
2276
  if (m === "RST") {
2110
- const n = evalExpr(ops[0], symbols);
2277
+ const n = evalExpr(ops[0], symbols, pc, lastLabel);
2111
2278
  return [199 | n << 3];
2112
2279
  }
2113
2280
  throw new Error(`cannot encode: ${m} ${ops.join(", ")}`);
2114
2281
  }
2115
- function dbBytes(operands, symbols) {
2282
+ function dbBytes(operands, symbols, pc = 0, lastLabel = "") {
2116
2283
  const out = [];
2117
2284
  for (const op of operands) {
2118
2285
  if (op.startsWith('"') && op.endsWith('"') || op.startsWith("'") && op.endsWith("'")) {
2119
2286
  for (const ch of op.slice(1, -1))
2120
2287
  out.push(ch.charCodeAt(0));
2121
2288
  } else {
2122
- out.push(evalExpr(op, symbols) & 255);
2289
+ out.push(evalExpr(op, symbols, pc, lastLabel) & 255);
2123
2290
  }
2124
2291
  }
2125
2292
  return out;
2126
2293
  }
2127
- function dwBytes(operands, symbols) {
2294
+ function dwBytes(operands, symbols, pc = 0, lastLabel = "") {
2128
2295
  const out = [];
2129
2296
  for (const op of operands) {
2130
- const v = evalExpr(op, symbols) & 65535;
2297
+ const v = evalExpr(op, symbols, pc, lastLabel) & 65535;
2131
2298
  out.push(v & 255, v >> 8 & 255);
2132
2299
  }
2133
2300
  return out;
@@ -2140,15 +2307,15 @@ function parseDs(operands) {
2140
2307
  return { count: m[1], fill: m[2] };
2141
2308
  return { count: operands[0], fill: "0" };
2142
2309
  }
2143
- function dsBytes(operands, symbols) {
2310
+ function dsBytes(operands, symbols, pc = 0, lastLabel = "") {
2144
2311
  const { count, fill } = parseDs(operands);
2145
- const n = evalExpr(count, symbols);
2146
- const f = evalExpr(fill, symbols) & 255;
2312
+ const n = evalExpr(count, symbols, pc, lastLabel);
2313
+ const f = evalExpr(fill, symbols, pc, lastLabel) & 255;
2147
2314
  return new Array(n).fill(f);
2148
2315
  }
2149
- function countDs(operands, symbols) {
2316
+ function countDs(operands, symbols, pc = 0, lastLabel = "") {
2150
2317
  const { count } = parseDs(operands);
2151
- return evalExpr(count, symbols);
2318
+ return evalExpr(count, symbols, pc, lastLabel);
2152
2319
  }
2153
2320
  function countDb(operands) {
2154
2321
  let n = 0;
@@ -2161,22 +2328,30 @@ function countDb(operands) {
2161
2328
  return n;
2162
2329
  }
2163
2330
  function asm(source) {
2164
- const lines = source.split(`
2165
- `);
2331
+ const pp = preprocess(source);
2166
2332
  const symbols = new Map;
2167
2333
  let pc = 0;
2334
+ let lastLabel = "";
2168
2335
  let ended = false;
2169
- for (let idx = 0;idx < lines.length && !ended; idx++) {
2170
- const line = lines[idx];
2336
+ for (let idx = 0;idx < pp.length && !ended; idx++) {
2337
+ const { text: line, orig } = pp[idx];
2171
2338
  try {
2172
2339
  for (const stmt of splitStatements(line)) {
2173
2340
  const parts = parseLine(stmt);
2174
2341
  if (parts.label) {
2342
+ let labelName = parts.label;
2343
+ if (labelName.startsWith("@") || labelName.startsWith(".")) {
2344
+ if (!lastLabel)
2345
+ throw new Error(`local label without preceding normal label: ${labelName}`);
2346
+ labelName = lastLabel + labelName;
2347
+ } else if (!parts.isEqu) {
2348
+ lastLabel = parts.label;
2349
+ }
2175
2350
  if (parts.isEqu) {
2176
- symbols.set(parts.label.toUpperCase(), evalExpr(parts.operands[0], symbols));
2351
+ symbols.set(labelName.toUpperCase(), evalExpr(parts.operands[0], symbols, pc, lastLabel));
2177
2352
  continue;
2178
2353
  }
2179
- symbols.set(parts.label.toUpperCase(), pc);
2354
+ symbols.set(labelName.toUpperCase(), pc);
2180
2355
  }
2181
2356
  if (!parts.mnemonic)
2182
2357
  continue;
@@ -2184,7 +2359,7 @@ function asm(source) {
2184
2359
  if (m === "EQU")
2185
2360
  continue;
2186
2361
  if (m === "ORG") {
2187
- pc = evalExpr(parts.operands[0], symbols);
2362
+ pc = evalExpr(parts.operands[0], symbols, pc, lastLabel);
2188
2363
  continue;
2189
2364
  }
2190
2365
  if (m === "SECTION")
@@ -2202,7 +2377,7 @@ function asm(source) {
2202
2377
  continue;
2203
2378
  }
2204
2379
  if (m === "DS") {
2205
- pc += countDs(parts.operands, symbols);
2380
+ pc += countDs(parts.operands, symbols, pc, lastLabel);
2206
2381
  continue;
2207
2382
  }
2208
2383
  pc += instrSize(m);
@@ -2210,29 +2385,34 @@ function asm(source) {
2210
2385
  } catch (e) {
2211
2386
  if (e instanceof AsmError)
2212
2387
  throw e;
2213
- throw new AsmError(e.message, idx + 1, line, firstNonSpaceCol(line));
2388
+ throw new AsmError(e.message, orig, line, firstNonSpaceCol(line));
2214
2389
  }
2215
2390
  }
2216
2391
  const sections = [];
2217
2392
  let current = null;
2218
2393
  const sectionNames = new Set;
2394
+ let lastLabel2 = "";
2219
2395
  let endedPass2 = false;
2220
- for (let idx = 0;idx < lines.length && !endedPass2; idx++) {
2221
- const line = lines[idx];
2396
+ for (let idx = 0;idx < pp.length && !endedPass2; idx++) {
2397
+ const { text: line, orig } = pp[idx];
2222
2398
  try {
2223
2399
  for (const stmt of splitStatements(line)) {
2224
2400
  const parts = parseLine(stmt);
2401
+ if (parts.label && !parts.label.startsWith("@") && !parts.label.startsWith(".") && !parts.isEqu) {
2402
+ lastLabel2 = parts.label;
2403
+ }
2225
2404
  if (parts.isEqu || !parts.mnemonic)
2226
2405
  continue;
2227
2406
  const m = parts.mnemonic.toUpperCase();
2228
2407
  if (m === "EQU")
2229
2408
  continue;
2409
+ const curPc = current ? current.start + current.data.length : 0;
2230
2410
  if (m === "ORG") {
2231
2411
  if (current && current.data.length) {
2232
2412
  current.end = current.start + current.data.length - 1;
2233
2413
  sections.push(current);
2234
2414
  }
2235
- const addr = evalExpr(parts.operands[0], symbols);
2415
+ const addr = evalExpr(parts.operands[0], symbols, curPc, lastLabel2);
2236
2416
  current = { start: addr, end: addr, data: [] };
2237
2417
  continue;
2238
2418
  }
@@ -2254,13 +2434,13 @@ function asm(source) {
2254
2434
  }
2255
2435
  if (!current)
2256
2436
  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);
2437
+ const bytes = m === "DB" ? dbBytes(parts.operands, symbols, curPc, lastLabel2) : m === "DW" ? dwBytes(parts.operands, symbols, curPc, lastLabel2) : m === "DS" ? dsBytes(parts.operands, symbols, curPc, lastLabel2) : encode(m, parts.operands, symbols, curPc, lastLabel2);
2258
2438
  current.data.push(...bytes);
2259
2439
  }
2260
2440
  } catch (e) {
2261
2441
  if (e instanceof AsmError)
2262
2442
  throw e;
2263
- throw new AsmError(e.message, idx + 1, line, firstNonSpaceCol(line));
2443
+ throw new AsmError(e.message, orig, line, firstNonSpaceCol(line));
2264
2444
  }
2265
2445
  }
2266
2446
  if (current && current.data.length) {
@@ -2277,7 +2457,7 @@ import { readFile, writeFile } from "fs/promises";
2277
2457
  // packages/rk86/package.json
2278
2458
  var package_default = {
2279
2459
  name: "rk86",
2280
- version: "2.0.16",
2460
+ version: "2.0.19",
2281
2461
  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",
2282
2462
  bin: {
2283
2463
  rk86: "rk86.js"
@@ -3707,16 +3887,19 @@ class Runner {
3707
3887
  init_sound(enabled) {
3708
3888
  if (enabled && this.sound == null && this.sound_factory) {
3709
3889
  this.sound = this.sound_factory();
3710
- console.log("\u0437\u0432\u0443\u043A \u0432\u043A\u043B\u044E\u0447\u0435\u043D");
3890
+ this.machine.log("\u0437\u0432\u0443\u043A \u0432\u043A\u043B\u044E\u0447\u0435\u043D");
3711
3891
  } else if (!enabled) {
3712
3892
  this.sound = null;
3713
- console.log("\u0437\u0432\u0443\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D");
3893
+ this.machine.log("\u0437\u0432\u0443\u043A \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D");
3714
3894
  }
3715
3895
  }
3716
3896
  execute(options = {}) {
3717
- const { terminate_address, on_terminate, exit_on_halt, armed } = options;
3897
+ const { terminate_address, on_terminate, exit_on_halt, on_batch_complete, turbo } = options;
3718
3898
  clearTimeout(this.execute_timer);
3719
- if (!this.paused) {
3899
+ const bursts = turbo ? 100 : 1;
3900
+ for (let burst = 0;burst < bursts; burst++) {
3901
+ if (this.paused)
3902
+ break;
3720
3903
  let batch_ticks = 0;
3721
3904
  let batch_instructions = 0;
3722
3905
  while (batch_ticks < this.TICK_PER_MS) {
@@ -3742,8 +3925,6 @@ class Runner {
3742
3925
  this.machine.ui.on_visualizer_hit(this.machine.memory.read_raw(this.machine.cpu.pc));
3743
3926
  }
3744
3927
  batch_instructions += 1;
3745
- if (armed?.value === false)
3746
- continue;
3747
3928
  if (terminate_address !== undefined && this.machine.cpu.pc === terminate_address) {
3748
3929
  on_terminate?.();
3749
3930
  return;
@@ -3758,8 +3939,10 @@ class Runner {
3758
3939
  this.previous_batch_time = now;
3759
3940
  this.instructions_per_millisecond = batch_instructions / elapsed;
3760
3941
  this.ticks_per_millisecond = batch_ticks / elapsed;
3942
+ this.machine.screen.tick_cursor(this.total_ticks, this.FREQ * (this.machine.screen.cursor_rate / 1000));
3943
+ on_batch_complete?.();
3761
3944
  }
3762
- this.execute_timer = setTimeout(() => this.execute(options), 10);
3945
+ this.execute_timer = setTimeout(() => this.execute(options), turbo ? 0 : 10);
3763
3946
  }
3764
3947
  pause() {
3765
3948
  this.paused = true;
@@ -3845,9 +4028,15 @@ class Screen {
3845
4028
  start(renderer) {
3846
4029
  this.renderer = renderer;
3847
4030
  this.renderer.connect(this.machine);
3848
- this.flip_cursor();
3849
4031
  this.render_loop();
3850
4032
  }
4033
+ last_flip_ticks = 0;
4034
+ tick_cursor(total_ticks, ticks_per_flip) {
4035
+ while (total_ticks - this.last_flip_ticks >= ticks_per_flip) {
4036
+ this.cursor_state = !this.cursor_state;
4037
+ this.last_flip_ticks += ticks_per_flip;
4038
+ }
4039
+ }
3851
4040
  render_loop() {
3852
4041
  if (this.ready)
3853
4042
  this.renderer.update();
@@ -3862,7 +4051,7 @@ class Screen {
3862
4051
  this.machine.ui.update_screen_geometry(this.width, this.height);
3863
4052
  if (this.last_width === this.width && this.last_height === this.height)
3864
4053
  return;
3865
- console.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0440\u0430\u0437\u043C\u0435\u0440 \u044D\u043A\u0440\u0430\u043D\u0430: ${width} x ${height}`);
4054
+ this.machine.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u0440\u0430\u0437\u043C\u0435\u0440 \u044D\u043A\u0440\u0430\u043D\u0430: ${width} x ${height}`);
3866
4055
  this.last_width = this.width;
3867
4056
  this.last_height = this.height;
3868
4057
  if (this.last_video_memory_base !== -1)
@@ -3874,7 +4063,7 @@ class Screen {
3874
4063
  this.machine.ui.update_video_memory_address(this.video_memory_base);
3875
4064
  if (this.last_video_memory_base === this.video_memory_base)
3876
4065
  return;
3877
- console.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0432\u0438\u0434\u0435\u043E\u043F\u0430\u043C\u044F\u0442\u044C \u0441 \u0430\u0434\u0440\u0435\u0441\u0430`, `${hex16(this.video_memory_base)}`, `\u0440\u0430\u0437\u043C\u0435\u0440\u043E\u043C ${hex16(this.video_memory_size)}`);
4066
+ this.machine.log(`\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0432\u0438\u0434\u0435\u043E\u043F\u0430\u043C\u044F\u0442\u044C \u0441 \u0430\u0434\u0440\u0435\u0441\u0430`, `${hex16(this.video_memory_base)}`, `\u0440\u0430\u0437\u043C\u0435\u0440\u043E\u043C ${hex16(this.video_memory_size)}`);
3878
4067
  this.last_video_memory_base = this.video_memory_base;
3879
4068
  if (this.last_width !== -1)
3880
4069
  this.ready = true;
@@ -3883,13 +4072,28 @@ class Screen {
3883
4072
  this.cursor_x = x;
3884
4073
  this.cursor_y = y;
3885
4074
  }
3886
- flip_cursor() {
3887
- this.cursor_state = !this.cursor_state;
3888
- setTimeout(() => this.flip_cursor(), this.cursor_rate);
3889
- }
3890
4075
  }
3891
4076
 
3892
4077
  // src/lib/core/rk86_snapshot.ts
4078
+ function rk86_snapshot(machine, version) {
4079
+ const { screen, cpu, keyboard, memory } = machine;
4080
+ const h16 = (n) => "0x" + hex16(n);
4081
+ const snapshot = {
4082
+ id: "rk86",
4083
+ created: new Date().toISOString(),
4084
+ format: "1",
4085
+ emulator: "rk86.ru",
4086
+ version,
4087
+ start: h16(0),
4088
+ end: h16(65535),
4089
+ boot: { keyboard: [] },
4090
+ cpu: cpu.export(),
4091
+ keyboard: keyboard.export(),
4092
+ screen: screen.export(),
4093
+ memory: memory.export()
4094
+ };
4095
+ return JSON.stringify(snapshot, null, 4);
4096
+ }
3893
4097
  function rk86_snapshot_restore(snapshot, machine, keys_injector) {
3894
4098
  try {
3895
4099
  const json = typeof snapshot === "string" ? JSON.parse(snapshot) : snapshot;
@@ -4156,7 +4360,6 @@ class TerminalUI {
4156
4360
  terminal = { put: () => {}, history: [] };
4157
4361
  i8080disasm;
4158
4362
  visualizer;
4159
- toggle_assembler;
4160
4363
  on_visualizer_hit;
4161
4364
  on_pause_changed;
4162
4365
  refreshDebugger;
@@ -4344,15 +4547,19 @@ function printHelp() {
4344
4547
  -m <\u0444\u0430\u0439\u043B> \u043C\u043E\u043D\u0438\u0442\u043E\u0440 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E: \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 mon32.bin)
4345
4548
  -p \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u0444\u0430\u0439\u043B \u0431\u0435\u0437 \u0437\u0430\u043F\u0443\u0441\u043A\u0430
4346
4549
  -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)
4550
+ -G <\u0430\u0434\u0440\u0435\u0441> \u0437\u0430\u043F\u0443\u0441\u043A \u0447\u0435\u0440\u0435\u0437 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 G \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0430 (\u0438\u043D\u044A\u0435\u043A\u0446\u0438\u044F \u043A\u043B\u0430\u0432\u0438\u0448)
4347
4551
  --exit-halt \u0432\u044B\u0445\u043E\u0434 \u043F\u0440\u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0438 HLT
4348
4552
  --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
4553
  --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)
4554
+ --turbo \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u0431\u0435\u0437 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u044F \u0441\u043A\u043E\u0440\u043E\u0441\u0442\u0438 (\u0434\u043B\u044F \u0430\u0432\u0442\u043E\u0442\u0435\u0441\u0442\u043E\u0432)
4350
4555
  --timeout <\u0441\u0435\u043A> \u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443
4351
4556
  --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
4557
  --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
4558
  --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
4559
  --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
4560
+ --snapshot <\u0444\u0430\u0439\u043B> \u0441\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0441\u043D\u0438\u043C\u043E\u043A \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F (JSON) \u043F\u0440\u0438 \u0432\u044B\u0445\u043E\u0434\u0435
4355
4561
  --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,...
4562
+ \u0442\u043E\u043A\u0435\u043D *N \u0437\u0430\u0434\u0430\u0451\u0442 \u043F\u0430\u0443\u0437\u0443 N \u043C\u0441 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 *200)
4356
4563
 
4357
4564
  \u041F\u0440\u0438\u043C\u0435\u0440\u044B:
4358
4565
  bunx rk86 \u0437\u0430\u043F\u0443\u0441\u043A \u043C\u043E\u043D\u0438\u0442\u043E\u0440\u0430
@@ -4424,6 +4631,7 @@ async function main() {
4424
4631
  const exitAddr = exitAddrValue !== undefined;
4425
4632
  const monitorFile_ = arg(args, "-m");
4426
4633
  const headless = flag(args, "--headless");
4634
+ const turbo = flag(args, "--turbo");
4427
4635
  const timeoutSec = arg(args, "--timeout", undefined, /^\d+(\.\d+)?$/, parseFloat);
4428
4636
  const memoryFile = arg(args, "--memory");
4429
4637
  const addrRe = /^(0x)?[0-9a-fA-F]+$/i;
@@ -4431,14 +4639,23 @@ async function main() {
4431
4639
  const memoryFrom = arg(args, "--memory-from", undefined, addrRe, parseAddr) ?? 0;
4432
4640
  const memoryTo = arg(args, "--memory-to", undefined, addrRe, parseAddr) ?? 65535;
4433
4641
  const screenFile = arg(args, "--screen");
4434
- const inputSeq = arg(args, "--input");
4642
+ const snapshotFile = arg(args, "--snapshot");
4643
+ const goViaMonitor = arg(args, "-G", undefined, addrRe, parseAddr);
4644
+ let inputSeq = arg(args, "--input");
4645
+ if (goViaMonitor !== undefined) {
4646
+ const hex2 = goViaMonitor.toString(16).toUpperCase();
4647
+ const keys = [...hex2].map((c) => c >= "0" && c <= "9" ? `Digit${c}` : `Key${c}`);
4648
+ const gSeq = ["KeyG", ...keys, "Enter"].join(",");
4649
+ inputSeq = inputSeq ? `${inputSeq},${gSeq}` : gSeq;
4650
+ }
4435
4651
  const programFile = args[0];
4436
4652
  const keyboard = new Keyboard;
4437
4653
  const io = new IO;
4438
4654
  const machineBuilder = {
4439
4655
  font: rk86_font_image(),
4440
4656
  keyboard,
4441
- io
4657
+ io,
4658
+ log: (...args2) => console.log(...args2)
4442
4659
  };
4443
4660
  const machine = machineBuilder;
4444
4661
  machine.ui = new TerminalUI;
@@ -4511,6 +4728,8 @@ async function main() {
4511
4728
  await writeFile(screenFile, dumpScreen(machine));
4512
4729
  if (memoryFile)
4513
4730
  await writeFile(memoryFile, new Uint8Array(machine.memory.buf.slice(memoryFrom, memoryTo + 1)));
4731
+ if (snapshotFile)
4732
+ await writeFile(snapshotFile, rk86_snapshot(machine, package_default.version));
4514
4733
  if (!headless)
4515
4734
  process.stdout.write("\x1B[?25h");
4516
4735
  if (message !== null && !headless) {
@@ -4524,39 +4743,49 @@ async function main() {
4524
4743
  renderer.update();
4525
4744
  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);
4526
4745
  } : undefined;
4527
- const armed = { value: entryPoint === undefined };
4528
- machine.runner.execute({
4529
- terminate_address: exitAddr ? exitAddrValue : undefined,
4530
- exit_on_halt: exitOnHalt,
4531
- on_terminate: onTerminate,
4532
- armed
4533
- });
4534
4746
  const armDelayMs = 500;
4535
4747
  if (entryPoint !== undefined && !loadOnly) {
4536
4748
  setTimeout(() => {
4537
4749
  machine.cpu.jump(entryPoint);
4538
- armed.value = true;
4539
4750
  }, armDelayMs);
4540
4751
  }
4752
+ const tickEvents = [];
4541
4753
  if (inputSeq) {
4542
4754
  const keys = inputSeq.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4755
+ const TICKS_PER_MS = machine.runner.FREQ / 1000;
4543
4756
  const settleMs = armDelayMs + 1000;
4544
4757
  const keyDownMs = 50;
4545
4758
  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);
4759
+ let t = settleMs * TICKS_PER_MS;
4760
+ for (const token of keys) {
4761
+ if (token.startsWith("*")) {
4762
+ const delayMs = parseInt(token.slice(1), 10);
4763
+ if (!Number.isFinite(delayMs) || delayMs < 0) {
4764
+ console.error(`\u043D\u0435\u0432\u0435\u0440\u043D\u0430\u044F \u0437\u0430\u0434\u0435\u0440\u0436\u043A\u0430 \u0432 --input: ${token}`);
4765
+ process.exit(1);
4766
+ }
4767
+ t += delayMs * TICKS_PER_MS;
4768
+ continue;
4769
+ }
4770
+ const code = token;
4771
+ tickEvents.push({ at_ticks: t, action: () => keyboard.onkeydown(code) });
4772
+ t += keyDownMs * TICKS_PER_MS;
4773
+ tickEvents.push({ at_ticks: t, action: () => keyboard.onkeyup(code) });
4774
+ t += keyGapMs * TICKS_PER_MS;
4775
+ }
4559
4776
  }
4777
+ machine.runner.execute({
4778
+ terminate_address: exitAddr ? exitAddrValue : undefined,
4779
+ exit_on_halt: exitOnHalt,
4780
+ on_terminate: onTerminate,
4781
+ turbo,
4782
+ on_batch_complete: () => {
4783
+ const now = machine.runner.total_ticks;
4784
+ while (tickEvents.length > 0 && tickEvents[0].at_ticks <= now) {
4785
+ tickEvents.shift().action();
4786
+ }
4787
+ }
4788
+ });
4560
4789
  if (timeoutSec !== undefined) {
4561
4790
  setTimeout(() => doExit(`\u0432\u044B\u0445\u043E\u0434 \u043F\u043E \u0442\u0430\u0439\u043C\u0430\u0443\u0442\u0443 ${timeoutSec}\u0441`), timeoutSec * 1000);
4562
4791
  }