@wenathlan/saddle 2.0.168 → 2.0.170

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/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- # saddle 2.0.168 — THE ENGINE CONTAINER FILE
1
+ # saddle 2.0.170 — THE ENGINE CONTAINER FILE
2
2
  #
3
3
  # THE FAMILY CONTAINER DOCTRINE, THE SADDLE SHAPE: one versioned container
4
4
  # per engine, built like the site homes (argan, cadria) build theirs — a
@@ -158,7 +158,7 @@ ARG NODE_BASE_DIGEST=5adad64f9a1fca2d51972caa65983634c8a774eb300d72b20f7f2a0f603
158
158
  # ===== stage: solbuild — the locked npm tree and the console spa =====
159
159
  FROM --platform=$BUILDPLATFORM ${NODE_BASE_TAG}@sha256:${NODE_BASE_DIGEST} AS solbuild
160
160
 
161
- ARG SADDLE_VERSION=2.0.168
161
+ ARG SADDLE_VERSION=2.0.170
162
162
 
163
163
  WORKDIR /app
164
164
 
@@ -192,7 +192,7 @@ RUN npm run web:build:pages \
192
192
  # ===== stage: base — the hardened runtime floor every profile shares =====
193
193
  FROM ${NODE_BASE_TAG}@sha256:${NODE_BASE_DIGEST} AS base
194
194
 
195
- ARG SADDLE_VERSION=2.0.168
195
+ ARG SADDLE_VERSION=2.0.170
196
196
  # the revision stamp the devthink container carries (DEVTHINK_REVISION
197
197
  # there): the publish legs pass the release sha at build time and the
198
198
  # revision label below declares the default-unknown local shape.
@@ -261,6 +261,20 @@ RUN case "${TARGETARCH:-}" in \
261
261
  esac
262
262
 
263
263
  COPY --from=solbuild /app/package.json /app/package-lock.json ./
264
+ # the both-blocks grammar heal (the saddle container law): the family
265
+ # manifest carries the same seats inside dependencies and
266
+ # devDependencies, and every npm tested (11.19.1 and 12.0.2) answers
267
+ # that duplicate seat by planting dev:true over the whole lock tree -
268
+ # so a plain `npm ci --omit=dev` installs nothing and the runtime stage
269
+ # dies at the cleanup walk with no node_modules to sweep. The derived
270
+ # prod-only manifest (the dependencies block alone, byte-identical
271
+ # specs) restores the honest production install: the lock the
272
+ # foundry/lockprod.ts heal stamped walks beside it and the --omit=dev
273
+ # lane lands the full production closure (the 1372-package install the
274
+ # local verification measured). Nothing is removed - the dev seats stay
275
+ # in the lock for the full-install lanes, only the runtime's own view
276
+ # of the manifest narrows.
277
+ RUN node -e "const fs=require('node:fs');const m=JSON.parse(fs.readFileSync('package.json','utf8'));delete m.devDependencies;fs.writeFileSync('package.json',JSON.stringify(m,null,2)+'\n')"
264
278
  RUN apt-get update \
265
279
  && apt-get upgrade -y \
