Skip to content

Commit ff71716

Browse files
stackdumpclaude
andcommitted
Add JS parity modules: safemath, model, state, bridge
Full feature parity between Go arc/ and JS public/: - safemath.js: U256 overflow-checked arithmetic, ERC-4626 conversions - model.js: Place, Transition, Arc, Invariant, Model with validation - state.js: Marking, State, fire, enabled, canReach - bridge.js: Schema <-> Model conversion Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0964571 commit ff71716

5 files changed

Lines changed: 422 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ jobs:
103103
104104
- name: JS syntax check
105105
run: |
106-
for f in public/mimc.js public/merkle.js public/witness-builder.js public/bitwrap.js; do
106+
for f in public/mimc.js public/merkle.js public/witness-builder.js public/safemath.js public/model.js public/state.js public/bridge.js public/bitwrap.js; do
107107
node --check "$f" || exit 1
108108
done
109109
echo "All JS files pass syntax check"

public/bridge.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Schema <-> Model bridge — matches Go internal/petri/bridge.go exactly
2+
3+
import { Model } from './model.js';
4+
5+
// Convert a metamodel Schema to a Petri net Model
6+
export function fromSchema(schema) {
7+
const m = new Model(schema.name || '', schema.version || '1.0.0');
8+
9+
if (schema.states) {
10+
for (const st of schema.states) {
11+
m.addPlace({
12+
id: st.id,
13+
schema: st.type || '',
14+
initial: toInt(st.initial),
15+
exported: st.exported || false,
16+
});
17+
}
18+
}
19+
20+
if (schema.actions) {
21+
for (const a of schema.actions) {
22+
m.addTransition({
23+
id: a.id,
24+
guard: a.guard || '',
25+
});
26+
}
27+
}
28+
29+
if (schema.arcs) {
30+
for (const arc of schema.arcs) {
31+
m.addArc({
32+
source: arc.source,
33+
target: arc.target,
34+
keys: arc.keys || [],
35+
value: arc.value || '',
36+
});
37+
}
38+
}
39+
40+
if (schema.constraints) {
41+
for (const c of schema.constraints) {
42+
m.addInvariant({
43+
id: c.id,
44+
expr: c.expr || '',
45+
});
46+
}
47+
}
48+
49+
return m;
50+
}
51+
52+
// Convert a Petri net Model to a metamodel Schema
53+
export function toSchema(model) {
54+
return {
55+
name: model.name,
56+
version: model.version,
57+
states: model.places.map(p => ({
58+
id: p.id,
59+
type: p.schema || undefined,
60+
initial: p.initial || undefined,
61+
exported: p.exported || undefined,
62+
})),
63+
actions: model.transitions.map(t => ({
64+
id: t.id,
65+
guard: t.guard || undefined,
66+
})),
67+
arcs: model.arcs.map(a => ({
68+
source: a.source,
69+
target: a.target,
70+
keys: a.keys && a.keys.length > 0 ? a.keys : undefined,
71+
value: a.value || undefined,
72+
})),
73+
constraints: model.invariants.map(inv => ({
74+
id: inv.id,
75+
expr: inv.expr,
76+
})),
77+
};
78+
}
79+
80+
// Type converters matching Go bridge.go
81+
82+
export function stateToPlace(st) {
83+
return {
84+
id: st.id,
85+
schema: st.type || '',
86+
initial: toInt(st.initial),
87+
exported: st.exported || false,
88+
};
89+
}
90+
91+
export function placeToState(p) {
92+
return {
93+
id: p.id,
94+
type: p.schema || '',
95+
initial: p.initial || 0,
96+
exported: p.exported || false,
97+
};
98+
}
99+
100+
export function actionToTransition(a) {
101+
return { id: a.id, guard: a.guard || '' };
102+
}
103+
104+
export function transitionToAction(t) {
105+
return { id: t.id, guard: t.guard || '' };
106+
}
107+
108+
export function constraintToInvariant(c) {
109+
return { id: c.id, expr: c.expr || '' };
110+
}
111+
112+
export function invariantToConstraint(inv) {
113+
return { id: inv.id, expr: inv.expr || '' };
114+
}
115+
116+
// Helper: coerce initial value to int (handles int, float, null)
117+
function toInt(v) {
118+
if (v == null) return 0;
119+
return Math.floor(Number(v));
120+
}

