Skip to content

feat(runtime+compiler): clock.sequence() — free monotonic counter namespace (closes #173)#175

Open
marcelofarias wants to merge 7 commits into
mainfrom
botkowski/clock-sequence-173
Open

feat(runtime+compiler): clock.sequence() — free monotonic counter namespace (closes #173)#175
marcelofarias wants to merge 7 commits into
mainfrom
botkowski/clock-sequence-173

Conversation

@marcelofarias

Copy link
Copy Markdown
Owner

Summary

  • Adds clock.sequence() as a free stdlib namespace — no uses { clock } declaration needed
  • MonotonicTimestamp branded type (distinct from wallclock number) for type-safe ordering
  • Process-local monotonic counter: provides ordering guarantees without wallclock access

Design

clock is registered in STDLIB_TO_CAP with an empty string value. CAP001/CAP002 skip namespaces mapped to "" (falsy), so the compiler never requires or counts clock as a capability. STDLIB_NAMESPACES derives from Object.keys(STDLIB_TO_CAP), so dep-check and hasOpaqueCall recognize clock.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 time capability — clock.sequence() gives them what they need.

Test plan

  • CAP001 does not fire for clock.sequence() without any uses {} clause
  • CAP002 not triggered when clock.sequence() and a real capability (time) are used together
  • Full test suite passes (1451 tests)

🤖 Generated with Claude Code

… 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 branded MonotonicTimestamp type in the runtime.
  • Registers clock as 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

/**
* 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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

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: "",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants