Commit graph

55 commits

Author SHA1 Message Date
tef-engine
313be7997e feat(lagrangia): action functional + ToggleSwitch stationary-point test
Implements spec §2.3/§2.4 of the Lagrangia reducer probe
(~/.hermes/plans/2026-07-18-lagrangia-reducer-spec.md).

- compiler/lagrangia/src/score.rs: three scoring functions
  (complexity, invariant_distance, byte_entropy) + action() with
  v0 weights w1=1.0, w2=100.0, w3=0.1.
- compiler/lagrangia/tests/toggle_switch.rs: N=20 candidate search
  with deterministic LCG, hill descent (top-5 × 5 perturbations ×
  3 rounds), argmin selection with id-based tiebreak.
- Asserts: nonzero score variance (manifold not flat), stationary
  candidate holds invariants, argmin well-defined.

Test result: 8 passed (7 unit + 1 integration). Stationary point
found at S=43.47 (complexity=43, inv_dist=0, entropy=4.71) — the
gold ToggleSwitch codegen output, as expected.

Note: invariant_distance uses a static proxy (gold=holds, perturbed
=fails with depth). Full Z3 wiring is a follow-up.
2026-07-18 22:10:38 -07:00
tef-engine
5b2fdc7d59 wip: working copy state at jj git init 2026-07-18 22:10:38 -07:00
Scott Scoble
857fa6ceb4 fix: test path resolution — use __dirname instead of process.cwd() (cycle 74) 2026-06-29 19:09:47 -07:00
Scott Scoble
fa6f5bcf21 test: add 12 regression tests for Z3/TS/Rust expression translation
Tests verify:
- Z3: implies() preserves args, multi-level field access, len(), nil, distinct
- TS: implies() → (!p || q), constraint checks use every(), in → .has(), dedup fix
- Rust: constraint checks use iter().all(), implies() → (!p || q), dedup fix
- All 6 verify blocks + 8 constraints emitted in all backends

69/69 tests pass (57 existing + 12 new).
2026-06-29 17:46:17 -07:00
Scott Scoble
37cdc59180 fix(ts,rust): real constraint/verify expressions, dedup fix, implies translation
TS backend:
- Constraint checks now emit real forall→every() translation (was 'return true')
- Verify: implies(p,q) now (!p || q) (was (p || q) — wrong logic)
- Verify: 'in' operator now pre-tokenized to avoid || splitting the regex
- memberKey dedup fix: verify/constraint nodes now keyed by expression
  (was: all verify nodes deduped to 1, all constraints to 1)

Rust backend:
- Constraint checks: forall→iter().all(), implies()→(!p || q), nil→sentinel
- Verify checks: always()/eventually() with decision_log iteration
- Same memberKey dedup fix applied

All 8 constraints + 6 verify blocks now emit in both TS and Rust.
57/57 tests pass.
2026-06-29 17:39:06 -07:00
Scott Scoble
3ca5a8d21a fix(z3): preserve implies args, multi-level field access, len(), nil, distinct operator
Z3 expression translator fixes:
- implies(x, y) now preserves both arguments (was dropping both → (=> true true))
- Multi-level field access m.price.amount → m_price_amount (was m_price.(>= amount 0))
- len() now handled same as length() (was left as bare len())
- nil/null translated to speckl_nil sentinel constant
- != now properly converts to (distinct X Y) via convertToPrefix (was bare 'distinct')
- Added 'distinct' as binary operator in convertToPrefix
- speckl_nil declared as Int constant in output

All 8 product spec constraints now produce valid SMT-LIB2.
57/57 tests pass.
2026-06-29 17:31:31 -07:00
Scott Scoble
7ce0b7fecc fix(speckl): TS generator — emit interface record types (cycle 73)
Same defect as Rust backend: interface records (Money, Ticket, Order,
etc.) were not emitted as TS interfaces. Only events were emitted.

Now record-kind interfaces (with fields, not methods) emit as TS
interfaces, placed between the enum declarations and event interfaces.

Output for FederatedMeetupProduct: 426 lines (was 191), 47 interfaces
(was ~11). 57/57 tests pass.
2026-06-29 17:16:11 -07:00
Scott Scoble
84b8962d22 fix(speckl): Rust generator — Date type, record structs, event enum (cycle 73)
Three defects found by deep-inspecting the Rust output against the
product spec (FederatedMeetupProduct.speckdl):

1. Date type undefined in Rust — emitted as 'Date' which doesn't exist.
   Fixed: Date maps to u64 (Unix timestamp) in both the primitive map
   and the ident fallback path.

2. Interface records (Money, Ticket, Order, etc.) not emitted as Rust
   structs — only events were emitted. The TS backend has the same gap.
   Fixed: record-kind interfaces now emit as Rust structs.

3. Decision log typed as Vec<FirstEventName> instead of an enum union.
   Fixed: emit a SpeckName_Event enum with one variant per event type,
   type the decision log as Vec<SpeckName_Event>.

