feat(runtime+compiler): clock.sequence() — free monotonic counter namespace (closes #173)#175
Open
marcelofarias wants to merge 7 commits into
Open
feat(runtime+compiler): clock.sequence() — free monotonic counter namespace (closes #173)#175marcelofarias wants to merge 7 commits into
marcelofarias wants to merge 7 commits into
Conversation
… namespace Closes #173. Implements a minimal clock namespace providing process-local monotonic ordering without wallclock access or capability declaration requirements. Runtime (effects.ts): - Add MonotonicTimestamp branded type (number & { __brand: "MonotonicTimestamp" }) - Add clock.sequence() — increments a process-local counter starting at 0, returns MonotonicTimestamp. No $require() call; no external state. Compiler (_stdlib.ts): - Add clock: "" to STDLIB_TO_CAP. Empty string is falsy — CAP001 and CAP002 skip capability checks for clock (if (!cap) continue), so uses { clock } is never required. STDLIB_NAMESPACES derives from Object.keys(STDLIB_TO_CAP), so dep-check and hasOpaqueCall correctly exclude clock.sequence() from opaque-call detection. Compiler (imports.ts): - Add "clock" to STDLIB_VALUE_SYMBOLS and STDLIB_NAMESPACE_NAMES so the transform pass emits the correct import for clock in compiled output. Tests (cap-check.test.ts): - Verify CAP001 does not fire for clock.sequence() without any uses {} clause. - Verify CAP002 is not triggered when clock.sequence() and a real capability (time) are used together. Design notes: - MonotonicTimestamp is distinct from number at the type level; it cannot be passed to APIs expecting wallclock values without an explicit cast. - clock.sequence() counter resets on process restart — suitable for intra-run ordering, not cross-service correlation. Cross-service correlation requires uses { time } and explicit wallclock injection. - No SYN020/SYN021 interaction: those check Date.now()/performance.now(), not clock.sequence(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a new “free” stdlib namespace, clock.sequence(), intended to provide process-local monotonic ordering without requiring a uses { ... } capability declaration. This spans both the runtime (new API + branded type) and the compiler (stdlib namespace registration + capability-check behavior), with targeted compiler tests to ensure CAP001/CAP002 do not fire for clock.sequence().
Changes:
- Introduces
clock.sequence()and a brandedMonotonicTimestamptype in the runtime. - Registers
clockas a stdlib namespace mapped to no capability ("") so CAP checks skip it. - Extends compiler import handling and adds cap-check tests to validate the new “free namespace” behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/runtime/src/effects.ts | Adds MonotonicTimestamp and the clock.sequence() monotonic counter implementation. |
| packages/compiler/tests/cap-check.test.ts | Adds tests verifying clock.sequence() does not trigger CAP001/CAP002. |
| packages/compiler/src/passes/imports.ts | Adds clock to stdlib auto-importable value symbols and namespace set. |
| packages/compiler/src/passes/_stdlib.ts | Registers clock as a stdlib namespace with no capability requirement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+85
to
+96
| // Process-local monotonic sequence counter. Starts at 0, increments by 1. | ||
| // Does not require a capability declaration — no external state, no wallclock. | ||
| let _clockSeq = 0; | ||
|
|
||
| export const clock = { | ||
| /** | ||
| * Returns the next MonotonicTimestamp for the current process execution. | ||
| * Each call returns a value strictly greater than the previous call's value. | ||
| * Suitable for ordering events in structured logs without declaring `uses { time }`. | ||
| */ | ||
| sequence: (): MonotonicTimestamp => _clockSeq++ as MonotonicTimestamp, | ||
| }; |
Addresses Copilot review comment on PR #175: clock.sequence() had no runtime test. Asserts (1) calling inside $enter([]) does not throw and (2) successive calls are strictly increasing. Co-Authored-By: Botkowski <noreply@botkowski.ai>
Comment on lines
+14
to
+18
| // clock is a free namespace — no capability declaration required. | ||
| // clock.sequence() returns a MonotonicTimestamp: a process-local monotonic counter | ||
| // that provides ordering guarantees without wallclock access. CAP001 skips namespaces | ||
| // mapped to "" (falsy), so `uses { clock }` is never required. | ||
| clock: "", |
…otent fns clock is a stateful-free namespace — no capability required, but clock.sequence() is non-deterministic (monotonic counter). INT002 (pure) and INT004 (idempotent) now fire when a fn body calls clock.sequence() via a new findFirstStatefulFreeUse scan backed by STATEFUL_FREE_NAMESPACES in _stdlib.ts. Addresses Copilot review comment on PR #175. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| /** | ||
| * MonotonicTimestamp — an opaque, process-local, ordered counter value. | ||
| * Provides ordering guarantees (A happened before B) without wallclock access. | ||
| * Not serializable as real time; not comparable across processes or restarts. |
Comment on lines
+398
to
+418
| function findFirstStatefulFreeUse( | ||
| tokens: Token[], | ||
| fn: FnDecl, | ||
| allDecls: FnDecl[], | ||
| ): { namespace: string; member: string; accessOp: "." | "?." } | null { | ||
| const inner = allDecls.filter( | ||
| (g) => g !== fn && g.tokenStart >= fn.tokenStart && g.tokenEnd <= fn.tokenEnd, | ||
| ); | ||
| for (let i = fn.bodyTokenStart ?? fn.tokenStart; i < fn.tokenEnd; i++) { | ||
| if (insideAny(i, inner)) continue; | ||
| const tok = tokens[i]; | ||
| if (!tok || tok.kind !== "ident") continue; | ||
| if (!STATEFUL_FREE_NAMESPACES.has(tok.text)) continue; | ||
| const j = nextSignificant(tokens, i + 1); | ||
| const next = tokens[j]; | ||
| const isDot = next?.kind === "punct" && next.text === "."; | ||
| const isOptChain = next?.kind === "questionDot"; | ||
| if (!isDot && !isOptChain) continue; | ||
| const member = nextIdent(tokens, j) ?? "…"; | ||
| return { namespace: tok.text, member, accessOp: isDot ? "." : "?." }; | ||
| } |
Comment on lines
+14
to
+18
| // clock is a free namespace — no capability declaration required. | ||
| // clock.sequence() returns a MonotonicTimestamp: a process-local monotonic counter | ||
| // that provides ordering guarantees without wallclock access. CAP001 skips namespaces | ||
| // mapped to "" (falsy), so `uses { clock }` is never required. | ||
| clock: "", |
…intent-check shadow/alias, overflow guard - uns-check: filter STDLIB_CAPS to truthy capabilities only so clock (mapped to "") never triggers UNS005 on capability-free calls - intent-check: pass declAliases + declBlockShadows into findFirstStatefulFreeUse so block-shadowed `clock` bindings don't false-positive and module-level aliases are resolved; mirrors findFirstCapabilityUse handling - effects.ts: throw RangeError on MAX_SAFE_INTEGER overflow so the monotonic ordering guarantee holds for the documented safe range Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+44
to
+46
| // Only namespaces with a truthy capability string trigger UNS005. | ||
| // Free namespaces (clock: "") have no external capability and must not fire. | ||
| const STDLIB_CAPS = new Set(Object.keys(STDLIB_TO_CAP).filter((k) => STDLIB_TO_CAP[k])); |
Comment on lines
+85
to
+89
| // Process-local monotonic sequence counter. Starts at 0, increments by 1. | ||
| // Does not require a capability declaration — no external state, no wallclock. | ||
| let _clockSeq = 0; | ||
|
|
||
| export const clock = { |
Comment on lines
+14
to
+18
| // clock is a free namespace — no capability declaration required. | ||
| // clock.sequence() returns a MonotonicTimestamp: a process-local monotonic counter | ||
| // that provides ordering guarantees without wallclock access. CAP001 skips namespaces | ||
| // mapped to "" (falsy), so `uses { clock }` is never required. | ||
| clock: "", |
- uns-check.test.ts: assert clock.sequence() does NOT trigger UNS005 (free namespace with empty capability string must be skipped by the UNS005 check, not treated as an external contract-required call) - examples/node-app/src/log-events.bs: end-to-end usage showing clock.sequence() for monotonic event ordering without the time capability Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+422
to
+428
| const j = nextSignificant(tokens, i + 1); | ||
| const next = tokens[j]; | ||
| const isDot = next?.kind === "punct" && next.text === "."; | ||
| const isOptChain = next?.kind === "questionDot"; | ||
| if (!isDot && !isOptChain) continue; | ||
| const member = nextIdent(tokens, j) ?? "…"; | ||
| return { namespace: canonical, member, accessOp: isDot ? "." : "?." }; |
…ember reads findFirstStatefulFreeUse was returning a violation when it saw clock.<member> even without a call (e.g. const f = clock.sequence). Fix requires the token after the member ident to be an open paren (or optional-call questionDot), matching the call-detection pattern used throughout syn-check.ts. Also adds INT002/INT004 regression tests for the non-invocation case. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+431
to
+437
| // Only flag actual invocations (clock.sequence()), not bare member reads (clock.sequence). | ||
| const afterMemberIdx = nextSignificant(tokens, memberIdx + 1); | ||
| const afterMember = tokens[afterMemberIdx]; | ||
| const isCall = | ||
| (afterMember?.kind === "open" && afterMember.text === "(") || | ||
| afterMember?.kind === "questionDot"; // clock.sequence?.() | ||
| if (!isCall) continue; |
…sequence?.length false-positive) findFirstStatefulFreeUse was treating any `questionDot` token after the member name as evidence of an optional call. `?.` is also used for optional property access (e.g. clock.sequence?.length), which should NOT count as an invocation and must not trigger INT002/INT004. Fix: require the token after `questionDot` to be `(` before treating it as an optional call, matching how findFirstCapabilityUse handles the same case. Add two regression tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| tokens[nextSignificant(tokens, afterMemberIdx + 1)]?.kind === "open" && | ||
| tokens[nextSignificant(tokens, afterMemberIdx + 1)]?.text === "("); // clock.sequence?.() | ||
| if (!isCall) continue; | ||
| return { namespace: canonical, member, accessOp: isDot ? "." : "?." }; |
Comment on lines
+201
to
+205
| // INT002: stateful-free namespace variant — clock.sequence() and similar are | ||
| // capability-free (no `uses {}` required) but non-deterministic. A pure fn | ||
| // must be deterministic, so calling these still violates the claim. | ||
| const statefulFreeUse = findFirstStatefulFreeUse(tokens, decl, allDecls, declAliases, declBlockShadows); | ||
| if (statefulFreeUse) { |
Comment on lines
+14
to
+18
| // clock is a free namespace — no capability declaration required. | ||
| // clock.sequence() returns a MonotonicTimestamp: a process-local monotonic counter | ||
| // that provides ordering guarantees without wallclock access. CAP001 skips namespaces | ||
| // mapped to "" (falsy), so `uses { clock }` is never required. | ||
| clock: "", |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
clock.sequence()as a free stdlib namespace — nouses { clock }declaration neededMonotonicTimestampbranded type (distinct from wallclocknumber) for type-safe orderingDesign
clockis registered inSTDLIB_TO_CAPwith an empty string value. CAP001/CAP002 skip namespaces mapped to""(falsy), so the compiler never requires or countsclockas a capability.STDLIB_NAMESPACESderives fromObject.keys(STDLIB_TO_CAP), so dep-check andhasOpaqueCallrecognizeclock.sequence()as stdlib and don't flag it as an opaque external call.Addresses the Moltbook discussion: structured loggers that need event ordering within a run don't need the
timecapability —clock.sequence()gives them what they need.Test plan
clock.sequence()without anyuses {}clauseclock.sequence()and a real capability (time) are used together🤖 Generated with Claude Code