public/model.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Petri net model types — matches Go internal/petri/model.go exactly
2+
3+
// Validation errors
4+
export const ErrEmptyID = () => new Error('empty ID');
5+
export const ErrDuplicateID = () => new Error('duplicate ID');
6+
export const ErrInvalidArcSource = () => new Error('invalid arc source');
7+
export const ErrInvalidArcTarget = () => new Error('invalid arc target');
8+
export const ErrInvalidArcConnection = () => new Error('invalid arc connection: must connect place to transition or vice versa');
9+
10+
export class Model {
11+
constructor(name, version = '1.0.0') {
12+
this.name = name;
13+
this.version = version;
14+
this.places = [];
15+
this.transitions = [];
16+
this.arcs = [];
17+
this.invariants = [];
18+
}
19+
20+
addPlace(place) {
21+
this.places.push({ id: '', schema: '', initial: 0, exported: false, ...place });
22+
return this;
23+
}
24+
25+
addTransition(transition) {
26+
this.transitions.push({ id: '', guard: '', ...transition });
27+
return this;
28+
}
29+
30+
addArc(arc) {
31+
this.arcs.push({ source: '', target: '', keys: [], value: '', ...arc });
32+
return this;
33+
}
34+
35+
addInvariant(invariant) {
36+
this.invariants.push({ id: '', expr: '', ...invariant });
37+
return this;
38+
}
39+
40+
placeByID(id) {
41+
return this.places.find(p => p.id === id) || null;
42+
}
43+
44+
placeIsExported(id) {
45+
const p = this.placeByID(id);
46+
return p ? p.exported : false;
47+
}
48+
49+
transitionByID(id) {
50+
return this.transitions.find(t => t.id === id) || null;
51+
}
52+
53+
inputArcs(transitionID) {
54+
return this.arcs.filter(a => a.target === transitionID);
55+
}
56+
57+
outputArcs(transitionID) {
58+
return this.arcs.filter(a => a.source === transitionID);
59+
}
60+
61+
validate() {
62+
const placeIDs = new Set();
63+
const transitionIDs = new Set();
64+
65+
for (const p of this.places) {
66+
if (!p.id) throw ErrEmptyID();
67+
if (placeIDs.has(p.id)) throw ErrDuplicateID();
68+
placeIDs.add(p.id);
69+
}
70+
71+
for (const t of this.transitions) {
72+
if (!t.id) throw ErrEmptyID();
73+
if (transitionIDs.has(t.id)) throw ErrDuplicateID();
74+
transitionIDs.add(t.id);
75+
}
76+
77+
for (const a of this.arcs) {
78+
const sourceIsPlace = placeIDs.has(a.source);
79+
const sourceIsTransition = transitionIDs.has(a.source);
80+
const targetIsPlace = placeIDs.has(a.target);
81+
const targetIsTransition = transitionIDs.has(a.target);
82+
83+
if (!sourceIsPlace && !sourceIsTransition) throw ErrInvalidArcSource();
84+
if (!targetIsPlace && !targetIsTransition) throw ErrInvalidArcTarget();
85+
if (sourceIsPlace && targetIsPlace) throw ErrInvalidArcConnection();
86+
if (sourceIsTransition && targetIsTransition) throw ErrInvalidArcConnection();
87+
}
88+
89+
return true;
90+
}
91+
}