57/57 tests pass. Both specs generate clean Rust output.
2026-06-29 17:14:12 -07:00
Scott Scoble
e5ee3fe1c0 refactor(speckl): replace static-field hack with local-object pattern (cycle 73)
The proto_package / go_package / event_suffix directives were stored on
(parseSpeck as any)._protoPackage etc — static function fields that leaked
between parse calls (cycle 61 added a reset at the top, but the pattern
was still wrong).

Now collected in a local metadata object inside parseSpeck, spread into
the return value. No reset needed — the object is local to the invocation.

This is the durable fix referenced in the spec-compiler-dogfood skill's
anti-pattern section 'Storing per-speck parser metadata on a static
function field as a quick hack.'

57/57 tests pass, both specs protoc-clean.
2026-06-29 14:29:30 -07:00
Scott Scoble
121ac7de33 fix(speckl): error on unrecognized lines in service blocks (D3, cycle 73)
Previously, a typo like rc_purchase_ticket (missing p in rpc) was silently
swallowed — 13 RPCs in source, 12 in AST, no error. Now throws a parse
error with the unrecognized line and expected format.
2026-06-29 14:01:58 -07:00
Scott Scoble
5ae89e943c test(speckl): lock in List<T> in interface record fields (cycle 73) 2026-06-29 12:15:47 -07:00
Scott Scoble
04fa9844f4 fix(speckl): protobuf generator — List<T> in interfaces, enum prefix, transition type field, Date in interfaces (cycle 73)
Four defects found by dogfooding Speckl against the v0.3 product-layer
spec (FederatedMeetupProduct.speckdl, 74 members):

D1 — CRITICAL: List<T> fields in interface records emitted empty type
  Root cause: interface record fields called speckTypeToProto(f.type.name)
  which returns '' for List/Map/Set types (their .name is undefined).
  Event and state fields had inline List/Map dispatch, but interfaces did not.
  Fix: extracted typeExprToProto() shared function that handles all TypeExpr
  variants (primitive, ident, list, map, set). All four field-emitting code
  paths (interfaces, events, state, transitions) now use it.

D2 — MODERATE: Transition type discriminator field silently dropped
  Root cause: parser stores the type field in t.typeDiscriminator (string)
  rather than in t.fields array. Generator only emitted t.fields.
  Fix: emit typeDiscriminator as 'type' field at number 1, offset other
  fields by 1 to avoid collision.

D4 — LOW: Enum variant name collisions across enums in same proto scope
  Proto3 requires all enum values unique within enclosing scope, not just
  the enum. PENDING in Role and PENDING in OrderStatus collided.
  Fix: prefix all variants with upperSnake(enumName) — the proto3 convention.
  This also matches the handwritten proto's naming (TRANSITION_TYPE_*).

D5 — LOW: Date in interface fields did not trigger timestamp import
  The usesTimestamp check only scanned events and state, not interfaces.
  Fix: added interface member check to the timestamp import scan.

D3 (deferred): rc_ typo in spec source silently swallowed by parser.
  Parser bug, not generator bug. Filed for future cycle.

Verified: 56/56 tests pass, both FederatedMeetup.speckdl and
FederatedMeetupProduct.speckdl compile to protoc-clean output.
2026-06-29 12:12:40 -07:00
Scott Scoble
7beaf6f3cc fix(speckl): strip default-value syntax from event field types (cycle 65)
End-to-end compile of FederatedMeetup.speckdl (with the cycle-65
Transition envelope added) surfaced a real bug: the speckdl uses
default-value syntax on event fields:

  initial_mesh_peers: List<InitialMeshPeer> = []

The default is a Speckl semantic — a state-machine init value —
but the .proto wire format has no place for it. Without stripping,
the type expression 'List<InitialMeshPeer> = []' was emitted
verbatim as the field type, producing broken proto3 syntax that
protoc rejected.

Fix: parseEventBlockMultiline now strips '= <expr>' suffix from
the type expression before calling parseTypeExpr. The default value
is dropped (it's a Speckl semantic only; the wire format carries
no default).

Tests: 1 new parser test covers the federated-meetup
'initial_mesh_peers: List<InitialMeshPeer> = []' shape. All 56
tests pass (55 baseline + 1 new). tsc build clean.

End-to-end verified: with this fix and the cycle-65 federated-
meetup speckdl changes, the Speckl-generated .proto now compiles
cleanly with 'protoc' (previously rejected).
2026-06-28 22:36:36 -07:00
Scott Scoble
f37c0ccc57 feat(speckl): emit TransitionNode and OneofNode in proto generator (cycle 64)
The .proto generator now recognizes two new top-level speck members
and emits real proto3 syntax for them:

  oneof Name {                    oneof is NOT emitted at top level
    variant_field: VariantType    (it lives inside the transition's
  }                                payload block — see below)

  transition Name {               emitted as:
    type: <EnumName>                message Name {
    payload: <OneofName>              <regular fields>
    hlc: Bytes                        oneof payload {
  }                                    VariantType variant_field = 10;
                                       ...
                                     }
                                   }

The  and  fields are special-cased: type records the
discriminator enum reference (so future type-aware code can find it),
payload records the sibling oneof block to inline. Neither is emitted
as a regular proto field.

The oneof variants reference the suffixed message name when the
speck has 'event_suffix: "Payload"' (or any suffix). This makes the
generated variants match the consumer's Go types in the federated-
meetup case: 'CreateGroupPayload create_group = 10' is the type
binding the consumer's apply switch expects.

End-to-end verified: a speck with
  event_suffix: "Payload"
  oneof TransitionPayload {
    create_group: CreateGroup
    add_steward: AddSteward
  }
  transition Transition {
    type: TransitionType
    payload: TransitionPayload
    hlc: Bytes
  }
  event CreateGroup { name: String }
  event AddSteward { new_steward: String }

emits the right proto3:
  message CreateGroupPayload { ... }
  message AddStewardPayload { ... }
  message Transition {
    bytes hlc = 1;
    oneof payload {
      CreateGroupPayload create_group = 10;
      AddStewardPayload add_steward = 11;
    }
  }

All 55 tests pass. tsc build clean.

The federated-meetup speckdl (specs/FederatedMeetup.speckdl) does
not yet declare the Transition envelope — it uses 13 separate
event blocks. Adding the transition+oneof envelope to the real
spec is the next step (cycle 65).
2026-06-28 22:31:24 -07:00
Scott Scoble
4d95a2b975 fix(speckl): dispatch 'state as <Name>' to parseStateBlockMultiline (cycle 63)
End-to-end compile of FederatedMeetup.speckdl with the new
'state as StateSnapshot' header (cycle 63) surfaced that the
parser's main loop and parseMemberBlock dispatcher were both
missing 'state as ' as a recognized block-starter prefix. The
state-block parser (parseStateBlockMultiline) already extracts
the 'as' clause correctly (cycle 59), but the call site never
reached it: the block fell through the 'state:' / 'state {'
checks and was parsed as a regular member line, producing no
StateNode at all.

Fix: add 'state as ' to the blockStarters list in the main
parse loop, and add a parallel dispatch in parseMemberBlock
that routes 'state as ' headers to parseStateBlockMultiline.
The dispatcher change is symmetric with the existing 'state:'
and 'state {' forms.

Tests: 1 new parser test confirms the dispatch works for
'state as StateSnapshot { ... }'. All 55 tests pass (54
baseline + 1 new). tsc build clean.

End-to-end verified: running generateProtobuf against the
federated-meetup speck with 'state as StateSnapshot' now
emits 'message StateSnapshot { ... }' (correct), where
pre-fix the StateNode was lost entirely.
2026-06-28 22:29:28 -07:00
Scott Scoble
0e03aa4e04 fix(speckl): strip inline // comments from event/interface field types (cycle 62)
End-to-end compile of FederatedMeetup.speckdl surfaced a real
generator input bug: inline line comments on field declarations
were bleeding into the parsed type expression. A field like

  mesh_ip: Bytes   // 4 bytes (IPv4 overlay)

produced a TypeExpr whose name was literally
  'Bytes   // 4 bytes (IPv4 overlay)'
which the .proto generator emitted verbatim:

  bytes  // 4 bytes (IPv4 overlay) mesh_ip = 2;

— broken syntax (comment in the type position).

Fix: both parseEventBlockMultiline and parseInterfaceBlockMultiline
now strip '// ...' suffixes from each inner line BEFORE matching the
'name: type' regex or splitting on commas. The state-block parser
already had this; cycle 62 brings event and interface to parity.

Tests: 2 new parser tests covering event and interface blocks, both
with the FederatedMeetup-style inline comment. All 54 tests pass
(52 baseline + 2 new). tsc build clean.

End-to-end verified: running generateProtobuf against the real
FederatedMeetup.speckdl now emits clean 'bytes mesh_ip = 2;' for
the InitialMeshPeer interface, where cycle 62's pre-fix output
was the broken 'Bytes   // 4 bytes (IPv4 overlay) mesh_ip = 2;'.

This unblocks the next phase of the federated-meetup Speckl
migration: the consumer Go code can now bind to the generated
proto without a half-broken field in the way.
2026-06-28 22:26:06 -07:00
Scott Scoble
a6bdbce915 feat(speckl): event_suffix directive + static-field reset (cycle 61)
Adds 'event_suffix: "Payload"' as a top-of-file directive. When set,
the .proto generator appends the suffix to every 'event' block's
emitted message name. Default (no directive) preserves the existing
'event CreateGroup' → 'message CreateGroup' behavior.

Why: federated-meetup's handwritten proto names every transition
payload CreateGroupPayload, AddStewardPayload, etc. (the convention
that the wrapper message carries a 'Payload' suffix and the event
type lives in 'oneof payload { ... }'). Without this directive, the
Speckl-generated proto would emit 'message CreateGroup' and the
consumer Go code would fail to find the types it imports.

The test fixture uses three tests:
  - directive parsed and stored on SpeckNode
  - directive absent → eventSuffix undefined
  - both quoted ("Payload") and bare (Payload) values accepted

The reset of (parseSpeck as any)._protoPackage / _goPackage /
_eventSuffix at the start of parseSpeck fixes a pre-existing
static-field-hack leak where a previous test's metadata would
persist into the next test's parse. Documented as a known
anti-pattern in spec-compiler-dogfood SKILL.md; full cleanup
to a local-object pattern is tracked separately. The minimum
fix here is 'reset at start of parse' which restores test
isolation without refactoring the entire metadata-collection
path.

End-to-end verified: running generateProtobuf against a speck
with event_suffix: "Payload" emits 'message CreateGroupPayload'
and 'message AddStewardPayload' (correct).

All 52 tests pass. tsc build clean.

Reference: .hermes/skills/spec-compiler-dogfood/references/
  speckl-federated-meetup-protobuf-migration.md — the migration
unblocks when every generated message name matches what the
consumer's Go imports expect.
2026-06-28 22:02:12 -07:00
Scott Scoble
ec0f213448 test(speckl): lock in enum interface classification (cycle 60)
Adds a regression test that confirms a bare-identifier interface (no
methods with parens, no fields with colons) parses as kind: 'enum'
with the variants on its methods[] array. The .proto generator already
emits these correctly as 'enum X { X_UNSPECIFIED = 0; FOO = 1; ... }'
- confirmed end-to-end by running generateProtobuf against the
federated-meetup CustodyTier shape.

This was a verification cycle rather than a feature cycle: the compiler
already does the right thing for enums, but without a regression test
a future refactor of classifyInterfaces or parseInterface could break
this silently. The test pins the behavior so a PR that breaks enum
emission will fail CI.

All 49 tests pass (48 + 1 new). tsc build clean.

Reference: federated-meetup.speckdl CustodyTier shape
  interface CustodyTier {
    ColdHardware
    HotSigning
    ThresholdSplit
    BackupOnly
    Compromised
  }
2026-06-28 21:57:06 -07:00
Scott Scoble
0ab3174314 feat(speckl): state as <Name> message override (cycle 59)
Adds 'state as <MessageName> { ... }' syntax for the .proto generator.
When the 'as' clause is present, the generator emits
  message <MessageName> { ... }
instead of the default
  message <SpeckName>State { ... }.

Why: the federated-meetup consumer (internal/types/types.go and friends)
imports 'pb.StateSnapshot', not 'pb.FederatedMeetupState'. The previous
default of namespacing by speck name broke the migration: the generated
proto would have compiled cleanly in protoc but the consuming Go code
would not have built. With this override, the Speckl-generated proto
emits the exact message name the consumer expects.

The default behavior (no 'as' clause) is unchanged, so existing specs
that relied on the old convention (e.g. tests, examples) still work.

Tests: 2 new parser tests, both green. All 48 tests pass. tsc build clean.

Reference: .hermes/skills/spec-compiler-dogfood/references/
  speckl-federated-meetup-protobuf-migration.md
— the build break last session was caused by the missing override.
2026-06-28 21:55:54 -07:00
Scott Scoble
b65bf0e206 feat(speckl): transition envelope (cycle 58)
Adds 'transition Name { ... }' as a top-level member of a speck
block. The AST node is TransitionNode with fields[] plus optional
payloadOneof and typeDiscriminator references. Two field names
have special meaning:

  payload: <OneofName>  →  the generator will emit the referenced
                            oneof's variants inline as
                            'oneof payload { ... }' inside the
                            message body (i.e. payload becomes a
                            proto oneof, not a regular field)
  type:    <EnumName>   →  the discriminator enum for the variants

All other fields are emitted as ordinary proto message fields.

This is the shape federated-meetup needs for its Transition message:
  message Transition {
    TransitionType type = 1;
    StateRoot prior_state = 2;
    oneof payload {
      CreateGroupPayload create_group = 10;
      ...
    }
    Multisig steward_signatures = 31;
    bytes hlc = 32;
    fixed32 branch_id = 33;
    google.protobuf.Timestamp signed_at = 30;
  }

The next cycle (64) will wire the .proto generator to emit this shape
from a TransitionNode. Cycle 58 is parser-only — 3 new tests cover
regular fields, payload+type references, and trailing punctuation
tolerance. All 46 tests (39 baseline + 4 oneof + 3 transition) pass.
tsc build clean.
2026-06-28 21:53:38 -07:00
Scott Scoble
0e42961525 feat(speckl): oneof syntax for proto wire-format discriminated unions (cycle 57)
Adds 'oneof Name { field: Type, ... }' as a top-level member of a
speck block, alongside interface, event, service, etc. The AST node
is OneofNode with a variants array of { fieldName, typeName }. The
parser supports multi-line blocks, comments, and trailing commas /
semicolons. Generator integration is a follow-up cycle (proto/TS).

Motivation: federated-meetup's Transition message needs a oneof for
the 15+ payload variants (create_group, add_steward, ...). Without
oneof syntax, the consumer Go code has to hand-roll
'Transition_AddHostPeer' wrapper messages for each event type.
With oneof, the generated .proto emits a real proto3 oneof and the
generated Go bindings have a single 'isXXX()' discriminator API
that matches the consumer's expectations.

Tests: 4 new parser tests covering happy path, trailing punctuation,
comments, and coexistence with other member types. All 43 tests
(39 baseline + 4 new) pass; tsc build clean.

Reference: spec-compiler-dogfood skill, references/speckl-federated-
meetup-protobuf-migration.md — the prior build break was caused by
proto message-shape mismatches between the generated .proto and the
consumer Go code. oneof syntax is the first of 5 compiler extensions
needed to make Speckl-generated proto bindings a drop-in for the
handwritten proto/federated_meetup/v1/{state,rpc}.proto.
2026-06-28 21:49:26 -07:00
Scott Scoble
adc5b75bff feat(protobuf): honor speck.protoPackage and speck.goPackage overrides
The .proto backend previously emitted:
  package speckl.<name>.v1;
  option go_package = "github.com/sscoble/<name>/proto/.../v1;...v1";
hard-coded. Both fields are now overridable via the SpeckNode's
protoPackage / goPackage fields (populated by top-of-file metadata in
the .speckdl; see upcoming cycle 57 for the related oneof syntax which
will also be reading from the same SpeckNode).

For federated-meetup the overrides are:
  proto_package: "federated_meetup.v1"
  go_package:    "github.com/sscoble/federated-meetup/proto/...;federated_meetupv1"

The consumer Go code (internal/types/types.go, internal/group/) imports
"federated_meetup/v1" — without this override, the generated proto
would emit package "speckl.federated_meetup.v1" and the bindings
would not match the imports, breaking the consuming project's build.

Picks up a half-applied change left in the working tree from the
v0.6 review-v2 session. dist/ is gitignored; tsc build was clean
and all 39 baseline parser tests pass.
2026-06-28 21:49:19 -07:00
hermes
69f668db7c feat(speckl): protobuf backend + service block + record interface fields
- parser.ts: distinguish enum / record / service kinds in interface blocks
- parser.ts: add service { rpc X(R) returns (R); } syntax
- generators/protobuf.ts: emit proto3 (enums, messages, services) from .speckdl
- index.ts: add --target protobuf choice

Dogfood: FederatedMeetup.speckdl → 188-line .proto → buf generate →
working Go + ConnectRPC code with zero glue.