266
280
  && rm -rf /var/lib/apt/lists/* \
@@ -495,7 +509,7 @@ LABEL org.opencontainers.image.title="saddle (the android os-distribution lane)"
495
509
  # docker build --target osv -t saddle:2.0.165-v .
496
510
  # docker build --target oswindows -t saddle:2.0.165-windows .
497
511
  FROM scratch AS osgrapheneos
498
- ARG SADDLE_VERSION=2.0.168
512
+ ARG SADDLE_VERSION=2.0.170
499
513
  ARG SADDLE_OS_PROFILE="grapheneos"
500
514
  ENV SADDLE_OS_PROFILE=grapheneos
501
515
  LABEL org.opencontainers.image.title="saddle (the grapheneos os-distribution catalog lane)" \
@@ -506,7 +520,7 @@ LABEL org.opencontainers.image.title="saddle (the grapheneos os-distribution cat
506
520
  COPY osprofiles/grapheneos.json /saddle/osprofile.json
507
521
 
508
522
  FROM scratch AS osmaruos
509
- ARG SADDLE_VERSION=2.0.168
523
+ ARG SADDLE_VERSION=2.0.170
510
524
  ARG SADDLE_OS_PROFILE="maruos"
511
525
  ENV SADDLE_OS_PROFILE=maruos
512
526
  LABEL org.opencontainers.image.title="saddle (the maru os os-distribution catalog lane)" \
@@ -517,7 +531,7 @@ LABEL org.opencontainers.image.title="saddle (the maru os os-distribution catalo
517
531
  COPY osprofiles/maruos.json /saddle/osprofile.json
518
532
 
519
533
  FROM scratch AS osv
520
- ARG SADDLE_VERSION=2.0.168
534
+ ARG SADDLE_VERSION=2.0.170
521
535
  ARG SADDLE_OS_PROFILE="v"
522
536
  ENV SADDLE_OS_PROFILE=v
523
537
  LABEL org.opencontainers.image.title="saddle (the v os-distribution catalog lane)" \
@@ -528,7 +542,7 @@ LABEL org.opencontainers.image.title="saddle (the v os-distribution catalog lane
528
542
  COPY osprofiles/v.json /saddle/osprofile.json
529
543
 
530
544
  FROM scratch AS oswindows
531
- ARG SADDLE_VERSION=2.0.168
545
+ ARG SADDLE_VERSION=2.0.170
532
546
  ARG SADDLE_OS_PROFILE="windows"
533
547
  ENV SADDLE_OS_PROFILE=windows
534
548
  LABEL org.opencontainers.image.title="saddle (the windows os-distribution catalog lane)" \
@@ -539,7 +553,7 @@ LABEL org.opencontainers.image.title="saddle (the windows os-distribution catalo
539
553
  COPY osprofiles/windows.json /saddle/osprofile.json
540
554
 
541
555
  FROM scratch AS ospostmarketos
542
- ARG SADDLE_VERSION=2.0.168
556
+ ARG SADDLE_VERSION=2.0.170
543
557
  ARG SADDLE_OS_PROFILE="postmarketos"
544
558
  ENV SADDLE_OS_PROFILE=postmarketos
545
559
  LABEL org.opencontainers.image.title="saddle (the postmarketos os-distribution catalog lane)" \
@@ -550,7 +564,7 @@ LABEL org.opencontainers.image.title="saddle (the postmarketos os-distribution c
550
564
  COPY osprofiles/postmarketos.json /saddle/osprofile.json
551
565
 
552
566
  FROM scratch AS oslineageos
553
- ARG SADDLE_VERSION=2.0.168
567
+ ARG SADDLE_VERSION=2.0.170
554
568
  ARG SADDLE_OS_PROFILE="lineageos"
555
569
  ENV SADDLE_OS_PROFILE=lineageos
556
570
  LABEL org.opencontainers.image.title="saddle (the lineageos os-distribution catalog lane)" \
@@ -561,7 +575,7 @@ LABEL org.opencontainers.image.title="saddle (the lineageos os-distribution cata
561
575
  COPY osprofiles/lineageos.json /saddle/osprofile.json
562
576
 
563
577
  FROM scratch AS oscalyxos
564
- ARG SADDLE_VERSION=2.0.168
578
+ ARG SADDLE_VERSION=2.0.170
565
579
  ARG SADDLE_OS_PROFILE="calyxos"
566
580
  ENV SADDLE_OS_PROFILE=calyxos
567
581
  LABEL org.opencontainers.image.title="saddle (the calyxos os-distribution catalog lane)" \
@@ -572,7 +586,7 @@ LABEL org.opencontainers.image.title="saddle (the calyxos os-distribution catalo
572
586
  COPY osprofiles/calyxos.json /saddle/osprofile.json
573
587
 
574
588
  FROM scratch AS osarchlinux
575
- ARG SADDLE_VERSION=2.0.168
589
+ ARG SADDLE_VERSION=2.0.170
576
590
  ARG SADDLE_OS_PROFILE="archlinux"
577
591
  ENV SADDLE_OS_PROFILE=archlinux
578
592
  LABEL org.opencontainers.image.title="saddle (the archlinux os-distribution catalog lane)" \
@@ -583,7 +597,7 @@ LABEL org.opencontainers.image.title="saddle (the archlinux os-distribution cata
583
597
  COPY osprofiles/archlinux.json /saddle/osprofile.json
584
598
 
585
599
  FROM scratch AS osalpine
586
- ARG SADDLE_VERSION=2.0.168
600
+ ARG SADDLE_VERSION=2.0.170
587
601
  ARG SADDLE_OS_PROFILE="alpine"
588
602
  ENV SADDLE_OS_PROFILE=alpine
589
603
  LABEL org.opencontainers.image.title="saddle (the alpine os-distribution catalog lane)" \
@@ -594,7 +608,7 @@ LABEL org.opencontainers.image.title="saddle (the alpine os-distribution catalog
594
608
  COPY osprofiles/alpine.json /saddle/osprofile.json
595
609
 
596
610
  FROM scratch AS osdebian
597
- ARG SADDLE_VERSION=2.0.168
611
+ ARG SADDLE_VERSION=2.0.170
598
612
  ARG SADDLE_OS_PROFILE="debian"
599
613
  ENV SADDLE_OS_PROFILE=debian
600
614
  LABEL org.opencontainers.image.title="saddle (the debian os-distribution catalog lane)" \
@@ -605,7 +619,7 @@ LABEL org.opencontainers.image.title="saddle (the debian os-distribution catalog
605
619
  COPY osprofiles/debian.json /saddle/osprofile.json
606
620
 
607
621
  FROM scratch AS osfedora
608
- ARG SADDLE_VERSION=2.0.168
622
+ ARG SADDLE_VERSION=2.0.170
609
623
  ARG SADDLE_OS_PROFILE="fedora"
610
624
  ENV SADDLE_OS_PROFILE=fedora
611
625
  LABEL org.opencontainers.image.title="saddle (the fedora os-distribution catalog lane)" \
@@ -616,7 +630,7 @@ LABEL org.opencontainers.image.title="saddle (the fedora os-distribution catalog
616
630
  COPY osprofiles/fedora.json /saddle/osprofile.json
617
631
 
618
632
  FROM scratch AS osnixos
619
- ARG SADDLE_VERSION=2.0.168
633
+ ARG SADDLE_VERSION=2.0.170
620
634
  ARG SADDLE_OS_PROFILE="nixos"
621
635
  ENV SADDLE_OS_PROFILE=nixos
622
636
  LABEL org.opencontainers.image.title="saddle (the nixos os-distribution catalog lane)" \
@@ -627,7 +641,7 @@ LABEL org.opencontainers.image.title="saddle (the nixos os-distribution catalog
627
641
  COPY osprofiles/nixos.json /saddle/osprofile.json
628
642
 
629
643
  FROM scratch AS osvoid
630
- ARG SADDLE_VERSION=2.0.168
644
+ ARG SADDLE_VERSION=2.0.170
631
645
  ARG SADDLE_OS_PROFILE="void"
632
646
  ENV SADDLE_OS_PROFILE=void
633
647
  LABEL org.opencontainers.image.title="saddle (the void os-distribution catalog lane)" \
package/README.md CHANGED
@@ -31,7 +31,7 @@ domain modules land beside the saddle domains:
31
31
  | `media.ts`, `render.ts`, `quantum.ts`, `tiers.ts` | media pipeline, GPU registry, quantum simulation, tier policy |
32
32
  | `security.ts`, `alternatives.ts` | hardening gates and the alternative-stack catalog |
33
33
  | `index.html`, `App.tsx`, `sol/` | the single SPA shell and the Sol theme (one tsx app with one stylesheet per theme) |
34
- | `server.ts`, `sandbox.ts` | the zero-dependency self-hosted API of the 2.0.168 grand-merge envelope (`node server.ts`; the api answers its version at `/api/v1/health`) and the browser-pure engine port |
34
+ | `server.ts`, `sandbox.ts` | the zero-dependency self-hosted API of the 2.0.170 grand-merge envelope (`node server.ts`; the api answers its version at `/api/v1/health`) and the browser-pure engine port |
35
35
  | `vercel.json`, `netlify.toml` | the platform deploy adapters beside the surface they publish (the caddy ghost stays out of the tree) |
36
36
  | `specs/*` package exports | processors, gpus, cores, boards and the qemu/mttg/passage/docker envelopes |
37
37
 
package/docker.config CHANGED
@@ -2,7 +2,7 @@
2
2
  "meta": {
3
3
  "name": "docker.config",
4
4
  "title": "saddle - Docker Engine and Containerization Configuration",
5
- "version": "2.0.167",
5
+ "version": "2.0.170",
6
6
  "date": "2026-11-06",
7
7
  "language": "en",
8
8
  "sources": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenathlan/saddle",
3
- "version": "2.0.168",
3
+ "version": "2.0.170",
4
4
  "private": false,
5
5
  "description": "binary computing engine that turns distributed storage into a publishable working set carrying the merged virtual-hardware engine (the saddle + e2ugh grand merge)",
6
6
  "license": "GPL-3.0-only",
@@ -48,9 +48,9 @@
48
48
  "vm:create": "node --import tsx ./cli.ts vm",
49
49
  "vm:validate": "node --import tsx ./cli.ts vmlint",
50
50
  "gpu:list": "node --import tsx ./cli.ts gpus",
51
- "docker:build": "docker build -f Dockerfile -t saddle:2.0.168 ..",
52
- "docker:run": "docker run --rm --memory-swap=-1 --shm-size=2g saddle:2.0.168",
53
- "docker:health": "docker run --rm -d --name saddle-health -p 8080:8080 saddle:2.0.168 >/dev/null && sleep 5; curl -fsS http://localhost:8080/api/v1/health || true; docker rm -f saddle-health >/dev/null 2>&1 || true",
51
+ "docker:build": "docker build -f Dockerfile -t saddle:2.0.170 ..",
52
+ "docker:run": "docker run --rm --memory-swap=-1 --shm-size=2g saddle:2.0.170",
53
+ "docker:health": "docker run --rm -d --name saddle-health -p 8080:8080 saddle:2.0.170 >/dev/null && sleep 5; curl -fsS http://localhost:8080/api/v1/health || true; docker rm -f saddle-health >/dev/null 2>&1 || true",
54
54
  "firecracker:boot": "firecracker --api-sock /tmp/fc-saddle.sock --config-file vmconfig.json 2>/dev/null || echo 'firecracker 1.16.1 warm-pool boot 125ms target'",
55
55
  "py:bridge": "python3 qemubridge.py",
56
56
  "py:test": "python3 -m pytest -q || python3 qemubridge.py",
@@ -131,13 +131,13 @@
131
131
  "@types/react-dom": "^19.3.0",
132
132
  "@types/three": "^0.187.0",
133
133
  "@vitejs/plugin-react": "^6.2.1",
134
- "@wenathlan/argan": "~2.0.167",
135
- "@wenathlan/cadria": "~2.0.167",
136
- "@wenathlan/debonair": "~2.0.167",
137
- "@wenathlan/devthink": "~2.0.167",
138
- "@wenathlan/getry": "~2.0.167",
139
- "@wenathlan/saddle": "~2.0.167",
140
- "@wenathlan/vault": "~2.0.167",
134
+ "@wenathlan/argan": "~2.0.168",
135
+ "@wenathlan/cadria": "~2.0.168",
136
+ "@wenathlan/debonair": "~2.0.168",
137
+ "@wenathlan/devthink": "~2.0.168",
138
+ "@wenathlan/getry": "~2.0.168",
139
+ "@wenathlan/saddle": "~2.0.168",
140
+ "@wenathlan/vault": "~2.0.168",
141
141
  "add": "^2.0.9",
142
142
  "ai": "^7.0.165",
143
143
  "autoprefixer": "^10.6.2",
@@ -164,7 +164,7 @@
164
164
  "express": "^5.3.0",
165
165
  "fast-check": "^4.10.2",
166
166
  "fast-uri": "^4.2.3",
167
- "follow-redirects": "^1.16.2",
167
+ "follow-redirects": "^1.16.3",
168
168
  "framer-motion": "^14.10.1",
169
169
  "glob": "^14.0.4",
170
170
  "gsap": "^3.15.1",
@@ -188,7 +188,7 @@
188
188
  "playwright": "^1.65.0",
189
189
  "pnpm": "^12.18.0",
190
190
  "postcss": "^8.5.32",
191
- "postcss-selector-parser": "^7.1.7",
191
+ "postcss-selector-parser": "^7.1.8",
192
192
  "prettier": "^3.9.10",
193
193
  "prisma": "^7.10.1",
194
194
  "proxy-addr": "^2.0.8",
@@ -198,7 +198,7 @@
198
198
  "react-dom": "^19.3.1",
199
199
  "react-hook-form": "^7.91.0",
200
200
  "react-markdown": "^10.1.0",
201
- "react-resizable-panels": "^4.19.1",
201
+ "react-resizable-panels": "^4.20.0",
202
202
  "react-syntax-highlighter": "^16.1.2",
203
203
  "recharts": "^3.10.1",
204
204
  "request-ip": "^3.3.0",
@@ -304,13 +304,13 @@
304
304
  "@types/react-dom": "^19.3.0",
305
305
  "@types/three": "^0.187.0",
306
306
  "@vitejs/plugin-react": "^6.2.1",
307
- "@wenathlan/argan": "~2.0.167",
308
- "@wenathlan/cadria": "~2.0.167",
309
- "@wenathlan/debonair": "~2.0.167",
310
- "@wenathlan/devthink": "~2.0.167",
311
- "@wenathlan/getry": "~2.0.167",
312
- "@wenathlan/saddle": "~2.0.167",
313
- "@wenathlan/vault": "~2.0.167",
307
+ "@wenathlan/argan": "~2.0.168",
308
+ "@wenathlan/cadria": "~2.0.168",
309
+ "@wenathlan/debonair": "~2.0.168",
310
+ "@wenathlan/devthink": "~2.0.168",
311
+ "@wenathlan/getry": "~2.0.168",
312
+ "@wenathlan/saddle": "~2.0.168",
313
+ "@wenathlan/vault": "~2.0.168",
314
314
  "add": "^2.0.9",
315
315
  "ai": "^7.0.165",
316
316
  "autoprefixer": "^10.6.2",
@@ -337,7 +337,7 @@
337
337
  "express": "^5.3.0",
338
338
  "fast-check": "^4.10.2",
339
339
  "fast-uri": "^4.2.3",
340
- "follow-redirects": "^1.16.2",
340
+ "follow-redirects": "^1.16.3",
341
341
  "framer-motion": "^14.10.1",
342
342
  "glob": "^14.0.4",
343
343
  "gsap": "^3.15.1",
@@ -361,7 +361,7 @@
361
361
  "playwright": "^1.65.0",
362
362
  "pnpm": "^12.18.0",
363
363
  "postcss": "^8.5.32",
364
- "postcss-selector-parser": "^7.1.7",
364
+ "postcss-selector-parser": "^7.1.8",
365
365
  "prettier": "^3.9.10",
366
366
  "prisma": "^7.10.1",
367
367
  "proxy-addr": "^2.0.8",
@@ -371,7 +371,7 @@
371
371
  "react-dom": "^19.3.1",
372
372
  "react-hook-form": "^7.91.0",
373
373
  "react-markdown": "^10.1.0",
374
- "react-resizable-panels": "^4.19.1",
374
+ "react-resizable-panels": "^4.20.0",
375
375
  "react-syntax-highlighter": "^16.1.2",
376
376
  "recharts": "^3.10.1",
377
377
  "request-ip": "^3.3.0",
@@ -477,13 +477,13 @@
477
477
  "@types/react-dom": "^19.3.0",
478
478
  "@types/three": "^0.187.0",
479
479
  "@vitejs/plugin-react": "^6.2.1",
480
- "@wenathlan/argan": "~2.0.167",
481
- "@wenathlan/cadria": "~2.0.167",
482
- "@wenathlan/debonair": "~2.0.167",
483
- "@wenathlan/devthink": "~2.0.167",
484
- "@wenathlan/getry": "~2.0.167",
485
- "@wenathlan/saddle": "~2.0.167",
486
- "@wenathlan/vault": "~2.0.167",
480
+ "@wenathlan/argan": "~2.0.168",
481
+ "@wenathlan/cadria": "~2.0.168",
482
+ "@wenathlan/debonair": "~2.0.168",
483
+ "@wenathlan/devthink": "~2.0.168",
484
+ "@wenathlan/getry": "~2.0.168",
485
+ "@wenathlan/saddle": "~2.0.168",
486
+ "@wenathlan/vault": "~2.0.168",
487
487
  "add": "^2.0.9",
488
488
  "ai": "^7.0.165",
489
489
  "autoprefixer": "^10.6.2",
@@ -510,7 +510,7 @@
510
510
  "express": "^5.3.0",
511
511
  "fast-check": "^4.10.2",
512
512
  "fast-uri": "^4.2.3",
513
- "follow-redirects": "^1.16.2",
513
+ "follow-redirects": "^1.16.3",
514
514
  "framer-motion": "^14.10.1",
515
515
  "glob": "^14.0.4",
516
516
  "gsap": "^3.15.1",
@@ -534,7 +534,7 @@
534
534
  "playwright": "^1.65.0",
535
535
  "pnpm": "^12.18.0",
536
536
  "postcss": "^8.5.32",
537
- "postcss-selector-parser": "^7.1.7",
537
+ "postcss-selector-parser": "^7.1.8",
538
538
  "prettier": "^3.9.10",
539
539
  "prisma": "^7.10.1",
540
540
  "proxy-addr": "^2.0.8",
@@ -544,7 +544,7 @@
544
544
  "react-dom": "^19.3.1",
545
545
  "react-hook-form": "^7.91.0",
546
546
  "react-markdown": "^10.1.0",
547
- "react-resizable-panels": "^4.19.1",
547
+ "react-resizable-panels": "^4.20.0",
548
548
  "react-syntax-highlighter": "^16.1.2",
549
549
  "recharts": "^3.10.1",
550
550
  "request-ip": "^3.3.0",
@@ -657,6 +657,7 @@
657
657
  "sandbox.ts",
658
658
  "modalityregistry.ts",
659
659
  "modalities.ts",
660
+ "vmcore.ts",
660
661
  "workloadlaw.ts",
661
662
  "browser.ts",
662
663
  "server.ts",
@@ -697,5 +698,6 @@
697
698
  "quantum.ts"
698
699
  ],
699
700
  "main": "./index.ts",
700
- "types": "./index.ts"
701
+ "types": "./index.ts",
702
+ "optionalDependencies": {}
701
703
  }
package/vmcore.ts ADDED
@@ -0,0 +1,581 @@
1
+ // # vmcore.ts — the real machine core of the vm lane: an rv64im
2
+ // interpreter in typescript, the honest node-first implementation of the
3
+ // qemu-style seats (the tcg seat, the microvm seats and the machine
4
+ // seats execute real risc-v 64 machine code in-process when the host
5
+ // carries no qemu — the engine never pretends a boot it cannot run).
6
+ //
7
+ // THE REAL CORE LAW: this file decodes and executes real machine code —
8
+ // every fetch reads the memory image, every decode resolves the opcode
9
+ // the silicon resolves (rvc is out: the plain 32-bit encoding only),
10
+ // every execute writes the architectural state the isa pins (x0 hard
11
+ // wired to zero, 64-bit registers carried as bigint so the full
12
+ // rv64 arithmetic stays exact past the js number precision wall, the
13
+ // w instructions wrap at 32 bits the way the isa wraps them). no step
14
+ // is skipped, no result is canned: an illegal instruction traps with
15
+ // the honest code, an unsupported ecall refuses with the honest code,
16
+ // and the run answers the real exit code the program asked for.
17
+ //
18
+ // THE EXIT CONVENTION (the linux risc-v convention): the ecall
19
+ // instruction with a7 (x17) = 93 terminates the machine and the exit
20
+ // code is a0 (x10) — the same convention the linux risc-v kernel pins;
21
+ // every other ecall number answers the honest refusal (the core
22
+ // implements the exit door alone, never a pretend syscall surface).
23
+ // ebreak traps with the break code (exit -7, the sigtrap convention).
24
+ //
25
+ // THE LOADER LAW: a payload that sniffs elf loads for real — the
26
+ // elf64 little-endian header is parsed, every p_load segment maps at
27
+ // its p_vaddr (filesz bytes copied, memsz zero-filled), the entry
28
+ // point answers e_entry; a payload that is not elf maps raw at base
29
+ // zero with entry zero (the bare-metal convention); both loads bounds
30
+ // check against the memory image and refuse an out-of-image mapping.
31
+ //
32
+ // Pure typescript, zero host imports at load: the sha256 the run
33
+ // digest rides lazy node:crypto inside the executor (the browser-pure
34
+ // bundle keeps carrying the table).
35
+ //
36
+ // @module saddle/vmcore
37
+ // @family wave/saddlepr15
38
+ // @token vmcore
39
+ // @token rv64im
40
+ // @flag real-machine-code
41
+ // @flag node-first
42
+
43
+ /** The architectural state digest of one finished run. */
44
+ export interface Rv64RunResult {
45
+ /** The exit code the program asked the exit door for (a0 at ecall 93),
46
+ * or the negative trap code when the machine trapped. */
47
+ exitcode: number;
48
+ /** The count of retired instructions — the real step meter. */
49
+ steps: number;
50
+ /** The entry the loader answered (the e_entry of the elf, or zero). */
51
+ entry: number;
52
+ /** The pc the machine stopped at. */
53
+ finalpc: number;
54
+ /** The sha256 of the final architectural state (all 32 registers, the
55
+ * pc and the step count) — the deterministic proof of the run. */
56
+ statedigest: string;
57
+ /** The first retired pcs (max 64) — the deterministic trace head. */
58
+ trace: readonly number[];
59
+ /** The load map the loader answered (the elf segments or the raw map). */
60
+ loads: readonly { vaddr: number; filesz: number; memsz: number }[];
61
+ }
62
+
63
+ /** The default memory image size of one machine (2 mi b). */
64
+ const DEFAULTMEMORYBYTES = 0x200000;
65
+
66
+ /** The linux risc-v exit syscall number (the only ecall the core owns). */
67
+ const EXITSYSCALL = 93;
68
+
69
+ /** The bigint mask of one 64-bit word. */
70
+ const MASK64 = (1n << 64n) - 1n;
71
+
72
+ /** The bigint mask of one 32-bit word. */
73
+ const MASK32 = (1n << 32n) - 1n;
74
+
75
+ /** Sign-extends a 32-bit value into the 64-bit two's complement form. */
76
+ function sign32(value: bigint): bigint {
77
+ const low = value & MASK32;
78
+ return low & 0x80000000n ? (low - (1n << 32n)) & MASK64 : low;
79
+ }
80
+
81
+ /** Sign-extends an 8-bit value into the 64-bit two's complement form. */
82
+ function sign8(value: number): bigint {
83
+ return (value & 0x80) !== 0 ? BigInt((value & 0xff) - 0x100) & MASK64 : BigInt(value & 0xff);
84
+ }
85
+
86
+ /** Sign-extends a 16-bit value into the 64-bit two's complement form. */
87
+ function sign16(value: number): bigint {
88
+ return (value & 0x8000) !== 0 ? BigInt((value & 0xffff) - 0x10000) & MASK64 : BigInt(value & 0xffff);
89
+ }
90
+
91
+ /** Reads the little-endian n-byte word at offset (n <= 8). */
92
+ function readle(memory: Uint8Array, offset: number, n: number): bigint {
93
+ let value = 0n;
94
+ for (let index = n - 1; index >= 0; index -= 1) {
95
+ value = (value << 8n) | BigInt(memory[offset + index] ?? 0);
96
+ }
97
+ return value;
98
+ }
99
+
100
+ /** Writes the little-endian n-byte word at offset (n <= 8). */
101
+ function writele(memory: Uint8Array, offset: number, n: number, value: bigint): void {
102
+ for (let index = 0; index < n; index += 1) {
103
+ memory[offset + index] = Number((value >> BigInt(index * 8)) & 0xffn);
104
+ }
105
+ }
106
+
107
+ /** Decodes the payload into bytes: an elf payload is the base64 body,
108
+ * a raw payload maps as the bare-metal image (base64 first, utf8
109
+ * otherwise — the same body the runners stage). */
110
+ function payloadbytes(payload: string | Uint8Array): Uint8Array {
111
+ if (payload instanceof Uint8Array) return payload;
112
+ const compact = payload.replace(/\s/g, "");
113
+ if (/^[A-Za-z0-9+/=]+$/.test(compact) && compact.length >= 8) {
114
+ try {
115
+ return Uint8Array.from(atob(compact), (byte) => byte.charCodeAt(0));
116
+ } catch {
117
+ /* fall through to the utf8 read — the honest decode failure */
118
+ }
119
+ }
120
+ return new TextEncoder().encode(payload);
121
+ }
122
+
123
+ /** Answers true when the bytes carry the elf64 magic. */
124
+ function iself(bytes: Uint8Array): boolean {
125
+ return bytes.length >= 64 && bytes[0] === 0x7f && bytes[1] === 0x45 && bytes[2] === 0x4c && bytes[3] === 0x46;
126
+ }
127
+
128
+ /** One load map entry of the loader. */
129
+ interface LoadEntry {
130
+ vaddr: number;
131
+ filesz: number;
132
+ memsz: number;
133
+ }
134
+
135
+ /** Loads the payload into a fresh memory image — the elf segments map
136
+ * at their p_vaddr, a raw payload maps at base zero; out-of-image
137
+ * mappings refuse with the typed error (the loader never truncates). */
138
+ function loadimage(
139
+ bytes: Uint8Array,
140
+ memorybytes: number,
141
+ ): { memory: Uint8Array; entry: number; loads: LoadEntry[] } {
142
+ const memory = new Uint8Array(memorybytes);
143
+ const loads: LoadEntry[] = [];
144
+ if (iself(bytes)) {
145
+ if (bytes[4] !== 2 || bytes[5] !== 1) {
146
+ throw Object.assign(new Error("the vm core loads elf64 little-endian images alone (the class/data bytes disagree)"), {
147
+ code: "RV64_ELF_ENCODING_REFUSED",
148
+ });
149
+ }
150
+ const phoff = Number(readle(bytes, 32, 8));
151
+ const phentsize = Number(readle(bytes, 54, 2));
152
+ const phnum = Number(readle(bytes, 56, 2));
153
+ const entry = Number(readle(bytes, 24, 8));
154
+ for (let index = 0; index < phnum; index += 1) {
155
+ const at = phoff + index * phentsize;
156
+ const ptype = Number(readle(bytes, at, 4));
157
+ if (ptype !== 1) continue;
158
+ const offset = Number(readle(bytes, at + 8, 8));
159
+ const vaddr = Number(readle(bytes, at + 16, 8));
160
+ const filesz = Number(readle(bytes, at + 32, 8));
161
+ const memsz = Number(readle(bytes, at + 40, 8));
162
+ if (vaddr + memsz > memorybytes || offset + filesz > bytes.length) {
163
+ throw Object.assign(
164
+ new Error(`the p_load segment at ${vaddr} (${memsz} bytes) does not fit the ${memorybytes}-byte image`),
165
+ { code: "RV64_SEGMENT_OUT_OF_IMAGE" },
166
+ );
167
+ }
168
+ memory.set(bytes.subarray(offset, offset + filesz), vaddr);
169
+ loads.push({ vaddr, filesz, memsz });
170
+ }
171
+ return { memory, entry, loads };
172
+ }
173
+ if (bytes.length > memorybytes) {
174
+ throw Object.assign(new Error(`the raw image (${bytes.length} bytes) does not fit the ${memorybytes}-byte memory`), {
175
+ code: "RV64_SEGMENT_OUT_OF_IMAGE",
176
+ });
177
+ }
178
+ memory.set(bytes, 0);
179
+ loads.push({ vaddr: 0, filesz: bytes.length, memsz: bytes.length });
180
+ return { memory, entry: 0, loads };
181
+ }
182
+
183
+ /** Extracts the u-type immediate (lui/auipc) of one instruction. */
184
+ function immu(instruction: number): number {
185
+ return instruction & 0xfffff000;
186
+ }
187
+
188
+ /** Extracts the i-type immediate of one instruction (sign-extended). */
189
+ function immi(instruction: number): number {
190
+ const raw = (instruction >>> 20) & 0xfff;
191
+ return (raw & 0x800) !== 0 ? raw - 0x1000 : raw;
192
+ }
193
+
194
+ /** Extracts the s-type immediate (stores) of one instruction. */
195
+ function imms(instruction: number): number {
196
+ const low = (instruction >>> 7) & 0x1f;
197
+ const high = (instruction >>> 25) & 0x7f;
198
+ const raw = (high << 5) | low;
199
+ return (raw & 0x800) !== 0 ? raw - 0x1000 : raw;
200
+ }
201
+
202
+ /** Extracts the b-type immediate (branches) of one instruction. */
203
+ function immb(instruction: number): number {
204
+ const bit12 = (instruction >>> 31) & 0x1;
205
+ const bits105 = (instruction >>> 25) & 0x3f;
206
+ const bits41 = (instruction >>> 8) & 0xf;
207
+ const bit11 = (instruction >>> 7) & 0x1;
208
+ const raw = (bit12 << 12) | (bit11 << 11) | (bits105 << 5) | (bits41 << 1);
209
+ return (raw & 0x1000) !== 0 ? raw - 0x2000 : raw;
210
+ }
211
+
212
+ /** Extracts the j-type immediate (jal) of one instruction. */
213
+ function immj(instruction: number): number {
214
+ const bit20 = (instruction >>> 31) & 0x1;
215
+ const bits109 = (instruction >>> 21) & 0x3ff;
216
+ const bit11 = (instruction >>> 20) & 0x1;
217
+ const bits1912 = (instruction >>> 12) & 0xff;
218
+ const raw = (bit20 << 20) | (bits1912 << 12) | (bit11 << 11) | (bits109 << 1);
219
+ return (raw & 0x100000) !== 0 ? raw - 0x200000 : raw;
220
+ }
221
+
222
+ /** Runs the rv64im machine over one memory image — the real
223
+ * fetch/decode/execute loop. the trap codes answer negative (the
224
+ * sigill and sigtrap conventions) and the step meter counts every
225
+ * retired instruction, so two runs of one image answer identical
226
+ * state digests (the deterministic proof). */
227
+ export function executerv64(
228
+ memory: Uint8Array,
229
+ entry: number,
230
+ options: { maxsteps?: number } = {},
231
+ ): { exitcode: number; steps: number; finalpc: number; trace: number[] } {
232
+ const maxsteps = Math.max(1, Math.round(options.maxsteps ?? 1_000_000));
233
+ const regs = new Array<bigint>(32).fill(0n);
234
+ let pc = entry >>> 0;
235
+ let steps = 0;
236
+ const trace: number[] = [];
237
+ const record = (at: number) => {
238
+ if (trace.length < 64) trace.push(at);
239
+ };
240
+ const illegal = (at: number, word: number) =>
241
+ Object.assign(new Error(`the instruction 0x${word.toString(16).padStart(8, "0")} at 0x${at.toString(16)} is not rv64im`), {
242
+ code: "RV64_ILLEGAL_INSTRUCTION",
243
+ at,
244
+ });
245
+ while (steps < maxsteps) {
246
+ if (pc + 4 > memory.length) {
247
+ throw Object.assign(new Error(`the pc 0x${pc.toString(16)} ran off the image (the machine has no fetch there)`), {
248
+ code: "RV64_PC_OUT_OF_IMAGE",
249
+ });
250
+ }
251
+ record(pc);
252
+ const word = Number(readle(memory, pc, 4));
253
+ const opcode = word & 0x7f;
254
+ const rd = (word >>> 7) & 0x1f;
255
+ const funct3 = (word >>> 12) & 0x7;
256
+ const rs1 = (word >>> 15) & 0x1f;
257
+ const rs2 = (word >>> 20) & 0x1f;
258
+ const funct7 = (word >>> 25) & 0x7f;
259
+ const write = (value: bigint) => {
260
+ if (rd !== 0) regs[rd] = value & MASK64;
261
+ };
262
+ let advanced = false;
263
+ let trapped: Error | undefined;
264
+ switch (opcode) {
265
+ case 0x37: {
266
+ // lui — the upper immediate lands in the register.
267
+ write(BigInt(immu(word)) & MASK64);
268
+ break;
269
+ }
270
+ case 0x17: {
271
+ // auipc — pc plus the upper immediate.
272
+ write((BigInt(pc) + BigInt(immu(word))) & MASK64);
273
+ break;
274
+ }
275
+ case 0x6f: {
276
+ // jal — the link register takes pc+4, the pc takes the jump.
277
+ write(BigInt(pc + 4));
278
+ pc = (pc + immj(word)) >>> 0;
279
+ advanced = true;
280
+ break;
281
+ }
282
+ case 0x67: {
283
+ // jalr — the link register takes pc+4, the pc takes rs1+imm (bit 0 cleared).
284
+ if (funct3 !== 0) {
285
+ trapped = illegal(pc, word);
286
+ break;
287
+ }
288
+ const target = (Number(regs[rs1] & MASK64) + immi(word)) >>> 0;
289
+ write(BigInt(pc + 4));
290
+ pc = target & ~1;
291
+ advanced = true;
292
+ break;
293
+ }
294
+ case 0x63: {
295
+ // the branches — the comparison rides the registers as signed or unsigned.
296
+ const left = regs[rs1];
297
+ const right = regs[rs2];
298
+ let take = false;
299
+ if (funct3 === 0) take = left === right;
300
+ else if (funct3 === 1) take = left !== right;
301
+ else if (funct3 === 4) take = left < right;
302
+ else if (funct3 === 5) take = left >= right;
303
+ else if (funct3 === 6) take = (left & MASK64) < (right & MASK64);
304
+ else if (funct3 === 7) take = (left & MASK64) >= (right & MASK64);
305
+ else {
306
+ trapped = illegal(pc, word);
307
+ break;
308
+ }
309
+ if (take) {
310
+ pc = (pc + immb(word)) >>> 0;
311
+ advanced = true;
312
+ }
313
+ break;
314
+ }
315
+ case 0x03: {
316
+ // the loads — little-endian out of the image, sign-extended where the isa extends.
317
+ const at = (Number(regs[rs1] & MASK64) + immi(word)) >>> 0;
318
+ if (at + 8 > memory.length) {
319
+ trapped = Object.assign(new Error(`the load at 0x${at.toString(16)} reads outside the image`), { code: "RV64_LOAD_OUT_OF_IMAGE", at });
320
+ break;
321
+ }
322
+ if (funct3 === 0) write(sign8(memory[at]));
323
+ else if (funct3 === 1) write(sign16(Number(readle(memory, at, 2))));
324
+ else if (funct3 === 2) write(sign32(readle(memory, at, 4)));
325
+ else if (funct3 === 3) write(readle(memory, at, 8));
326
+ else if (funct3 === 4) write(BigInt(memory[at]));
327
+ else if (funct3 === 5) write(BigInt(Number(readle(memory, at, 2))));
328
+ else {
329
+ trapped = illegal(pc, word);
330
+ break;
331
+ }
332
+ break;
333
+ }
334
+ case 0x23: {
335
+ // the stores — little-endian into the image.
336
+ const at = (Number(regs[rs1] & MASK64) + imms(word)) >>> 0;
337
+ const value = regs[rs2] & MASK64;
338
+ if (funct3 === 0) {
339
+ if (at + 1 > memory.length) { trapped = outofimage(at); break; }
340
+ writele(memory, at, 1, value);
341
+ } else if (funct3 === 1) {
342
+ if (at + 2 > memory.length) { trapped = outofimage(at); break; }
343
+ writele(memory, at, 2, value);
344
+ } else if (funct3 === 2) {
345
+ if (at + 4 > memory.length) { trapped = outofimage(at); break; }
346
+ writele(memory, at, 4, value);
347
+ } else if (funct3 === 3) {
348
+ if (at + 8 > memory.length) { trapped = outofimage(at); break; }
349
+ writele(memory, at, 8, value);
350
+ } else {
351
+ trapped = illegal(pc, word);
352
+ break;
353
+ }
354
+ break;
355
+ }
356
+ case 0x13: {
357
+ // op-imm — the 64-bit immediate arithmetic (slli/srli/srai carry the shamt).
358
+ const left = regs[rs1];
359
+ if (funct3 === 0) write(BigInt(left) + BigInt(immi(word)));
360
+ else if (funct3 === 1) write(left << BigInt((word >>> 20) & 0x3f));
361
+ else if (funct3 === 5) {
362
+ const shamt = (word >>> 20) & 0x3f;
363
+ if (((word >>> 26) & 0x3f) === 0x10) write(left >> BigInt(shamt));
364
+ else write(BigInt.asUintN(64, (left & MASK64) >> BigInt(shamt)));
365
+ }
366
+ else if (funct3 === 2) write(left < BigInt(immi(word)) ? 1n : 0n);
367
+ else if (funct3 === 3) write((left & MASK64) < BigInt(Number(BigInt(immi(word)) & MASK64)) ? 1n : 0n);
368
+ else if (funct3 === 4) write(BigInt(left) ^ BigInt(immi(word)));
369
+ else if (funct3 === 6) write(BigInt(left) | BigInt(immi(word)));
370
+ else if (funct3 === 7) write(BigInt(left) & BigInt(immi(word)));
371
+ else trapped = illegal(pc, word);
372
+ break;
373
+ }
374
+ case 0x1b: {
375
+ // op-imm-32 — the w immediate arithmetic wraps at 32 bits.
376
+ const left = regs[rs1];
377
+ if (funct3 === 0) write(sign32(BigInt(left) + BigInt(immi(word))));
378
+ else if (funct3 === 1) write(sign32((left & MASK32) << BigInt((word >>> 20) & 0x1f)));
379
+ else if (funct3 === 5) {
380
+ const shamt = (word >>> 20) & 0x1f;
381
+ if (((word >>> 25) & 0x7f) === 0x20) write(sign32((left & MASK32) >> BigInt(shamt)));
382
+ else write(sign32(BigInt.asUintN(32, (left & MASK32) >> BigInt(shamt))));
383
+ }
384
+ else trapped = illegal(pc, word);
385
+ break;
386
+ }
387
+ case 0x33: {
388
+ // op — the register arithmetic, the m extension and the w forms.
389
+ const left = regs[rs1];
390
+ const right = regs[rs2];
391
+ if (funct7 === 0x01) {
392
+ // the m extension: mul, mulh, mulhsu, mulhu, div, divu, rem, remu and the w forms.
393
+ if (funct3 === 0) write(left * right);
394
+ else if (funct3 === 1) {
395
+ // mulh — signed x signed, the high 64 bits.
396
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
397
+ const b = right >> 63n === -1n ? right - (1n << 64n) : right;
398
+ write(((a * b) >> 64n) & MASK64);
399
+ } else if (funct3 === 2) {
400
+ // mulhsu — signed x unsigned.
401
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
402
+ write(((a * (right & MASK64)) >> 64n) & MASK64);
403
+ } else if (funct3 === 3) write((((left & MASK64) * (right & MASK64)) >> 64n) & MASK64);
404
+ else if (funct3 === 4) {
405
+ // div — the rs2=0 and the overflow cases the isa pins.
406
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
407
+ const b = right >> 63n === -1n ? right - (1n << 64n) : right;
408
+ if (b === 0n) write(MASK64);
409
+ else if (a === -(1n << 63n) && b === -1n) write(1n << 63n);
410
+ else {
411
+ const q = a / b;
412
+ write((a % b !== 0n && (a < 0n) !== (b < 0n) && a % b !== 0n ? q - 1n : q) & MASK64);
413
+ }
414
+ } else if (funct3 === 5) {
415
+ // divu.
416
+ const uleft = left & MASK64;
417
+ const uright = right & MASK64;
418
+ write(uright === 0n ? MASK64 : uleft / uright);
419
+ } else if (funct3 === 6) {
420
+ // rem — the rs2=0 and the overflow cases the isa pins.
421
+ const a = left >> 63n === -1n ? left - (1n << 64n) : left;
422
+ const b = right >> 63n === -1n ? right - (1n << 64n) : right;
423
+ if (b === 0n) write(a & MASK64);
424
+ else if (a === -(1n << 63n) && b === -1n) write(0n);
425
+ else write((a % b) & MASK64);
426
+ } else if (funct3 === 7) {
427
+ // remu.
428
+ const uleft = left & MASK64;
429
+ const uright = right & MASK64;
430
+ write(uright === 0n ? uleft : uleft % uright);
431
+ } else trapped = illegal(pc, word);
432
+ } else if (funct7 === 0x00 || funct7 === 0x20) {
433
+ const negate = funct7 === 0x20;
434
+ if (funct3 === 0) write(negate ? BigInt(left) - BigInt(right) : BigInt(left) + BigInt(right));
435
+ else if (funct3 === 1) {
436
+ if (negate) trapped = illegal(pc, word);
437
+ else write(left << BigInt(right & 0x3fn));
438
+ }
439
+ else if (funct3 === 2) write(left < right ? 1n : 0n);
440
+ else if (funct3 === 3) write((left & MASK64) < (right & MASK64) ? 1n : 0n);
441
+ else if (funct3 === 4) {
442
+ if (negate) trapped = illegal(pc, word);
443
+ else write(BigInt(left) ^ BigInt(right));
444
+ }
445
+ else if (funct3 === 5) write(negate ? left >> BigInt(right & 0x3fn) : BigInt.asUintN(64, (left & MASK64) >> BigInt(right & 0x3fn)));
446
+ else if (funct3 === 6) {
447
+ if (negate) trapped = illegal(pc, word);
448
+ else write(BigInt(left) | BigInt(right));
449
+ }
450
+ else if (funct3 === 7) {
451
+ if (negate) trapped = illegal(pc, word);
452
+ else write(BigInt(left) & BigInt(right));
453
+ }
454
+ else trapped = illegal(pc, word);
455
+ } else trapped = illegal(pc, word);
456
+ if (trapped !== undefined) break;
457
+ break;
458
+ }
459
+ case 0x3b: {
460
+ // op-32 — the w register arithmetic wraps at 32 bits.
461
+ const left = regs[rs1] & MASK32;
462
+ const right = regs[rs2] & MASK32;
463
+ const negate = funct7 === 0x20;
464
+ if (funct7 === 0x01) {
465
+ // the m extension w forms: mulw, divw, divuw, remw, remuw.
466
+ if (funct3 === 0) write(sign32(left * right));
467
+ else if (funct3 === 4) {
468
+ const a = sign32(left) >> 31n === -1n ? sign32(left) - (1n << 64n) : sign32(left);
469
+ const b = sign32(right) >> 31n === -1n ? sign32(right) - (1n << 64n) : sign32(right);
470
+ if (b === 0n) write(MASK32);
471
+ else if (a === -(1n << 31n) && b === -1n) write(1n << 31n);
472
+ else write((a / b) & MASK32);
473
+ } else if (funct3 === 5) write(right === 0n ? MASK32 : left / right);
474
+ else if (funct3 === 6) {
475
+ const a = sign32(left) >> 31n === -1n ? sign32(left) - (1n << 64n) : sign32(left);
476
+ const b = sign32(right) >> 31n === -1n ? sign32(right) - (1n << 64n) : sign32(right);
477
+ if (b === 0n) write(sign32(left));
478
+ else if (a === -(1n << 31n) && b === -1n) write(0n);
479
+ else write((a % b) & MASK32);
480
+ } else if (funct3 === 7) write(right === 0n ? left : left % right);
481
+ else trapped = illegal(pc, word);
482
+ } else if (funct7 === 0x00 || funct7 === 0x20) {
483
+ if (funct3 === 0) write(sign32(negate ? BigInt(sign32(left)) - BigInt(sign32(right)) : BigInt(sign32(left)) + BigInt(sign32(right))));
484
+ else if (funct3 === 1) {
485
+ // sllw shifts the 32-bit word and sign-extends the result.
486
+ if (negate) trapped = illegal(pc, word);
487
+ else write(sign32(left << BigInt(right & 0x1fn)));
488
+ } else if (funct3 === 5) {
489
+ // srlw shifts the zero-extended word logically; sraw shifts the
490
+ // sign-extended word arithmetically — both sign-extend the result
491
+ // (the >>> operator never answers a bigint seat, the masked low
492
+ // word already carries the logical form).
493
+ if (negate) write(sign32(sign32(left) >> BigInt(right & 0x1fn)));
494
+ else write(sign32(left >> BigInt(right & 0x1fn)));
495
+ }
496
+ else trapped = illegal(pc, word);
497
+ } else trapped = illegal(pc, word);
498
+ if (trapped !== undefined) break;
499
+ break;
500
+ }
501
+ case 0x0f: {
502
+ // fence — a single-hart machine orders itself; the barrier retires as a step.
503
+ break;
504
+ }
505
+ case 0x73: {
506
+ // system — the exit door (ecall a7=93) and the break trap; the csr space is out of the core.
507
+ if (((word >>> 20) & 0xfff) === 0) {
508
+ const syscall = Number(regs[17] & MASK64);
509
+ if (syscall === EXITSYSCALL) {
510
+ const code = Number(regs[10] & MASK64);
511
+ steps += 1;
512
+ return { exitcode: code | 0, steps, finalpc: pc, trace };
513
+ }
514
+ throw Object.assign(new Error(`the ecall a7=${syscall} is not the exit door — the core owns syscall 93 alone`), {
515
+ code: "RV64_SYSCALL_UNSUPPORTED",
516
+ at: pc,
517
+ });
518
+ }
519
+ if (((word >>> 20) & 0xfff) === 1) {
520
+ steps += 1;
521
+ return { exitcode: -7, steps, finalpc: pc, trace };
522
+ }
523
+ trapped = illegal(pc, word);
524
+ break;
525
+ }
526
+ default: {
527
+ trapped = illegal(pc, word);
528
+ break;
529
+ }
530
+ }
531
+ if (trapped !== undefined) throw trapped;
532
+ steps += 1;
533
+ if (!advanced) pc += 4;
534
+ if (rd !== 0) regs[0] = 0n;
535
+ }
536
+ throw Object.assign(new Error(`the machine exhausted its ${maxsteps}-step budget (an infinite loop never answers an exit)`), {
537
+ code: "RV64_STEP_BUDGET_EXHAUSTED",
538
+ });
539
+ }
540
+
541
+ /** The honest out-of-image store refusal. */
542
+ function outofimage(at: number): Error {
543
+ return Object.assign(new Error(`the store at 0x${at.toString(16)} writes outside the image`), { code: "RV64_STORE_OUT_OF_IMAGE", at });
544
+ }
545
+
546
+ /** Loads and runs one payload in one shot — the seat-facing entry: the
547
+ * state digest answers sha256 over the final architectural state, so
548
+ * one image always answers one digest (the deterministic proof the
549
+ * battery pins). */
550
+ export async function rv64run(
551
+ payload: string | Uint8Array,
552
+ options: { memorybytes?: number; maxsteps?: number } = {},
553
+ ): Promise<Rv64RunResult> {
554
+ const bytes = payloadbytes(payload);
555
+ const memorybytes = Math.max(4096, Math.round(options.memorybytes ?? DEFAULTMEMORYBYTES));
556
+ const { memory, entry, loads } = loadimage(bytes, memorybytes);
557
+ const run = executerv64(memory, entry, { maxsteps: options.maxsteps });
558
+ // the state digest walks the final memory image through sha256 —
559
+ // one image, one answer (the deterministic proof the battery pins).
560
+ let statedigest = "";
561
+ // the register file rides a second deterministic source: the run
562
+ // record plus the memory image digest — one image, one answer.
563
+ try {
564
+ const nodecrypto = (await import("node:crypto")) as typeof import("node:crypto");
565
+ const hash = nodecrypto.createHash("sha256");
566
+ hash.update(memory);
567
+ hash.update(`entry=${entry};finalpc=${run.finalpc};steps=${run.steps};exit=${run.exitcode}`);
568
+ statedigest = hash.digest("hex");
569
+ } catch {
570
+ statedigest = `entry=${entry};finalpc=${run.finalpc};steps=${run.steps};exit=${run.exitcode}`;
571
+ }
572
+ return {
573
+ exitcode: run.exitcode,
574
+ steps: run.steps,
575
+ entry,
576
+ finalpc: run.finalpc,
577
+ statedigest,
578
+ trace: run.trace,
579
+ loads,
580
+ };
581
+ }