public/safemath.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Safe math for unsigned 256-bit integers using native BigInt
2+
// Matches Go arc/safemath.go exactly
3+
4+
const MAX_U256 = (1n << 256n) - 1n;
5+
6+
export class SafeMathError extends Error {
7+
constructor(message) { super(message); this.name = 'SafeMathError'; }
8+
}
9+
10+
export const ErrOverflow = () => new SafeMathError('arithmetic overflow');
11+
export const ErrUnderflow = () => new SafeMathError('arithmetic underflow');
12+
export const ErrDivisionByZero = () => new SafeMathError('division by zero');
13+
14+
// Clamp to U256 range [0, 2^256 - 1]
15+
function toU256(v) {
16+
v = BigInt(v);
17+
if (v < 0n) throw ErrUnderflow();
18+
if (v > MAX_U256) throw ErrOverflow();
19+
return v;
20+
}
21+
22+
// SafeAdd returns a + b, throws on overflow
23+
export function safeAdd(a, b) {
24+
a = BigInt(a); b = BigInt(b);
25+
const result = a + b;
26+
if (result > MAX_U256) throw ErrOverflow();
27+
return result;
28+
}
29+
30+
// SafeSub returns a - b, throws on underflow
31+
export function safeSub(a, b) {
32+
a = BigInt(a); b = BigInt(b);
33+
if (b > a) throw ErrUnderflow();
34+
return a - b;
35+
}
36+
37+
// SafeMul returns a * b, throws on overflow
38+
export function safeMul(a, b) {
39+
a = BigInt(a); b = BigInt(b);
40+
const result = a * b;
41+
if (result > MAX_U256) throw ErrOverflow();
42+
return result;
43+
}
44+
45+
// SafeDiv returns a / b, throws on division by zero
46+
export function safeDiv(a, b) {
47+
a = BigInt(a); b = BigInt(b);
48+
if (b === 0n) throw ErrDivisionByZero();
49+
return a / b;
50+
}
51+
52+
// SafeMod returns a % b, throws on division by zero
53+
export function safeMod(a, b) {
54+
a = BigInt(a); b = BigInt(b);
55+
if (b === 0n) throw ErrDivisionByZero();
56+
return a % b;
57+
}
58+
59+
// MulDiv computes (a * b) / c with full precision intermediate result
60+
export function mulDiv(a, b, c) {
61+
a = BigInt(a); b = BigInt(b); c = BigInt(c);
62+
if (c === 0n) throw ErrDivisionByZero();
63+
return (a * b) / c;
64+
}
65+
66+
// ConvertToShares computes shares = (assets * totalShares) / totalAssets
67+
// Standard ERC-4626 conversion with rounding down
68+
export function convertToShares(assets, totalShares, totalAssets) {
69+
assets = BigInt(assets);
70+
totalShares = BigInt(totalShares);
71+
totalAssets = BigInt(totalAssets);
72+
if (totalAssets === 0n) return assets; // First deposit: 1:1
73+
return mulDiv(assets, totalShares, totalAssets);
74+
}
75+
76+
// ConvertToAssets computes assets = (shares * totalAssets) / totalShares
77+
// Standard ERC-4626 conversion with rounding down
78+
export function convertToAssets(shares, totalAssets, totalShares) {
79+
shares = BigInt(shares);
80+
totalAssets = BigInt(totalAssets);
81+
totalShares = BigInt(totalShares);
82+
if (totalShares === 0n) return shares; // No shares: 1:1
83+
return mulDiv(shares, totalAssets, totalShares);
84+
}
85+
86+
// Min returns the smaller of a and b
87+
export function min(a, b) {
88+
a = BigInt(a); b = BigInt(b);
89+
return a < b ? a : b;
90+
}
91+
92+
// Max returns the larger of a and b
93+
export function max(a, b) {
94+
a = BigInt(a); b = BigInt(b);
95+
return a > b ? a : b;
96+
}
97+
98+
export { MAX_U256 };

0 commit comments

Comments
 (0)