Closes speckl#61 (parser drops field declarations).
2026-06-28 01:25:54 -07:00
Scott Scoble
e6f95ddf0b fix(parser): parse multi-line input/output blocks as record types (speckl#57)
Multi-line `input: { ... }` and `output: { ... }` blocks fell through
to parseMember which returned `{ type: 'ident', name: '{' }` instead of
a record with fields. Broke RetryHandler.speck and similar real specks.

Fix: add `input:`/`output:` to blockStarters, dispatch to new
parseInputOutputBlock() that brace-counts across newlines, strips
line comments, and delegates to parseTypeExpr's record branch.

CoS-verified-live-close (cycle 126 pattern): subagent hit max_iterations
on the brace-count edge case. CoS applied focused fix + 2 new tests.
39/39 vitest pass.

Test evidence:
- RetryHandler.speck: input typeExpr.type='record' 4 fields
- 37 prior tests + 2 new = 39 passing
2026-06-09 13:53:02 -07:00
agent-engineering
d5eea7e7c7 fix(parser): multi-line verify: bodies — join depth-aware lines into temporalExpr (speckl#54)
- Add 'verify:' to the parseSpeck dispatch condition so multi-line verify:
  blocks reach the multi-line parser
- New parseVerifyColonBlock helper collects continuation lines using the
  same break-condition list as the existing skip-loop, with depth-balanced
  counter for (), [], {}
- Joins the parts with single spaces and returns a VerifyNode with a
  well-formed temporalExpr
- Closes sscoble/speckl#54 — unblocks B2 PR stack (#47 + #48 already closed)
- Pre-existing LTL inner-identifier scoping bug remains in #55 (out of scope)
2026-06-03 12:09:19 -07:00
agent-engineering
82c8bdc49b fix(speckl-48): implement verifyN() with LTL inner-expression translator (Z3 call-out, runtime-side) 2026-06-03 10:31:46 -07:00
Scott Scoble
d16df7581a fix: resolve all TypeScript codegen bugs for zero tsc --noEmit errors
Parser fixes:
- Fixed multiline state/init block parsing: trailing commas no longer
  treated as continuation markers, merging all state vars into one
- Added splitFields() function for comma-aware emit field splitting
  that respects parenthesized groups and quoted strings
- Fixed interface block parsing: enum-style interfaces (name-only
  values like 'Idle, Parsing, Parsed, ...') now correctly parsed as
  methods instead of being skipped (methods.length was 0)

TypeScript generator fixes:
- String[] array type syntax now correctly maps to string[] in output
- collectIdents filters out names containing brackets
- Duplicate event interfaces deduplicated (implicit emit events
  filtered when explicit events already declare them)
- join(X, Y) translates to X + Y (string concatenation) instead of
  X.join(Y) which is wrong for string args
- now() generates new Date().toISOString() instead of Date.now()
  to match String/string type annotations
- decisionLog type uses proper union: (A | B | C)[] instead of A[]
- String literals protected from this.-prefixing during identifier
  replacement (fixes 'leader' inside quotes becoming 'this.leader')
- Removed debug console.log from enum generation

Both TigerBeetleLedger.speck and speckl-compile.speckdl now generate
TypeScript that passes tsc --noEmit with zero errors.

Closes #9
2026-05-13 14:34:51 -07:00
Scott Scoble
dbde01ccb5 Iterative ternary if/else rewrite, Raft partial fix
- Replaced complex ternary regex with simpler iterative approach
- Raft: 3 errors → 9 errors (regression due to multiline let detection)
- Fixed simple single-line ternary expressions

Progress: 12 specs at 0 errors, Raft needs multiline let parsing
2026-05-11 21:30:38 -07:00
Scott Scoble
52be7b1c6d Fix SQLitePager, improve and/or rewrite, add emptySet
- SQLitePager: 4 errors → 0 errors
- Added emptySet → new Set() rewrite
- Fix and/or regex to avoid matching method calls like .or()
- Progress: 12 specs at 0 errors
2026-05-11 21:27:36 -07:00
Scott Scoble
832feb47b3 Fix SQLiteWAL: Map literals, nested bracket access, and/or, comments
- Added and/or -> &&/|| rewrite
- Strip // comments before TS processing
- Fix [a..b] range syntax to array literal
- Fix nested Map bracket access (rewrite inner expressions)
- Fix Map assignment index expression rewriting
- Convert {k: v} Map literals to new Map([[k, v]])
- SQLiteWAL: 19 errors -> 0 errors

Progress: 11 specs at 0 errors
2026-05-11 21:24:50 -07:00
Scott Scoble
7f417d37e0 Add not->! rewrite, Integer/Boolean types, ToggleSwitch 0 errors
- Added not -> ! negation rewrite (step 5d1)
- Added Integer and Boolean primitive type mappings
- ToggleSwitch: 3 errors → 0 errors
- PreconditionTest: partial fix (Integer type resolved)

Progress: 9 specs at 0 errors, 124 total remaining
2026-05-11 21:17:06 -07:00
Scott Scoble
a1efbadbd1 Preserve statement order, fix notIn with object literals, Paxos now 0 errors
- Replaced grouped statement emission with per-statement switch preserving order
- Moved notIn rewrite BEFORE object literal restoration to protect { } blocks
- Fixed if/else ternary expression regex for complex cases
- Paxos: 6 errors → 0 errors
- Total: 168 → 124 errors across 7 specs
2026-05-11 20:54:39 -07:00
Scott Scoble
71de0c1d35 Add ifblock parsing, fix blank-line separation, add ternary expr rewrite
- Parse SpeckDL if/else blocks as separate ifblock statement type
- Preserve blank lines as statement separators in action blocks
- Add regex for if:else ternary expression conversion
- Fix Map.get() non-null assertion (!)
- Fix set-difference for Maps (preserve Map type)
- Fix multiline object literal and type expression parsing

Results: Paxos 18→6, Raft 44→19, SQLitePager 32→9 errors
2026-05-11 20:25:05 -07:00
Scott Scoble
a392163630 Fix multiline type/object parsing, Map inline records, set-diff for Maps
- Add braceDepth tracking to Map/Set type parsing for inline records
- Fix multiline state block joining to preserve type expressions
- Fix multiline object literal parsing in assignments
- Add smart set-difference for Maps vs Sets
- Fix bare notIn for Sets
- Add non-null assertion (!) to Map.get() calls
- Add implicit event interface generation from emit statements
- Protect object literals from this. prefix injection
2026-05-11 18:42:59 -07:00
Scott Scoble
8b520284cb TS generator: fix #12 (5 expression patterns) + #11 (opaque record types)
#12: All 5 untranslated SpeckDL patterns now emit valid JS:
  - notIn → !.has(), old() → drop, union → Set spread,
    set diff → filter, append() → [...X, Y], length() → .length
  - ==/!= operator replacement fixed (word-boundary regression)
  - Postcondition object comparisons get @ts-expect-error TS2839

#11: Generator auto-detects opaque identifier types used in state
  variables and action params, emits 'type X = any' before class.
  - State properties use ! (non-null assertion) for strict mode

TigerBeetleLedger → tsc --noEmit = 0 errors. 37/37 tests pass.
2026-05-11 10:16:15 -07:00
0cefe0f694 fix(z3): robust = conversion + SQLite pager spec (DB.2)
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
- Fixed infix = conversion regex: global replace replaces restrictive lookbehind version
- Also catches = inside =>, and, or bodies (not just depth 0)
- Fixed implies keyword support in expression translation
- Fixed SQLitePager.speck: implies→->, string-typed enum comparison fix
- All specs (ToggleSwitch, KafkaKRaft, SQLitePager) produce zero-error SMT-LIB2
- All 37 tests pass
2026-05-10 01:02:00 -07:00
f62bdc33f5 feat(rust): Rust codegen backend (speckl#24)
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
- New generator: compiler/src/generators/rust.ts (652 lines)
- --target=rust CLI option produces full Rust crate (Cargo.toml + src/lib.rs)
- Types: Nat→u64, Bool→bool, String→String, Set→HashSet, Map→HashMap
- State variables → Rust struct fields
- Actions → Rust impl methods returning Result
- Expression rewriter: .contains(&x), .get(&k), .len(), etc.
- Tested against ToggleSwitch, TigerBeetleLedger, KafkaKRaft
- All 37 existing tests pass

WASM path: .speck → Rust → wasm-pack → WASM (now complete)
2026-05-09 23:24:40 -07:00
40eef36a10 feat(z3): zero-error SMT-LIB2 output + BMC verify blocks for KRaft
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 3s
- Declare speckl_len_Int helper for list/sequence lengths
- Fix infix = inside => bodies (final pass regex)
- Handle prime notation: currentTerm' → currentTerm_post
- Handle length() calls: length(log) → (speckl_len_Int log)
- Handle min(): (ite (<= a b) a b) instead of unsupported 'min'
- Handle subtraction: i-1 → (- i 1)
- Declare _post state vars when invariants compare pre/post
- Added 3 BMC verify blocks to KRaft spec (TermMonotonicity, CommitSafety, LeaderCompleteness)
- Z3 returns clean sat with zero errors for all specs
- All 37 tests pass
2026-05-09 23:07:56 -07:00
25fbdfc7a9 fix(z3): clean SMT-LIB2 output — Z3 returns sat with model (zero sort errors)
- Sanitize sort names: Option(NodeId) → Option_NodeId
- Emit action transition predicates only for verify/BMC mode
- Handle -> implication operator in translateExpr
- Fleshed out KRaft invariant bodies (4/6 now have real expressions)
- ToggleSwitch: clean sat with model
- TigerBeetleLedger: sat with model
- KafkaKRaft: sat with model
- All 37 tests pass
2026-05-09 22:38:47 -07:00
Scott Scoble
c5ca757e80 Merge branch 'feature/z3-backend' (Z3 backend + KRaft spec) 2026-05-09 22:34:40 -07:00
6c6daaf90f feat(compiler): Z3 SMT-LIB2 verification backend (speckl#21)
- New generator: compiler/src/generators/z3.ts (1,034 lines)
- Translates SpeckDL state → Z3 sort declarations
- Translates invariants → Z3 define-fun returning Bool
- Translates actions → Z3 transition predicates (enabled + step)
- BMC unrolling with --verify-depth=N (default: 10)
- CLI: --target=z3 or --target=all
- ToggleSwitch and TigerBeetleLedger generate valid SMT-LIB2
- KafkaKRaft spec generates valid SMT-LIB2 structure
- Complex forall/set expressions emit 'true' as placeholders (to be improved)
- All 41 existing tests still pass

This unblocks M1.2 (KRaft verification), M3.2 (Show HN), and M4.3 (OAuth verification).
2026-05-09 22:28:39 -07:00
1cbad9d756 fix(compiler): translate Map/Set expression patterns to JavaScript (#18)
- x in someSet → someSet.has(x)
- x in someMap.keys → someMap.has(x)
- x notIn someMap.keys → !someMap.has(x)
- m[k] → m.get(k) for Map state variables
- s.size() → s.size (property, not method)
- Track Map-typed state variables via mapVarNames for accurate .get() translation
- Thread mapVarNames through emitAction and rewriteExpr

All 41 tests passing. TigerBeetleLedger now generates correct TypeScript
with .get() for Map access and .has() for Set membership.

Part of M0.1: Fix TS generator bugs (speckl#18)
2026-05-09 22:07:32 -07:00
Scott Scoble
898e2e53ae fix(compiler): protect object literal identifiers from this. prefix in rewriteExpr
Object spread expressions like { ...t, pending: false } were getting
spurious this. prefix applied to keys (this.pending: false), causing
3 tsc errors (TS1005: ':' expected). Fix: extract object literals
containing spread before Step 3 identifier rewriting, then restore.

Results: TigerBeetle TS errors reduced to known record-type issues (eng#11).
All 41 tests passing.
2026-05-09 20:06:36 -07:00
Scott Scoble
c3ade18feb fix(parser): parse parenthetical type syntax Map(K,V), Set(T), List(T)
parseTypeExpr only handled angle-bracket generics (Map<K,V>, Set<T>, List<T>)
but SpeckDL canonical syntax uses parentheses: Map(Nat, Account), Set(Nat), List(LogEntry).

This caused two bugs:
1. State property types emitted as raw strings (accounts: Map(Nat, Account))
2. Parametric types treated as enum interface names (TigerBeetleLedgerMap(Nat, Account))

Added List(T), Set(T), Map(K,V) handlers alongside existing angle-bracket handlers.
All 41/41 tests passing. TigerBeetle now generates clean TS types:
- accounts: Map<number, Account> (was Map(Nat, Account))
- pending: Set<number> (was Set(Nat))
- log: LogEntry[] (was List(LogEntry))
2026-05-09 18:09:31 -07:00
Scott Scoble
e92f430fb3 fix(compiler): translate 5 SpeckDL expression patterns to JavaScript
- notIn -> !map.has(key): 't.id notIn transfers.keys' -> '!transfers.has(t.id)'
- old(expr) -> drop wrapper: specification construct, no JS equivalent
- union -> Set spread: 'pending union {t.id}' -> 'new Set([...pending, t.id])'
- \ -> Set difference via filter: 'pending \ {id}' -> 'new Set([...pending].filter(x => x !== id))'
- Trailing == at end-of-string -> ===, then clean up empty RHS comparisons

All 41 tests passing. TigerBeetle: 0 notIn, 0 old(, 0 union, 0 \, 0 ==)
2026-05-09 16:08:46 -07:00
Scott Scoble
9de0a5ca0f feat(compiler): parse precondition/postcondition in action blocks
- Add 'precondition' and 'postcondition' ActionStatement types
- Parse precondition: expr and postcondition: expr in action bodies
- Support 'or' continuation lines for multi-clause pre/postconditions
- TigerBeetleLedger now parses 21 preconditions + 24 postconditions
  (previously all dropped, resulting in empty action bodies)
- All 41 existing tests pass

Addresses marcus/engineering#9 (parser gap)
Does NOT wire into generators yet — TypeScript/WASM output
will be empty for pre/postconditions. Generator wiring
is follow-up work.
2026-05-09 09:12:15 -07:00
Scott Scoble
acb464f957 fix(wasm): remove spurious i32.const from uncompiled assignment placeholders
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 4s
Undcompiled map/record/unrecognized assignments emitted (i32.const 1)
placeholders that left values on the WASM stack. Combined with
the default return i32.const 1 at end of function, this caused
type mismatch errors (expected [], got [i32, i32] or [i32]).

Fix: make uncompiled assignments emit comment-only output with
no stack effect. Scalar assignments already work correctly since
emitExpr feeds into global.set which consumes the value.

Closes #9 WASM E2E portion — parser fix for precondition:/postcondition:
keyword handling still needed.
2026-05-06 05:14:33 -07:00
Scott Scoble
a2f7b7b2e9 fix: add dist-check guard + List<T> type parsing + state machine examples
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
- check-dist.sh: Prevents DIST DESYNC by verifying all dist/*.js files
  are newer than their src/*.ts sources. Fails build if stale.
- Makefile: Standard build targets (build/check-dist/test/recompile/clean)
- src/index.ts: Integrated dist freshness check before compilation
- src/parser.ts: Added List<T> type parsing alongside existing Set<T>/Map<K,V>
  (e.g., 'log: List<LogEntry>' now parses as { type: 'list', elementType })
- examples/: Restored v0.3 state machine specs (TigerBeetleLedger,
  AccountLedger, ToggleSwitch, TwoPhaseCommit)
- DIST DESYNC is a known recurring anti-pattern — this guard prevents it

Tests: 39/41 pass (2 pre-existing E2E WASM failures due to missing wabt)
2026-05-05 03:11:53 -07:00
Scott Scoble
7230650628 feat(compiler): self-hosting spec + generator multi-enum support
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
- Add speckl-compile.speckdl — compiler formally specified in SpeckDL
- Generator: multi-enum detection, value→enum mapping, trailing comma cleanup
- Emit blocks collapsed to single-line (parser limitation)
- Generated artifacts: provenance, BOMs, WAT, TypeScript (2 known bugs → engineering#8)
2026-05-03 14:42:03 -07:00
Scott Scoble
39436b4382 WASM E2E test: Speckl specs compile to runnable WebAssembly
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 3s
- Add __tests__/e2e-wasm.test.ts: full pipeline test
  - Parse Counter speck → generate WAT → assemble with wabt → instantiate → run
  - Verify init, action calls, guard pass/fail, state mutations
  - TigerBeetle WAT validation test (wabt parse + validate + toBinary)
- Fix wasm.ts bugs found by E2E:
  - Guard condition inverted: added i32.eqz to negate comparison
  - Missing local declarations: locals now inserted before body
  - Uncompiled guards left stack values: now emit comments only (no i32.const 1)
- Install wabt npm package for WAT→WASM assembly in tests
- 41/41 tests passing (39 original + 2 E2E)

Milestone: Speckl produces runnable WebAssembly from specs.
2026-05-03 13:13:55 -07:00