chore: import upstream snapshot with attribution
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
CI / Deep Native Runtime Cases (1/6) (push) Has been skipped
CI / Native Preflight (push) Failing after 1s
CI / Native Runtime Cases (1/2) (push) Failing after 0s
CI / Native Runtime Cases (2/2) (push) Failing after 1s
CI / Native Metadata Reports (push) Failing after 0s
CI / Native Direct Backend Artifacts (push) Failing after 0s
CI / Native Sanitizer Smoke (push) Failing after 1s
CI / Command Contract Snapshots (push) Failing after 1s
CI / Deep Conformance Suite (push) Has been skipped
CI / Graph Build Perf (push) Failing after 1s
CI / Deep Native Preflight (push) Has been skipped
CI / Deep Native Runtime Cases (2/6) (push) Has been skipped
CI / Deep Native Runtime Cases (3/6) (push) Has been skipped
CI / Conformance Suite (push) Failing after 1s
CI / Workspace Checks (push) Failing after 0s
CI / Deep Native Runtime Cases (5/6) (push) Has been skipped
CI / Deep Native Runtime Cases (6/6) (push) Has been skipped
CI / Deep Native Runtime Cases (4/6) (push) Has been skipped
CI / Deep Graph Build Perf (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.next
|
||||
out
|
||||
.env*.local
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,155 @@
|
||||
import Link from "next/link";
|
||||
import type { Metadata } from "next";
|
||||
import { notFound } from "next/navigation";
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import rehypeSlug from "rehype-slug";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { docs, getAdjacentDocs } from "@/lib/docs";
|
||||
import { extractHeadings, readArticleByPath } from "@/lib/articles";
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
import { DocsToc } from "@/components/docs-toc";
|
||||
import { CopyCodeButton } from "@/components/copy-button";
|
||||
import { HeadingAnchor } from "@/components/heading-anchor";
|
||||
import { rehypeZeroHighlight } from "@/lib/rehype-zero-highlight";
|
||||
import { rehypeJsonRender } from "@/lib/rehype-json-render";
|
||||
import { AgentChat } from "@/components/agent-chat";
|
||||
import { FlowChart } from "@/components/flow-chart";
|
||||
|
||||
type DocsPageParams = { slug?: string[] };
|
||||
type DocsPageProps = { params: Promise<DocsPageParams> };
|
||||
|
||||
export function generateStaticParams(): DocsPageParams[] {
|
||||
return docs.map((doc) => ({
|
||||
slug: doc.path.replace(/^\//, "").split("/"),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: DocsPageProps): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
return pageMetadata((slug ?? []).join("/"));
|
||||
}
|
||||
|
||||
const rehypePrettyCodeOptions = {
|
||||
theme: { light: "github-light", dark: "github-dark" },
|
||||
keepBackground: false,
|
||||
};
|
||||
|
||||
type HeadingProps = HTMLAttributes<HTMLHeadingElement> & {
|
||||
id?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
function makeHeading(Tag: "h2" | "h3" | "h4") {
|
||||
return function Heading({ id, children, ...rest }: HeadingProps) {
|
||||
return (
|
||||
<Tag id={id} {...rest} className="group">
|
||||
{children}
|
||||
<HeadingAnchor id={id} />
|
||||
</Tag>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const mdxComponents = {
|
||||
pre: ({ children, ...props }: HTMLAttributes<HTMLPreElement>) => (
|
||||
<div data-code-block className="relative">
|
||||
<CopyCodeButton />
|
||||
<pre {...props}>{children}</pre>
|
||||
</div>
|
||||
),
|
||||
h2: makeHeading("h2"),
|
||||
h3: makeHeading("h3"),
|
||||
h4: makeHeading("h4"),
|
||||
agentchat: ({ value }: { value?: string }) => {
|
||||
if (!value) return null;
|
||||
try {
|
||||
const spec = JSON.parse(Buffer.from(value, "base64").toString("utf8"));
|
||||
return <AgentChat spec={spec} />;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
flowchart: ({ value }: { value?: string }) => {
|
||||
if (!value) return null;
|
||||
try {
|
||||
const spec = JSON.parse(Buffer.from(value, "base64").toString("utf8"));
|
||||
return <FlowChart spec={spec} />;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default async function DocsPage({ params }: DocsPageProps) {
|
||||
const { slug } = await params;
|
||||
const routePath = "/" + (slug ?? []).join("/");
|
||||
const result = await readArticleByPath(routePath);
|
||||
if (!result) notFound();
|
||||
|
||||
const { doc, source } = result;
|
||||
const headings = extractHeadings(source);
|
||||
const { prev, next } = getAdjacentDocs(doc.slug);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="mx-auto w-full max-w-[54rem] px-4 pb-[50vh] pt-6 md:px-12">
|
||||
<header className="mb-8 border-b border-border pb-6">
|
||||
<p className="mb-2 text-[0.8125rem] font-medium tracking-wide text-muted">
|
||||
{doc.section ?? "Documentation"}
|
||||
</p>
|
||||
<h1 className="m-0 text-[clamp(1.75rem,5vw,2.5rem)] font-bold leading-[1.15] tracking-[-0.04em]">
|
||||
{doc.title}
|
||||
</h1>
|
||||
<p className="m-0 mt-3 max-w-[42rem] leading-[1.7] text-muted">{doc.description}</p>
|
||||
</header>
|
||||
|
||||
<article className="prose prose-zinc dark:prose-invert max-w-none prose-headings:scroll-mt-20 prose-headings:tracking-[-0.03em] prose-headings:font-semibold prose-a:text-blue prose-a:font-medium prose-a:no-underline prose-a:hover:underline prose-code:font-normal">
|
||||
<MDXRemote
|
||||
source={source}
|
||||
components={mdxComponents}
|
||||
options={{
|
||||
mdxOptions: {
|
||||
format: "md",
|
||||
remarkPlugins: [remarkGfm],
|
||||
rehypePlugins: [
|
||||
rehypeJsonRender,
|
||||
rehypeSlug,
|
||||
[rehypePrettyCode, rehypePrettyCodeOptions],
|
||||
rehypeZeroHighlight,
|
||||
],
|
||||
},
|
||||
parseFrontmatter: false,
|
||||
}}
|
||||
/>
|
||||
</article>
|
||||
|
||||
<nav aria-label="Pagination" className="mt-16 grid grid-cols-1 gap-4 border-t border-border pt-8 md:grid-cols-2">
|
||||
{prev ? (
|
||||
<Link
|
||||
href={prev.path}
|
||||
className="flex flex-col gap-1 rounded-lg border border-border p-4 no-underline transition hover:border-muted"
|
||||
>
|
||||
<span className="text-xs font-medium uppercase tracking-[0.04em] text-muted">Previous</span>
|
||||
<span className="text-[0.9375rem] font-semibold text-blue">{prev.title}</span>
|
||||
</Link>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{next ? (
|
||||
<Link
|
||||
href={next.path}
|
||||
className="flex flex-col items-end gap-1 rounded-lg border border-border p-4 text-right no-underline transition hover:border-muted"
|
||||
>
|
||||
<span className="text-xs font-medium uppercase tracking-[0.04em] text-muted">Next</span>
|
||||
<span className="text-[0.9375rem] font-semibold text-blue">{next.title}</span>
|
||||
</Link>
|
||||
) : null}
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<DocsToc headings={headings} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { DocsSidebarShell } from "@/components/docs-sidebar";
|
||||
import { docs, groupBySection } from "@/lib/docs";
|
||||
|
||||
export default function DocsLayout({ children }: { children: ReactNode }) {
|
||||
const groups = groupBySection(docs);
|
||||
|
||||
return (
|
||||
<div className="grid min-h-screen grid-cols-1 md:grid-cols-[15rem_minmax(0,1fr)] lg:grid-cols-[15rem_minmax(0,1fr)_14rem]">
|
||||
<DocsSidebarShell groups={groups} />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { convertToModelMessages, stepCountIs, streamText } from "ai";
|
||||
import type { ModelMessage, UIMessage } from "ai";
|
||||
import { createBashTool } from "bash-tool";
|
||||
import { docs } from "@/lib/docs";
|
||||
import { readArticleBySlug } from "@/lib/articles";
|
||||
import {
|
||||
docsChatDailyRateLimit,
|
||||
docsChatMinuteRateLimit,
|
||||
isDocsChatRateLimitConfigured,
|
||||
} from "@/lib/rate-limit";
|
||||
|
||||
export const maxDuration = 60;
|
||||
|
||||
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";
|
||||
|
||||
const SYSTEM_PROMPT = `You are a helpful documentation assistant for Zero, a pre-1 experimental programming language project exploring agent-first design. Zero intentionally makes breaking changes while searching for the language, library, and tooling patterns that work best for agents. Security vulnerabilities should be expected; Zero should be run or developed only in safe, non-production environments.
|
||||
|
||||
GitHub repository: https://github.com/vercel-labs/zerolang
|
||||
Documentation: https://zerolang.ai
|
||||
|
||||
You have access to the full Zero documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory. The file layout matches each page's URL slug — e.g. /workspace/index.md is the home, /workspace/getting-started.md is /getting-started, /workspace/modules/parse.md is /modules/parse.
|
||||
|
||||
When answering questions:
|
||||
- Use the bash tool to list files (ls /workspace/) or search for content (grep -r "keyword" /workspace/)
|
||||
- Use the readFile tool to read specific documentation pages (e.g. readFile with path "/workspace/getting-started.md")
|
||||
- Do NOT use bash to write, create, modify, or delete files (no tee, cat >, sed -i, echo >, cp, mv, rm, mkdir, touch, etc.) — you are read-only
|
||||
- Always base your answers on the actual documentation content
|
||||
- Be concise and accurate
|
||||
- If the docs don't cover a topic, say so honestly
|
||||
- Do NOT include source references or file paths in your response
|
||||
- Do NOT use emojis in your responses
|
||||
- When showing Zero projection syntax, use \`\`\`zero fenced blocks and say it is a human-readable projection of the graph. When showing shell commands, use \`\`\`sh.`;
|
||||
|
||||
type DocsFiles = Record<string, string>;
|
||||
|
||||
let cachedDocsFiles: DocsFiles | null = null;
|
||||
|
||||
async function loadDocsFiles(): Promise<DocsFiles> {
|
||||
if (cachedDocsFiles) return cachedDocsFiles;
|
||||
|
||||
const files: DocsFiles = {};
|
||||
const results = await Promise.allSettled(
|
||||
docs.map(async (doc) => {
|
||||
const md = await readArticleBySlug(doc.slug);
|
||||
if (!md) return null;
|
||||
const fileName = doc.path === "/" ? "/index.md" : `${doc.path}.md`;
|
||||
return { fileName: fileName.replace(/^\//, ""), md };
|
||||
}),
|
||||
);
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled" && result.value) {
|
||||
files[result.value.fileName] = result.value.md;
|
||||
}
|
||||
}
|
||||
cachedDocsFiles = files;
|
||||
return files;
|
||||
}
|
||||
|
||||
function addCacheControl(messages: ModelMessage[]): ModelMessage[] {
|
||||
if (messages.length === 0) return messages;
|
||||
return messages.map((message, index) => {
|
||||
if (index === messages.length - 1) {
|
||||
return {
|
||||
...message,
|
||||
providerOptions: {
|
||||
...message.providerOptions,
|
||||
anthropic: { cacheControl: { type: "ephemeral" } },
|
||||
},
|
||||
};
|
||||
}
|
||||
return message;
|
||||
});
|
||||
}
|
||||
|
||||
function getRateLimitIdentifier(req: Request): string {
|
||||
const forwardedFor = req.headers.get("x-forwarded-for");
|
||||
const realIp = req.headers.get("x-real-ip");
|
||||
return forwardedFor?.split(",")[0]?.trim() || realIp?.trim() || "anonymous";
|
||||
}
|
||||
|
||||
async function rateLimitRequest(req: Request): Promise<NextResponse | null> {
|
||||
if (!isDocsChatRateLimitConfigured()) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Chat rate limiting is not configured",
|
||||
message: "Set KV_REST_API_URL and KV_REST_API_TOKEN to enable docs chat.",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const identifier = getRateLimitIdentifier(req);
|
||||
let minuteResult;
|
||||
let dailyResult;
|
||||
try {
|
||||
[minuteResult, dailyResult] = await Promise.all([
|
||||
docsChatMinuteRateLimit.limit(identifier),
|
||||
docsChatDailyRateLimit.limit(identifier),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error("Docs chat rate limiting failed", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Chat rate limiting unavailable",
|
||||
message: "Docs chat is temporarily unavailable because rate limiting could not be applied.",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
if (minuteResult.success && dailyResult.success) return null;
|
||||
|
||||
const isMinuteLimit = !minuteResult.success;
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Rate limit exceeded",
|
||||
message: isMinuteLimit
|
||||
? "Too many requests. Please wait a moment before trying again."
|
||||
: "Daily limit reached. Please try again tomorrow.",
|
||||
},
|
||||
{ status: 429 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
if (!process.env.AI_GATEWAY_API_KEY && !process.env.VERCEL_OIDC_TOKEN) {
|
||||
return NextResponse.json(
|
||||
{ error: "Chat is not configured. Set AI_GATEWAY_API_KEY or configure deployment OIDC." },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
|
||||
const rateLimitResponse = await rateLimitRequest(req);
|
||||
if (rateLimitResponse) return rateLimitResponse;
|
||||
|
||||
let body: { messages?: UIMessage[] } | null;
|
||||
try {
|
||||
body = (await req.json()) as { messages?: UIMessage[] };
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const messages = Array.isArray(body?.messages) ? body.messages : [];
|
||||
|
||||
const docsFiles = await loadDocsFiles();
|
||||
const {
|
||||
tools: { bash, readFile },
|
||||
} = await createBashTool({ files: docsFiles });
|
||||
|
||||
const result = streamText({
|
||||
model: DEFAULT_MODEL,
|
||||
system: SYSTEM_PROMPT,
|
||||
messages: await convertToModelMessages(messages),
|
||||
stopWhen: stepCountIs(5),
|
||||
tools: { bash, readFile },
|
||||
prepareStep: ({ messages: stepMessages }) => ({
|
||||
messages: addCacheControl(stepMessages),
|
||||
}),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSearchIndex } from "@/lib/search-index";
|
||||
import type { SearchResult } from "@/lib/types";
|
||||
|
||||
type ScoredSearchResult = SearchResult & { score: number };
|
||||
|
||||
function isScoredResult(result: ScoredSearchResult | null): result is ScoredSearchResult {
|
||||
return result !== null;
|
||||
}
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const q = req.nextUrl.searchParams.get("q")?.trim().toLowerCase();
|
||||
if (!q) return NextResponse.json({ results: [] });
|
||||
|
||||
const index = await getSearchIndex();
|
||||
const terms = q.split(/\s+/).filter(Boolean);
|
||||
|
||||
const results = index
|
||||
.map((entry) => {
|
||||
const titleLower = entry.title.toLowerCase();
|
||||
const contentLower = entry.content.toLowerCase();
|
||||
|
||||
const titleMatch = terms.every((t) => titleLower.includes(t));
|
||||
const contentMatch = terms.every((t) => contentLower.includes(t));
|
||||
|
||||
if (!titleMatch && !contentMatch) return null;
|
||||
|
||||
let snippet = "";
|
||||
if (contentMatch) {
|
||||
const firstTermIdx = Math.min(
|
||||
...terms.map((t) => {
|
||||
const idx = contentLower.indexOf(t);
|
||||
return idx === -1 ? Infinity : idx;
|
||||
}),
|
||||
);
|
||||
if (firstTermIdx !== Infinity) {
|
||||
const start = Math.max(0, firstTermIdx - 40);
|
||||
const end = Math.min(entry.content.length, firstTermIdx + 120);
|
||||
snippet =
|
||||
(start > 0 ? "..." : "") +
|
||||
entry.content.slice(start, end).replace(/\n/g, " ") +
|
||||
(end < entry.content.length ? "..." : "");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: entry.title,
|
||||
href: entry.href,
|
||||
section: entry.section,
|
||||
snippet,
|
||||
score: titleMatch ? 2 : 1,
|
||||
};
|
||||
})
|
||||
.filter(isScoredResult)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 20)
|
||||
.map(({ score: _, ...rest }) => rest);
|
||||
|
||||
return NextResponse.json({ results }, { headers: { "Cache-Control": "public, max-age=60" } });
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,312 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "@tailwindcss/typography";
|
||||
@plugin "tailwindcss-animate";
|
||||
|
||||
@custom-variant dark (&:is(.dark *, .dark));
|
||||
|
||||
@theme {
|
||||
--color-bg: #ffffff;
|
||||
--color-fg: #171717;
|
||||
--color-muted: #666666;
|
||||
--color-border: #eaeaea;
|
||||
--color-surface: #ffffff;
|
||||
--color-surface-muted: #fafafa;
|
||||
--color-accent: #000000;
|
||||
--color-accent-fg: #ffffff;
|
||||
--color-blue: #0070f3;
|
||||
--color-blue-hover: #0060df;
|
||||
--color-success: #0070f3;
|
||||
--color-danger: #ee0000;
|
||||
--color-danger-bg: #fff0f0;
|
||||
--color-code-bg: #f5f5f5;
|
||||
--color-code-fg: #171717;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
--font-mono: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
|
||||
--container-content: 72rem;
|
||||
|
||||
--shadow-card: 0 4px 14px 0 rgb(0 0 0 / 0.1);
|
||||
--shadow-soft: 0 2px 8px rgb(0 0 0 / 0.08);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-bg: #000000;
|
||||
--color-fg: #ededed;
|
||||
--color-muted: #888888;
|
||||
--color-border: #333333;
|
||||
--color-surface: #111111;
|
||||
--color-surface-muted: #0a0a0a;
|
||||
--color-accent: #ffffff;
|
||||
--color-accent-fg: #000000;
|
||||
--color-blue: #3291ff;
|
||||
--color-blue-hover: #52a9ff;
|
||||
--color-danger: #ff4444;
|
||||
--color-danger-bg: #2a0000;
|
||||
--color-code-bg: #111111;
|
||||
--color-code-fg: #ededed;
|
||||
--shadow-card: 0 4px 14px 0 rgb(0 0 0 / 0.25);
|
||||
--shadow-soft: 0 2px 8px rgb(0 0 0 / 0.2);
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-fg);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in srgb, var(--color-blue) 25%, transparent);
|
||||
}
|
||||
|
||||
/* ─── Shiki (rehype-pretty-code) ───────────────────── */
|
||||
|
||||
[data-rehype-pretty-code-figure] {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] pre {
|
||||
overflow-x: auto;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--color-code-bg);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] pre > code {
|
||||
display: grid;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] pre > code [data-line] {
|
||||
padding: 0 0.25rem;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-title] {
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--color-muted);
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-bottom: 0;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-title] + pre {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.prose :where(h1, h2, h3, h4, h5, h6) a,
|
||||
.prose :where(h1, h2, h3, h4, h5, h6) a:hover {
|
||||
color: inherit;
|
||||
font-weight: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] [data-theme] code {
|
||||
color: inherit;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
[data-rehype-pretty-code-figure] span {
|
||||
color: var(--shiki-light);
|
||||
}
|
||||
|
||||
.dark [data-rehype-pretty-code-figure] span {
|
||||
color: var(--shiki-dark);
|
||||
}
|
||||
|
||||
/* ─── Inline syntax highlight (home page) ──────────── */
|
||||
|
||||
.hl-comment { color: #6b7280; font-style: italic; }
|
||||
.hl-string { color: #067a6e; }
|
||||
.hl-keyword { color: #d6409f; }
|
||||
.hl-type { color: #0070c0; }
|
||||
.hl-function { color: #6f42c1; }
|
||||
.hl-number { color: #0070c0; }
|
||||
.hl-variable { color: var(--color-fg); }
|
||||
.hl-id { color: #6f42c1; }
|
||||
.hl-key { color: #d6409f; }
|
||||
.hl-boolean { color: #0070c0; }
|
||||
.hl-punctuation { color: var(--color-fg); }
|
||||
.hl-operator { color: #d6409f; }
|
||||
|
||||
.dark .hl-comment { color: #a1a1a1; }
|
||||
.dark .hl-string { color: #00ca50; }
|
||||
.dark .hl-keyword { color: #ff4d8d; }
|
||||
.dark .hl-type { color: #c472fb; }
|
||||
.dark .hl-function { color: #c472fb; }
|
||||
.dark .hl-number { color: #47a8ff; }
|
||||
.dark .hl-variable { color: #47a8ff; }
|
||||
.dark .hl-id { color: #c472fb; }
|
||||
.dark .hl-key { color: #ff4d8d; }
|
||||
.dark .hl-boolean { color: #47a8ff; }
|
||||
.dark .hl-punctuation { color: #ededed; }
|
||||
.dark .hl-operator { color: #ff4d8d; }
|
||||
|
||||
.hl-bash-function {
|
||||
color: var(--color-fg);
|
||||
font-weight: 600;
|
||||
}
|
||||
.hl-bash-key { color: #0070f3; }
|
||||
.hl-bash-string { color: #067a6e; }
|
||||
.hl-bash-keyword { color: #d6409f; }
|
||||
.hl-bash-variable { color: #6f42c1; }
|
||||
.hl-bash-operator,
|
||||
.hl-bash-punctuation {
|
||||
color: var(--color-muted);
|
||||
}
|
||||
.hl-bash-number { color: var(--color-fg); }
|
||||
|
||||
.dark .hl-bash-function { color: #ffffff; }
|
||||
.dark .hl-bash-key { color: #52a9ff; }
|
||||
.dark .hl-bash-string { color: #00ca50; }
|
||||
.dark .hl-bash-keyword { color: #ff4d8d; }
|
||||
.dark .hl-bash-variable { color: #c472fb; }
|
||||
.dark .hl-bash-operator,
|
||||
.dark .hl-bash-punctuation {
|
||||
color: #888888;
|
||||
}
|
||||
.dark .hl-bash-number { color: #ffffff; }
|
||||
|
||||
/* ─── Home chat motion ────────────────────────────── */
|
||||
|
||||
@keyframes home-chat-reveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(0.875rem) scale(0.985);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes home-command-sheen {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-120%);
|
||||
}
|
||||
28% {
|
||||
opacity: 0.18;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(120%);
|
||||
}
|
||||
}
|
||||
|
||||
.home-chat-shell {
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
.home-chat-code {
|
||||
background: color-mix(in srgb, var(--color-fg) 9%, var(--color-bg));
|
||||
border: 1px solid color-mix(in srgb, var(--color-fg) 14%, transparent);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--color-fg);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.78125rem;
|
||||
padding: 0.125rem 0.45rem;
|
||||
}
|
||||
|
||||
.home-chat-reveal,
|
||||
.home-tool-row {
|
||||
opacity: 0;
|
||||
transform: translateY(0.875rem) scale(0.985);
|
||||
will-change: opacity, transform;
|
||||
}
|
||||
|
||||
.home-chat-viewport[data-chat-visible="true"] .home-chat-reveal,
|
||||
.home-chat-viewport[data-chat-visible="true"] .home-tool-row {
|
||||
animation: home-chat-reveal 620ms cubic-bezier(0.16, 1, 0.3, 1) forwards;
|
||||
animation-delay: var(--chat-delay, 0ms);
|
||||
}
|
||||
|
||||
.home-tool-row {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.home-tool-row::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.home-chat-viewport[data-chat-visible="true"] .home-tool-row::after {
|
||||
animation: home-command-sheen 1150ms ease-out forwards;
|
||||
animation-delay: calc(var(--chat-delay, 0ms) + 140ms);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
color-mix(in srgb, var(--color-fg) 18%, transparent),
|
||||
transparent
|
||||
);
|
||||
content: "";
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transform: translateX(-120%);
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.home-chat-reveal,
|
||||
.home-tool-row {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.home-tool-row::after {
|
||||
animation: none;
|
||||
content: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Prose adjustments ────────────────────────────── */
|
||||
|
||||
.prose pre {
|
||||
background: var(--color-code-bg) !important;
|
||||
color: var(--color-code-fg) !important;
|
||||
}
|
||||
|
||||
.prose :where(code):not(:where([class~="not-prose"] *)) {
|
||||
background: var(--color-surface-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.prose :where(code):not(:where([class~="not-prose"] *))::before,
|
||||
.prose :where(code):not(:where([class~="not-prose"] *))::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.prose :where(pre code):not(:where([class~="not-prose"] *)) {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import "./globals.css";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import type { ReactNode } from "react";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { SiteHeader } from "@/components/site-header";
|
||||
import { DocsChat } from "@/components/docs-chat";
|
||||
import { getStarCount } from "@/lib/github";
|
||||
|
||||
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://zerolang.ai";
|
||||
const SITE_DESCRIPTION =
|
||||
"Zero is a pre-1 agent-first language experiment. Expect breaking changes and run it only in safe, non-production environments.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(SITE_URL),
|
||||
title: {
|
||||
default: "Zero - Agent-first language experiment.",
|
||||
template: "%s | Zero",
|
||||
},
|
||||
description: SITE_DESCRIPTION,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
url: SITE_URL,
|
||||
siteName: "Zero",
|
||||
title: "Zero - Agent-first language experiment.",
|
||||
description: SITE_DESCRIPTION,
|
||||
images: [{ url: "/og", width: 1200, height: 630, alt: "Zero" }],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: "Zero - Agent-first language experiment.",
|
||||
description: SITE_DESCRIPTION,
|
||||
images: ["/og"],
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
};
|
||||
|
||||
export default async function RootLayout({ children }: Readonly<{ children: ReactNode }>) {
|
||||
const stars = await getStarCount();
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className="bg-bg text-fg antialiased">
|
||||
<ThemeProvider>
|
||||
<SiteHeader stars={stars} />
|
||||
{children}
|
||||
<DocsChat />
|
||||
</ThemeProvider>
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ButtonLink } from "@/components/button";
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="mx-auto w-[min(100%-3rem,42rem)] py-16">
|
||||
<p className="mb-2 text-[0.8125rem] font-medium uppercase tracking-[0.04em] text-muted">404</p>
|
||||
<h1 className="mb-4 text-3xl font-bold tracking-tight">Not found</h1>
|
||||
<p className="mb-6 text-muted">The page you requested does not exist.</p>
|
||||
<ButtonLink href="/">Go home</ButtonLink>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPageTitle, renderOgImage } from "../og-image";
|
||||
|
||||
type OgRouteContext = { params: Promise<{ slug: string[] }> };
|
||||
|
||||
export async function GET(_request: Request, { params }: OgRouteContext) {
|
||||
const { slug } = await params;
|
||||
const title = getPageTitle(slug.join("/"));
|
||||
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { ImageResponse } from "next/og";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
export { getPageTitle } from "@/lib/page-titles";
|
||||
|
||||
type FontCache = {
|
||||
geistRegular: Buffer;
|
||||
geistPixelSquare: Buffer;
|
||||
};
|
||||
|
||||
let fontCache: FontCache | null = null;
|
||||
|
||||
async function loadFonts(): Promise<FontCache> {
|
||||
if (fontCache) return fontCache;
|
||||
const [geistRegular, geistPixelSquare] = await Promise.all([
|
||||
readFile(join(process.cwd(), "public/Geist-Regular.ttf")),
|
||||
readFile(join(process.cwd(), "public/GeistPixel-Square.ttf")),
|
||||
]);
|
||||
fontCache = { geistRegular, geistPixelSquare };
|
||||
return fontCache;
|
||||
}
|
||||
|
||||
export async function renderOgImage(title: string): Promise<ImageResponse> {
|
||||
const { geistRegular, geistPixelSquare } = await loadFonts();
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "black",
|
||||
padding: "60px 80px",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
|
||||
<svg width="36" height="32" viewBox="0 0 76 65" fill="white">
|
||||
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" />
|
||||
</svg>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 36,
|
||||
color: "#666",
|
||||
fontFamily: "Geist",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 36,
|
||||
fontFamily: "GeistPixelSquare",
|
||||
fontWeight: 400,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
zero
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flex: 1,
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{title.split("\n").map((line, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontFamily: "Geist",
|
||||
fontWeight: 400,
|
||||
color: "white",
|
||||
letterSpacing: "-0.02em",
|
||||
textAlign: "center",
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{line}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: "Geist",
|
||||
data: geistRegular,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
{
|
||||
name: "GeistPixelSquare",
|
||||
data: geistPixelSquare,
|
||||
style: "normal",
|
||||
weight: 400,
|
||||
},
|
||||
],
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { getPageTitle, renderOgImage } from "./og-image";
|
||||
|
||||
export async function GET() {
|
||||
const title = getPageTitle("") ?? "Zero";
|
||||
return renderOgImage(title);
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
import { ArrowRightIcon, LogoIcon } from "@/components/icons";
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { ButtonLink } from "@/components/button";
|
||||
import { InstallCopy } from "@/components/install-copy";
|
||||
import { HomeChatViewport } from "@/components/home-chat-viewport";
|
||||
import { ChatToolRuns } from "@/components/home-chat-tools";
|
||||
import { LoopDiagram } from "@/components/loop-diagram";
|
||||
import { highlight } from "@/lib/highlight";
|
||||
import { pageMetadata } from "@/lib/page-metadata";
|
||||
|
||||
export const metadata = pageMetadata("");
|
||||
|
||||
const CODE_EXAMPLE = `<span class="hl-keyword">pub</span> <span class="hl-keyword">fn</span> <span class="hl-variable">main</span>(world: <span class="hl-type">World</span>) -> <span class="hl-type">Void</span> <span class="hl-keyword">raises</span> {
|
||||
<span class="hl-keyword">check</span> world.out.<span class="hl-variable">write</span>(<span class="hl-string">"hello from zero\\n"</span>)
|
||||
}`;
|
||||
|
||||
const GRAPH_EXAMPLE = `zero-graph v1
|
||||
origin source-text
|
||||
module "hello"
|
||||
hash "graph:a7f7e6899a73f3b4"
|
||||
|
||||
node #decl_ad8d9028 Function name:"main" type:"Void" public:true fallible:true
|
||||
node #param_4610ae76 Param name:"world" type:"World"
|
||||
node #expr_c403020c MethodCall name:"write" type:"Void"
|
||||
node #expr_653eeb6e Literal type:"String" value:"hello from zero\\n"
|
||||
edge #expr_c403020c arg #expr_653eeb6e order:0`;
|
||||
|
||||
const PATCH_EXAMPLE = `zero patch \\
|
||||
--expect-graph-hash graph:a7f7e6899a73f3b4 \\
|
||||
--op 'set node="#expr_653eeb6e" \\
|
||||
field="value" \\
|
||||
expect="hello from zero\\n" \\
|
||||
value="hello graph\\n"'`;
|
||||
|
||||
const CONSTRAINTS = [
|
||||
"Token efficiency",
|
||||
"Low memory",
|
||||
"Fast startup",
|
||||
"Fast builds",
|
||||
"Low latency",
|
||||
"Zero dependencies",
|
||||
];
|
||||
|
||||
const ARCHITECTURE_STEPS = [
|
||||
{
|
||||
label: "Human",
|
||||
title: "Asks for an outcome",
|
||||
body: "Build a CRM API, add auth, or fix a failing route",
|
||||
},
|
||||
{
|
||||
label: "Agent",
|
||||
title: "Uses Graph",
|
||||
body: "Symbols, calls, types, effects, node IDs, and graph hashes",
|
||||
},
|
||||
{
|
||||
label: "Compiler",
|
||||
title: "Checks patches",
|
||||
body: "Shape, type, stale-state, and repository metadata checks",
|
||||
},
|
||||
{
|
||||
label: "Human",
|
||||
title: "Reviews projection",
|
||||
body: "Readable .0 projections stay available for review and rare manual edits",
|
||||
},
|
||||
];
|
||||
|
||||
/* ─── Primitives ──────────────────────────────────── */
|
||||
|
||||
function GridGlow() {
|
||||
return (
|
||||
<div aria-hidden className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.4] dark:opacity-[0.25]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"linear-gradient(to right, var(--color-border) 1px, transparent 1px), linear-gradient(to bottom, var(--color-border) 1px, transparent 1px)",
|
||||
backgroundSize: "64px 64px",
|
||||
maskImage:
|
||||
"radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 75%)",
|
||||
WebkitMaskImage:
|
||||
"radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 75%)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="mb-12 max-w-[44rem]">
|
||||
<h2 className="m-0 max-w-[14ch] text-[clamp(2.25rem,6vw,4.25rem)] font-semibold leading-[0.98] tracking-[-0.045em]">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="mt-6 max-w-[34rem] text-pretty text-[1.0625rem] leading-[1.65] text-muted">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VisualGlow({ className = "", children }: { className?: string; children: ReactNode }) {
|
||||
return <div className={className}>{children}</div>;
|
||||
}
|
||||
|
||||
function Panel({ className = "", children }: { className?: string; children: ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-2xl border border-fg/30 ${className}`}
|
||||
style={{ boxShadow: "0 1px 0 0 color-mix(in srgb, var(--color-fg) 4%, transparent)" }}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function WindowBar({ title }: { title: string }) {
|
||||
return (
|
||||
<div className="relative flex items-center border-b border-fg/30 px-4 py-3">
|
||||
<div className="flex gap-1.5">
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-border" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-border" />
|
||||
<span className="h-2.5 w-2.5 rounded-full bg-border" />
|
||||
</div>
|
||||
<span className="absolute left-1/2 -translate-x-1/2 font-mono text-xs font-medium text-muted">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CodeWindow({
|
||||
title,
|
||||
html,
|
||||
className = "",
|
||||
}: {
|
||||
title: string;
|
||||
html: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Panel className={`bg-bg ${className}`}>
|
||||
<WindowBar title={title} />
|
||||
<pre className="m-0 overflow-x-auto bg-bg p-5 text-[0.8125rem] leading-[1.7]">
|
||||
<code className="text-code-fg" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</pre>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
function ArchitectureDiagram() {
|
||||
return (
|
||||
<div className="mt-12 w-full text-left">
|
||||
<div className="grid grid-cols-1 gap-px overflow-hidden rounded-2xl border border-border bg-border sm:grid-cols-2 lg:grid-cols-4">
|
||||
{ARCHITECTURE_STEPS.map((item, index) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="group relative flex flex-col bg-bg p-7 transition-colors duration-200 hover:bg-surface-muted"
|
||||
>
|
||||
<div className="mb-7 flex items-center gap-2.5">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-md border border-border font-mono text-[0.6875rem] tabular-nums text-muted transition-colors group-hover:border-fg/40 group-hover:text-fg">
|
||||
{index + 1}
|
||||
</span>
|
||||
<span className="font-mono text-[0.6875rem] font-semibold uppercase tracking-[0.16em] text-muted">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="m-0 text-[1.0625rem] font-semibold leading-snug tracking-[-0.02em] text-fg">
|
||||
{item.title}
|
||||
</h2>
|
||||
<p className="m-0 mt-2.5 text-[0.8125rem] leading-[1.65] text-muted">
|
||||
{item.body}
|
||||
</p>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute right-0 top-1/2 z-10 hidden -translate-y-1/2 translate-x-[calc(50%+1px)] lg:flex"
|
||||
>
|
||||
{index < ARCHITECTURE_STEPS.length - 1 ? (
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full border border-border bg-bg text-muted">
|
||||
<ArrowRightIcon width={12} height={12} />
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Chat mockup ─────────────────────────────────── */
|
||||
|
||||
function chatRevealStyle(delay: string): CSSProperties {
|
||||
return { "--chat-delay": delay } as CSSProperties;
|
||||
}
|
||||
|
||||
function ChatMockup() {
|
||||
return (
|
||||
<HomeChatViewport>
|
||||
<Panel className="home-chat-shell bg-bg shadow-card">
|
||||
<WindowBar title="your agent" />
|
||||
|
||||
<div className="flex flex-col gap-5 p-5 sm:p-6">
|
||||
<div className="home-chat-reveal flex justify-end" style={chatRevealStyle("80ms")}>
|
||||
<div className="max-w-[82%] rounded-2xl rounded-br-md bg-fg px-4 py-2.5 text-[0.875rem] leading-relaxed text-bg">
|
||||
build hello world in zerolang
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="home-chat-reveal max-w-[92%] text-[0.875rem] leading-[1.65] text-fg"
|
||||
style={chatRevealStyle("360ms")}
|
||||
>
|
||||
I'll set up the package, patch the graph, and run it.
|
||||
</div>
|
||||
|
||||
<ChatToolRuns startDelayMs={640} />
|
||||
|
||||
<div
|
||||
className="home-chat-reveal max-w-[92%] text-[0.875rem] leading-[1.65] text-fg"
|
||||
style={chatRevealStyle("1320ms")}
|
||||
>
|
||||
Done. <code className="home-chat-code">zero.graph</code> validated at{" "}
|
||||
<code className="home-chat-code">graph:a7f7e689</code> with symbol{" "}
|
||||
<code className="home-chat-code">main</code>. It prints:
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="home-chat-reveal home-chat-output rounded-lg border border-border bg-code-bg px-4 py-3 font-mono text-[0.8125rem] text-code-fg"
|
||||
style={chatRevealStyle("1520ms")}
|
||||
>
|
||||
hello from zero
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</HomeChatViewport>
|
||||
);
|
||||
}
|
||||
|
||||
/* ─── Section wrapper ─────────────────────────────── */
|
||||
|
||||
function Section({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<section className="relative z-10 mx-auto w-[min(100%-3rem,var(--container-content))] border-t border-border py-[clamp(5rem,11vh,8rem)]">
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="relative min-h-screen overflow-hidden">
|
||||
<main>
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden px-6 pb-24 pt-[clamp(4rem,11vh,7rem)]">
|
||||
<GridGlow />
|
||||
<div className="relative z-10 mx-auto flex max-w-[54rem] flex-col items-center text-center">
|
||||
<div className="mb-7 inline-flex items-center rounded-full border border-border bg-surface/70 px-3.5 py-1 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-muted backdrop-blur">
|
||||
Experimental
|
||||
</div>
|
||||
<h1 className="m-0 text-[clamp(2.5rem,7vw,5rem)] font-semibold leading-[0.98] tracking-[-0.05em]">
|
||||
The Programming
|
||||
<br />
|
||||
Language for Agents
|
||||
</h1>
|
||||
<p className="mt-7 max-w-[40rem] text-pretty text-[clamp(1.0625rem,2vw,1.25rem)] leading-[1.6] text-muted">
|
||||
A programming language where the graph is the program. Humans ask
|
||||
for outcomes. Agents query the program graph, submit checked
|
||||
edits, and prove the result.
|
||||
</p>
|
||||
<InstallCopy />
|
||||
<ArchitectureDiagram />
|
||||
<p className="mt-5 max-w-[34rem] text-[0.8125rem] leading-relaxed text-muted">
|
||||
Expect breaking changes. Run it in a safe environment, not against
|
||||
production systems.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 01 — Chat */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
title="Start with a request."
|
||||
description="The expected workflow is a normal conversation. The graph discipline lives in the agent skills and compiler commands, not in stiff human prompts."
|
||||
/>
|
||||
<VisualGlow className="mx-auto max-w-[40rem]">
|
||||
<ChatMockup />
|
||||
</VisualGlow>
|
||||
</Section>
|
||||
|
||||
{/* 02 — Loop */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
title="Tighter agent loop."
|
||||
description="A traditional loop writes text, then runs tools to learn what the edit meant. Zerolang puts the compiler in the loop, so an edit is a checked change to the graph."
|
||||
/>
|
||||
<LoopDiagram />
|
||||
</Section>
|
||||
|
||||
{/* 03 — Patch */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
title="Checked by default."
|
||||
description="Graph patches target semantic nodes and fields, guarded by graph hashes and expected values. Stale or invalid edits fail before they touch the store."
|
||||
/>
|
||||
<VisualGlow className="grid grid-cols-1 gap-5 lg:grid-cols-[1.4fr_1fr]">
|
||||
<CodeWindow title="zero patch" html={highlight(PATCH_EXAMPLE, "sh")} />
|
||||
<Panel className="bg-bg">
|
||||
<dl className="m-0 divide-y divide-border/50 font-mono text-[0.8125rem]">
|
||||
{[
|
||||
["graph hash", "graph:b3c1d04f"],
|
||||
["node", "#expr_653eeb6e"],
|
||||
["field", "value"],
|
||||
["symbols", "main"],
|
||||
["projection", "src/main.0"],
|
||||
].map(([k, v]) => (
|
||||
<div key={k} className="flex items-center justify-between gap-4 px-5 py-3">
|
||||
<dt className="text-muted">{k}</dt>
|
||||
<dd className="m-0 truncate text-code-fg">{v}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</Panel>
|
||||
</VisualGlow>
|
||||
</Section>
|
||||
|
||||
{/* 04 — Graph */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
title="The program database."
|
||||
description="Readable text stays useful for review. The compiler-owned graph is the program database agents edit and the compiler consumes."
|
||||
/>
|
||||
<VisualGlow className="grid grid-cols-1 gap-5 lg:grid-cols-2">
|
||||
<CodeWindow title="zero query · graph" html={highlight(GRAPH_EXAMPLE, "zero-graph")} />
|
||||
<CodeWindow title="src/main.0 · projection" html={CODE_EXAMPLE} />
|
||||
</VisualGlow>
|
||||
</Section>
|
||||
|
||||
{/* 04 — Constraints */}
|
||||
<Section>
|
||||
<SectionHeader
|
||||
title="Built for runtime constraints."
|
||||
description="The graph model should reduce guessing without relaxing the runtime goals. Zerolang still aims to stay small, fast, explicit, and dependency-free."
|
||||
/>
|
||||
<VisualGlow>
|
||||
<div className="grid grid-cols-2 gap-px overflow-hidden rounded-2xl border border-border bg-border md:grid-cols-3">
|
||||
{CONSTRAINTS.map((c) => (
|
||||
<div
|
||||
key={c}
|
||||
className="flex items-center gap-3 bg-bg px-6 py-7 text-[0.9375rem] font-medium tracking-[-0.01em] transition-colors hover:bg-surface-muted"
|
||||
>
|
||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-fg/30" />
|
||||
{c}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</VisualGlow>
|
||||
</Section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="relative z-10 border-t border-border">
|
||||
<div className="mx-auto flex w-[min(100%-3rem,var(--container-content))] flex-col items-center px-6 py-[clamp(6rem,14vh,9rem)] text-center">
|
||||
<h2 className="m-0 text-[clamp(2rem,6vw,3.5rem)] font-semibold leading-[1.02] tracking-[-0.045em]">
|
||||
Explore with us.
|
||||
</h2>
|
||||
<p className="mx-auto mt-6 max-w-[38rem] text-pretty text-[1.0625rem] leading-[1.6] text-muted">
|
||||
Start with the getting started guide, then read the graph architecture
|
||||
and compile-path pages to see why the program database matters.
|
||||
</p>
|
||||
<div className="mt-9 flex flex-wrap items-center justify-center gap-3">
|
||||
<ButtonLink href="/getting-started" variant="primary" size="lg">
|
||||
Get started <ArrowRightIcon />
|
||||
</ButtonLink>
|
||||
<ButtonLink href="/concepts/graph-architecture" variant="default" size="lg">
|
||||
Graph architecture <ArrowRightIcon />
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer className="relative z-10 border-t border-border py-8">
|
||||
<div className="mx-auto flex w-[min(100%-3rem,var(--container-content))] flex-col items-center gap-2 text-center md:flex-row md:justify-between md:text-left">
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-fg">
|
||||
<LogoIcon width="14" height="12" />
|
||||
<span>zerolang</span>
|
||||
</div>
|
||||
<p className="m-0 text-[0.8125rem] text-muted">
|
||||
Experimental graph-first language design.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
## Measure The Current Compiler
|
||||
|
||||
Zerolang benchmarks are regression signals for the current compiler. They are not broad
|
||||
marketing claims. Use them to compare graph inputs, artifact sizes, build time,
|
||||
startup/runtime behavior, and memory use across changes.
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
pnpm run bench
|
||||
```
|
||||
|
||||
Outputs:
|
||||
|
||||
```sh
|
||||
.zero/bench/latest.json
|
||||
.zero/bench/trends/latest.json
|
||||
.zero/bench/trends/summary.md
|
||||
```
|
||||
|
||||
## Cases
|
||||
|
||||
Current benchmark cases include:
|
||||
|
||||
- `hello`, `add`, `structs`, `params`
|
||||
- `buffers`, `parser`, `codec`, `parse`
|
||||
- `slices`, `arena`, `fallibility`, `branches`
|
||||
- `module-package`, `rescue`
|
||||
- `fs-resource`, `mem-copy-fill`, `zero-hash`
|
||||
|
||||
The Zero inputs live under `benchmarks/zero`. Host targets that cannot run a
|
||||
case report `skipped` with a reason instead of failing the entire benchmark.
|
||||
|
||||
## Metrics
|
||||
|
||||
Important fields:
|
||||
|
||||
- `buildMs`
|
||||
- `runMs`
|
||||
- `runMinMs`
|
||||
- `runRuns`
|
||||
- `artifactBytes`
|
||||
- `compressedArtifactBytes`
|
||||
- `peakRssBytes`
|
||||
- `expectedStdout`
|
||||
- `outputMatches`
|
||||
|
||||
## Options
|
||||
|
||||
```sh
|
||||
ZERO_BENCH_RUNS=<n> pnpm run bench
|
||||
ZERO_BENCH_MODE=sandbox pnpm run bench
|
||||
```
|
||||
|
||||
Use one run for smoke checks and more runs when comparing trends.
|
||||
@@ -0,0 +1,64 @@
|
||||
## Use A Checkout Compiler
|
||||
|
||||
Use this page when you are working on Zerolang itself or testing a local compiler
|
||||
change. Public docs use `zero`; contributor notes cover checkout wrapper
|
||||
details.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
make -C native/zero-c
|
||||
zero --version
|
||||
```
|
||||
|
||||
## Focused Loop
|
||||
|
||||
```sh
|
||||
zero check examples/hello.graph
|
||||
zero run examples/add.graph
|
||||
zero build --emit exe --target linux-musl-x64 examples/add.graph --out .zero/out/add
|
||||
```
|
||||
|
||||
Inspect graph and size facts:
|
||||
|
||||
```sh
|
||||
zero inspect --json examples/systems-package
|
||||
zero size --json examples/point.graph
|
||||
zero targets
|
||||
```
|
||||
|
||||
Explain diagnostics:
|
||||
|
||||
```sh
|
||||
zero explain TAR002
|
||||
zero explain --json TYP009
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
pnpm run conformance
|
||||
pnpm run command-contracts:local
|
||||
pnpm run native:test
|
||||
pnpm run docs:build
|
||||
```
|
||||
|
||||
Before ending an agent turn that changed the repository, run
|
||||
`pnpm run conformance`.
|
||||
|
||||
## Native Targets
|
||||
|
||||
The documented native target names are:
|
||||
|
||||
- `darwin-arm64`
|
||||
- `darwin-x64`
|
||||
- `linux-arm64`
|
||||
- `linux-musl-arm64`
|
||||
- `linux-musl-x64`
|
||||
- `linux-x64`
|
||||
- `win32-arm64.exe`
|
||||
- `win32-x64.exe`
|
||||
|
||||
Unsupported target or feature requests report diagnostics instead of silently
|
||||
choosing another backend.
|
||||
@@ -0,0 +1,76 @@
|
||||
## The C Boundary
|
||||
|
||||
Zerolang supports a small explicit C ABI surface from graph inputs. C-facing code
|
||||
should stay narrow, inspectable, and target-aware.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "can this call my tiny c helper on linux musl?"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll check the target link plan and tell you what blocks the call."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero inspect --json --target linux-musl-x64",
|
||||
"output": "{\"cLibraries\":[{\"linkPlan\":{\"sysrootStatus\":\"configured\"}}]}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## What This Means
|
||||
|
||||
Projection example:
|
||||
|
||||
```zero
|
||||
export c fn add(a: i32, b: i32) -> i32 {
|
||||
return a + b
|
||||
}
|
||||
```
|
||||
|
||||
Graph checks:
|
||||
|
||||
```sh
|
||||
zero check conformance/native/pass/c-abi-export.graph
|
||||
zero abi dump --json conformance/native/pass/c-abi-export.graph
|
||||
```
|
||||
|
||||
`zero abi dump --json` reports exported symbols and generated header facts such
|
||||
as `generatedHeader.available`.
|
||||
|
||||
## Import Metadata
|
||||
|
||||
Header imports expose typed metadata:
|
||||
|
||||
```sh
|
||||
zero inspect --json --target linux-musl-x64 conformance/check/pass/c-header-import.graph
|
||||
```
|
||||
|
||||
The JSON includes `cImports[].typedModel` with imported functions, constants,
|
||||
structs, enums, and typedefs.
|
||||
|
||||
Callable imports are limited to direct scalar ABI types today: `Void`, `Bool`,
|
||||
`u8`, `u16`, `usize`, `i32`, `u32`, `i64`, and `u64`. Pointer, array, struct,
|
||||
and unsupported-width parameters should be wrapped by a small C shim.
|
||||
|
||||
## Link Plans
|
||||
|
||||
Executable builds with direct extern C calls require package link metadata in
|
||||
`zero.toml`. Imported headers must appear in `c.libs.*.headers`, and the
|
||||
library must provide `lib` or `link` inputs.
|
||||
|
||||
`zero inspect --json` reports each `cLibraries[].linkPlan` with include paths,
|
||||
library paths, sysroot status, target ABI, host discovery status, and header
|
||||
hash data.
|
||||
|
||||
Unsafe foreign-target discovery fails with `CIMP003`. Missing or unsafe link
|
||||
inputs fail with `CIMP005`.
|
||||
@@ -0,0 +1,290 @@
|
||||
## Use Commands By Workflow
|
||||
|
||||
The Zerolang CLI is organized around graph-first agent work. Humans ask for a task;
|
||||
agents inspect and patch the graph; projections are exported only for review or
|
||||
manual edits.
|
||||
|
||||
Most commands default to the current directory:
|
||||
|
||||
```sh
|
||||
zero status
|
||||
zero query
|
||||
zero patch --op help
|
||||
zero check
|
||||
zero run -- <args>
|
||||
```
|
||||
|
||||
Pass an explicit graph input or package when you are outside the project:
|
||||
|
||||
```sh
|
||||
zero check examples/hello.graph
|
||||
zero query examples/crm-api
|
||||
zero run examples/json-api-router.graph -- $'GET /health\n\n'
|
||||
```
|
||||
|
||||
## Create A Project
|
||||
|
||||
Use `zero init` for all project creation.
|
||||
|
||||
```sh
|
||||
zero init
|
||||
zero init --template cli crm-tool
|
||||
zero init --manifest toml --format binary --template package api-server
|
||||
```
|
||||
|
||||
If no path is given, `zero init` creates the package in the current directory.
|
||||
For `.` or an omitted path, the package name comes from the directory name.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "start a cli here"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll initialize this directory and add the starting CLI shape."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero init --template cli",
|
||||
"output": "graph project init ok\nwrote: ./zero.toml\nwrote: ./zero.graph"
|
||||
},
|
||||
{
|
||||
"command": "zero patch --op 'addMain'",
|
||||
"output": "program graph patch ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Inspect Before Editing
|
||||
|
||||
Agents should query for the exact thing they need instead of dumping the whole
|
||||
program.
|
||||
|
||||
```sh
|
||||
zero status
|
||||
zero query --fn main
|
||||
zero query --find customer
|
||||
zero query --refs handle
|
||||
zero query --calls write
|
||||
zero inspect --json
|
||||
zero size --json
|
||||
zero mem --json
|
||||
```
|
||||
|
||||
Use plain text first. Use `--json` when a tool needs exact fields such as node
|
||||
ids, graph hashes, `interfaceFingerprints`, `targetToolchains`,
|
||||
`usedStdlibHelpers`, `memoryBudgets`, or `releaseTargetContract`.
|
||||
|
||||
## Patch The Graph
|
||||
|
||||
Patch commands are checked graph edits:
|
||||
|
||||
```sh
|
||||
zero patch --op 'addFunction name="add" ret="i32"'
|
||||
zero patch --op 'addParam fn="add" name="x" type="i32"'
|
||||
zero patch --op 'addParam fn="add" name="y" type="i32"'
|
||||
zero patch --op 'addReturnBinary fn="add" name="+" left="x" right="y" type="i32"'
|
||||
```
|
||||
|
||||
For larger edits, use a patch file under `/tmp`:
|
||||
|
||||
```text
|
||||
zero-program-graph-patch v1
|
||||
expect graphHash "graph:a7f7e6899a73f3b4"
|
||||
replaceFunctionBody main
|
||||
check world.out.write "hello\n"
|
||||
end
|
||||
```
|
||||
|
||||
Dry-run a repository graph patch without writing:
|
||||
|
||||
```sh
|
||||
zero patch --check-only /tmp/main.patch
|
||||
zero patch --dry-run --json /tmp/main.patch
|
||||
```
|
||||
|
||||
Apply it:
|
||||
|
||||
```sh
|
||||
zero patch /tmp/main.patch
|
||||
```
|
||||
|
||||
To replace one function body without patch syntax, pass only the new body rows
|
||||
(exactly what `zero view --fn <name>` prints between the signature braces).
|
||||
`--body-file -` reads them from stdin, so a heredoc does the whole edit in one
|
||||
call:
|
||||
|
||||
```sh
|
||||
zero patch --replace-fn main --body-file - <<'EOF'
|
||||
check world.out.write("hello agent\n")
|
||||
EOF
|
||||
```
|
||||
|
||||
A file path works as the alternative:
|
||||
|
||||
```sh
|
||||
zero patch --replace-fn main --body-file /tmp/main.body
|
||||
```
|
||||
|
||||
To change a few characters inside a function without retyping the body,
|
||||
`--replace-in-fn` replaces one unique literal occurrence of `--old` in the
|
||||
function's canonical body text (what `zero view --fn <name>` prints) with
|
||||
`--new`, then revalidates exactly like `--replace-fn`:
|
||||
|
||||
```sh
|
||||
zero patch --replace-in-fn main --old 'limit + 1' --new 'limit + 2'
|
||||
```
|
||||
|
||||
A missing or non-unique `--old` fails with the occurrence count. Inline
|
||||
`--old`/`--new` accept `\n` escapes for multi-line text; `--old-file` and
|
||||
`--new-file <file|->` read the text from a file or stdin.
|
||||
|
||||
The patch step validates graph shape and repository metadata. A stale graph
|
||||
hash, missing required edge, sparse ordered child group, or invalid row body
|
||||
fails before the package store is updated.
|
||||
|
||||
## Validate Only What You Need
|
||||
|
||||
Do not run every command after every patch. `zero patch` already reports whether
|
||||
the edit applied. Run the next command that proves the user-visible behavior.
|
||||
|
||||
```sh
|
||||
zero check
|
||||
zero test
|
||||
zero test --json --filter add
|
||||
zero run -- add 40 2
|
||||
```
|
||||
|
||||
Use `zero check --json` when an editor, CI job, or agent needs stable
|
||||
diagnostic fields. Test JSON includes `expectedFailures`, `fixtures`,
|
||||
`snapshotKey`, and per-test results.
|
||||
|
||||
## Run And Build
|
||||
|
||||
Use `zero run` for local behavior:
|
||||
|
||||
```sh
|
||||
zero run -- help
|
||||
zero run examples/hello.graph
|
||||
```
|
||||
|
||||
Use `zero build` for artifacts:
|
||||
|
||||
```sh
|
||||
zero build --emit exe --target linux-musl-x64 --out .zero/out/app
|
||||
zero build --emit obj --target darwin-arm64 examples/direct-call-add.graph --out .zero/out/add.o
|
||||
zero build --emit llvm-ir examples/hello.graph --out .zero/out/hello.ll
|
||||
```
|
||||
|
||||
Build JSON reports profile and target readiness:
|
||||
|
||||
```sh
|
||||
zero build --json --profile tiny --target linux-musl-x64 examples/hello.graph --out .zero/out/hello
|
||||
```
|
||||
|
||||
Important fields include `profileSemantics`, `profileBudget`,
|
||||
`releaseTargetContract`, `targetToolchains`, `compileTime`, and repeat-build
|
||||
hash policy data for artifact determinism.
|
||||
|
||||
## Review Projections
|
||||
|
||||
Projection commands are for humans:
|
||||
|
||||
```sh
|
||||
zero export
|
||||
zero verify-projection
|
||||
zero import
|
||||
zero diff
|
||||
zero view
|
||||
```
|
||||
|
||||
Use `zero export` when a human wants the current `.0` projection. Use
|
||||
`zero import` after a human intentionally edits projection text. Use
|
||||
`zero verify-projection` to catch drift without writing.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "show me the projection before we keep going"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll export the current projection and verify it matches the graph."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero export",
|
||||
"output": "repository graph export ok\nwrote: ./src/main.0"
|
||||
},
|
||||
{
|
||||
"command": "zero verify-projection",
|
||||
"output": "repository graph verify-projection ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Diagnose And Repair
|
||||
|
||||
```sh
|
||||
zero explain NAM003
|
||||
zero fix --plan --json
|
||||
zero doctor
|
||||
zero dev --json
|
||||
zero dev --json --trace
|
||||
```
|
||||
|
||||
`zero dev --json` is the editor-facing snapshot. It includes diagnostics,
|
||||
document symbols, hover data, completions, definition targets, and
|
||||
`interfaceFingerprints`.
|
||||
|
||||
## Command Groups
|
||||
|
||||
| Workflow | Commands |
|
||||
| --- | --- |
|
||||
| create | `init` |
|
||||
| inspect | `status`, `query`, `inspect`, `size`, `mem`, `doc`, `source-map` |
|
||||
| edit graph | `patch`, `reconcile`, `merge` |
|
||||
| validate | `check`, `test`, `verify-projection`, `validate`, `roundtrip` |
|
||||
| run/build | `run`, `build`, `targets`, `abi` |
|
||||
| projection review | `export`, `import`, `view`, `diff`, `fmt`, `tokens`, `parse` |
|
||||
| support | `skills`, `explain`, `fix`, `doctor`, `clean`, `dev`, `time` |
|
||||
|
||||
## Input Forms
|
||||
|
||||
| Input | Meaning |
|
||||
| --- | --- |
|
||||
| `project/` | A package directory. Normal package commands compile from `zero.graph`. |
|
||||
| `zero.toml` | Preferred package manifest. Takes precedence over `zero.json` for directory inputs. |
|
||||
| `zero.json` | Compatibility manifest. Prefer `zero.toml` for new packages. |
|
||||
| `file.graph` | Binary or text graph store/artifact. |
|
||||
| `file.0` | Human-readable projection for formatting, import/export, and review workflows. It is not the normal compiler input. |
|
||||
|
||||
## JSON Rule
|
||||
|
||||
Humans and interactive agents should start with concise text output. Use JSON
|
||||
when a program needs exact structured data:
|
||||
|
||||
```sh
|
||||
zero check --json
|
||||
zero test --json
|
||||
zero inspect --json
|
||||
zero size --json
|
||||
zero doctor --json
|
||||
```
|
||||
|
||||
JSON is a contract for tools, not the default reading experience for humans.
|
||||
@@ -0,0 +1,117 @@
|
||||
## From Graph To Code
|
||||
|
||||
Zerolang is easiest to understand against parse-first compilers. Most languages
|
||||
have a parse-first compile path: the normal compiler input is text, so every
|
||||
compile starts by parsing text into compiler data structures. Zerolang's
|
||||
package path is graph-first. It is the same kind of pipeline with fewer stages,
|
||||
and the input is the graph store instead of text:
|
||||
|
||||
```json-render
|
||||
{
|
||||
"type": "flow",
|
||||
"title": "Parse-first compile path vs Zero graph-first path",
|
||||
"nodes": [
|
||||
{ "id": "p1", "label": "source files", "x": 0, "y": 0, "tone": "text" },
|
||||
{ "id": "p2", "label": "lexer / parser", "x": 0, "y": 96, "tone": "text" },
|
||||
{ "id": "p3", "label": "AST", "x": 0, "y": 192, "tone": "text" },
|
||||
{ "id": "p4", "label": "name resolution", "x": 0, "y": 288, "tone": "text" },
|
||||
{ "id": "p5", "label": "type checking", "x": 0, "y": 384, "tone": "text" },
|
||||
{ "id": "p6", "label": "IR lowering", "x": 0, "y": 480, "tone": "text" },
|
||||
{ "id": "p7", "label": "optimization", "x": 0, "y": 576, "tone": "text" },
|
||||
{ "id": "p8", "label": "codegen", "x": 0, "y": 672, "tone": "text" },
|
||||
{ "id": "p9", "label": "artifact", "x": 0, "y": 768, "tone": "text" },
|
||||
|
||||
{ "id": "g1", "label": "zero.graph", "x": 380, "y": 0, "tone": "graph" },
|
||||
{ "id": "g2", "label": "repository graph tables", "x": 380, "y": 96, "tone": "graph" },
|
||||
{ "id": "g3", "label": "semantic validation", "x": 380, "y": 192, "tone": "compiler" },
|
||||
{ "id": "g4", "label": "type checking", "x": 380, "y": 288, "tone": "compiler" },
|
||||
{ "id": "g5", "label": "MIR and backend facts", "x": 380, "y": 384, "tone": "graph" },
|
||||
{ "id": "g6", "label": "direct codegen", "x": 380, "y": 480, "tone": "compiler" },
|
||||
{ "id": "g7", "label": "artifact", "x": 380, "y": 576, "tone": "graph" }
|
||||
],
|
||||
"edges": [
|
||||
{ "source": "p1", "target": "p2" },
|
||||
{ "source": "p2", "target": "p3" },
|
||||
{ "source": "p3", "target": "p4" },
|
||||
{ "source": "p4", "target": "p5" },
|
||||
{ "source": "p5", "target": "p6" },
|
||||
{ "source": "p6", "target": "p7" },
|
||||
{ "source": "p7", "target": "p8" },
|
||||
{ "source": "p8", "target": "p9" },
|
||||
{ "source": "g1", "target": "g2" },
|
||||
{ "source": "g2", "target": "g3" },
|
||||
{ "source": "g3", "target": "g4" },
|
||||
{ "source": "g4", "target": "g5" },
|
||||
{ "source": "g5", "target": "g6" },
|
||||
{ "source": "g6", "target": "g7" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Rust, Go, Zig, C, and many other languages differ in details, but the normal
|
||||
compiler input is text. The `.0` projection can be exported and imported, but it
|
||||
is not the normal package compile input. The compiler loads the graph store
|
||||
directly.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "why is this less work for the agent?"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "The agent can ask the compiler for graph facts, submit one checked semantic patch, and avoid a separate write-format-parse cycle. The compiler still checks and builds, but the edit itself is closer to the final compiler model."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero query --calls write",
|
||||
"output": "call write\n receiver: world.out\n arg0: #expr_653eeb6e String"
|
||||
},
|
||||
{
|
||||
"command": "zero patch --op 'set node=\"#expr_653eeb6e\" field=\"value\" expect=\"old\\n\" value=\"new\\n\"'",
|
||||
"output": "program graph patch ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Why The Path Matters
|
||||
|
||||
For agents, the compile path is also the authoring path. If text is primary,
|
||||
the agent writes text and waits for the compiler to tell it whether the text
|
||||
meant what it intended.
|
||||
|
||||
If the graph is primary, the agent can ask the compiler for node handles,
|
||||
symbol facts, calls, references, diagnostics, and patch operations before it
|
||||
edits. The edit is already expressed in compiler terms.
|
||||
|
||||
That is why Zero keeps investing in binary graph storage and direct graph
|
||||
loading. The long-term goal is to memory-map final compiler IR and codegen from
|
||||
semantic data with as little redundant parsing and reconstruction as possible.
|
||||
|
||||
## Performance And Size Angle
|
||||
|
||||
The graph model is not only about agent ergonomics. It also supports Zero's
|
||||
systems goals:
|
||||
|
||||
- fewer reparsed text inputs on normal package commands
|
||||
- deterministic graph identity and stable diff/review output
|
||||
- pay-as-used standard library helpers
|
||||
- explicit capability and allocation facts
|
||||
- small direct artifacts when the selected profile and target support them
|
||||
|
||||
Use `zero size --json`, `zero mem --json`, and the benchmark docs to inspect
|
||||
those facts for a graph input instead of treating performance claims as prose.
|
||||
|
||||
## Current Boundary
|
||||
|
||||
Zero is still experimental. Some commands and targets expose readiness facts or
|
||||
structured diagnostics when a backend cannot build a graph shape yet. That is
|
||||
intentional: the docs should show what works today and what the compiler can
|
||||
explain, not imply production completeness.
|
||||
@@ -0,0 +1,123 @@
|
||||
## The Program Database
|
||||
|
||||
Zerolang exists because humans increasingly ask agents to write programs.
|
||||
|
||||
Most programming languages still make text the primary program database. That
|
||||
works for humans, but it is a poor interface for agents. An agent has to infer
|
||||
semantic structure from text, make a text edit, run tools to learn whether the
|
||||
edit was valid, format the result, and then inspect failures after the fact.
|
||||
|
||||
In Zero, the graph is the program database. The graph stores declarations,
|
||||
types, calls, blocks, imports, capabilities, and source-map facts directly.
|
||||
Agents edit those facts with checked graph patches. Humans read `.0`
|
||||
projections when they want a source-like review view.
|
||||
|
||||
## The Editing Loop
|
||||
|
||||
A traditional agent loop writes text, then runs check, format, and build to find
|
||||
out what the text meant. Zero's loop queries the graph, submits one checked
|
||||
patch, and only runs the validation a task actually needs:
|
||||
|
||||
```json-render
|
||||
{
|
||||
"type": "flow",
|
||||
"title": "Traditional source loop vs Zero graph loop",
|
||||
"height": 520,
|
||||
"nodes": [
|
||||
{ "id": "t1", "label": "agent writes text", "x": 0, "y": 0, "tone": "text" },
|
||||
{ "id": "t2", "label": "check", "x": 0, "y": 90, "tone": "compiler" },
|
||||
{ "id": "t3", "label": "format", "x": 0, "y": 180, "tone": "text" },
|
||||
{ "id": "t4", "label": "build", "x": 0, "y": 270, "tone": "compiler" },
|
||||
{ "id": "t5", "label": "inspect failures", "x": 0, "y": 360, "tone": "human" },
|
||||
{ "id": "z1", "label": "agent queries graph", "x": 360, "y": 0, "tone": "graph" },
|
||||
{ "id": "z2", "label": "agent submits checked patch", "x": 360, "y": 105, "tone": "graph" },
|
||||
{ "id": "z3", "label": "compiler rejects invalid graph edits immediately", "x": 360, "y": 210, "tone": "compiler" },
|
||||
{ "id": "z4", "label": "agent runs only task validation", "x": 360, "y": 315, "tone": "graph" },
|
||||
{ "id": "z5", "label": "human reviews projection when useful", "x": 360, "y": 420, "tone": "human" }
|
||||
],
|
||||
"edges": [
|
||||
{ "source": "t1", "target": "t2" },
|
||||
{ "source": "t2", "target": "t3" },
|
||||
{ "source": "t3", "target": "t4" },
|
||||
{ "source": "t4", "target": "t5" },
|
||||
{ "source": "t5", "target": "t1", "label": "repeat" },
|
||||
{ "source": "z1", "target": "z2" },
|
||||
{ "source": "z2", "target": "z3" },
|
||||
{ "source": "z3", "target": "z4" },
|
||||
{ "source": "z4", "target": "z5" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The difference is not just syntax. A graph patch can target “the literal
|
||||
argument to this write call” or “the body of this block” instead of asking an
|
||||
agent to locate and rewrite a span of text.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "change the greeting to hello graph"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll patch the greeting and run the program so you can see the output."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero query --fn main",
|
||||
"output": "main\n check world.out.write \"hello from zero\\n\"\n graphHash graph:a7f7e6899a73f3b4"
|
||||
},
|
||||
{
|
||||
"command": "zero patch --expect-graph-hash graph:a7f7e6899a73f3b4 --op 'set node=\"#expr_653eeb6e\" field=\"value\" expect=\"hello from zero\\n\" value=\"hello graph\\n\"'",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero run",
|
||||
"output": "hello graph"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Invalid Edits Fail Earlier
|
||||
|
||||
The graph store has shape rules. Required edges, ordered child groups, node
|
||||
kinds, type facts, and repository metadata are validated when patches are
|
||||
applied. If an edit would leave a sparse argument list, a missing expression, a
|
||||
stale graph hash, or an invalid repository store, the patch fails before the
|
||||
package becomes the new compiler input.
|
||||
|
||||
That is the agent-facing contract: write checked semantic edits, not hopeful
|
||||
text diffs.
|
||||
|
||||
## Human Review Stays Textual
|
||||
|
||||
Humans should not have to inspect graph dumps to trust a change. `.0`
|
||||
projections exist so people can read, review, and occasionally manually edit a
|
||||
program.
|
||||
|
||||
The important distinction is ownership:
|
||||
|
||||
- agents normally author through `zero query` and `zero patch`
|
||||
- humans review through projections
|
||||
- humans may edit projections as an escape hatch
|
||||
- `zero import` reconstructs the graph from reviewed projection text
|
||||
- `zero verify-projection` catches drift instead of hiding it
|
||||
|
||||
Zero is a graph-native language with human-editable text projections.
|
||||
|
||||
## The Payoff
|
||||
|
||||
The graph-first model is meant to reduce guessing and reduce tool calls. A
|
||||
checked patch combines edit intent, stale-state protection, shape validation,
|
||||
and formatting-normalized projection output into one compiler-mediated step.
|
||||
|
||||
That gives agents a smaller, more precise work surface. It gives humans a
|
||||
reviewable source-like view. It gives the compiler a direct path to semantic
|
||||
program facts without reparsing text on the normal package compile path.
|
||||
@@ -0,0 +1,100 @@
|
||||
## Human Review Surface
|
||||
|
||||
In Zerolang, `.0` files are human-readable projections of the graph.
|
||||
|
||||
They exist for trust. A human can read the program, review a change, and make a
|
||||
manual edit when needed. They are also useful for diagnostics because source
|
||||
maps can point to familiar line and column spans.
|
||||
|
||||
They are not the normal agent write surface.
|
||||
|
||||
## Explicit Sync Model
|
||||
|
||||
Zero uses explicit commands so graph and projection state cannot silently drift:
|
||||
|
||||
```sh
|
||||
zero export
|
||||
zero import
|
||||
zero verify-projection
|
||||
zero status
|
||||
```
|
||||
|
||||
Use `zero export` when a human needs the latest `.0` review text. Use
|
||||
`zero import` after a human intentionally edits projection text and wants to
|
||||
reconstruct the graph. Use `zero verify-projection` in review or CI when you
|
||||
want a no-write drift check.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "show me the projection so i can review it"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll export the current projection and verify it is in sync."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero export",
|
||||
"output": "repository graph export ok\nwrote: ./src/main.0"
|
||||
},
|
||||
{
|
||||
"command": "zero verify-projection",
|
||||
"output": "repository graph verify-projection ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## No Silent Divergence
|
||||
|
||||
`zero status` reports whether the projection is clean, missing, stale, conflicting, or unavailable.
|
||||
When the checked-in `.0` sources were edited after `zero.graph` was written, commands that consume the store, including `zero check`, `zero build`, `zero run`, `zero test`, `zero query`, `zero view`, and `zero diff`, refresh the store from the edited source first and report the refresh on stderr.
|
||||
When the graph is the newer side, for example right after `zero patch`, those commands keep using `zero.graph` until `zero export` syncs the projection, and they say so on stderr.
|
||||
When both sides were edited independently, they fail with an `RGP006` diagnostic that offers `zero import` and `zero export` as repairs instead of picking a side.
|
||||
Which side moved is decided by content: every store write records a hash of the source projection inside `zero.graph`, so a freshly staged, cloned, or extracted workspace classifies the same way everywhere regardless of file timestamps.
|
||||
Set `ZERO_STALE=fail` to fail with an `RGP008` diagnostic instead of refreshing automatically.
|
||||
|
||||
That rule prevents the worst ambiguity: an agent editing text, seeing `zero check` pass, and then running a binary built from different code.
|
||||
|
||||
## Human Escape Hatch
|
||||
|
||||
The escape hatch is deliberate. A project should remain reconstructable from
|
||||
text projections. A human can edit `src/main.0`, reconcile it back into the
|
||||
graph, and confirm the projection still matches:
|
||||
|
||||
```json-render
|
||||
{
|
||||
"type": "flow",
|
||||
"title": "Human escape hatch: edit text, reconcile to the graph",
|
||||
"nodes": [
|
||||
{ "id": "n1", "label": "human reviews src/main.0", "x": 0, "y": 0, "tone": "human" },
|
||||
{ "id": "n2", "label": "human edits src/main.0", "x": 0, "y": 96, "tone": "human" },
|
||||
{ "id": "n3", "label": "zero import", "x": 0, "y": 192, "tone": "compiler" },
|
||||
{ "id": "n4", "label": "zero check", "x": 0, "y": 288, "tone": "compiler" },
|
||||
{ "id": "n5", "label": "zero export", "x": 0, "y": 384, "tone": "graph" },
|
||||
{ "id": "n6", "label": "zero verify-projection", "x": 0, "y": 480, "tone": "compiler" }
|
||||
],
|
||||
"edges": [
|
||||
{ "source": "n1", "target": "n2" },
|
||||
{ "source": "n2", "target": "n3" },
|
||||
{ "source": "n3", "target": "n4" },
|
||||
{ "source": "n4", "target": "n5" },
|
||||
{ "source": "n5", "target": "n6" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This keeps the system auditable. The graph is first-class, but humans are not
|
||||
locked out of a readable format.
|
||||
|
||||
## Review Rule
|
||||
|
||||
Use graph commands for authoring. Use projections for review. Import
|
||||
projections only when a human intentionally edits them.
|
||||
@@ -0,0 +1,94 @@
|
||||
## Semantic Edits
|
||||
|
||||
In Zerolang, text is an excellent human review format. It is not an ideal agent edit format.
|
||||
|
||||
When an agent edits text, it must keep many details in its context at once:
|
||||
syntax, indentation, imports, symbol names, surrounding control flow, stale
|
||||
file contents, and the compiler's current capabilities. A small mistake can
|
||||
produce invalid code that is only discovered after another tool call.
|
||||
|
||||
Graph edits carry more intent:
|
||||
|
||||
```text
|
||||
replaceFunctionBody main
|
||||
check world.out.write "hello\n"
|
||||
end
|
||||
```
|
||||
|
||||
That patch says which function body changes and what rows should become the new
|
||||
body. The compiler parses those rows, builds graph nodes, checks graph shape,
|
||||
updates repository metadata, and can export a stable projection for humans.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "add a help command and then show me the source"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll add the help branch, run it, and then show the projection."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero patch --check-only /tmp/help.patch",
|
||||
"output": "program graph patch ok (check-only)"
|
||||
},
|
||||
{
|
||||
"command": "zero patch /tmp/help.patch",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero run -- help",
|
||||
"output": "usage: zero run -- <command>"
|
||||
},
|
||||
{
|
||||
"command": "zero export",
|
||||
"output": "repository graph export ok\nwrote: ./src/main.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Text Edit Failure Modes
|
||||
|
||||
Common text-edit problems for agents:
|
||||
|
||||
- editing the wrong overload or similarly named function
|
||||
- losing an import or closing brace
|
||||
- creating syntax that looks plausible but is not accepted by this compiler
|
||||
- formatting code that later changes the span the agent intended to patch
|
||||
- using stale file contents after another edit
|
||||
- changing source while the graph store remains the actual compiler input
|
||||
|
||||
Zero does not remove all errors. It moves the primary edit operation closer to
|
||||
the compiler's semantic model so more errors are caught at patch time.
|
||||
|
||||
## Graph Patch Guardrails
|
||||
|
||||
Graph patches can include:
|
||||
|
||||
- graph hash expectations
|
||||
- node hash expectations
|
||||
- field expectations
|
||||
- typed operation names
|
||||
- function body or block body replacement
|
||||
- dry-run and check-only modes
|
||||
|
||||
Those guardrails make stale edits explicit. They also make the failure useful:
|
||||
the agent can query the graph again and patch the current node rather than
|
||||
guessing from text.
|
||||
|
||||
## Where Projections Fit
|
||||
|
||||
Projection text is still part of the system. It gives humans a compact review
|
||||
format and a manual edit escape hatch. It also gives diagnostics stable spans
|
||||
and makes examples readable.
|
||||
|
||||
The rule is simple: agents write the graph by default; humans review
|
||||
projections by choice.
|
||||
@@ -0,0 +1,75 @@
|
||||
## Targets Are Explicit
|
||||
|
||||
In Zerolang, start with the result the human wants. The agent decides whether it needs target
|
||||
facts, a no-write readiness check, or an artifact build.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "build this for linux musl and tell me if anything blocks it"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll check the target facts first, then build if it is ready."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero check --json --target linux-musl-x64",
|
||||
"output": "{\"ok\":true,\"targetReadiness\":{\"ok\":true}}"
|
||||
},
|
||||
{
|
||||
"command": "zero build --target linux-musl-x64 --out .zero/out/app",
|
||||
"output": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## What This Means
|
||||
|
||||
Zero cross-compilation starts from graph facts, target facts, and direct
|
||||
emitters. Unsupported target or feature combinations should fail with
|
||||
diagnostics rather than fallback silently.
|
||||
|
||||
## Check Target Readiness
|
||||
|
||||
Use `check --json` before writing artifacts when an agent needs readiness data:
|
||||
|
||||
```sh
|
||||
zero targets
|
||||
zero check --json --emit obj --target linux-musl-x64 examples/direct-call-add.graph
|
||||
zero check --target linux-musl-x64 examples/memory-package
|
||||
```
|
||||
|
||||
Target readiness can include `target`, `objectFormat`, `backend`, `stage`, and
|
||||
unsupported feature facts.
|
||||
|
||||
## Build Direct Artifacts
|
||||
|
||||
```sh
|
||||
zero build --target linux-musl-x64 examples/memory-package --out .zero/out/memory-package
|
||||
zero build --emit exe --target linux-musl-x64 examples/direct-exe-return.graph --out .zero/out/direct-exe-return
|
||||
zero build --emit obj --target darwin-arm64 examples/direct-call-add.graph --out .zero/out/direct-call-add.o
|
||||
```
|
||||
|
||||
Use `zero size --json --target <target>` when the question is about retained
|
||||
helpers, section sizes, or artifact facts rather than producing a new file.
|
||||
|
||||
## Sysroots And C Boundaries
|
||||
|
||||
Cross-target C interop must use explicit package metadata, vendored inputs, or
|
||||
target sysroots. The compiler should not silently reuse host headers and
|
||||
libraries for a foreign target.
|
||||
|
||||
Use:
|
||||
|
||||
```sh
|
||||
zero inspect --json --target linux-musl-x64 <package>
|
||||
zero abi dump --json --target linux-musl-x64 <graph-input>
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
## Read The Error As A Repair Contract
|
||||
|
||||
Zerolang diagnostics are written for humans and agents together. A human should see what
|
||||
went wrong. An agent should see the safest next edit shape.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
error[NAM003]: Unknown identifier
|
||||
unknown identifier 'message'
|
||||
examples/hello.0:2:27
|
||||
|
||||
2 | check world.out.write(message)
|
||||
| ^^^^^^^
|
||||
rule: Names must be declared before use in the current lexical scope.
|
||||
expected: local binding, parameter, function, builtin value
|
||||
actual: no visible symbol named 'message'
|
||||
fix: Introduce a local binding before this use (local-edit)
|
||||
explain: zero explain NAM003
|
||||
```
|
||||
|
||||
The path may name a `.0` projection because source maps point at readable text.
|
||||
That does not mean the agent should edit that file. In a graph-first package,
|
||||
the repair should normally be a graph patch.
|
||||
|
||||
## Agent Repair Flow
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "this fails with NAM003. can you fix it?"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll inspect the diagnostic, patch the missing binding, and rerun the focused check."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero explain NAM003",
|
||||
"output": "NAM003: unresolved name\nfix: introduce a binding or use an in-scope symbol"
|
||||
},
|
||||
{
|
||||
"command": "zero query --fn main",
|
||||
"output": "main\n check world.out.write message"
|
||||
},
|
||||
{
|
||||
"command": "zero patch /tmp/introduce-message.patch",
|
||||
"output": "program graph patch ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Text And JSON Modes
|
||||
|
||||
Use text first:
|
||||
|
||||
```sh
|
||||
zero check
|
||||
zero explain NAM003
|
||||
```
|
||||
|
||||
Use JSON when a tool needs exact spans, codes, fix ids, or nested readiness
|
||||
facts:
|
||||
|
||||
```sh
|
||||
zero check --json
|
||||
zero fix --plan --json
|
||||
zero doctor --json
|
||||
```
|
||||
|
||||
Agents should not default to JSON for every command. JSON is for automation,
|
||||
edit planning, CI, editors, and exact diagnostic fields.
|
||||
|
||||
## Common Graph-First Diagnostics
|
||||
|
||||
`BLD002` means a command expected graph-backed input but received a projection
|
||||
or another unsupported input. Import the projection or pass the package/graph
|
||||
store.
|
||||
|
||||
`RGP003` means projection state is invalid, stale, conflicting, or impossible
|
||||
to verify. Use `zero status`, then choose `zero export`, `zero import`, or
|
||||
manual review.
|
||||
|
||||
`GRF013` and related graph diagnostics mean the proposed graph edit would leave
|
||||
an invalid shape, such as a sparse ordered group.
|
||||
|
||||
`CIMP003` and `CIMP005` report unsafe or incomplete C import/link metadata.
|
||||
Use package-relative headers/libraries or configure target C dependency facts
|
||||
instead of relying on host paths.
|
||||
|
||||
## Fix Plans
|
||||
|
||||
`zero fix --plan --json` returns a repair plan, not an automatic edit:
|
||||
|
||||
```sh
|
||||
zero fix --plan --json
|
||||
```
|
||||
|
||||
Use it when an agent needs a safer next step. The agent should still explain
|
||||
the patch it will apply and validate the user-visible behavior afterward.
|
||||
@@ -0,0 +1,112 @@
|
||||
## Start With Graph Examples
|
||||
|
||||
The Zerolang examples directory is the fastest way to understand what works today. Most
|
||||
examples are checked-in `.graph` inputs or graph-first packages with
|
||||
`zero.graph` stores. Sibling `.0` files, when present, are human-readable
|
||||
projections for review.
|
||||
|
||||
## Expected Usage
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "make a tiny contacts api like the crm example"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll use the CRM example as the pattern and run the route checks."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero query examples/crm-api --find route",
|
||||
"output": "routes: health, contacts list, contact by id, create, update, delete"
|
||||
},
|
||||
{
|
||||
"command": "zero patch /tmp/contacts-api.patch",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero run -- $'GET /health\\n\\n'",
|
||||
"output": "HTTP/1.1 200 OK"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Learning Order
|
||||
|
||||
Small programs:
|
||||
|
||||
- `examples/hello.graph`
|
||||
- `examples/add.graph`
|
||||
- `examples/point.graph`
|
||||
- `examples/result-choice.graph`
|
||||
- `examples/fallibility.graph`
|
||||
|
||||
Memory and ownership:
|
||||
|
||||
- `examples/memory-primitives.graph`
|
||||
- `examples/allocator-collections.graph`
|
||||
- `examples/ownership-cleanup.graph`
|
||||
- `examples/fixed-vec.graph`
|
||||
|
||||
CLI and files:
|
||||
|
||||
- `examples/cli-file.graph`
|
||||
- `examples/cli-config.graph`
|
||||
- `examples/file-copy.graph`
|
||||
- `examples/readall-cli/`
|
||||
- `examples/resource-cli/`
|
||||
|
||||
Data and web:
|
||||
|
||||
- `examples/std-data-formats.graph`
|
||||
- `examples/json-api-client.graph`
|
||||
- `examples/json-api-router.graph`
|
||||
- `examples/crm-api/`
|
||||
- `examples/std-http-json.graph`
|
||||
- `examples/std-http-request.graph`
|
||||
|
||||
Compiler and agent workflows:
|
||||
|
||||
- `examples/compile-time-v1.graph`
|
||||
- `examples/agent-repair-demo/`
|
||||
- `examples/error-tour/`
|
||||
- `examples/memory-package/`
|
||||
- `examples/zero-hash/`
|
||||
|
||||
## Inspect And Run Examples
|
||||
|
||||
```sh
|
||||
zero query examples/hello.graph
|
||||
zero check examples/hello.graph
|
||||
zero run examples/add.graph
|
||||
zero build --emit exe --target linux-musl-x64 examples/add.graph --out .zero/out/add
|
||||
```
|
||||
|
||||
## Build And Size Examples
|
||||
|
||||
```sh
|
||||
zero build --release tiny --target linux-musl-x64 examples/hello.graph --out .zero/out/hello-linux-musl
|
||||
zero build --release tiny --target win32-x64.exe examples/hello.graph --out .zero/out/hello-win32
|
||||
zero size --json --release tiny --target linux-musl-x64 examples/hello.graph
|
||||
```
|
||||
|
||||
Size output reports retained helpers, section sizes, profile facts, target
|
||||
report data, and artifact size. Use it when an agent needs to explain why bytes
|
||||
were retained.
|
||||
|
||||
## Native Workflow Coverage
|
||||
|
||||
The examples intentionally cover arguments and environment, filesystem
|
||||
resources, deterministic exit status, unhandled error exit paths, fixed-capacity
|
||||
storage, HTTP routing, JSON helpers, target status, and repair workflows.
|
||||
|
||||
Use them as prompts for agents and as smoke checks for humans reviewing a
|
||||
language change.
|
||||
@@ -0,0 +1,120 @@
|
||||
## Start With An Agent
|
||||
|
||||
Zerolang is designed for a human working with an agent.
|
||||
|
||||
The agent should author the program through the graph. The human should review
|
||||
the graph summary, command output, and the `.0` projection when useful. A
|
||||
projection is readable and bidirectional, but it is not the normal place for an
|
||||
agent to write code.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
curl -fsSL https://zerolang.ai/install.sh | bash
|
||||
export PATH="$HOME/.zero/bin:$PATH"
|
||||
zero --version
|
||||
```
|
||||
|
||||
Then install the agent bootstrap skill:
|
||||
|
||||
```sh
|
||||
npx skills add vercel-labs/zerolang
|
||||
```
|
||||
|
||||
Use the installed `zero` command in public examples. If you are developing Zero
|
||||
itself, follow the repository contributor notes for checkout-local compiler
|
||||
work.
|
||||
|
||||
## Hello World
|
||||
|
||||
Start by asking:
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "build hello world for zerolang"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll initialize this directory, add main, and run it."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero init",
|
||||
"output": "graph project init ok\nwrote: ./zero.toml\nwrote: ./zero.graph"
|
||||
},
|
||||
{
|
||||
"command": "zero patch --op 'addMain' --op 'addCheckWrite fn=\"main\" text=\"hello from zero\\n\"'",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero run",
|
||||
"output": "hello from zero"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
The expected projection is:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
check world.out.write("hello from zero\n")
|
||||
}
|
||||
```
|
||||
|
||||
That file is a projection of `zero.graph`. Humans can read it, review it, and
|
||||
occasionally edit it. Agents should normally keep using `zero query` and
|
||||
`zero patch`.
|
||||
|
||||
## The Daily Loop
|
||||
|
||||
Use this loop for most tasks:
|
||||
|
||||
```sh
|
||||
zero query
|
||||
zero patch --op help
|
||||
zero patch --op 'addMain'
|
||||
zero check
|
||||
zero test
|
||||
zero run -- <args>
|
||||
```
|
||||
|
||||
The default input is the current directory, so a package command does not need
|
||||
`.` unless you want to be explicit.
|
||||
|
||||
## Reviewing A Projection
|
||||
|
||||
When a human wants to see readable text:
|
||||
|
||||
```sh
|
||||
zero export
|
||||
zero verify-projection
|
||||
```
|
||||
|
||||
When a human intentionally edits `src/main.0`, import the projection back into
|
||||
the graph before checking or running:
|
||||
|
||||
```sh
|
||||
zero import
|
||||
zero check
|
||||
```
|
||||
|
||||
Do not use projection export as an automatic agent step. Export when a human
|
||||
asks to review source-like text or when CI wants a projection drift gate.
|
||||
|
||||
## Build An Artifact
|
||||
|
||||
Use `zero build` for executable, object, or LLVM IR artifacts:
|
||||
|
||||
```sh
|
||||
zero build --emit exe --target linux-musl-x64 --out .zero/out/app
|
||||
```
|
||||
|
||||
For early exploration, `zero run` is usually enough.
|
||||
@@ -0,0 +1,57 @@
|
||||
## Install The Compiler
|
||||
|
||||
Install Zerolang when you want an agent to build graph-first programs on your
|
||||
machine. The compiler is experimental. Use it in isolated workspaces and avoid
|
||||
production data.
|
||||
|
||||
## Install The Latest Release
|
||||
|
||||
```sh
|
||||
curl -fsSL https://zerolang.ai/install.sh | bash
|
||||
export PATH="$HOME/.zero/bin:$PATH"
|
||||
zero --version
|
||||
```
|
||||
|
||||
The installer downloads the latest release asset for your platform and checks
|
||||
the release checksum file before installing the binary.
|
||||
|
||||
## Verify The Environment
|
||||
|
||||
```sh
|
||||
zero doctor
|
||||
zero targets
|
||||
zero skills
|
||||
```
|
||||
|
||||
`zero doctor --json` includes host and toolchain readiness. `zero targets --json`
|
||||
includes `targetToolchains`, target aliases, hosted capability facts, and
|
||||
cross-target support notes.
|
||||
|
||||
## Load Version-Matched Agent Knowledge
|
||||
|
||||
Agents should not rely on a stale external Zero guide. Ask the installed
|
||||
compiler for the skills bundled with that exact binary:
|
||||
|
||||
```sh
|
||||
zero skills
|
||||
zero skills get agent
|
||||
zero skills get graph
|
||||
zero skills get language
|
||||
zero skills get stdlib
|
||||
```
|
||||
|
||||
The thin external Zero skill is only a bootstrap stub. The compiler-bundled
|
||||
skills are the current command and language reference for that release.
|
||||
|
||||
## Repository Checkout
|
||||
|
||||
When working inside the Zero compiler repository, build the local compiler and
|
||||
then use the checkout's `zero` binary for experiments:
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
make -C native/zero-c
|
||||
zero --version
|
||||
```
|
||||
|
||||
The repository contributor notes cover checkout-specific wrapper commands.
|
||||
@@ -0,0 +1,194 @@
|
||||
## How Programs Are Shaped
|
||||
|
||||
Zerolang programs are semantic graph declarations with a human-readable `.0`
|
||||
projection. This page names the language pieces that appear in both views.
|
||||
|
||||
Read **Primitives And Types** first when you want scalar types, `Maybe<T>`,
|
||||
spans, arrays, ownership, and layout. Use this page for declarations, function
|
||||
bodies, capabilities, packages, and projection rules.
|
||||
|
||||
## Declarations
|
||||
|
||||
The graph stores declarations for functions, types, enums, constants, imports,
|
||||
tests, and package modules. Projection syntax makes those declarations readable:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
check world.out.write("hello\n")
|
||||
}
|
||||
```
|
||||
|
||||
Public declarations should have explicit type information. That makes graph
|
||||
facts, diagnostics, docs, and repair plans stable.
|
||||
|
||||
## Functions
|
||||
|
||||
```zero
|
||||
fn add(x: i32, y: i32) -> i32 {
|
||||
return x + y
|
||||
}
|
||||
```
|
||||
|
||||
Function graph facts include the name, parameters, return type, fallibility,
|
||||
body block, references, and call edges. Agents should use `zero query --fn add`
|
||||
before editing a function body or signature.
|
||||
|
||||
Fallible functions use `raises`:
|
||||
|
||||
```zero
|
||||
fn requirePositive(value: i32) -> i32 raises [Invalid] {
|
||||
if value > 0 {
|
||||
return value
|
||||
}
|
||||
raise Invalid
|
||||
}
|
||||
```
|
||||
|
||||
`check` propagates failure through explicit control flow. There are no hidden
|
||||
exceptions.
|
||||
|
||||
## Blocks And Control Flow
|
||||
|
||||
Blocks are graph nodes. Agents can patch a whole function body or a specific
|
||||
block body:
|
||||
|
||||
```text
|
||||
replaceFunctionBody main
|
||||
check world.out.write "hello\n"
|
||||
end
|
||||
```
|
||||
|
||||
```text
|
||||
replaceBlockBody #block_then_1234
|
||||
check world.out.write "ready\n"
|
||||
end
|
||||
```
|
||||
|
||||
Projection syntax:
|
||||
|
||||
```zero
|
||||
if ready {
|
||||
check world.out.write("ready\n")
|
||||
} else {
|
||||
check world.out.write("not ready\n")
|
||||
}
|
||||
```
|
||||
|
||||
Conditions must be `Bool`. `while` loops and `match` expressions also lower to
|
||||
explicit graph control-flow nodes.
|
||||
|
||||
## Capabilities
|
||||
|
||||
Zero avoids ambient global runtime access. Programs receive capabilities
|
||||
explicitly:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
check world.out.write("ok\n")
|
||||
}
|
||||
```
|
||||
|
||||
Standard library helpers document their effects and target support. Use
|
||||
`zero inspect --json` and `zero size --json` to see which helpers and
|
||||
capabilities a graph input actually retains.
|
||||
|
||||
## Packages
|
||||
|
||||
Graph-first packages normally have:
|
||||
|
||||
```text
|
||||
zero.toml
|
||||
zero.graph
|
||||
src/main.0
|
||||
```
|
||||
|
||||
The target `main` path points at the readable projection for source maps and
|
||||
review. It does not make `src/main.0` the normal package compile input.
|
||||
Package commands compile from `zero.graph`.
|
||||
|
||||
## Imports And Modules
|
||||
|
||||
Package-local modules resolve from `src/` projection paths so humans have
|
||||
stable review files:
|
||||
|
||||
- `src/foo.0` defines module `foo`
|
||||
- `src/foo/mod.0` defines directory module `foo`
|
||||
|
||||
The graph store records the module declarations and relationships. Import
|
||||
cycles and duplicate public exports are diagnosed before build output.
|
||||
|
||||
## Compile-Time Facts
|
||||
|
||||
Zero exposes a small compile-time metadata surface for target and type facts.
|
||||
Current compile-time values include integer, `Bool`, and enum static values.
|
||||
Representative metadata includes `compileTime`, `target.pointerWidth`,
|
||||
`fieldType`, and `hasEnumCase`.
|
||||
|
||||
Invalid compile-time queries report diagnostics such as `MET001`. The design
|
||||
goal is explicit graph metadata, not runtime registries or raw token-string
|
||||
builders.
|
||||
|
||||
## Projection Rules
|
||||
|
||||
Projection syntax is the human-readable view of the graph:
|
||||
|
||||
- export projections for review with `zero export`
|
||||
- import projections after human edits with `zero import`
|
||||
- verify drift with `zero verify-projection`
|
||||
- use graph commands for normal agent authoring
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "change just the ready branch"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll change that branch only and run the behavior it affects."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero query --fn main",
|
||||
"output": "if block\n then #block_then_1234\n else #block_else_5678"
|
||||
},
|
||||
{
|
||||
"command": "zero patch /tmp/replace-then.patch",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero export",
|
||||
"output": "repository graph export ok\nwrote: ./src/main.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## What Is Not Hidden
|
||||
|
||||
Zero intentionally avoids hidden method registries, vtables, reflection,
|
||||
ambient heap allocation, and process-global cleanup lists in the current
|
||||
language model. When a program uses owned resources, allocator state, hosted
|
||||
I/O, network capability, or C interop, those facts should be visible through
|
||||
graph inspection and diagnostics.
|
||||
|
||||
## Targets
|
||||
|
||||
The public native target names are:
|
||||
|
||||
- `darwin-arm64`
|
||||
- `darwin-x64`
|
||||
- `linux-arm64`
|
||||
- `linux-musl-arm64`
|
||||
- `linux-musl-x64`
|
||||
- `linux-x64`
|
||||
- `win32-arm64.exe`
|
||||
- `win32-x64.exe`
|
||||
|
||||
Use `zero targets --json` and `zero check --json --target <target>` before
|
||||
asking an agent to rely on target-specific capabilities.
|
||||
@@ -0,0 +1,192 @@
|
||||
## The Human Model
|
||||
|
||||
Zerolang has two views of the same program:
|
||||
|
||||
- The graph is the program database. Agents inspect and patch it.
|
||||
- The `.0` projection is readable text. Humans use it for review and rare
|
||||
manual edits.
|
||||
|
||||
When this page shows Zero syntax, it is showing projection syntax. The graph
|
||||
contains the same declarations, types, calls, and edges as structured facts.
|
||||
|
||||
## Expected Usage
|
||||
|
||||
Ask for the behavior in normal language. The Zero skills tell the agent to use
|
||||
the graph:
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "make a cli that adds two numbers"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll add the function, wire the CLI, and run a sample input."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero query --fn main",
|
||||
"output": "main\n check world.out.write \"hello\\n\""
|
||||
},
|
||||
{
|
||||
"command": "zero patch /tmp/add-cli.patch",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero run -- 40 2",
|
||||
"output": "42"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Under the hood, the agent gathers current compiler knowledge with `zero skills`,
|
||||
inspects the package with `zero status` or `zero query`, patches the graph, then
|
||||
runs `zero check`, `zero test`, or `zero run` only when useful for the task.
|
||||
|
||||
## A Minimal Program
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
check world.out.write("hello\n")
|
||||
}
|
||||
```
|
||||
|
||||
Pieces visible in both graph and projection:
|
||||
|
||||
- `pub fn main` declares the entry point.
|
||||
- `world: World` is an explicit capability parameter.
|
||||
- `Void` means no useful return value.
|
||||
- `raises` marks a fallible function.
|
||||
- `check` propagates a fallible operation.
|
||||
- `world.out.write(...)` writes through an explicit output capability.
|
||||
|
||||
Run the graph input:
|
||||
|
||||
```sh
|
||||
zero run examples/hello.graph
|
||||
```
|
||||
|
||||
## Values And Bindings
|
||||
|
||||
```zero
|
||||
let name: String = "Ada"
|
||||
var count: u32 = 0
|
||||
count = count + 1
|
||||
```
|
||||
|
||||
`let` is immutable. `var` is mutable. Public constants and declarations should
|
||||
carry explicit types because the graph, diagnostics, and docs all benefit from
|
||||
stable type facts.
|
||||
|
||||
## Functions
|
||||
|
||||
```zero
|
||||
fn add(x: i32, y: i32) -> i32 {
|
||||
return x + y
|
||||
}
|
||||
|
||||
test "add works" {
|
||||
expect (add(40, 2) == 42)
|
||||
}
|
||||
```
|
||||
|
||||
Agents should usually add this through patch operations such as `addFunction`,
|
||||
`addParam`, `addReturnBinary`, and `addTest`, or through the row-based body DSL
|
||||
when replacing a function or block body.
|
||||
|
||||
## Types
|
||||
|
||||
```zero
|
||||
type Point {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
|
||||
enum Status {
|
||||
Pending,
|
||||
Ready,
|
||||
}
|
||||
```
|
||||
|
||||
Types are graph declarations. Projection snippets make them readable for
|
||||
humans, but tools should inspect declaration nodes and symbol references.
|
||||
|
||||
## Control Flow
|
||||
|
||||
```zero
|
||||
if ready {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
|
||||
while index < count {
|
||||
index = index + 1
|
||||
}
|
||||
```
|
||||
|
||||
Conditions must be `Bool`. Branch and loop bodies are blocks in the graph, so
|
||||
agents can patch a specific block without replacing the entire function.
|
||||
|
||||
## Absence And Errors
|
||||
|
||||
```zero
|
||||
let value: Maybe<u32> = std.args.parseU32(1)
|
||||
if value.has {
|
||||
return value.value
|
||||
}
|
||||
return 0
|
||||
```
|
||||
|
||||
`Maybe<T>` represents absence. Fallible functions use `raises`; `check`
|
||||
propagates failure through explicit control flow rather than exceptions.
|
||||
|
||||
## Memory Views
|
||||
|
||||
```zero
|
||||
let bytes: Span<u8> = std.mem.span("hello")
|
||||
var scratch: [16]u8 = [0_u8; 16]
|
||||
let copied: usize = std.mem.copy(scratch, bytes)
|
||||
```
|
||||
|
||||
`Span<T>` borrows contiguous storage. `[N]T` is fixed storage. Standard library
|
||||
helpers prefer caller-owned buffers so the graph can expose allocation and
|
||||
ownership facts.
|
||||
|
||||
## Packages And Projections
|
||||
|
||||
A graph-first package has:
|
||||
|
||||
```text
|
||||
zero.toml
|
||||
zero.graph
|
||||
src/main.0
|
||||
```
|
||||
|
||||
`zero.graph` is the normal compile input. `src/main.0` is the readable
|
||||
projection named by the package target for source maps and review.
|
||||
|
||||
Use:
|
||||
|
||||
```sh
|
||||
zero export
|
||||
zero import
|
||||
zero verify-projection
|
||||
```
|
||||
|
||||
Only use `export` or `import` when a human review or manual edit calls for it.
|
||||
|
||||
## What To Read Next
|
||||
|
||||
- Read **CLI Reference** for command groups and graph patch forms.
|
||||
- Read **Primitives And Types** for the language pieces behind graph facts.
|
||||
- Read **Standard Library** before asking an agent for CLI, HTTP, JSON, or
|
||||
filesystem programs.
|
||||
- Read **Diagnostics And Repair** when an agent hits a compiler error.
|
||||
@@ -0,0 +1,46 @@
|
||||
## When To Use std.args
|
||||
|
||||
In Zerolang, use `std.args` for hosted command-line programs that need positional
|
||||
arguments, option lookup, or simple numeric argument parsing.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.args.len()` | `usize` | Returns the process argument count. |
|
||||
| `std.args.get(index)` | `Maybe<String>` | Returns the argument at `index` when present. |
|
||||
| `std.args.has(index)` | `Bool` | Reports whether `index` has an argument. |
|
||||
| `std.args.getOr(index, fallback)` | `String` | Returns the argument or a caller-provided fallback. |
|
||||
| `std.args.find(name)` | `Maybe<usize>` | Finds the first exact argument match after the executable path. |
|
||||
| `std.args.valueAfter(name)` | `Maybe<String>` | Returns the argument immediately after a matched option name. |
|
||||
| `std.args.parseU32(index)` | `Maybe<u32>` | Parses an indexed argument as `u32`. |
|
||||
|
||||
Current limits:
|
||||
|
||||
- Iterator-style argument APIs.
|
||||
- Target diagnostics for platforms without process arguments.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let count: usize = std.args.len()
|
||||
let first: String = std.args.getOr(1, "default")
|
||||
let maybe_count: Maybe<u32> = std.args.parseU32(2)
|
||||
if count > 2 && maybe_count.has {
|
||||
check world.out.write(first)
|
||||
check world.out.write("\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
The module is hosted-only. Freestanding, edge, and embedded targets should
|
||||
reject it unless they explicitly provide an argument capability.
|
||||
|
||||
On native Windows-style targets, `std.args` is byte-oriented process input. It
|
||||
is not a Unicode argv normalization layer.
|
||||
|
||||
Programs that need portable argument semantics should keep target-specific
|
||||
decoding outside the target-neutral core.
|
||||
@@ -0,0 +1,38 @@
|
||||
## When To Use std.ascii
|
||||
|
||||
In Zerolang, use `std.ascii` when a program needs byte-level ASCII predicates, case
|
||||
conversion, or digit values without Unicode normalization.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.ascii.isDigit(byte)` | `Bool` | Checks `0` through `9`. |
|
||||
| `std.ascii.isAlpha(byte)` | `Bool` | Checks `A` through `Z` or `a` through `z`. |
|
||||
| `std.ascii.isAlnum(byte)` | `Bool` | Checks ASCII alphabetic or digit bytes. |
|
||||
| `std.ascii.isWhitespace(byte)` | `Bool` | Checks space, tab, line feed, and carriage return. |
|
||||
| `std.ascii.isLower(byte)` / `std.ascii.isUpper(byte)` | `Bool` | Checks ASCII case ranges. |
|
||||
| `std.ascii.isHexDigit(byte)` | `Bool` | Checks decimal digits and `a-f` / `A-F`. |
|
||||
| `std.ascii.toLower(byte)` / `std.ascii.toUpper(byte)` | `u8` | Converts ASCII letters and leaves other bytes unchanged. |
|
||||
| `std.ascii.digitValue(byte)` | `Maybe<u8>` | Converts an ASCII decimal digit to `0..9`. |
|
||||
| `std.ascii.hexValue(byte)` | `Maybe<u8>` | Converts an ASCII hexadecimal digit to `0..15`. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let digit: Maybe<u8> = std.ascii.digitValue(55_u8)
|
||||
let hex: Maybe<u8> = std.ascii.hexValue(70_u8)
|
||||
if std.ascii.isAlpha(65_u8) && std.ascii.toLower(90_u8) == 122_u8 && digit.has && digit.value == 7_u8 && hex.has && hex.value == 15_u8 {
|
||||
check world.out.write("ascii ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: none.
|
||||
|
||||
Allocation behavior: no allocation.
|
||||
|
||||
Error behavior: value helpers return `null` when the byte is outside the accepted range.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,87 @@
|
||||
## When To Use std.cli
|
||||
|
||||
In Zerolang, use `std.cli` for hosted command-line flag and option helpers that sit one
|
||||
level above raw `std.args` access.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.cli.argEquals(index, expected)` | `Bool` | Checks one argument against an exact string. |
|
||||
| `std.cli.command()` | `Maybe<String>` | Returns argument 1 as the command name. |
|
||||
| `std.cli.commandOr(fallback)` | `String` | Returns the command name or a fallback. |
|
||||
| `std.cli.commandEquals(expected)` | `Bool` | Checks argument 1 against an exact command string. |
|
||||
| `std.cli.argOr(index, fallback)` | `String` | Returns an argument or a fallback. |
|
||||
| `std.cli.argU32Or(index, fallback)` | `u32` | Parses an argument as `u32` or returns a fallback. |
|
||||
| `std.cli.hasFlag(name)` | `Bool` | Reports whether an exact flag is present. |
|
||||
| `std.cli.optionValue(name)` | `Maybe<String>` | Returns the value immediately after an option name. |
|
||||
| `std.cli.optionValueOr(name, fallback)` | `String` | Returns the option value or a fallback. |
|
||||
| `std.cli.optionU32(name)` | `Maybe<u32>` | Parses an option value as `u32`. |
|
||||
| `std.cli.successExitCode()` | `i32` | Returns the conventional success exit code. |
|
||||
| `std.cli.usageExitCode()` | `i32` | Returns the conventional command-line usage error code. |
|
||||
| `std.cli.isHelp(command)` | `Bool` | Recognizes `help`, `--help`, and `-h`. |
|
||||
| `std.cli.needsHelp()` | `Bool` | Reports whether the current invocation has no command or asks for help. |
|
||||
| `std.cli.commandIn2(command, first, second)` | `Bool` | Checks a command against two accepted command names. |
|
||||
| `std.cli.commandIn3(command, first, second, third)` | `Bool` | Checks a command against three accepted command names. |
|
||||
| `std.cli.formatUsage(buffer, program, syntax)` | `Maybe<Span<u8>>` | Writes `usage: <program> <syntax>` into caller storage. |
|
||||
| `std.cli.formatCommand(buffer, name, syntax, summary)` | `Maybe<Span<u8>>` | Writes one indented command help row. |
|
||||
| `std.cli.formatOption(buffer, name, valueName, summary)` | `Maybe<Span<u8>>` | Writes one indented option help row. |
|
||||
| `std.cli.formatSection(buffer, title)` | `Maybe<Span<u8>>` | Writes a section heading such as `Options:\n`. |
|
||||
| `std.cli.formatHelpRow(buffer, label, summary)` | `Maybe<Span<u8>>` | Writes one padded, newline-terminated help row using the default label width. |
|
||||
| `std.cli.formatHelpRowCustom(buffer, label, summary, indent, width)` | `Maybe<Span<u8>>` | Writes one padded help row using caller-supplied indentation and label width. |
|
||||
| `std.cli.formatHelpRowWithWidth(buffer, label, summary, width)` | `Maybe<Span<u8>>` | Writes one padded help row using a caller-supplied label width. |
|
||||
| `std.cli.formatHelp(buffer, usage, description)` | `Maybe<Span<u8>>` | Writes a help header with `Usage:` and an optional description. |
|
||||
| `std.cli.formatError(buffer, message)` | `Maybe<Span<u8>>` | Writes an `error: ...` line. |
|
||||
| `std.cli.formatUnknownCommand(buffer, command)` | `Maybe<Span<u8>>` | Writes a conventional unknown-command error. |
|
||||
| `std.cli.formatMissingOperand(buffer, operand)` | `Maybe<Span<u8>>` | Writes a conventional missing-operand error. |
|
||||
| `std.cli.formatInvalidOption(buffer, option)` | `Maybe<Span<u8>>` | Writes a conventional invalid-option error. |
|
||||
|
||||
Current limits:
|
||||
|
||||
- Table-driven command schemas.
|
||||
- Bool, signed integer, and `usize` option shortcuts; compose `optionValue` with `std.parse`.
|
||||
- Process exit from inside `std.cli`; use the returned exit-code constants with host process handling.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
if std.cli.needsHelp() {
|
||||
var command_storage: [96]u8 = [0_u8; 96]
|
||||
let commands: Maybe<Span<u8>> = std.cli.formatHelpRow(command_storage, "hello [name]", "print a greeting")
|
||||
if commands.has {
|
||||
var help_storage: [256]u8 = [0_u8; 256]
|
||||
let help: Maybe<Span<u8>> = std.cli.formatHelp(help_storage, "zero-test [command]", "Small hosted CLI.")
|
||||
if help.has {
|
||||
check world.out.write(help.value)
|
||||
check world.out.write("\nCommands:\n")
|
||||
check world.out.write(commands.value)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
let command: String = std.cli.commandOr("")
|
||||
if std.mem.eql(command, "hello") {
|
||||
let name: String = std.cli.argOr(2, "world")
|
||||
check world.out.write("hello ")
|
||||
check world.out.write(name)
|
||||
check world.out.write("\n")
|
||||
return
|
||||
}
|
||||
var error_storage: [64]u8 = [0_u8; 64]
|
||||
let error: Maybe<Span<u8>> = std.cli.formatUnknownCommand(error_storage, command)
|
||||
if error.has {
|
||||
check world.err.write(error.value)
|
||||
check world.err.write("\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.cli` is a thin, hosted layer over `std.args`. It keeps subcommand, fallback,
|
||||
typed argument, help row, and usage error patterns regular without hiding
|
||||
process arguments behind a global parser or allocating command tables. For
|
||||
custom help layouts, compose `formatHelp`, `formatSection`, `formatHelpRow`,
|
||||
`formatHelpRowWithWidth`, and `formatHelpRowCustom` rather than relying on a
|
||||
global formatter state.
|
||||
@@ -0,0 +1,82 @@
|
||||
## When To Use std.codec
|
||||
|
||||
In Zerolang, use `std.codec` for byte encodings: endian integer reads/writes,
|
||||
varints, base32, base64, hex, and checksums over caller-owned storage.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.codec.crc32(bytes)` | `u32` | Computes CRC-32 for a string-backed byte input. |
|
||||
| `std.codec.crc32Bytes(bytes)` | `u32` | Computes CRC-32 for a span or mutable span without allocation. |
|
||||
| `std.codec.encodedVarintLen(value)` | `usize` | Returns the byte length of an unsigned varint encoding. |
|
||||
| `std.codec.encodedVarintLen64(value)` | `usize` | Returns the byte length of an unsigned 64-bit varint encoding. |
|
||||
| `std.codec.encodedSignedVarintLen(value)` / `std.codec.encodedSignedVarintLen64(value)` | `usize` | Returns the byte length of a ZigZag signed varint encoding. |
|
||||
| `std.codec.varintEncode(buffer, value)` | `Maybe<Span<u8>>` | Writes an unsigned varint into caller storage. |
|
||||
| `std.codec.varintDecode(bytes)` | `Maybe<u32>` | Decodes a bounded unsigned varint. |
|
||||
| `std.codec.varintEncode64(buffer, value)` / `std.codec.varintDecode64(bytes)` | `Maybe<Span<u8>>` / `Maybe<u64>` | Writes and reads bounded unsigned 64-bit varints. |
|
||||
| `std.codec.signedVarintEncode(buffer, value)` / `std.codec.signedVarintDecode(bytes)` | `Maybe<Span<u8>>` / `Maybe<i32>` | Writes and reads ZigZag signed 32-bit varints. |
|
||||
| `std.codec.signedVarintEncode64(buffer, value)` / `std.codec.signedVarintDecode64(bytes)` | `Maybe<Span<u8>>` / `Maybe<i64>` | Writes and reads ZigZag signed 64-bit varints. |
|
||||
| `std.codec.readU16Le(bytes)` | `Maybe<u16>` | Bounds-checked little-endian read from a byte span. |
|
||||
| `std.codec.readU16Be(bytes)` | `Maybe<u16>` | Bounds-checked big-endian read from a byte span. |
|
||||
| `std.codec.readU32Le(bytes)` | `Maybe<u32>` | Bounds-checked little-endian read from a byte span. |
|
||||
| `std.codec.readU32Be(bytes)` | `Maybe<u32>` | Bounds-checked big-endian read from a byte span. |
|
||||
| `std.codec.readU64Le(bytes)` | `Maybe<u64>` | Bounds-checked little-endian read from a byte span. |
|
||||
| `std.codec.readU64Be(bytes)` | `Maybe<u64>` | Bounds-checked big-endian read from a byte span. |
|
||||
| `std.codec.writeU16Le(buffer, value)` | `Maybe<Span<u8>>` | Writes little-endian bytes into caller storage. |
|
||||
| `std.codec.writeU16Be(buffer, value)` | `Maybe<Span<u8>>` | Writes big-endian bytes into caller storage. |
|
||||
| `std.codec.writeU32Le(buffer, value)` | `Maybe<Span<u8>>` | Writes little-endian bytes into caller storage. |
|
||||
| `std.codec.writeU32Be(buffer, value)` | `Maybe<Span<u8>>` | Writes big-endian bytes into caller storage. |
|
||||
| `std.codec.writeU64Le(buffer, value)` | `Maybe<Span<u8>>` | Writes little-endian `u64` bytes into caller storage. |
|
||||
| `std.codec.writeU64Be(buffer, value)` | `Maybe<Span<u8>>` | Writes big-endian `u64` bytes into caller storage. |
|
||||
| `std.codec.base32EncodedLen(len)` / `std.codec.base32RawEncodedLen(len)` | `usize` | Returns padded or unpadded base32 encoded length. |
|
||||
| `std.codec.base32Encode(buffer, bytes)` / `std.codec.base32RawEncode(buffer, bytes)` | `Maybe<Span<u8>>` | Writes RFC 4648 base32 text into caller storage. |
|
||||
| `std.codec.base32DecodedLen(bytes)` / `std.codec.base32RawDecodedLen(bytes)` | `Maybe<usize>` | Validates padded or unpadded base32 text and returns decoded length. |
|
||||
| `std.codec.base32Decode(buffer, bytes)` / `std.codec.base32RawDecode(buffer, bytes)` | `Maybe<Span<u8>>` | Writes decoded base32 bytes into caller storage. |
|
||||
| `std.codec.base64EncodedLen(len)` | `usize` | Returns the encoded length for a base64 payload. |
|
||||
| `std.codec.base64Encode(buffer, bytes)` | `Maybe<String>` | Writes base64 text into caller storage. |
|
||||
| `std.codec.base64DecodedLen(bytes)` | `Maybe<usize>` | Validates padded base64 text and returns decoded length. |
|
||||
| `std.codec.base64Decode(buffer, bytes)` | `Maybe<Span<u8>>` | Writes decoded base64 bytes into caller storage. |
|
||||
| `std.codec.base64RawEncodedLen(len)` / `std.codec.base64RawEncode(buffer, bytes)` | `usize` / `Maybe<Span<u8>>` | Uses the standard base64 alphabet without padding. |
|
||||
| `std.codec.base64RawDecodedLen(bytes)` / `std.codec.base64RawDecode(buffer, bytes)` | `Maybe<usize>` / `Maybe<Span<u8>>` | Validates and decodes unpadded standard base64. |
|
||||
| `std.codec.base64UrlEncodedLen(len)` / `std.codec.base64UrlEncode(buffer, bytes)` | `usize` / `Maybe<Span<u8>>` | Uses the URL-safe base64 alphabet without padding. |
|
||||
| `std.codec.base64UrlDecodedLen(bytes)` / `std.codec.base64UrlDecode(buffer, bytes)` | `Maybe<usize>` / `Maybe<Span<u8>>` | Validates and decodes unpadded URL-safe base64. |
|
||||
| `std.codec.hexEncode(buffer, bytes)` | `Maybe<String>` | Writes lowercase hexadecimal text into caller storage. |
|
||||
| `std.codec.hexDecodedLen(bytes)` | `Maybe<usize>` | Validates hex text and returns decoded length. |
|
||||
| `std.codec.hexDecode(buffer, bytes)` | `Maybe<Span<u8>>` | Writes decoded hex bytes into caller storage. |
|
||||
| `std.codec.utf8Valid(bytes)` | `Bool` | Validates a byte span as UTF-8. |
|
||||
| `std.codec.urlEncode(buffer, text)` | `Maybe<String>` | Percent-encodes a string into caller storage. |
|
||||
|
||||
Current limits:
|
||||
|
||||
- Buffer-backed write APIs.
|
||||
- Streaming encoders and decoders.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
use std.codec
|
||||
|
||||
use std.mem
|
||||
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let len: usize = std.codec.encodedVarintLen(300)
|
||||
let checksum: u32 = std.codec.crc32("zero")
|
||||
let bytes: Span<u8> = std.mem.span("zero")
|
||||
let byte_checksum: u32 = std.codec.crc32Bytes(bytes)
|
||||
var encoded: [5]u8 = [0_u8; 5]
|
||||
let varint: Maybe<Span<u8>> = std.codec.varintEncode(encoded, 300_u32)
|
||||
var decoded: [4]u8 = [0_u8; 4]
|
||||
let text: Maybe<Span<u8>> = std.codec.base64Decode(decoded, "emVybw==")
|
||||
var base32_storage: [8]u8 = [0_u8; 8]
|
||||
let base32: Maybe<Span<u8>> = std.codec.base32Encode(base32_storage, "zero")
|
||||
if len == 2 && checksum == byte_checksum && varint.has && text.has && std.mem.eql(text.value, "zero") && base32.has && std.mem.eql(base32.value, "PJSXE3Y=") {
|
||||
check world.out.write("codec primitives ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
Codec helpers are byte-oriented and allocation-free. Decoders return
|
||||
`Maybe<T>` on malformed input or insufficient caller storage.
|
||||
@@ -0,0 +1,256 @@
|
||||
## When To Use std.collections
|
||||
|
||||
In Zerolang, use `std.collections` for fixed-capacity collection operations where the caller
|
||||
owns the storage and growth must be explicit.
|
||||
|
||||
Runnable today:
|
||||
|
||||
The collection helpers operate over caller-owned fixed arrays or `MutSpan<T>`
|
||||
storage plus an explicit live length. They do not allocate, grow, or retain
|
||||
hidden state. Generic helpers currently support the same non-owned scalar item
|
||||
types as the generic `std.mem` item helpers: `Bool`, `u8`, `u16`, `usize`,
|
||||
`i32`, `u32`, `i64`, and `u64`.
|
||||
|
||||
Use `FixedSet<T>`, `FixedDeque<T>`, `FixedRingBuffer<T>`, or
|
||||
`FixedMap<K, V>` when it is clearer to carry storage and live length as one
|
||||
value. These resource values still borrow caller-owned mutable storage;
|
||||
inserting, removing, clearing, or truncating values never allocates.
|
||||
|
||||
For byte collections, storage can come from a fixed array or from an explicit
|
||||
allocator. Use `std.mem.allocBytes(alloc, capacity)` to request a
|
||||
`MutSpan<u8>`, then pass that mutable span to `FixedSet<u8>`,
|
||||
`FixedDeque<u8>`, or `FixedMap<u8, u8>`. The wrapper does not own the storage;
|
||||
the allocator-backed span must remain live for as long as the collection value
|
||||
is used.
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.collections.push(items, len, value)` | `usize` | Writes `value` at `len` when capacity remains and returns the next length. Returns the unchanged length on overflow. |
|
||||
| `std.collections.append(items, len, values)` | `usize` | Copies all non-overlapping `values` into `items` at `len` when the full append fits. Returns the unchanged length on overflow or invalid length. |
|
||||
| `std.collections.clear(items, len)` | `usize` | Returns `0` as the next live length. Storage contents are left unchanged. |
|
||||
| `std.collections.truncate(items, len, newLen)` | `usize` | Returns the smaller of the current live length and `newLen`, after clamping invalid `len` to storage capacity. Storage contents are left unchanged. |
|
||||
| `std.collections.first(items, len)` | `Maybe<T>` | Returns the first live item, or `null` when the live prefix is empty. |
|
||||
| `std.collections.last(items, len)` | `Maybe<T>` | Returns the last live item, or `null` when the live prefix is empty. |
|
||||
| `std.collections.pop(items, len)` | `usize` | Returns the next live length after removing the last item. Returns `0` when empty. Storage contents are left unchanged. |
|
||||
| `std.collections.dequePushBack(items, len, value)` | `usize` | Writes `value` at the back when capacity remains and returns the next length. |
|
||||
| `std.collections.dequePushFront(items, len, value)` | `usize` | Inserts `value` at the front by shifting the live prefix right. Returns the unchanged length when full or invalid. |
|
||||
| `std.collections.dequeFront(items, len)` | `Maybe<T>` | Returns the first live deque item, or `null` when empty. |
|
||||
| `std.collections.dequeBack(items, len)` | `Maybe<T>` | Returns the last live deque item, or `null` when empty. |
|
||||
| `std.collections.dequePopBack(items, len)` | `usize` | Returns the next live length after removing the back item. Storage contents are left unchanged. |
|
||||
| `std.collections.dequePopFront(items, len)` | `usize` | Removes the front item by shifting the live suffix left and returns the next length. Returns the unchanged length when empty or invalid. |
|
||||
| `std.collections.fixedDeque(items, len)` | `FixedDeque<T>` | Builds a fixed-capacity deque value over caller-owned mutable storage and clamps the initial live length to capacity. |
|
||||
| `std.collections.fixedDequeBack(deque)` | `Maybe<T>` | Returns the last live deque item, or `null` when empty. |
|
||||
| `std.collections.fixedDequeClear(deque)` | `usize` | Clears a `FixedDeque<T>` and returns `0` as the next live length. |
|
||||
| `std.collections.fixedDequeFront(deque)` | `Maybe<T>` | Returns the first live deque item, or `null` when empty. |
|
||||
| `std.collections.fixedDequeIsFull(deque)` | `Bool` | Reports whether a `FixedDeque<T>` has no remaining capacity. |
|
||||
| `std.collections.fixedDequeLen(deque)` | `usize` | Returns the current live length of a `FixedDeque<T>`. |
|
||||
| `std.collections.fixedDequePopBack(deque)` | `Maybe<T>` | Removes and returns the back item when present. |
|
||||
| `std.collections.fixedDequePopFront(deque)` | `Maybe<T>` | Removes and returns the front item when present, shifting the live suffix left. |
|
||||
| `std.collections.fixedDequePushBack(deque, value)` | `Bool` | Appends `value` at the back when capacity remains. Returns whether the live deque changed. |
|
||||
| `std.collections.fixedDequePushFront(deque, value)` | `Bool` | Inserts `value` at the front when capacity remains. Returns whether the live deque changed. |
|
||||
| `std.collections.fixedDequeRemaining(deque)` | `usize` | Returns remaining storage capacity for a `FixedDeque<T>`. |
|
||||
| `std.collections.fixedDequeTruncate(deque, newLen)` | `usize` | Updates a `FixedDeque<T>` live prefix to the smaller of the current live length and `newLen`, clamped to storage. |
|
||||
| `std.collections.fixedDequeView(deque)` | `Span<T>` | Returns a read-only prefix view over live `FixedDeque<T>` items. |
|
||||
| `std.collections.fixedRingBuffer(items, head, len)` | `FixedRingBuffer<T>` | Builds a fixed-capacity ring buffer over caller-owned mutable storage, normalizing `head` and clamping `len` to capacity. |
|
||||
| `std.collections.fixedRingBufferBack(ring)` | `Maybe<T>` | Returns the last live logical item, or `null` when empty. |
|
||||
| `std.collections.fixedRingBufferCapacity(ring)` | `usize` | Returns the storage capacity. |
|
||||
| `std.collections.fixedRingBufferClear(ring)` | `usize` | Clears a `FixedRingBuffer<T>`, resets its head to `0`, and returns `0` as the next live length. |
|
||||
| `std.collections.fixedRingBufferFront(ring)` | `Maybe<T>` | Returns the first live logical item, or `null` when empty. |
|
||||
| `std.collections.fixedRingBufferGet(ring, index)` | `Maybe<T>` | Reads a logical index through wrap-around storage, or returns `null` outside the live length. |
|
||||
| `std.collections.fixedRingBufferIsFull(ring)` | `Bool` | Reports whether a `FixedRingBuffer<T>` has no remaining capacity. |
|
||||
| `std.collections.fixedRingBufferLen(ring)` | `usize` | Returns the current live length. |
|
||||
| `std.collections.fixedRingBufferPopBack(ring)` | `Maybe<T>` | Removes and returns the back logical item when present. |
|
||||
| `std.collections.fixedRingBufferPopFront(ring)` | `Maybe<T>` | Removes and returns the front logical item when present, advancing the stored head. |
|
||||
| `std.collections.fixedRingBufferPushBack(ring, value)` | `Bool` | Appends `value` at the logical back when capacity remains. |
|
||||
| `std.collections.fixedRingBufferPushFront(ring, value)` | `Bool` | Inserts `value` at the logical front when capacity remains. |
|
||||
| `std.collections.fixedRingBufferRemaining(ring)` | `usize` | Returns remaining storage capacity. |
|
||||
| `std.collections.fixedRingBufferTruncate(ring, newLen)` | `usize` | Updates the live logical prefix to the smaller of the current live length and `newLen`, clamped to storage. |
|
||||
| `std.collections.fill(items, len, value)` | `Bool` | Writes `value` across the live prefix. Returns `false` for invalid length. |
|
||||
| `std.collections.insertAt(items, len, index, value)` | `usize` | Inserts `value` at `index` by shifting the live suffix right. Returns the unchanged length when full or invalid. |
|
||||
| `std.collections.replaceAt(items, len, index, value)` | `Bool` | Replaces the live item at `index`. Returns `false` for invalid length or index. |
|
||||
| `std.collections.swapAt(items, len, left, right)` | `Bool` | Swaps two live items. Returns `false` for invalid length or index. |
|
||||
| `std.collections.insertUnique(items, len, value)` | `usize` | Treats the live prefix as a fixed-capacity set. Appends `value` only when absent and capacity remains. |
|
||||
| `std.collections.setClear(items, len)` | `usize` | Returns `0` as the next live set length. Storage contents are left unchanged. |
|
||||
| `std.collections.setContains(items, len, value)` | `Bool` | Reports whether the live prefix contains `value` as a fixed-capacity set. |
|
||||
| `std.collections.setInsert(items, len, value)` | `usize` | Appends `value` only when absent and capacity remains. |
|
||||
| `std.collections.setRemaining(items, len)` | `usize` | Returns remaining fixed-capacity set storage. Returns `0` when `len` is at or past capacity. |
|
||||
| `std.collections.setIsFull(items, len)` | `Bool` | Reports whether the fixed-capacity set has no remaining storage. |
|
||||
| `std.collections.setRemove(items, len, value)` | `usize` | Removes the first matching set value with swap-remove and returns the next length. |
|
||||
| `std.collections.setTruncate(items, len, newLen)` | `usize` | Returns the smaller of the current live set length and `newLen`, after clamping invalid `len` to storage capacity. |
|
||||
| `std.collections.setView(items, len)` | `Span<T>` | Returns a clamped read-only prefix view over the live fixed-capacity set items. |
|
||||
| `std.collections.fixedSet(items, len)` | `FixedSet<T>` | Builds a fixed-capacity set value over caller-owned mutable storage and clamps the initial live length to capacity. |
|
||||
| `std.collections.fixedSetClear(set)` | `usize` | Clears a `FixedSet<T>` and returns `0` as the next live length. |
|
||||
| `std.collections.fixedSetContains(set, value)` | `Bool` | Reports whether a `FixedSet<T>` contains `value` in its live prefix. |
|
||||
| `std.collections.fixedSetInsert(set, value)` | `Bool` | Inserts `value` when absent and capacity remains. Returns whether the live set changed. |
|
||||
| `std.collections.fixedSetIsFull(set)` | `Bool` | Reports whether a `FixedSet<T>` has no remaining capacity. |
|
||||
| `std.collections.fixedSetLen(set)` | `usize` | Returns the current live length of a `FixedSet<T>`. |
|
||||
| `std.collections.fixedSetRemaining(set)` | `usize` | Returns remaining storage capacity for a `FixedSet<T>`. |
|
||||
| `std.collections.fixedSetRemove(set, value)` | `Bool` | Removes `value` with swap-remove when present. Returns whether the live set changed. |
|
||||
| `std.collections.fixedSetTruncate(set, newLen)` | `usize` | Updates a `FixedSet<T>` live prefix to the smaller of the current live length and `newLen`, clamped to storage. |
|
||||
| `std.collections.fixedSetView(set)` | `Span<T>` | Returns a read-only prefix view over live `FixedSet<T>` items. |
|
||||
| `std.collections.fixedMap(keys, values, len)` | `FixedMap<K, V>` | Builds a fixed-capacity map value over caller-owned mutable key/value storage and clamps the initial live length to the shorter storage. |
|
||||
| `std.collections.fixedMapClear(map)` | `usize` | Clears a `FixedMap<K, V>` and returns `0` as the next live length. |
|
||||
| `std.collections.fixedMapContains(map, key)` | `Bool` | Reports whether a `FixedMap<K, V>` contains `key` in its live key prefix. |
|
||||
| `std.collections.fixedMapGet(map, key)` | `Maybe<V>` | Returns the value for `key` when present. |
|
||||
| `std.collections.fixedMapIndex(map, key)` | `usize` | Searches live keys and returns the matching index, or the live length when absent. |
|
||||
| `std.collections.fixedMapIsFull(map)` | `Bool` | Reports whether a `FixedMap<K, V>` has no remaining capacity in the shorter storage. |
|
||||
| `std.collections.fixedMapKeys(map)` | `Span<K>` | Returns a read-only prefix view over live `FixedMap<K, V>` keys. |
|
||||
| `std.collections.fixedMapLen(map)` | `usize` | Returns the current live length of a `FixedMap<K, V>`. |
|
||||
| `std.collections.fixedMapPut(map, key, value)` | `Bool` | Updates an existing key's value or appends a key/value pair when capacity remains. Returns whether the key exists afterward. |
|
||||
| `std.collections.fixedMapRemaining(map)` | `usize` | Returns remaining storage capacity in the shorter key/value storage. |
|
||||
| `std.collections.fixedMapRemove(map, key)` | `Bool` | Removes a key/value pair with swap-remove when present. Returns whether the live map changed. |
|
||||
| `std.collections.fixedMapTruncate(map, newLen)` | `usize` | Updates the live prefix to the smaller of the current live length and `newLen`, clamped to storage. |
|
||||
| `std.collections.fixedMapValues(map)` | `Span<V>` | Returns a read-only prefix view over live `FixedMap<K, V>` values. |
|
||||
| `std.collections.mapClear(keys, values, len)` | `usize` | Returns `0` as the next live map length. Key/value storage contents are left unchanged. |
|
||||
| `std.collections.mapContains(keys, len, key)` | `Bool` | Reports whether the live key prefix contains `key`. |
|
||||
| `std.collections.mapIndex(keys, len, key)` | `usize` | Searches the live key prefix and returns the matching index. Returns the live length when absent. |
|
||||
| `std.collections.mapIsFull(keys, values, len)` | `Bool` | Reports whether the shorter of key/value storage has no remaining capacity. |
|
||||
| `std.collections.mapKeys(keys, len)` | `Span<K>` | Returns a clamped read-only prefix view over the live map keys. |
|
||||
| `std.collections.mapGet(keys, values, len, key)` | `Maybe<V>` | Treats parallel key/value storage as a fixed-capacity map and returns the value for `key` when present. |
|
||||
| `std.collections.mapPut(keys, values, len, key, value)` | `usize` | Updates an existing key's value or appends a key/value pair when capacity remains. Returns the unchanged length on overflow or invalid length. |
|
||||
| `std.collections.mapRemaining(keys, values, len)` | `usize` | Returns remaining capacity in the shorter of key/value storage. |
|
||||
| `std.collections.mapRemove(keys, values, len, key)` | `usize` | Removes the first matching key/value pair with swap-remove and returns the next length. Returns the unchanged length when absent or invalid. |
|
||||
| `std.collections.mapTruncate(keys, values, len, newLen)` | `usize` | Returns the smaller of the live map length and `newLen`, clamped to key and value storage. |
|
||||
| `std.collections.mapValues(keys, values, len)` | `Span<V>` | Returns a clamped read-only prefix view over live map values, bounded by both key and value storage. |
|
||||
| `std.collections.view(items, len)` | `Span<T>` | Returns a clamped read-only prefix view over the live collection items. |
|
||||
| `std.collections.remaining(items, len)` | `usize` | Returns remaining capacity after the live prefix. Returns `0` when `len` is at or past capacity. |
|
||||
| `std.collections.isFull(items, len)` | `Bool` | Reports whether no capacity remains. Invalid lengths at or past capacity count as full. |
|
||||
| `std.collections.contains(items, len, needle)` | `Bool` | Searches only the live prefix for `needle`. |
|
||||
| `std.collections.count(items, len, needle)` | `usize` | Counts matching values in the live prefix. |
|
||||
| `std.collections.removeAt(items, len, index)` | `usize` | Removes `index` by shifting the live suffix left and returns the next length. Returns the unchanged length for invalid length or index. |
|
||||
| `std.collections.removeValue(items, len, value)` | `usize` | Removes the first matching live value with swap-remove and returns the next length. Returns the unchanged length when absent. |
|
||||
| `std.collections.removeSwap(items, len, index)` | `usize` | Replaces `index` with the last live item and returns the next length. Returns the unchanged length for invalid length or index. |
|
||||
| `std.collections.reverse(items, len)` | `Bool` | Reverses the live prefix in place. Returns `false` for invalid length. |
|
||||
| `std.collections.rotateLeft(items, len, count)` | `Bool` | Rotates the live prefix left by `count`. Returns `false` for invalid length. |
|
||||
| `std.collections.rotateRight(items, len, count)` | `Bool` | Rotates the live prefix right by `count`. Returns `false` for invalid length. |
|
||||
| `std.collections.moveToFront(items, len, index)` | `usize` | Moves the item at `index` to the front by shifting the live prefix. Returns the unchanged length for invalid length or index. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var values: [5]i32 = [0, 0, 0, 0, 0]
|
||||
let extra: [2]i32 = [4, 1]
|
||||
var len: usize = 0
|
||||
len = std.collections.push(values, len, 3)
|
||||
len = std.collections.push(values, len, 1)
|
||||
len = std.collections.setInsert(values, len, 3)
|
||||
len = std.collections.setInsert(values, len, 2)
|
||||
let has_three: Bool = std.collections.setContains(values, len, 3)
|
||||
let set_live: Span<i32> = std.collections.setView(values, len)
|
||||
let set_remaining: usize = std.collections.setRemaining(values, len)
|
||||
len = std.collections.setRemove(values, len, 2)
|
||||
let set_truncated: usize = std.collections.setTruncate(values, len, 1)
|
||||
var fixed_storage: [4]i32 = [1, 2, 0, 0]
|
||||
var fixed_set: FixedSet<i32> = std.collections.fixedSet(fixed_storage, 2_usize)
|
||||
let fixed_inserted: Bool = std.collections.fixedSetInsert(&mut fixed_set, 3)
|
||||
let fixed_removed: Bool = std.collections.fixedSetRemove(&mut fixed_set, 1)
|
||||
let fixed_live: Span<i32> = std.collections.fixedSetView(&fixed_set)
|
||||
let fixed_remaining: usize = std.collections.fixedSetRemaining(&fixed_set)
|
||||
let fixed_len: usize = std.collections.fixedSetLen(&fixed_set)
|
||||
let fixed_truncated: usize = std.collections.fixedSetTruncate(&mut fixed_set, 1_usize)
|
||||
var fixed_keys: [3]u8 = [1_u8, 2_u8, 0_u8]
|
||||
var fixed_scores: [3]u16 = [10_u16, 20_u16, 0_u16]
|
||||
var fixed_map: FixedMap<u8, u16> = std.collections.fixedMap(fixed_keys, fixed_scores, 2_usize)
|
||||
let fixed_map_added: Bool = std.collections.fixedMapPut(&mut fixed_map, 3_u8, 30_u16)
|
||||
let fixed_map_updated: Bool = std.collections.fixedMapPut(&mut fixed_map, 2_u8, 25_u16)
|
||||
let fixed_map_score: Maybe<u16> = std.collections.fixedMapGet(&fixed_map, 2_u8)
|
||||
let fixed_map_keys: Span<u8> = std.collections.fixedMapKeys(&fixed_map)
|
||||
let fixed_map_values: Span<u16> = std.collections.fixedMapValues(&fixed_map)
|
||||
let fixed_map_index: usize = std.collections.fixedMapIndex(&fixed_map, 2_u8)
|
||||
let fixed_map_full: Bool = std.collections.fixedMapIsFull(&fixed_map)
|
||||
let fixed_map_removed: Bool = std.collections.fixedMapRemove(&mut fixed_map, 1_u8)
|
||||
let inserted_len: usize = std.collections.insertAt(values, len, 1_usize, 2)
|
||||
let replaced: Bool = std.collections.replaceAt(values, inserted_len, 1_usize, 6)
|
||||
let swapped: Bool = std.collections.swapAt(values, inserted_len, 0_usize, 1_usize)
|
||||
len = std.collections.removeAt(values, inserted_len, 1_usize)
|
||||
let first: Maybe<i32> = std.collections.first(values, len)
|
||||
let last: Maybe<i32> = std.collections.last(values, len)
|
||||
let popped_len: usize = std.collections.pop(values, len)
|
||||
len = std.collections.truncate(values, len, 2)
|
||||
len = std.collections.append(values, len, extra)
|
||||
let live: Span<i32> = std.collections.view(values, len)
|
||||
var deque_values: [4]i32 = [0, 0, 0, 0]
|
||||
var deque_len: usize = 0
|
||||
deque_len = std.collections.dequePushBack(deque_values, deque_len, 2)
|
||||
deque_len = std.collections.dequePushFront(deque_values, deque_len, 1)
|
||||
deque_len = std.collections.dequePushBack(deque_values, deque_len, 3)
|
||||
let deque_front: Maybe<i32> = std.collections.dequeFront(deque_values, deque_len)
|
||||
let deque_back: Maybe<i32> = std.collections.dequeBack(deque_values, deque_len)
|
||||
let deque_after_pop_back: usize = std.collections.dequePopBack(deque_values, deque_len)
|
||||
deque_len = std.collections.dequePopFront(deque_values, deque_after_pop_back)
|
||||
var fixed_deque_values: [4]i32 = [0, 0, 0, 0]
|
||||
var fixed_deque: FixedDeque<i32> = std.collections.fixedDeque(fixed_deque_values, 0_usize)
|
||||
let fixed_deque_pushed: Bool = std.collections.fixedDequePushBack(&mut fixed_deque, 2)
|
||||
let fixed_deque_front_pushed: Bool = std.collections.fixedDequePushFront(&mut fixed_deque, 1)
|
||||
let fixed_deque_back: Maybe<i32> = std.collections.fixedDequeBack(&fixed_deque)
|
||||
let fixed_deque_front: Maybe<i32> = std.collections.fixedDequeFront(&fixed_deque)
|
||||
let fixed_deque_live: Span<i32> = std.collections.fixedDequeView(&fixed_deque)
|
||||
let fixed_deque_removed: Maybe<i32> = std.collections.fixedDequePopFront(&mut fixed_deque)
|
||||
var fixed_ring_values: [4]i32 = [0, 0, 0, 0]
|
||||
var fixed_ring: FixedRingBuffer<i32> = std.collections.fixedRingBuffer(fixed_ring_values, 0_usize, 0_usize)
|
||||
let fixed_ring_back_pushed: Bool = std.collections.fixedRingBufferPushBack(&mut fixed_ring, 2)
|
||||
let fixed_ring_front_pushed: Bool = std.collections.fixedRingBufferPushFront(&mut fixed_ring, 1)
|
||||
let fixed_ring_middle: Maybe<i32> = std.collections.fixedRingBufferGet(&fixed_ring, 1_usize)
|
||||
let fixed_ring_front: Maybe<i32> = std.collections.fixedRingBufferPopFront(&mut fixed_ring)
|
||||
let fixed_ring_wrapped: Bool = std.collections.fixedRingBufferPushBack(&mut fixed_ring, 3)
|
||||
var transform: [4]i32 = [1, 2, 3, 4]
|
||||
let reversed: Bool = std.collections.reverse(transform, 4_usize)
|
||||
let filled: Bool = std.collections.fill(transform, 2_usize, 9)
|
||||
let rotated_left: Bool = std.collections.rotateLeft(transform, 4_usize, 1_usize)
|
||||
let rotated_right: Bool = std.collections.rotateRight(transform, 4_usize, 2_usize)
|
||||
var keys: [3]u8 = [1_u8, 2_u8, 3_u8]
|
||||
var scores: [3]u16 = [10_u16, 20_u16, 30_u16]
|
||||
var map_len: usize = 2
|
||||
map_len = std.collections.mapPut(keys, scores, map_len, 3_u8, 30_u16)
|
||||
map_len = std.collections.mapPut(keys, scores, map_len, 2_u8, 25_u16)
|
||||
let has_score: Bool = std.collections.mapContains(keys, map_len, 2_u8)
|
||||
let score: Maybe<u16> = std.collections.mapGet(keys, scores, map_len, 2_u8)
|
||||
let live_keys: Span<u8> = std.collections.mapKeys(keys, map_len)
|
||||
let live_scores: Span<u16> = std.collections.mapValues(keys, scores, map_len)
|
||||
let map_remaining: usize = std.collections.mapRemaining(keys, scores, map_len)
|
||||
let map_full: Bool = std.collections.mapIsFull(keys, scores, map_len)
|
||||
map_len = std.collections.mapRemove(keys, scores, map_len, 2_u8)
|
||||
let removed_index: usize = std.collections.mapIndex(keys, map_len, 2_u8)
|
||||
if len == 4 && inserted_len == 3 && replaced && swapped && std.collections.clear(values, len) == 0 && std.collections.setClear(values, len) == 0 && first.has && first.value == 6 && last.has && last.value == 1 && popped_len == 1 && std.collections.remaining(values, len) == 1 && !std.collections.isFull(values, len) && has_three && std.mem.len(set_live) == 3 && set_remaining == 2 && set_truncated == 1 && fixed_inserted && fixed_removed && std.mem.len(fixed_live) == 2 && fixed_remaining == 2 && fixed_len == 2 && fixed_truncated == 1 && std.collections.fixedSetClear(&mut fixed_set) == 0 && fixed_map_added && fixed_map_updated && fixed_map_score.has && fixed_map_score.value == 25_u16 && std.mem.len(fixed_map_keys) == 3 && std.mem.len(fixed_map_values) == 3 && fixed_map_index == 1 && fixed_map_full && fixed_map_removed && std.collections.fixedMapClear(&mut fixed_map) == 0 && std.collections.contains(values, len, 4) && std.collections.count(values, len, 1) == 2 && std.mem.len(live) == 4 && deque_front.has && deque_front.value == 1 && deque_back.has && deque_back.value == 3 && deque_after_pop_back == 2 && deque_len == 1 && deque_values[0] == 2 && fixed_deque_pushed && fixed_deque_front_pushed && fixed_deque_back.has && fixed_deque_back.value == 2 && fixed_deque_front.has && fixed_deque_front.value == 1 && std.mem.len(fixed_deque_live) == 2 && fixed_deque_removed.has && fixed_deque_removed.value == 1 && fixed_ring_back_pushed && fixed_ring_front_pushed && fixed_ring_middle.has && fixed_ring_middle.value == 2 && fixed_ring_front.has && fixed_ring_front.value == 1 && fixed_ring_wrapped && reversed && filled && rotated_left && rotated_right && transform[0] == 1 && transform[1] == 9 && transform[2] == 9 && transform[3] == 2 && has_score && score.has && score.value == 25_u16 && std.mem.len(live_keys) == 3 && std.mem.len(live_scores) == 3 && map_remaining == 0 && map_full && std.collections.mapClear(keys, scores, map_len) == 0 && std.collections.mapTruncate(keys, scores, map_len, 2_usize) == 2 && map_len == 2 && removed_index == 2 {
|
||||
check world.out.write("collections ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Allocator-Backed Storage
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var key_storage: [4]u8 = [0_u8; 4]
|
||||
var value_storage: [4]u8 = [0_u8; 4]
|
||||
var key_alloc: FixedBufAlloc = std.mem.fixedBufAlloc(key_storage)
|
||||
var value_alloc: FixedBufAlloc = std.mem.fixedBufAlloc(value_storage)
|
||||
let keys_maybe: Maybe<MutSpan<u8>> = std.mem.allocBytes(key_alloc, 4_usize)
|
||||
let values_maybe: Maybe<MutSpan<u8>> = std.mem.allocBytes(value_alloc, 4_usize)
|
||||
if keys_maybe.has && values_maybe.has {
|
||||
var map: FixedMap<u8, u8> = std.collections.fixedMap(keys_maybe.value, values_maybe.value, 0_usize)
|
||||
let stored: Bool = std.collections.fixedMapPut(&mut map, 7_u8, 42_u8)
|
||||
let value: Maybe<u8> = std.collections.fixedMapGet(&map, 7_u8)
|
||||
if stored && value.has && value.value == 42_u8 {
|
||||
check world.out.write("allocator-backed map ok\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: writes to caller-provided mutable storage.
|
||||
|
||||
Allocation behavior: no allocation.
|
||||
|
||||
Error behavior: capacity and index failures are value-level. Helpers return the
|
||||
unchanged length instead of growing or raising.
|
||||
|
||||
`append` rejects source spans that the checker can prove overlap the destination
|
||||
storage. Use separate storage when copying a live prefix back into the same
|
||||
collection.
|
||||
|
||||
Ownership: helpers reject owned item elements; move or transfer owned values
|
||||
explicitly.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,58 @@
|
||||
## When To Use std.crypto
|
||||
|
||||
In Zerolang, use `std.crypto` for small hashes, SHA-256 digests, keyed hashes,
|
||||
constant-time equality, and target entropy helpers with explicit capability
|
||||
boundaries.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.crypto.hash32(bytes)` | `u32` | Computes the current 32-bit hash helper over bytes. |
|
||||
| `std.crypto.hmac32(key, bytes)` | `u32` | Computes the current keyed 32-bit helper over bytes. |
|
||||
| `std.crypto.constantTimeEql(a, b)` | `Bool` | Compares byte spans without data-dependent early exit. |
|
||||
| `std.crypto.secureRandomU32()` | `u32` | Reads target entropy where the target provides it. |
|
||||
| `std.crypto.fixedHex32(buffer, value)` | `Maybe<Span<u8>>` | Writes an 8-byte lowercase hex value into caller storage. |
|
||||
| `std.crypto.hashHex32(buffer, bytes)` | `Maybe<Span<u8>>` | Writes the 32-bit hash as fixed-width lowercase hex. |
|
||||
| `std.crypto.hmacHex32(buffer, key, bytes)` | `Maybe<Span<u8>>` | Writes the keyed 32-bit helper as fixed-width lowercase hex. |
|
||||
| `std.crypto.stableId32(buffer, bytes)` | `Maybe<Span<u8>>` | Writes a deterministic 8-byte ID from input bytes. |
|
||||
| `std.crypto.randomId32(buffer)` | `Maybe<Span<u8>>` | Writes an 8-byte random ID from target entropy. |
|
||||
| `std.crypto.sha256(buffer, bytes)` | `Maybe<Span<u8>>` | Writes the 32-byte SHA-256 digest into caller storage. |
|
||||
| `std.crypto.sha256Hex(buffer, bytes)` | `Maybe<Span<u8>>` | Writes the SHA-256 digest as 64 lowercase hex bytes. |
|
||||
| `std.crypto.hmacSha256(buffer, key, bytes)` | `Maybe<Span<u8>>` | Writes the 32-byte HMAC-SHA256 digest into caller storage. |
|
||||
| `std.crypto.hmacSha256Hex(buffer, key, bytes)` | `Maybe<Span<u8>>` | Writes the HMAC-SHA256 digest as 64 lowercase hex bytes. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: codec, memory, or rand
|
||||
- allocation behavior: no allocation; text helpers write caller-provided buffers
|
||||
- target support: hash helpers are target-neutral; secure random requires a rand-capable target
|
||||
- error behavior: caller-buffer helpers return `null` when storage is too small
|
||||
- ownership notes: borrows caller-provided byte spans
|
||||
- example: `examples/std-platform.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let hash: u32 = std.crypto.hash32(std.mem.span("message"))
|
||||
let hmac: u32 = std.crypto.hmac32(std.mem.span("key"), std.mem.span("message"))
|
||||
var id_buf: [8]u8 = [0_u8; 8]
|
||||
var sha_buf: [64]u8 = [0_u8; 64]
|
||||
var hmac_buf: [64]u8 = [0_u8; 64]
|
||||
let id: Maybe<Span<u8>> = std.crypto.stableId32(id_buf, std.mem.span("message"))
|
||||
let sha: Maybe<Span<u8>> = std.crypto.sha256Hex(sha_buf, std.mem.span("abc"))
|
||||
let hmac_sha: Maybe<Span<u8>> = std.crypto.hmacSha256Hex(hmac_buf, std.mem.span("key"), std.mem.span("message"))
|
||||
if hash > 0 && hmac > 0 && id.has && sha.has && hmac_sha.has && std.crypto.constantTimeEql(std.mem.span("same"), std.mem.span("same")) {
|
||||
check world.out.write("crypto ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.crypto` is a small helper surface. The SHA-256 and HMAC-SHA256 helpers
|
||||
cover common digest, keyed digest, and fixture needs without allocation. The
|
||||
fixed-width ID helpers are useful for deterministic labels, cache keys,
|
||||
fixtures, and examples. The module is not a TLS stack, certificate store,
|
||||
password hashing API, or secret-management API.
|
||||
@@ -0,0 +1,53 @@
|
||||
## When To Use std.csv
|
||||
|
||||
In Zerolang, use `std.csv` for allocation-free CSV validation, record scanning,
|
||||
field decoding, and small fixed-arity CSV writers.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.csv.valid(bytes)` | `Bool` | Validates bounded CSV input with quoted fields and CRLF or LF records. |
|
||||
| `std.csv.recordCount(bytes)` | `Maybe<usize>` | Counts valid records, returning null on malformed input. |
|
||||
| `std.csv.record(bytes, index)` | `Maybe<Span<u8>>` | Borrows one record slice by ordinal, excluding the line terminator. |
|
||||
| `std.csv.fieldCount(record)` | `Maybe<usize>` | Counts fields in one valid record. |
|
||||
| `std.csv.field(buffer, record, index)` | `Maybe<Span<u8>>` | Decodes one field into caller storage. |
|
||||
| `std.csv.encodedFieldLen(field)` | `usize` | Computes the bytes needed to write one CSV field. |
|
||||
| `std.csv.writeField(buffer, field)` | `Maybe<Span<u8>>` | Writes one CSV field with quotes when required. |
|
||||
| `std.csv.writeRecord2(buffer, left, right)` | `Maybe<Span<u8>>` | Writes a two-field record ending in `\n`. |
|
||||
| `std.csv.writeRecord3(buffer, first, second, third)` | `Maybe<Span<u8>>` | Writes a three-field record ending in `\n`. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: parse
|
||||
- allocation behavior: no allocation; decoded fields and writer output use caller storage
|
||||
- target support: target-neutral
|
||||
- error behavior: `Maybe` helpers return null on malformed input or insufficient storage
|
||||
- ownership notes: records borrow from the input; fields and writer output borrow from caller buffers
|
||||
- examples: `conformance/native/pass/std-csv.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let input: Span<u8> = "name,quote\nAda,\"a,b\"\n"
|
||||
let record: Maybe<Span<u8>> = std.csv.record(input, 1_usize)
|
||||
var field_buf: [16]u8 = [0_u8; 16]
|
||||
var quote: Maybe<Span<u8>> = null
|
||||
if record.has {
|
||||
quote = std.csv.field(field_buf, record.value, 1_usize)
|
||||
}
|
||||
if std.csv.valid(input) && quote.has && std.mem.eql(quote.value, "a,b") {
|
||||
check world.out.write("csv ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
CSV helpers follow the common RFC 4180 field rules: comma-separated fields,
|
||||
quoted fields, doubled quotes inside quoted fields, and CRLF or LF record
|
||||
separators. Quoted fields may contain newlines.
|
||||
|
||||
The writer surface is fixed-arity. Use `writeField` when a custom writer loop
|
||||
is needed.
|
||||
@@ -0,0 +1,47 @@
|
||||
## When To Use std.diag
|
||||
|
||||
In Zerolang, use `std.diag` to turn byte offsets into source locations and
|
||||
small diagnostic snippets without allocating.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.diag.line(bytes, offset)` | `usize` | Returns the 1-based line for a byte offset, clamping offsets past the end. |
|
||||
| `std.diag.column(bytes, offset)` | `usize` | Returns the 1-based byte column for a byte offset. |
|
||||
| `std.diag.lineStart(bytes, offset)` | `usize` | Returns the byte index where the containing line starts. |
|
||||
| `std.diag.lineEnd(bytes, offset)` | `usize` | Returns the byte index where the containing line ends, trimming a trailing CR before LF. |
|
||||
| `std.diag.lineText(bytes, offset)` | `Span<u8>` | Borrows the containing line without its newline. |
|
||||
| `std.diag.rangeLen(bytes, start, end)` | `usize` | Returns the clamped byte length for a half-open range. |
|
||||
| `std.diag.rangeText(bytes, start, end)` | `Span<u8>` | Borrows the clamped half-open byte range. |
|
||||
| `std.diag.formatLocation(buffer, path, line, column)` | `Maybe<Span<u8>>` | Writes `path:line:column` into caller storage. |
|
||||
| `std.diag.formatOffsetLocation(buffer, path, bytes, offset)` | `Maybe<Span<u8>>` | Computes line and column from an offset, then writes `path:line:column`. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: parse for source offset scanning; memory for caller-buffer formatting
|
||||
- allocation behavior: no allocation, except formatting writes into caller storage
|
||||
- target support: target-neutral
|
||||
- error behavior: formatting returns `null` when the buffer is too small
|
||||
- ownership notes: text helpers return borrowed views into the input bytes
|
||||
- example: `conformance/native/pass/std-diag.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let source: Span<u8> = "one\ntwo\nthree"
|
||||
var storage: [32]u8 = [0_u8; 32]
|
||||
let location: Maybe<Span<u8>> = std.diag.formatOffsetLocation(storage, "input.0", source, 5)
|
||||
if location.has && std.mem.eql(location.value, "input.0:2:2") && std.mem.eql(std.diag.lineText(source, 5), "two") {
|
||||
check world.out.write("diag ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
Offsets are byte offsets, not Unicode scalar indexes or terminal display
|
||||
columns. That keeps parser diagnostics deterministic and cheap across targets.
|
||||
Line and column numbers are 1-based for user-facing output. Range helpers use
|
||||
half-open byte ranges and clamp both ends to the input length.
|
||||
@@ -0,0 +1,52 @@
|
||||
## When To Use std.env
|
||||
|
||||
In Zerolang, use `std.env` for hosted environment variable lookup and simple typed
|
||||
configuration values.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.env.get(name)` | `Maybe<String>` | Returns a hosted process environment value when present. |
|
||||
| `std.env.has(name)` | `Bool` | Reports whether a hosted environment variable exists. |
|
||||
| `std.env.getOr(name, fallback)` | `String` | Returns the environment value or a caller-provided fallback. |
|
||||
| `std.env.equals(name, expected)` | `Bool` | Compares an environment value with expected text without exposing a missing value as success. |
|
||||
| `std.env.parseBool(name)` | `Maybe<Bool>` | Parses an environment value as `Bool`. |
|
||||
| `std.env.parseBoolOr(name, fallback)` | `Bool` | Parses a boolean environment value or returns a caller-provided fallback. |
|
||||
| `std.env.parseI32(name)` | `Maybe<i32>` | Parses an environment value as `i32`. |
|
||||
| `std.env.parseI32Or(name, fallback)` | `i32` | Parses an `i32` environment value or returns a caller-provided fallback. |
|
||||
| `std.env.parseU32(name)` | `Maybe<u32>` | Parses an environment value as `u32`. |
|
||||
| `std.env.parseU32Or(name, fallback)` | `u32` | Parses a `u32` environment value or returns a caller-provided fallback. |
|
||||
| `std.env.parseUsize(name)` | `Maybe<usize>` | Parses an environment value as `usize`. |
|
||||
| `std.env.parseUsizeOr(name, fallback)` | `usize` | Parses a `usize` environment value or returns a caller-provided fallback. |
|
||||
|
||||
Current limits:
|
||||
|
||||
- Dotenv/source composition.
|
||||
- Secret redaction metadata.
|
||||
- Rich diagnostics for missing keys, invalid values, and source precedence.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let mode: String = std.env.getOr("ZERO_MODE", "default")
|
||||
let verbose: Bool = std.env.parseBoolOr("ZERO_VERBOSE", false)
|
||||
let delta: i32 = std.env.parseI32Or("ZERO_DELTA", 0)
|
||||
let limit: u32 = std.env.parseU32Or("ZERO_LIMIT", 10)
|
||||
let workers: usize = std.env.parseUsizeOr("ZERO_WORKERS", 1)
|
||||
if std.env.equals("ZERO_MODE", "debug") && verbose && delta >= 0 && limit > 0_u32 && workers > 0 {
|
||||
check world.out.write(mode)
|
||||
check world.out.write("\n")
|
||||
} else {
|
||||
check world.out.write("default\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
Environment access is a hosted capability. Non-host targets reject `std.env`
|
||||
unless they explicitly provide an environment capability.
|
||||
|
||||
Diagnostics name the selected target context.
|
||||
@@ -0,0 +1,62 @@
|
||||
## When To Use std.fmt
|
||||
|
||||
In Zerolang, use `std.fmt` when a program needs to format booleans or integers into a
|
||||
caller-owned buffer instead of allocating text.
|
||||
|
||||
Runnable today:
|
||||
|
||||
Formatting helpers write into caller-provided `MutSpan<u8>` storage and return a
|
||||
borrowed prefix on success.
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.fmt.bool(buffer, value)` | `Maybe<Span<u8>>` | Writes `true` or `false`. |
|
||||
| `std.fmt.u32(buffer, value)` | `Maybe<Span<u8>>` | Writes decimal unsigned 32-bit text. |
|
||||
| `std.fmt.u32Base(buffer, value, base)` | `Maybe<Span<u8>>` | Writes unsigned 32-bit text in base 2 through 36. |
|
||||
| `std.fmt.u64(buffer, value)` | `Maybe<Span<u8>>` | Writes decimal unsigned 64-bit text. |
|
||||
| `std.fmt.u64Base(buffer, value, base)` | `Maybe<Span<u8>>` | Writes unsigned 64-bit text in base 2 through 36. |
|
||||
| `std.fmt.usize(buffer, value)` | `Maybe<Span<u8>>` | Writes decimal `usize` text. |
|
||||
| `std.fmt.usizeBase(buffer, value, base)` | `Maybe<Span<u8>>` | Writes `usize` text in base 2 through 36. |
|
||||
| `std.fmt.i32(buffer, value)` | `Maybe<Span<u8>>` | Writes decimal signed 32-bit text, including the minimum value. |
|
||||
| `std.fmt.i32Base(buffer, value, base)` | `Maybe<Span<u8>>` | Writes signed 32-bit text in base 2 through 36. |
|
||||
| `std.fmt.i32Sign(buffer, value)` | `Maybe<Span<u8>>` | Writes decimal signed 32-bit text with an explicit `+` for non-negative values. |
|
||||
| `std.fmt.i64(buffer, value)` | `Maybe<Span<u8>>` | Writes decimal signed 64-bit text, including the minimum value. |
|
||||
| `std.fmt.i64Base(buffer, value, base)` | `Maybe<Span<u8>>` | Writes signed 64-bit text in base 2 through 36. |
|
||||
| `std.fmt.i64Sign(buffer, value)` | `Maybe<Span<u8>>` | Writes decimal signed 64-bit text with an explicit `+` for non-negative values. |
|
||||
| `std.fmt.hexLowerU32(buffer, value)` | `Maybe<Span<u8>>` | Writes lowercase hexadecimal without a prefix. |
|
||||
| `std.fmt.padLeft(buffer, text, width, pad)` | `Maybe<Span<u8>>` | Left-pads `text` with `pad` until `width`, or copies `text` when already wide enough. |
|
||||
| `std.fmt.padRight(buffer, text, width, pad)` | `Maybe<Span<u8>>` | Right-pads `text` with `pad` until `width`, or copies `text` when already wide enough. |
|
||||
| `std.fmt.writeSpan(writer, text)` | `Bool` | Writes bytes into a `FixedWriter`. |
|
||||
| `std.fmt.writeBool(writer, value)` | `Bool` | Formats a boolean into a `FixedWriter`. |
|
||||
| `std.fmt.writeU32(writer, value)` / `std.fmt.writeU64(writer, value)` / `std.fmt.writeUsize(writer, value)` | `Bool` | Formats unsigned decimal text into a `FixedWriter`. |
|
||||
| `std.fmt.writeI32(writer, value)` / `std.fmt.writeI64(writer, value)` | `Bool` | Formats signed decimal text into a `FixedWriter`. |
|
||||
| `std.fmt.writeI32Sign(writer, value)` / `std.fmt.writeI64Sign(writer, value)` | `Bool` | Formats signed decimal text with an explicit `+` for non-negative values into a `FixedWriter`. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var number_buf: [12]u8 = [0_u8; 12]
|
||||
var big_buf: [20]u8 = [0_u8; 20]
|
||||
var hex_buf: [8]u8 = [0_u8; 8]
|
||||
var padded_buf: [6]u8 = [0_u8; 6]
|
||||
let number: Maybe<Span<u8>> = std.fmt.i32(number_buf, -42)
|
||||
let big: Maybe<Span<u8>> = std.fmt.u64(big_buf, 18446744073709551615_u64)
|
||||
let hex: Maybe<Span<u8>> = std.fmt.hexLowerU32(hex_buf, 48879_u32)
|
||||
let padded: Maybe<Span<u8>> = std.fmt.padLeft(padded_buf, "42", 5, 48_u8)
|
||||
var writer_storage: [24]u8 = [0_u8; 24]
|
||||
var writer: FixedWriter = std.io.fixedWriter(writer_storage, 0)
|
||||
let wrote: Bool = std.fmt.writeSpan(&mut writer, "n=") && std.fmt.writeI32(&mut writer, -42)
|
||||
if number.has && big.has && hex.has && padded.has && wrote && std.mem.eql(number.value, "-42") && std.mem.eql(big.value, "18446744073709551615") && std.mem.eql(hex.value, "beef") && std.mem.eql(padded.value, "00042") && std.mem.eql(std.io.fixedWriterView(&writer), "n=-42") {
|
||||
check world.out.write("fmt ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: writes to caller-provided mutable storage or an explicit fixed writer.
|
||||
|
||||
Allocation behavior: no allocation.
|
||||
|
||||
Error behavior: returns `null` when the buffer is too small.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,82 @@
|
||||
## When To Use std.fs
|
||||
|
||||
In Zerolang, use `std.fs` for hosted file reads, writes, existence checks, copies, renames,
|
||||
and explicit file-resource cleanup.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.fs.read(path, buf)` | `usize` | Reads bytes from a hosted path into a caller-provided `MutSpan<u8>` buffer. |
|
||||
| `std.fs.write(path, bytes)` | `usize` | Writes bytes to a hosted path and returns the byte count. |
|
||||
| `std.fs.host()` | `Fs` | Creates the hosted filesystem capability. |
|
||||
| `std.fs.open(fs, path)` | `Maybe<owned<File>>` | Opens a file and returns `null` when unavailable. |
|
||||
| `std.fs.openOrRaise(fs, path)` | `owned<File>` | Opens a file or raises `raises [NotFound, TooLarge, Io]`. |
|
||||
| `std.fs.create(fs, path)` | `Maybe<owned<File>>` | Creates a file and returns `null` when unavailable. |
|
||||
| `std.fs.createOrRaise(fs, path)` | `owned<File>` | Creates a file or raises `raises [NotFound, TooLarge, Io]`. |
|
||||
| `std.fs.readOrRaise(&mut file, buf)` | `usize` | Reads into caller storage or raises. |
|
||||
| `std.fs.writeAll(&mut file, bytes)` | `Bool` | Writes bytes to an owned file handle. |
|
||||
| `std.fs.writeAllOrRaise(&mut file, bytes)` | `Void` | Writes all bytes or raises. |
|
||||
| `std.fs.fileLen(&mut file)` | `Maybe<usize>` | Reports file length when available. |
|
||||
| `std.fs.fileLenOrRaise(&mut file)` | `usize` | Reports the file length or raises. |
|
||||
| `std.fs.fileSize(fs, path)` | `Maybe<usize>` | Opens a hosted path through `fs` and reports file length when available. |
|
||||
| `std.fs.readAll(alloc, fs, path, limit)` | `Maybe<owned<ByteBuf>>` | Reads through an explicit allocator and size limit. |
|
||||
| `std.fs.readAllOrRaise(alloc, fs, path, limit)` | `owned<ByteBuf>` | Reads through an explicit allocator and size limit. |
|
||||
| `std.fs.readBytes(path, buf)` | `Maybe<usize>` | Fills caller storage and returns the total file size; a value above `len(buf)` means the buffer holds only the first `len(buf)` bytes. |
|
||||
| `std.fs.readBytesAt(path, offset, buf)` | `Maybe<usize>` | Fills caller storage starting at a byte offset and returns the total file size, so bounded buffers can process larger files in chunks. |
|
||||
| `std.fs.writeBytes(path, bytes)` | `Maybe<usize>` | Writes byte spans to a hosted path. |
|
||||
| `std.fs.appendBytes(path, bytes)` | `Maybe<usize>` | Appends byte spans to a hosted path, creating the file when missing. |
|
||||
| `std.fs.exists(path)` | `Bool` | Checks whether a hosted path exists. |
|
||||
| `std.fs.isFile(path)` | `Bool` | Checks whether a hosted path opens and reports a file length. |
|
||||
| `std.fs.isDir(path)` | `Bool` | Checks whether a hosted path is a directory. |
|
||||
| `std.fs.makeDir(path)` | `Bool` | Creates a hosted directory. |
|
||||
| `std.fs.ensureDir(path)` | `Bool` | Succeeds when a hosted directory already exists or can be created. |
|
||||
| `std.fs.removeDir(path)` | `Bool` | Removes a hosted directory. |
|
||||
| `std.fs.remove(path)` | `Bool` | Removes a hosted file path. |
|
||||
| `std.fs.rename(old, new)` | `Bool` | Renames a hosted file path. |
|
||||
| `std.fs.dirEntryCount(path)` | `Maybe<usize>` | Counts entries in a hosted directory. |
|
||||
| `std.fs.dirEntryName(buffer, path, index)` | `Maybe<Span<u8>>` | Writes one hosted directory entry name into caller storage. |
|
||||
| `std.fs.tempName(buffer, prefix)` | `Maybe<String>` | Writes a temporary path into caller storage. |
|
||||
| `std.fs.atomicWrite(path, temp, bytes)` | `Bool` | Writes through a caller-provided temporary path and renames. |
|
||||
| `std.fs.close(&mut file)` | `Void` | Closes an owned file handle explicitly; remaining owned files are cleaned up deterministically. |
|
||||
| `std.fs.readFile(fs, path, buffer)` | `Maybe<usize>` | Opens, fills caller storage, and closes through explicit `Fs`; returns the total file size, so a value above `len(buffer)` signals truncation. |
|
||||
| `std.fs.writeFile(fs, path, bytes)` | `Bool` | Creates, writes all bytes, and closes through explicit `Fs`. |
|
||||
| `std.fs.appendFile(fs, path, bytes)` | `Bool` | Opens, appends all bytes, and closes through explicit `Fs`. |
|
||||
| `std.fs.readFileBytes(fs, path, buffer)` | `Maybe<Span<u8>>` | Opens, reads a full file, closes it, and returns the live prefix of caller storage; `null` when the file exceeds the buffer. |
|
||||
| `std.fs.readFileEquals(fs, path, buffer, expected)` | `Bool` | Reads a full file through caller storage and compares the bytes with an expected span. |
|
||||
| `std.fs.copyFile(from, to, buffer)` | `Bool` | Copies a hosted file through caller-provided scratch storage. |
|
||||
|
||||
Current limits:
|
||||
|
||||
- Richer permissions and platform-specific file modes.
|
||||
- Recursive directory walking helpers.
|
||||
- Async or nonblocking I/O.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises [NotFound, TooLarge, Io] {
|
||||
let fs: Fs = std.fs.host()
|
||||
var buf: [32]u8 = [0_u8; 32]
|
||||
if std.fs.ensureDir(".zero/out") && std.fs.writeFile(fs, ".zero/out/example.txt", "hello\n") {
|
||||
let bytes: Maybe<Span<u8>> = std.fs.readFileBytes(fs, ".zero/out/example.txt", buf)
|
||||
let size: Maybe<usize> = std.fs.fileSize(fs, ".zero/out/example.txt")
|
||||
if bytes.has && size.has && size.value == 6 && std.fs.isFile(".zero/out/example.txt") && std.fs.readFileEquals(fs, ".zero/out/example.txt", buf, "hello\n") && std.fs.rename(".zero/out/example.txt", ".zero/out/example-renamed.txt") {
|
||||
if std.fs.remove(".zero/out/example-renamed.txt") {
|
||||
check world.out.write("fs ok\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
The path helpers are a small current API, not a hidden global filesystem.
|
||||
|
||||
Stable file APIs make effects, ownership, and cleanup visible through
|
||||
capabilities.
|
||||
|
||||
Hosted filesystem APIs are denied on non-host targets with `TAR002`.
|
||||
Target-neutral packages should keep filesystem code outside their cross-target
|
||||
entry point.
|
||||
@@ -0,0 +1,406 @@
|
||||
## When To Use std.http
|
||||
|
||||
In Zerolang, use `std.http` for HTTP request parsing, response envelope writing, hosted
|
||||
fetch, local listen support, and web API helpers.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.http.parseMethod(text)` | `HttpMethod` | Parses a small HTTP method token. |
|
||||
| `std.http.client(net)` | `HttpClient` | Creates hosted client metadata from a network capability. |
|
||||
| `std.http.server(net, address)` | `HttpServer` | Creates hosted server metadata from a network capability and address. |
|
||||
| `std.http.listen(world)` | `Void raises [Io]` | Starts a loopback HTTP listener from `zero run`, auto-selecting a development port from 3000 upward. |
|
||||
| `std.http.listen(world, port)` | `Void raises [Io]` | Starts a loopback HTTP listener on exactly `port`, failing if the port is occupied. |
|
||||
| `std.http.fetch(client, request, response, timeout)` | `HttpResult` | Performs a hosted HTTP(S) request with a `Duration` timeout and writes response metadata, headers, and body into caller-owned storage. |
|
||||
| `std.http.resultOk(result)` | `Bool` | True when transport succeeded and the status is 2xx. |
|
||||
| `std.http.resultStatus(result)` | `u16` | Reads the HTTP status, or `0` when no status was available. |
|
||||
| `std.http.resultBodyLen(result)` | `usize` | Reads the number of response body bytes written into the response buffer. |
|
||||
| `std.http.resultError(result)` | `HttpError` | Reads the transport/provider error. |
|
||||
| `std.http.errorNone()` | `HttpError` | Transport succeeded. |
|
||||
| `std.http.errorInvalidUrl()` | `HttpError` | The request URL was invalid. |
|
||||
| `std.http.errorUnsupportedProtocol()` | `HttpError` | The URL protocol is not supported. |
|
||||
| `std.http.errorDns()` | `HttpError` | DNS lookup failed. |
|
||||
| `std.http.errorConnect()` | `HttpError` | Connecting to the remote host failed. |
|
||||
| `std.http.errorTls()` | `HttpError` | TLS verification or connection setup failed. |
|
||||
| `std.http.errorTimeout()` | `HttpError` | The request timed out. |
|
||||
| `std.http.errorTooLarge()` | `HttpError` | The response did not fit in the caller-owned buffer. |
|
||||
| `std.http.errorProviderUnavailable()` | `HttpError` | The hosted HTTP provider is unavailable. |
|
||||
| `std.http.errorIo()` | `HttpError` | The provider reported an I/O failure. |
|
||||
| `std.http.errorInvalidRequest()` | `HttpError` | The request envelope was invalid. |
|
||||
| `std.http.errorName(error)` | `String` | Returns a stable label for a transport error. |
|
||||
| `std.http.responseLen(response)` | `usize` | Reads the response byte count written after the internal metadata prefix. |
|
||||
| `std.http.responseHeadersLen(response)` | `usize` | Reads the raw response header byte count. |
|
||||
| `std.http.responseBodyOffset(response)` | `usize` | Reads the body start offset within the response buffer. |
|
||||
| `std.http.headerValue(response, name)` | `HttpHeaderValue` | Locates a response header value by case-insensitive header name. |
|
||||
| `std.http.headerFound(value)` | `Bool` | True when `headerValue` found a matching header. |
|
||||
| `std.http.headerOffset(value)` | `usize` | Reads the header value byte offset within the response buffer. |
|
||||
| `std.http.headerLen(value)` | `usize` | Reads the header value byte length. |
|
||||
| `std.http.tlsBoundary()` | `String` | Names the platform or C-library TLS boundary. |
|
||||
| `std.http.statusReason(status)` | `String` | Returns a common reason phrase for status-line writing. |
|
||||
| `std.http.statusIsInformational(status)` | `Bool` | True for 1xx statuses. |
|
||||
| `std.http.statusIsSuccess(status)` | `Bool` | True for 2xx statuses. |
|
||||
| `std.http.statusIsRedirect(status)` | `Bool` | True for 3xx statuses. |
|
||||
| `std.http.statusIsClientError(status)` | `Bool` | True for 4xx statuses. |
|
||||
| `std.http.statusIsServerError(status)` | `Bool` | True for 5xx statuses. |
|
||||
| `std.http.writeRequest(buffer, startLine, body)` | `Maybe<Span<u8>>` | Writes `METHOD URL`, optional content length, blank line, and body into caller storage. |
|
||||
| `std.http.writeRequestWithHeader(buffer, startLine, headerLine, body)` | `Maybe<Span<u8>>` | Writes a request envelope with one validated `name: value` header line. |
|
||||
| `std.http.writeRequestWithHeaders(buffer, startLine, headers, body)` | `Maybe<Span<u8>>` | Writes a request envelope with a validated newline-separated header block. |
|
||||
| `std.http.writeMethodRequest(buffer, method, target, body)` | `Maybe<Span<u8>>` | Writes a request envelope from separate method and target spans. |
|
||||
| `std.http.writeGetRequest(buffer, target)` | `Maybe<Span<u8>>` | Writes an empty GET request envelope. |
|
||||
| `std.http.writeHeadRequest(buffer, target)` | `Maybe<Span<u8>>` | Writes an empty HEAD request envelope. |
|
||||
| `std.http.writeDeleteRequest(buffer, target)` | `Maybe<Span<u8>>` | Writes an empty DELETE request envelope. |
|
||||
| `std.http.writeJsonRequest(buffer, startLine, body)` | `Maybe<Span<u8>>` | Writes a JSON request envelope with `content-type` and `content-length`. |
|
||||
| `std.http.writeJsonMethodRequest(buffer, method, target, body)` | `Maybe<Span<u8>>` | Writes a JSON request envelope from separate method and target spans. |
|
||||
| `std.http.writeJsonRequestWithHeader(buffer, startLine, headerLine, body)` | `Maybe<Span<u8>>` | Writes a JSON request envelope with one validated extra header line. |
|
||||
| `std.http.writeJsonRequestWithHeaders(buffer, startLine, headers, body)` | `Maybe<Span<u8>>` | Writes a JSON request envelope with a validated extra header block. |
|
||||
| `std.http.writePostJsonRequest(buffer, target, body)` | `Maybe<Span<u8>>` | Writes a POST JSON request envelope. |
|
||||
| `std.http.writePutJsonRequest(buffer, target, body)` | `Maybe<Span<u8>>` | Writes a PUT JSON request envelope. |
|
||||
| `std.http.writePatchJsonRequest(buffer, target, body)` | `Maybe<Span<u8>>` | Writes a PATCH JSON request envelope. |
|
||||
| `std.http.writeResponse(buffer, status, body)` | `Maybe<Span<u8>>` | Writes an HTTP/1.1 response envelope into caller storage. |
|
||||
| `std.http.writeResponseWithHeader(buffer, status, headerLine, body)` | `Maybe<Span<u8>>` | Writes a response envelope with one validated `name: value` header line. |
|
||||
| `std.http.writeResponseWithHeaders(buffer, status, headers, body)` | `Maybe<Span<u8>>` | Writes a response envelope with a validated newline-separated header block. |
|
||||
| `std.http.writeJsonResponse(buffer, status, body)` | `Maybe<Span<u8>>` | Writes a JSON HTTP/1.1 response envelope into caller storage. |
|
||||
| `std.http.writeJsonResponseWithHeader(buffer, status, headerLine, body)` | `Maybe<Span<u8>>` | Writes a JSON response envelope with one validated `name: value` header line. |
|
||||
| `std.http.writeJsonResponseWithHeaders(buffer, status, headers, body)` | `Maybe<Span<u8>>` | Writes a JSON response envelope with a validated extra header block. |
|
||||
| `std.http.writeJsonResponseWithCookie(buffer, status, cookie, body)` | `Maybe<Span<u8>>` | Writes a JSON response envelope with one `Set-Cookie` header value. |
|
||||
| `std.http.writeJsonError(buffer, status, code)` | `Maybe<Span<u8>>` | Writes `{"error":"code"}` after validating the code is JSON-safe lower-case ASCII, digits, `_`, or `-`. |
|
||||
| `std.http.writeCorsPreflight(buffer, allowOrigin, allowMethods, allowHeaders)` | `Maybe<Span<u8>>` | Writes a 204 CORS preflight response with caller-provided allow headers. |
|
||||
| `std.http.writeCorsJsonResponse(buffer, statusLine, body, allowOrigin)` | `Maybe<Span<u8>>` | Writes a JSON response with `access-control-allow-origin`; `statusLine` is a fragment such as `"200 OK"`. |
|
||||
| `std.http.writeTextResponse(buffer, status, body)` | `Maybe<Span<u8>>` | Writes a `text/plain; charset=utf-8` response envelope into caller storage. |
|
||||
| `std.http.writeTextOk(buffer, body)` | `Maybe<Span<u8>>` | Writes a 200 plain-text response envelope into caller storage. |
|
||||
| `std.http.writeHtmlResponse(buffer, status, body)` | `Maybe<Span<u8>>` | Writes a `text/html; charset=utf-8` response envelope into caller storage. |
|
||||
| `std.http.writeHtmlOk(buffer, body)` | `Maybe<Span<u8>>` | Writes a 200 HTML response envelope into caller storage. |
|
||||
| `std.http.writeRedirect(buffer, status, location)` | `Maybe<Span<u8>>` | Writes a redirect response with a safe `Location` header; rejects non-3xx statuses and empty or control-character locations. |
|
||||
| `std.http.writeFound(buffer, location)` | `Maybe<Span<u8>>` | Writes a 302 redirect response. |
|
||||
| `std.http.writeSeeOther(buffer, location)` | `Maybe<Span<u8>>` | Writes a 303 redirect response. |
|
||||
| `std.http.writeMovedPermanently(buffer, location)` | `Maybe<Span<u8>>` | Writes a 301 redirect response. |
|
||||
| `std.http.writePermanentRedirect(buffer, location)` | `Maybe<Span<u8>>` | Writes a 308 redirect response. |
|
||||
| `std.http.contentTypeForPath(path)` | `String` | Returns a small static-file media type from a path suffix. |
|
||||
| `std.http.writeStaticResponse(buffer, status, path, body)` | `Maybe<Span<u8>>` | Writes a response with `content-type` inferred from the path suffix. |
|
||||
| `std.http.writeJsonOk(buffer, body)` | `Maybe<Span<u8>>` | Writes a 200 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonCreated(buffer, body)` | `Maybe<Span<u8>>` | Writes a 201 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonBadRequest(buffer, body)` | `Maybe<Span<u8>>` | Writes a 400 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonUnauthorized(buffer, body)` | `Maybe<Span<u8>>` | Writes a 401 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonForbidden(buffer, body)` | `Maybe<Span<u8>>` | Writes a 403 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonNotFound(buffer, body)` | `Maybe<Span<u8>>` | Writes a 404 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonMethodNotAllowed(buffer, body)` | `Maybe<Span<u8>>` | Writes a 405 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonConflict(buffer, body)` | `Maybe<Span<u8>>` | Writes a 409 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonUnprocessable(buffer, body)` | `Maybe<Span<u8>>` | Writes a 422 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonTooManyRequests(buffer, body)` | `Maybe<Span<u8>>` | Writes a 429 JSON response envelope into caller storage. |
|
||||
| `std.http.writeJsonInternalServerError(buffer, body)` | `Maybe<Span<u8>>` | Writes a 500 JSON response envelope into caller storage. |
|
||||
| `std.http.writeNoContent(buffer)` | `Maybe<Span<u8>>` | Writes a 204 response envelope with an empty body. |
|
||||
| `std.http.requestMethodName(request)` | `Maybe<Span<u8>>` | Borrows the method token from a request envelope. |
|
||||
| `std.http.requestTarget(request)` | `Maybe<Span<u8>>` | Borrows the raw target from a request envelope. |
|
||||
| `std.http.requestPath(request)` | `Maybe<Span<u8>>` | Borrows the path from an absolute or origin-form request target. |
|
||||
| `std.http.pathSegmentCount(path)` | `usize` | Counts non-empty path segments in a path span. |
|
||||
| `std.http.pathSegment(path, index)` | `Maybe<Span<u8>>` | Borrows a zero-based non-empty path segment from a path span. |
|
||||
| `std.http.pathMatchesPattern(path, pattern)` | `Bool` | Matches path segments against literals, `:params`, and a trailing `*` wildcard. |
|
||||
| `std.http.pathParam(path, pattern, name)` | `Maybe<Span<u8>>` | Borrows a named path parameter from a matched path pattern. |
|
||||
| `std.http.requestPathSegmentCount(request)` | `usize` | Counts non-empty path segments from a request envelope path. |
|
||||
| `std.http.requestPathSegment(request, index)` | `Maybe<Span<u8>>` | Borrows a zero-based non-empty request path segment. |
|
||||
| `std.http.requestQuery(request)` | `Maybe<Span<u8>>` | Borrows the query string from a request target. |
|
||||
| `std.http.requestQueryValue(request, name)` | `Maybe<Span<u8>>` | Borrows a query value by name from a request envelope. |
|
||||
| `std.http.requestHeader(request, name)` | `Maybe<Span<u8>>` | Borrows a case-insensitive request header value. |
|
||||
| `std.http.requestBearerToken(request)` | `Maybe<Span<u8>>` | Borrows the bearer token from the `Authorization` request header. |
|
||||
| `std.http.requestCookie(request, name)` | `Maybe<Span<u8>>` | Borrows a named cookie value from the `Cookie` request header. |
|
||||
| `std.http.requestContentLength(request)` | `Maybe<usize>` | Parses the `Content-Length` request header. |
|
||||
| `std.http.requestContentType(request)` | `Maybe<Span<u8>>` | Borrows the media type from `Content-Type`, excluding parameters. |
|
||||
| `std.http.requestAccepts(request, media)` | `Bool` | Checks whether `Accept` allows an exact media type, type wildcard, or `*/*`; absent `Accept` allows any media and `q=0` rejects a range. |
|
||||
| `std.http.requestAcceptsJson(request)` | `Bool` | Checks whether `Accept` allows `application/json`. |
|
||||
| `std.http.requestBody(request)` | `Maybe<Span<u8>>` | Borrows the request body after the blank line. |
|
||||
| `std.http.requestBodyWithin(request, max)` | `Maybe<Span<u8>>` | Borrows the request body only when it is at most `max` bytes. |
|
||||
| `std.http.requestHasJsonContentType(request)` | `Bool` | True when the request content type is `application/json`, ignoring ASCII case and allowing parameters. |
|
||||
| `std.http.requestJsonBodyWithin(request, max)` | `Maybe<Span<u8>>` | Borrows the body only when content type is JSON, body length is within `max`, and bytes validate as JSON. |
|
||||
| `std.http.requestJsonField(request, name, max)` | `Maybe<Span<u8>>` | Borrows a top-level JSON field from a bounded JSON request body. |
|
||||
| `std.http.requestMatches(request, method, path)` | `Bool` | True when a request envelope has the exact method and normalized path. |
|
||||
| `std.http.methodAllowed(method, allowed)` | `Bool` | Checks a method against a comma-separated allow list. |
|
||||
| `std.http.requestMethodAllowed(request, allowed)` | `Bool` | Checks a request method against a comma-separated allow list. |
|
||||
| `std.http.requestMethodIs(request, method)` | `Bool` | True when a request envelope has the exact method. |
|
||||
| `std.http.requestIsGet(request, path)` | `Bool` | True when a request envelope is `GET` for the normalized path. |
|
||||
| `std.http.requestIsHead(request, path)` | `Bool` | True when a request envelope is `HEAD` for the normalized path. |
|
||||
| `std.http.requestIsOptions(request, path)` | `Bool` | True when a request envelope is `OPTIONS` for the normalized path. |
|
||||
| `std.http.requestIsPost(request, path)` | `Bool` | True when a request envelope is `POST` for the normalized path. |
|
||||
| `std.http.requestIsPut(request, path)` | `Bool` | True when a request envelope is `PUT` for the normalized path. |
|
||||
| `std.http.requestIsPatch(request, path)` | `Bool` | True when a request envelope is `PATCH` for the normalized path. |
|
||||
| `std.http.requestIsDelete(request, path)` | `Bool` | True when a request envelope is `DELETE` for the normalized path. |
|
||||
| `std.http.requestPathStartsWith(request, prefix)` | `Bool` | True when the normalized request path starts with `prefix`. |
|
||||
| `std.http.requestPathTailAfter(request, prefix)` | `Maybe<Span<u8>>` | Borrows the normalized request path after `prefix`, or `null` when it does not match. |
|
||||
| `std.http.requestRouteMatches(request, method, pattern)` | `Bool` | True when the method matches and the path matches a segment pattern. |
|
||||
| `std.http.requestRouteMethodAllowed(request, pattern, allowed)` | `Bool` | True when the path pattern matches and the method is in a comma-separated allow list. |
|
||||
| `std.http.requestPathParam(request, pattern, name)` | `Maybe<Span<u8>>` | Borrows a named request path parameter from a matched pattern. |
|
||||
| `std.http.headerBlockSafe(headers)` | `Bool` | Validates a newline-separated header block before writing it. |
|
||||
| `std.http.headerBytes(response, value)` | `Maybe<Span<u8>>` | Borrows a response header value after validating packed metadata. |
|
||||
| `std.http.responseBody(response, result)` | `Maybe<Span<u8>>` | Borrows the response body when the transport result succeeded. |
|
||||
| `std.http.responseBodyBytes(response)` | `Maybe<Span<u8>>` | Borrows the body bytes from a local response envelope written by response helpers. |
|
||||
| `std.http.responseStatus(response)` | `Maybe<u16>` | Parses the status code from a local response envelope. |
|
||||
| `std.http.responseStatusIs(response, status)` | `Bool` | Checks the status code in a local response envelope. |
|
||||
| `std.http.responseHeader(response, name)` | `Maybe<Span<u8>>` | Borrows a case-insensitive header value from a local response envelope. |
|
||||
| `std.http.responseContentType(response)` | `Maybe<Span<u8>>` | Borrows the media type from a local response envelope. |
|
||||
| `std.http.responseRedirectLocation(response)` | `Maybe<Span<u8>>` | Borrows a redirect `Location` header from a local response envelope. |
|
||||
| `std.http.responseBodyEquals(response, expected)` | `Bool` | Compares a local response body with expected bytes. |
|
||||
| `std.http.responseMatches(response, status, contentType, body)` | `Bool` | Checks local response status, optional content type, and body bytes. |
|
||||
| `std.http.testRequest(buffer, method, target, body)` | `Maybe<Span<u8>>` | Writes a synthetic request envelope for handler tests. |
|
||||
| `std.http.testJsonRequest(buffer, method, target, body)` | `Maybe<Span<u8>>` | Writes a synthetic JSON request envelope for handler tests. |
|
||||
|
||||
The `WithHeader` and `WithHeaders` writers reject header names they manage
|
||||
themselves. Request writers reject `content-length` and `transfer-encoding`;
|
||||
JSON request writers also reject `content-type`; response writers reject
|
||||
`content-length`, `transfer-encoding`, and `connection`; JSON response writers
|
||||
also reject `content-type`.
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: net, memory, parse, or none for named error constants
|
||||
- allocation behavior: metadata helpers do not allocate; `fetch` writes into a
|
||||
caller-owned response buffer and uses the provider runtime internally
|
||||
- target support: request/response parsing and writing helpers are
|
||||
target-neutral; client/server require a net-capable target; `fetch` and
|
||||
`listen` run on supported Darwin arm64 and Linux x64 host executable targets
|
||||
- error behavior: metadata helpers are infallible; `fetch` returns status,
|
||||
body length, and error metadata so non-2xx responses are distinguishable from
|
||||
transport failures
|
||||
- ownership notes: HTTP helpers borrow network capability metadata and write
|
||||
only to caller-owned buffers
|
||||
- examples: `conformance/native/pass/std-http-metadata-neutral.graph`,
|
||||
`conformance/native/pass/std-http-fetch.graph`,
|
||||
`conformance/native/pass/std-http-errors.graph`,
|
||||
`conformance/native/pass/std-http-response-helpers.graph`,
|
||||
`conformance/native/pass/std-http-api-helpers.graph`,
|
||||
`conformance/native/pass/std-http-cors-helpers.graph`,
|
||||
`conformance/native/pass/std-http-auth-helpers.graph`,
|
||||
`conformance/native/pass/std-http-path-segments.graph`,
|
||||
`examples/json-api-client.graph`,
|
||||
`examples/json-api-router.graph`,
|
||||
`examples/std-http-json.graph`,
|
||||
`examples/std-http-request.graph`,
|
||||
`examples/std-http-headers.graph`
|
||||
|
||||
## Example
|
||||
|
||||
Metadata helpers:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let net: Net = std.net.host()
|
||||
let addr: Address = std.net.address("localhost", 8080_u16)
|
||||
let _client: HttpClient = std.http.client(net)
|
||||
let _server: HttpServer = std.http.server(net, addr)
|
||||
let method: HttpMethod = std.http.parseMethod("GET")
|
||||
if method == std.http.parseMethod("GET") && std.mem.len(std.mem.span("body")) == 4 {
|
||||
check world.out.write("http ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
GET request:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [512]u8 = [0_u8; 512]
|
||||
let request: Span<u8> = std.mem.span("GET https://example.com\n\n")
|
||||
let result: HttpResult = std.http.fetch(client, request, response, std.time.ms(1000))
|
||||
if std.http.resultOk(result) {
|
||||
check world.out.write("http get ok\n")
|
||||
return
|
||||
}
|
||||
check world.err.write("http get failed\n")
|
||||
}
|
||||
```
|
||||
|
||||
Request with headers and body:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var request_buf: [256]u8 = [0_u8; 256]
|
||||
let request: Maybe<Span<u8>> = std.http.writeJsonRequestWithHeader(request_buf, "POST https://example.com/api", "accept: application/json", "{\"ping\":1}")
|
||||
var response: [512]u8 = [0_u8; 512]
|
||||
if request.has {
|
||||
let result: HttpResult = std.http.fetch(client, request.value, response, std.time.ms(1000))
|
||||
if std.http.resultOk(result) {
|
||||
check world.out.write("http post ok\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
check world.err.write("http post failed\n")
|
||||
}
|
||||
```
|
||||
|
||||
Request routing:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let request: Span<u8> = std.mem.span("POST /users/7?tenant=demo\naccept: application/json\ncontent-type: application/json; charset=utf-8\n\n{\"id\":7}")
|
||||
var response: [256]u8 = [0_u8; 256]
|
||||
let id: Maybe<Span<u8>> = std.http.requestPathParam(request, "/users/:id", "id")
|
||||
let body_id: Maybe<Span<u8>> = std.http.requestJsonField(request, "id", 64)
|
||||
let tenant: Maybe<Span<u8>> = std.http.requestQueryValue(request, "tenant")
|
||||
if std.http.requestRouteMatches(request, "POST", "/users/:id") && id.has && tenant.has && body_id.has && std.http.requestAcceptsJson(request) {
|
||||
let written: Maybe<Span<u8>> = std.http.writeJsonResponseWithHeader(response, 201_u16, "location: /users/7", "{\"created\":true}")
|
||||
if written.has {
|
||||
check world.out.write("http route ok\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
let failed: Maybe<Span<u8>> = std.http.writeJsonError(response, 400, "bad_request")
|
||||
if failed.has {
|
||||
check world.err.write("http route failed\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
CORS preflight and JSON response:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let request: Span<u8> = std.mem.span("OPTIONS /users\naccess-control-request-method: POST\n\n")
|
||||
var response: [256]u8 = [0_u8; 256]
|
||||
if std.http.requestIsOptions(request, "/users") {
|
||||
let written: Maybe<Span<u8>> = std.http.writeCorsPreflight(response, "*", "GET, POST, OPTIONS", "content-type, authorization")
|
||||
if written.has {
|
||||
check world.out.write("http cors preflight ok\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
let failed: Maybe<Span<u8>> = std.http.writeCorsJsonResponse(response, "400 Bad Request", "{\"error\":\"bad_request\"}", "*")
|
||||
if failed.has {
|
||||
check world.err.write("http cors failed\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Authorization and session cookie helpers:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let request: Span<u8> = std.mem.span("GET /me\nauthorization: Bearer token-123\ncookie: sid=abc; theme=dark\n\n")
|
||||
let token: Maybe<Span<u8>> = std.http.requestBearerToken(request)
|
||||
let session: Maybe<Span<u8>> = std.http.requestCookie(request, "sid")
|
||||
if token.has && session.has {
|
||||
check world.out.write("http auth ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Response body:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let maybe_request: Maybe<String> = std.args.get(1)
|
||||
if !maybe_request.has {
|
||||
check world.err.write("usage: pass HTTP request envelope\n")
|
||||
return
|
||||
}
|
||||
let net: Net = std.net.host()
|
||||
let client: HttpClient = std.http.client(net)
|
||||
var response: [512]u8 = [0_u8; 512]
|
||||
let result: HttpResult = std.http.fetch(client, std.mem.span(maybe_request.value), response, std.time.ms(5000))
|
||||
let bytes: Maybe<Span<u8>> = std.http.responseBody(response, result)
|
||||
var arena_buf: [16]u8 = [0_u8; 16]
|
||||
var arena: FixedBufAlloc = std.mem.fixedBufAlloc(arena_buf)
|
||||
var parsed: Maybe<JsonDoc> = null
|
||||
if bytes.has {
|
||||
parsed = std.json.parseBytes(arena, bytes.value)
|
||||
}
|
||||
if std.http.resultOk(result) && bytes.has && parsed.has {
|
||||
check world.out.write("http response json ok\n")
|
||||
return
|
||||
}
|
||||
check world.err.write("http response json failed\n")
|
||||
}
|
||||
```
|
||||
|
||||
Loopback API server:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
check std.http.listen(world)
|
||||
}
|
||||
|
||||
fn handle(request: Span<u8>, response: MutSpan<u8>) -> Maybe<Span<u8>> {
|
||||
if std.http.requestIsGet(request, "/ping") {
|
||||
return std.http.writeJsonOk(response, "{\"message\":\"pong\"}")
|
||||
}
|
||||
if std.http.requestIsGet(request, "/robots.txt") {
|
||||
return std.http.writeTextOk(response, "user-agent: *\nallow: /\n")
|
||||
}
|
||||
if std.http.requestIsGet(request, "/old") {
|
||||
return std.http.writeMovedPermanently(response, "/ping")
|
||||
}
|
||||
return std.http.writeJsonError(response, 404, "not_found")
|
||||
}
|
||||
```
|
||||
|
||||
Run it and use the port printed by the listener:
|
||||
|
||||
```sh
|
||||
zero run .
|
||||
# listening on http://127.0.0.1:3001
|
||||
curl -sS -i http://127.0.0.1:3001/ping
|
||||
```
|
||||
|
||||
When no port is passed, `listen(world)` starts at `3000` and increments by one
|
||||
until it finds a free loopback port. This lets small local services coexist on a
|
||||
development machine. When the program passes an explicit port, such as
|
||||
`std.http.listen(world, 3000_u16)`, Zero tries exactly that port and reports the
|
||||
bind failure instead of auto-incrementing.
|
||||
|
||||
Synthetic handler check:
|
||||
|
||||
```zero
|
||||
fn handle(request: Span<u8>, response: MutSpan<u8>) -> Maybe<Span<u8>> {
|
||||
if std.http.requestRouteMethodAllowed(request, "/users/:id", "GET, HEAD") {
|
||||
return std.http.writeJsonResponseWithHeaders(response, 200_u16, "cache-control: no-store", "{\"ok\":true}")
|
||||
}
|
||||
return std.http.writeJsonError(response, 404_u16, "not_found")
|
||||
}
|
||||
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var request_storage: [128]u8 = [0_u8; 128]
|
||||
var response_storage: [256]u8 = [0_u8; 256]
|
||||
let request: Maybe<Span<u8>> = std.http.testRequest(request_storage, "GET", "/users/7", "")
|
||||
if request.has {
|
||||
let response: Maybe<Span<u8>> = handle(request.value, response_storage)
|
||||
if response.has && std.http.responseMatches(response.value, 200_u16, "application/json", "{\"ok\":true}") {
|
||||
check world.out.write("handler ok\n")
|
||||
return
|
||||
}
|
||||
}
|
||||
check world.err.write("handler failed\n")
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.http.fetch` is the outbound HTTP client primitive. The request argument is
|
||||
one byte envelope: `METHOD URL`, followed by zero or more `Header-Name: value`
|
||||
lines, a blank line, and optional request body bytes. Use `writeRequest` and
|
||||
`writeJsonRequest` when you want the standard library to write that envelope
|
||||
into caller storage. The timeout is a `Duration`, typically built with
|
||||
`std.time.ms(...)`.
|
||||
|
||||
`fetch` supports `http://` and `https://` URLs on supported host executable
|
||||
targets, uses the C-library curl provider, does not follow redirects, and
|
||||
verifies TLS through the provider. TLS verification is always enabled.
|
||||
`ZERO_HTTP_TEST_CA_BUNDLE` can point the provider at an explicit CA bundle
|
||||
while keeping certificate verification on.
|
||||
|
||||
The response buffer starts with internal metadata, followed by raw response
|
||||
headers and then the response body. Use `responseHeadersLen` and
|
||||
`responseBodyOffset` rather than hard-coding offsets. `headerValue` scans the
|
||||
response buffer and returns packed offset/length metadata for the matching
|
||||
value; `headerFound`, `headerOffset`, and `headerLen` inspect that metadata
|
||||
without allocating. Prefer `headerBytes` and `responseBody` when you need a
|
||||
borrowed byte span and want metadata bounds checked in one call.
|
||||
|
||||
Compare `resultError` with the named `HttpError` helpers rather than raw
|
||||
numbers. Use `errorName(resultError(result))` for diagnostics and logs.
|
||||
`errorNone` means the transport succeeded; HTTP non-2xx statuses still carry
|
||||
`errorNone` and can be inspected with `resultStatus`.
|
||||
|
||||
The module does not expose raw socket read/write APIs, streaming bodies, a
|
||||
global router, or a heap-allocated response object.
|
||||
@@ -0,0 +1,56 @@
|
||||
## When To Use std.inet
|
||||
|
||||
In Zerolang, use `std.inet` to validate and parse internet address literals:
|
||||
IPv4, IPv6, and RFC 1123 hostnames. These helpers are target-neutral and need
|
||||
no network capability, so they work in validators and parsers on any compiler
|
||||
target. Use `std.net` when a program actually opens connections or listeners.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.inet.isIpv4(text)` | `Bool` | Validates a dotted-quad IPv4 literal: four decimal octets 0-255 with no leading zeros. |
|
||||
| `std.inet.parseIpv4(text)` | `Maybe<u32>` | Parses an IPv4 literal into a big-endian packed `u32`. |
|
||||
| `std.inet.writeIpv4(buffer, value)` | `Maybe<Span<u8>>` | Writes a packed IPv4 value as dotted-quad text into caller storage. |
|
||||
| `std.inet.isIpv4Unspecified(value)` | `Bool` | Checks `0.0.0.0/32`. |
|
||||
| `std.inet.isIpv4Loopback(value)` | `Bool` | Checks `127.0.0.0/8`. |
|
||||
| `std.inet.isIpv4Private(value)` | `Bool` | Checks RFC 1918 private ranges. |
|
||||
| `std.inet.isIpv4LinkLocal(value)` | `Bool` | Checks `169.254.0.0/16`. |
|
||||
| `std.inet.isIpv4Multicast(value)` | `Bool` | Checks `224.0.0.0/4`. |
|
||||
| `std.inet.isIpv6(text)` | `Bool` | Validates an RFC 4291 IPv6 literal, including `::` compression and embedded IPv4. |
|
||||
| `std.inet.parseIpv6(buffer, text)` | `Maybe<Span<u8>>` | Parses an IPv6 literal into 16 network-order bytes in a caller buffer. |
|
||||
| `std.inet.isIp(text)` | `Bool` | Validates either a strict IPv4 literal or an RFC 4291 IPv6 literal. |
|
||||
| `std.inet.parseIp(buffer, text)` | `Maybe<Span<u8>>` | Parses IPv4 into 4 bytes or IPv6 into 16 bytes in caller storage. |
|
||||
| `std.inet.isIpv6Unspecified(bytes)` | `Bool` | Checks `::/128` over a 16-byte IPv6 span. |
|
||||
| `std.inet.isIpv6Loopback(bytes)` | `Bool` | Checks `::1/128` over a 16-byte IPv6 span. |
|
||||
| `std.inet.isIpv6Multicast(bytes)` | `Bool` | Checks `ff00::/8`. |
|
||||
| `std.inet.isIpv6LinkLocal(bytes)` | `Bool` | Checks `fe80::/10`. |
|
||||
| `std.inet.isIpv6Private(bytes)` | `Bool` | Checks the `fc00::/7` unique-local range. |
|
||||
| `std.inet.isIpv6UniqueLocal(bytes)` | `Bool` | Alias for the `fc00::/7` unique-local range. |
|
||||
| `std.inet.isIpv6MappedIpv4(bytes)` | `Bool` | Checks `::ffff:0:0/96` IPv4-mapped addresses. |
|
||||
| `std.inet.ipv6MappedIpv4(bytes)` | `Maybe<u32>` | Extracts the packed IPv4 value from an IPv4-mapped IPv6 span. |
|
||||
| `std.inet.isHostname(text)` | `Bool` | Validates an RFC 1123 hostname: dot-separated 1-63 byte alphanumeric/hyphen labels, 253 bytes total. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var storage: [16]u8 = [0; 16]
|
||||
let buffer: MutSpan<u8> = storage
|
||||
let quad: Maybe<u32> = std.inet.parseIpv4("192.168.0.1")
|
||||
let mapped: Maybe<Span<u8>> = std.inet.parseIpv6(buffer, "::ffff:192.168.1.1")
|
||||
if std.inet.isHostname("example.com") && (quad.has && mapped.has) && (std.inet.isIpv4Private(quad.value) && std.inet.isIpv6MappedIpv4(mapped.value)) {
|
||||
check world.out.write("inet ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: none.
|
||||
|
||||
Allocation behavior: `parseIp` and `parseIpv6` write into caller buffers;
|
||||
`writeIpv4` writes into caller storage. The other helpers allocate nothing.
|
||||
|
||||
Error behavior: validators return `Bool`; parsers return `null` for invalid
|
||||
literals or undersized buffers.
|
||||
|
||||
Target support: current compiler targets; no network capability required.
|
||||
@@ -0,0 +1,128 @@
|
||||
## When To Use std.io
|
||||
|
||||
In Zerolang, use `std.io` for byte reads and writes over caller-owned storage.
|
||||
The module provides explicit cursor helpers for spans plus fixed reader and
|
||||
writer values for sequential stream-style code.
|
||||
|
||||
Span helpers:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.io.copy(dst, src)` | `usize` | Copies as many bytes as fit and returns the count. |
|
||||
| `std.io.copyN(dst, src, count)` | `Maybe<usize>` | Copies exactly `count` bytes when both spans are large enough. |
|
||||
| `std.io.read(bytes, offset, dst)` | `Maybe<usize>` | Copies available bytes and returns the next cursor. |
|
||||
| `std.io.readExact(bytes, offset, dst)` | `Maybe<usize>` | Fills the destination only when enough input bytes remain. |
|
||||
| `std.io.readAt(bytes, offset, dst)` | `Maybe<usize>` | Reads from an explicit offset without owning state. |
|
||||
| `std.io.readAll(bytes, dst)` | `Maybe<Span<u8>>` | Copies all input bytes and returns the written prefix. |
|
||||
| `std.io.readByte(bytes, offset)` | `Maybe<u8>` | Reads one byte at an explicit cursor. |
|
||||
| `std.io.writeByte(buffer, offset, byte)` | `Maybe<usize>` | Writes one byte and returns the next cursor. |
|
||||
| `std.io.writeSpan(buffer, offset, bytes)` | `Maybe<usize>` | Writes bytes and returns the next cursor. |
|
||||
| `std.io.writeAll(buffer, offset, bytes)` | `Maybe<usize>` | Writes all bytes only when the full input fits. |
|
||||
| `std.io.writeAt(buffer, offset, bytes)` | `Maybe<usize>` | Writes at an explicit offset. |
|
||||
| `std.io.written(buffer, len)` | `Span<u8>` | Borrows the written prefix, clamped to buffer length. |
|
||||
| `std.io.remaining(buffer, offset)` | `usize` | Reports remaining byte capacity from an explicit cursor. |
|
||||
|
||||
Line and delimiter helpers:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.io.nextLine(bytes, start)` | `Maybe<Span<u8>>` | Borrows the next line without `\n` or trailing `\r`. |
|
||||
| `std.io.nextLineStart(bytes, start)` | `usize` | Advances to the next line start or end of input. |
|
||||
| `std.io.readLine(bytes, start)` | `Maybe<Span<u8>>` | Alias for line-oriented reads using the same line rules. |
|
||||
| `std.io.readLineStart(bytes, start)` | `usize` | Advances to the next line start using the same line rules. |
|
||||
| `std.io.readUntilDelimiter(bytes, start, delimiter)` | `Maybe<Span<u8>>` | Borrows bytes up to the delimiter or end of input. |
|
||||
| `std.io.readUntilDelimiterStart(bytes, start, delimiter)` | `usize` | Advances past the delimiter when found, otherwise to end of input. |
|
||||
| `std.io.countLines(bytes)` | `usize` | Counts lines using the same next-line rules. |
|
||||
|
||||
Fixed stream helpers:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.io.fixedReader(bytes, cursor)` | `FixedReader` | Builds a sequential reader over borrowed bytes. |
|
||||
| `std.io.fixedReaderRead(&mut reader, dst)` | `usize` | Reads up to `dst` length and advances the reader cursor. |
|
||||
| `std.io.fixedReaderReadExact(&mut reader, dst)` | `Bool` | Reads exactly `dst` length or leaves the cursor unchanged. |
|
||||
| `std.io.fixedReaderReadLine(&mut reader)` | `Maybe<Span<u8>>` | Borrows the next line and advances the cursor. |
|
||||
| `std.io.fixedReaderReadUntilDelimiter(&mut reader, delimiter)` | `Maybe<Span<u8>>` | Borrows bytes up to a delimiter and advances the cursor. |
|
||||
| `std.io.fixedReaderReadAll(&mut reader, dst)` | `Maybe<Span<u8>>` | Copies remaining bytes into caller storage. |
|
||||
| `std.io.fixedReaderReadAt(&reader, offset, dst)` | `Maybe<usize>` | Reads from an offset without moving the cursor. |
|
||||
| `std.io.fixedReaderReadByte(&mut reader)` | `Maybe<u8>` | Reads one byte and advances the cursor. |
|
||||
| `std.io.fixedReaderLimit(&reader, count)` | `FixedReader` | Creates a bounded reader view from the current cursor. |
|
||||
| `std.io.fixedReaderSeek(&mut reader, cursor)` | `Bool` | Moves the cursor when the target is in range. |
|
||||
| `std.io.fixedReaderCursor(&reader)` | `usize` | Reports the cursor. |
|
||||
| `std.io.fixedReaderLen(&reader)` | `usize` | Reports input length. |
|
||||
| `std.io.fixedReaderRemaining(&reader)` | `usize` | Reports unread bytes. |
|
||||
| `std.io.fixedReaderDone(&reader)` | `Bool` | Reports whether the cursor reached the end. |
|
||||
| `std.io.fixedWriter(buffer, cursor)` | `FixedWriter` | Builds a sequential writer over caller storage. |
|
||||
| `std.io.fixedWriterWrite(&mut writer, bytes)` | `Bool` | Writes bytes and advances the writer cursor. |
|
||||
| `std.io.fixedWriterWriteAll(&mut writer, bytes)` | `Bool` | Writes all bytes only when they fit. |
|
||||
| `std.io.fixedWriterWriteByte(&mut writer, byte)` | `Bool` | Writes one byte and advances the cursor. |
|
||||
| `std.io.fixedWriterWriteAt(&mut writer, offset, bytes)` | `Bool` | Writes at an offset without moving the cursor. |
|
||||
| `std.io.fixedWriterView(&writer)` | `Span<u8>` | Borrows the live writer prefix. |
|
||||
| `std.io.fixedWriterSeek(&mut writer, cursor)` | `Bool` | Moves the cursor when the target is in range. |
|
||||
| `std.io.fixedWriterTruncate(&mut writer, len)` | `usize` | Clamps the live prefix length. |
|
||||
| `std.io.fixedWriterClear(&mut writer)` | `usize` | Resets the cursor to zero. |
|
||||
| `std.io.fixedWriterCursor(&writer)` | `usize` | Reports the cursor. |
|
||||
| `std.io.fixedWriterCapacity(&writer)` | `usize` | Reports storage length. |
|
||||
| `std.io.fixedWriterRemaining(&writer)` | `usize` | Reports unwritten capacity. |
|
||||
|
||||
Stream composition helpers:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.io.copyBuffer(&mut reader, &mut writer, scratch)` | `usize` | Copies until EOF or writer capacity using caller scratch storage. |
|
||||
| `std.io.copyReaderN(&mut reader, &mut writer, count, scratch)` | `Maybe<usize>` | Copies exactly `count` bytes through scratch storage. |
|
||||
| `std.io.discard(&mut reader, scratch)` | `usize` | Reads and drops bytes through scratch storage. |
|
||||
| `std.io.teeRead(&mut reader, &mut writer, dst)` | `Maybe<usize>` | Reads into `dst` and writes the same bytes to the writer. |
|
||||
| `std.io.multiRead(&mut first, &mut second, dst)` | `usize` | Fills `dst` from the first reader, then the second. |
|
||||
|
||||
Error labels:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.io.errorNone()` | `u32` | No IO error. |
|
||||
| `std.io.errorEof()` | `u32` | End of input. |
|
||||
| `std.io.errorShortRead()` | `u32` | Input ended before an exact read completed. |
|
||||
| `std.io.errorShortWrite()` | `u32` | Output capacity ended before an exact write completed. |
|
||||
| `std.io.errorCapacity()` | `u32` | Caller storage was too small. |
|
||||
| `std.io.errorPermission()` | `u32` | Permission was denied. |
|
||||
| `std.io.errorTimeout()` | `u32` | Operation timed out. |
|
||||
| `std.io.errorIo()` | `u32` | General IO failure. |
|
||||
| `std.io.errorName(code)` | `String` | Returns a stable label for a known code. |
|
||||
|
||||
Buffered capacity helpers:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.io.bufferedReader(buffer)` | `BufferedReader` | Builds a reader descriptor over caller storage. |
|
||||
| `std.io.bufferedWriter(buffer)` | `BufferedWriter` | Builds a writer descriptor over caller storage. |
|
||||
| `std.io.readerCapacity(&reader)` | `usize` | Reports reader storage capacity. |
|
||||
| `std.io.writerCapacity(&writer)` | `usize` | Reports writer storage capacity. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: memory
|
||||
- allocation behavior: uses caller buffer; no hidden heap
|
||||
- target support: target-neutral
|
||||
- error behavior: exact cursor reads and writes return `Maybe.none` or `false` on overflow or insufficient input
|
||||
- ownership notes: borrows or writes caller-owned storage
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var scratch: [4]u8 = [0_u8; 4]
|
||||
var output: [16]u8 = [0_u8; 16]
|
||||
var reader: FixedReader = std.io.fixedReader("one\ntwo", 0)
|
||||
var writer: FixedWriter = std.io.fixedWriter(output, 0)
|
||||
let line: Maybe<Span<u8>> = std.io.fixedReaderReadLine(&mut reader)
|
||||
let copied: usize = std.io.copyBuffer(&mut reader, &mut writer, scratch)
|
||||
if line.has && copied == 3 && std.mem.eql(std.io.fixedWriterView(&writer), "two") {
|
||||
check world.out.write("io ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.io` is a caller-owned buffer surface, not an ambient process I/O layer.
|
||||
Process stdin/stdout stay behind explicit capabilities such as `World` streams.
|
||||
@@ -0,0 +1,179 @@
|
||||
## When To Use std.json
|
||||
|
||||
In Zerolang, use `std.json` for validation, shallow field lookup, object and
|
||||
array cursor access, explicit-allocator parsing, and caller-buffer JSON writing.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.json.validate(text)` | `Bool` | Checks the current JSON subset without allocation. |
|
||||
| `std.json.validateBytes(bytes)` | `Bool` | Checks a `Span<u8>` JSON payload without allocation. |
|
||||
| `std.json.parse(alloc, text)` | `Maybe<JsonDoc>` | Parses with an explicit allocator and returns `null` on failure. |
|
||||
| `std.json.parseBytes(alloc, bytes)` | `Maybe<JsonDoc>` | Parses a `Span<u8>` payload with an explicit allocator and returns `null` on failure. |
|
||||
| `std.json.streamTokens(text)` | `usize` | Counts stream tokens without building an owned tree. |
|
||||
| `std.json.streamTokensBytes(bytes)` | `usize` | Counts stream tokens from a `Span<u8>` payload. |
|
||||
| `std.json.writeString(buffer, text)` | `Maybe<String>` | Writes an escaped JSON string into caller storage. |
|
||||
| `std.json.decodeBoundary()` | `String` | Documents the typed decode boundary exposed by current metadata. |
|
||||
| `std.json.errorNone()` | `u32` | Validation status for a clean payload. |
|
||||
| `std.json.errorInvalid()` | `u32` | Validation status for malformed JSON. |
|
||||
| `std.json.errorTrailing()` | `u32` | Validation status for trailing non-whitespace bytes. |
|
||||
| `std.json.errorName(code)` | `String` | Returns a stable label for a validation status. |
|
||||
| `std.json.errorExpected(code)` | `String` | Returns stable expected-token text for a validation status. |
|
||||
| `std.json.validateError(bytes)` | `u32` | Validates a byte span and returns a structured status code. |
|
||||
| `std.json.errorOffset(bytes)` | `usize` | Returns the byte offset where validation fails, or the input length for valid JSON. |
|
||||
| `std.json.errorLine(bytes)` | `usize` | Returns the one-based line for the validation error offset. |
|
||||
| `std.json.errorColumn(bytes)` | `usize` | Returns the one-based column for the validation error offset. |
|
||||
| `std.json.field(bytes, key)` | `Maybe<Span<u8>>` | Returns the raw top-level object field value. |
|
||||
| `std.json.objectFieldCount(bytes)` | `Maybe<usize>` | Counts fields in a JSON object slice. |
|
||||
| `std.json.objectKey(buffer, bytes, ordinal)` | `Maybe<Span<u8>>` | Decodes an ordinal object key into caller storage. |
|
||||
| `std.json.objectValue(bytes, ordinal)` | `Maybe<Span<u8>>` | Returns an ordinal object value as a raw JSON slice. |
|
||||
| `std.json.arrayCount(bytes)` | `Maybe<usize>` | Counts items in a JSON array slice. |
|
||||
| `std.json.arrayValue(bytes, ordinal)` | `Maybe<Span<u8>>` | Returns an ordinal array value as a raw JSON slice. |
|
||||
| `std.json.path(bytes, path)` | `Maybe<Span<u8>>` | Returns a raw value for a dot-separated object path. |
|
||||
| `std.json.pathString(buffer, bytes, path)` | `Maybe<Span<u8>>` | Looks up and decodes a string at a dot-separated object path. |
|
||||
| `std.json.pathU32(bytes, path)` | `Maybe<u32>` | Looks up and decodes a `u32` at a dot-separated object path. |
|
||||
| `std.json.pathBool(bytes, path)` | `Maybe<Bool>` | Looks up and decodes a bool at a dot-separated object path. |
|
||||
| `std.json.stringDecode(buffer, value)` | `Maybe<Span<u8>>` | Decodes a JSON string value, including Unicode escapes as UTF-8, into caller storage. |
|
||||
| `std.json.string(buffer, bytes, key)` | `Maybe<Span<u8>>` | Looks up and decodes a top-level string field. |
|
||||
| `std.json.u32(bytes, key)` | `Maybe<u32>` | Looks up and decodes a top-level unsigned integer field. |
|
||||
| `std.json.bool(bytes, key)` | `Maybe<Bool>` | Looks up and decodes a top-level boolean field. |
|
||||
| `std.json.writeStringBytes(buffer, text)` | `Maybe<Span<u8>>` | Writes an escaped JSON string from byte input. |
|
||||
| `std.json.writeObject1String(buffer, key, value)` | `Maybe<Span<u8>>` | Writes a one-field object with a string value. |
|
||||
| `std.json.writeObject1U32(buffer, key, value)` | `Maybe<Span<u8>>` | Writes a one-field object with a `u32` value. |
|
||||
| `std.json.writeObject1Bool(buffer, key, value)` | `Maybe<Span<u8>>` | Writes a one-field object with a bool value. |
|
||||
| `std.json.writeFieldRaw(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one object field from a key and validated raw JSON value. |
|
||||
| `std.json.writeFieldString(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one object field with an escaped string value. |
|
||||
| `std.json.writeFieldU32(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one object field with a `u32` value. |
|
||||
| `std.json.writeFieldBool(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one object field with a bool value. |
|
||||
| `std.json.writeObject2Fields(buffer, field0, field1)` | `Maybe<Span<u8>>` | Writes a two-field object from field fragments and validates the final object. |
|
||||
| `std.json.writeObject2StringField(buffer, key, value, field1)` | `Maybe<Span<u8>>` | Writes a two-field object from a string field and a prebuilt field fragment. |
|
||||
| `std.json.writeObject2U32Field(buffer, key, value, field1)` | `Maybe<Span<u8>>` | Writes a two-field object from a `u32` field and a prebuilt field fragment. |
|
||||
| `std.json.writeObject2BoolField(buffer, key, value, field1)` | `Maybe<Span<u8>>` | Writes a two-field object from a bool field and a prebuilt field fragment. |
|
||||
| `std.json.writeArray2Strings(buffer, value0, value1)` | `Maybe<Span<u8>>` | Writes a two-item array with escaped string values. |
|
||||
| `std.json.writeArray2U32(buffer, value0, value1)` | `Maybe<Span<u8>>` | Writes a two-item array with `u32` values. |
|
||||
| `std.json.writeArray2Bools(buffer, value0, value1)` | `Maybe<Span<u8>>` | Writes a two-item array with bool values. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: memory, parse, or alloc
|
||||
- allocation behavior: validation and streaming are allocation-free; parse uses explicit allocator only; direct writers write caller buffers
|
||||
- target support: target-neutral
|
||||
- error behavior: `Maybe` helpers return null on failure
|
||||
- diagnostics: `errorOffset`, `errorLine`, and `errorColumn` locate validation failures; valid JSON reports the end of input
|
||||
- ownership notes: parsed documents are owned by explicit allocator storage in this compiler slice
|
||||
- examples: `examples/std-data-formats.graph`, `examples/std-json-bytes.graph`, `conformance/native/pass/std-codec-json-url.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var arena_buf: [16]u8 = [0_u8; 16]
|
||||
var arena: FixedBufAlloc = std.mem.fixedBufAlloc(arena_buf)
|
||||
let parsed: Maybe<JsonDoc> = std.json.parse(arena, "{\"ok\":true}")
|
||||
var out: [16]u8 = [0_u8; 16]
|
||||
let text: Maybe<String> = std.json.writeString(out, "zero")
|
||||
if parsed.has && text.has && std.json.streamTokens("{\"ok\":true}") == 3 {
|
||||
check world.out.write("json ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Byte-span parse form:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let bytes: Span<u8> = std.mem.span("{\"ok\":1}")
|
||||
var arena_buf: [16]u8 = [0_u8; 16]
|
||||
var arena: FixedBufAlloc = std.mem.fixedBufAlloc(arena_buf)
|
||||
let parsed: Maybe<JsonDoc> = std.json.parseBytes(arena, bytes)
|
||||
if parsed.has && std.json.validateBytes(bytes) && std.json.streamTokensBytes(bytes) == 3 {
|
||||
check world.out.write("json bytes ok\n")
|
||||
return
|
||||
}
|
||||
check world.err.write("json bytes failed\n")
|
||||
}
|
||||
```
|
||||
|
||||
Top-level object lookup and caller-buffer writing:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let input: Span<u8> = "{\"name\":\"zero\",\"count\":42,\"ok\":true}"
|
||||
var name_buf: [8]u8 = [0_u8; 8]
|
||||
let name: Maybe<Span<u8>> = std.json.string(name_buf, input, "name")
|
||||
let count: Maybe<u32> = std.json.u32(input, "count")
|
||||
var count_field_buf: [24]u8 = [0_u8; 24]
|
||||
let count_field: Maybe<Span<u8>> = std.json.writeFieldU32(count_field_buf, "count", 42_u32)
|
||||
var out: [48]u8 = [0_u8; 48]
|
||||
var written: Maybe<Span<u8>> = null
|
||||
if count_field.has {
|
||||
written = std.json.writeObject2StringField(out, "name", "zero", count_field.value)
|
||||
}
|
||||
if name.has && count.has && written.has && std.json.validateError(written.value) == std.json.errorNone() {
|
||||
check world.out.write("json lookup ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Object and array cursors return borrowed raw JSON slices. Object keys are
|
||||
decoded into caller-owned storage:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let input: Span<u8> = "{\"user\":{\"name\":\"zero\",\"count\":42},\"items\":[1,2]}"
|
||||
let field_count: Maybe<usize> = std.json.objectFieldCount(input)
|
||||
var key_buf: [8]u8 = [0_u8; 8]
|
||||
let first_key: Maybe<Span<u8>> = std.json.objectKey(key_buf, input, 0)
|
||||
let items: Maybe<Span<u8>> = std.json.field(input, "items")
|
||||
var name_buf: [8]u8 = [0_u8; 8]
|
||||
let name: Maybe<Span<u8>> = std.json.pathString(name_buf, input, "user.name")
|
||||
if field_count.has && first_key.has && items.has && std.json.arrayCount(items.value).has && name.has {
|
||||
check world.out.write("json cursors ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
JSON validation diagnostics are allocation-free. Use `std.diag` when a
|
||||
human-readable file location is needed:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let input: Span<u8> = "{\n \"ok\": tru\n}"
|
||||
var location_buf: [24]u8 = [0_u8; 24]
|
||||
let location: Maybe<Span<u8>> = std.diag.formatOffsetLocation(location_buf, "config.json", input, std.json.errorOffset(input))
|
||||
if std.json.validateError(input) == std.json.errorInvalid() && std.json.errorLine(input) == 2 && location.has {
|
||||
check world.out.write(location.value)
|
||||
check world.out.write("\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Small array responses use the same caller-buffer pattern:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var out: [32]u8 = [0_u8; 32]
|
||||
let tags: Maybe<Span<u8>> = std.json.writeArray2Strings(out, "api", "agent")
|
||||
if tags.has {
|
||||
check world.out.write(tags.value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
JSON should not fake allocation-free semantics. Validation, field lookup,
|
||||
string decode, and writing stay allocation-free. String decode writes UTF-8 for
|
||||
Unicode escapes and rejects malformed surrogate pairs.
|
||||
|
||||
Parsing into an owned document requires an explicit allocator. The current
|
||||
`JsonDoc` value is opaque; examples inspect `Maybe.has` and use token streaming
|
||||
for allocation-free summaries. Field lookup is intentionally shallow: it reads
|
||||
top-level object fields and returns raw slices or typed scalar decodes. Object
|
||||
path lookup follows dot-separated object keys; array indexing remains explicit
|
||||
through `arrayValue`. When an object contains duplicate keys, name-based lookup
|
||||
returns the first matching value, while ordinal object cursors preserve the
|
||||
source order and expose every field. Validation diagnostics report byte offsets
|
||||
in the source payload; line and column helpers treat lines and columns as
|
||||
one-based byte positions.
|
||||
@@ -0,0 +1,65 @@
|
||||
## When To Use std.log
|
||||
|
||||
In Zerolang, use `std.log` for explicit-buffer structured log record formatting.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.log.levelDebug()` | `String` | Static `debug` level text. |
|
||||
| `std.log.levelInfo()` | `String` | Static `info` level text. |
|
||||
| `std.log.levelWarn()` | `String` | Static `warn` level text. |
|
||||
| `std.log.levelError()` | `String` | Static `error` level text. |
|
||||
| `std.log.message(buffer, level, message)` | `Maybe<Span<u8>>` | Writes one newline-terminated JSON Lines record with `level` and `message`. |
|
||||
| `std.log.keyValue(buffer, level, key, value)` | `Maybe<Span<u8>>` | Writes one newline-terminated JSON Lines record with `level`, `key`, and `value`. |
|
||||
| `std.log.stringField(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one JSON string field fragment. |
|
||||
| `std.log.messageField(buffer, level, message, field)` | `Maybe<Span<u8>>` | Writes one JSON Lines message record with one field fragment. |
|
||||
| `std.log.redacted(buffer, level, key)` | `Maybe<Span<u8>>` | Writes one JSON Lines record marking a field name as redacted. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: memory; `messageField` also validates JSON field fragments
|
||||
- allocation behavior: writes caller buffer; no hidden heap
|
||||
- target support: target-neutral
|
||||
- error behavior: returns `null` when the buffer is too small or a value cannot be JSON-escaped
|
||||
- ownership notes: borrows returned bytes from caller-owned storage
|
||||
- example: `examples/std-testing-log.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var storage: [128]u8 = [0_u8; 128]
|
||||
var field_storage: [64]u8 = [0_u8; 64]
|
||||
let field: Maybe<Span<u8>> = std.log.stringField(field_storage, "event", "startup")
|
||||
if field.has {
|
||||
let entry: Maybe<Span<u8>> = std.log.messageField(storage, std.log.levelInfo(), "started", field.value)
|
||||
if entry.has {
|
||||
check world.out.write(entry.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```json
|
||||
{"level":"info","message":"started","event":"startup"}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.log` is a formatting surface, not a global logger. The caller owns the
|
||||
storage and chooses where to write the resulting span, such as `World.out`, a
|
||||
file handle, or a test assertion.
|
||||
|
||||
Records use JSON Lines so downstream tools can parse them without guessing at
|
||||
ad hoc separators. The helpers write exactly one record and include a trailing
|
||||
newline.
|
||||
|
||||
`messageField` validates the final JSON object before returning it. Build field
|
||||
fragments with `stringField` unless the field fragment is already known to be
|
||||
valid JSON.
|
||||
|
||||
Use `redacted` for logs that need to state which field was intentionally
|
||||
withheld without writing the sensitive value.
|
||||
@@ -0,0 +1,86 @@
|
||||
## When To Use std.math
|
||||
|
||||
In Zerolang, use `std.math` for pure fixed-width integer helpers, checked/saturating
|
||||
arithmetic, and small number-theory routines.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.math.minU32(left, right)` | `u32` | Returns the smaller unsigned value. |
|
||||
| `std.math.minI32(left, right)` | `i32` | Returns the smaller signed 32-bit value. |
|
||||
| `std.math.minUsize(left, right)` | `usize` | Returns the smaller pointer-width unsigned value. |
|
||||
| `std.math.minI64(left, right)` | `i64` | Returns the smaller signed 64-bit value. |
|
||||
| `std.math.minU64(left, right)` | `u64` | Returns the smaller unsigned 64-bit value. |
|
||||
| `std.math.maxU32(left, right)` | `u32` | Returns the larger unsigned value. |
|
||||
| `std.math.maxI32(left, right)` | `i32` | Returns the larger signed 32-bit value. |
|
||||
| `std.math.maxUsize(left, right)` | `usize` | Returns the larger pointer-width unsigned value. |
|
||||
| `std.math.maxI64(left, right)` | `i64` | Returns the larger signed 64-bit value. |
|
||||
| `std.math.maxU64(left, right)` | `u64` | Returns the larger unsigned 64-bit value. |
|
||||
| `std.math.clampU32(value, low, high)` | `u32` | Clamps between the two bounds; swapped bounds are normalized. |
|
||||
| `std.math.clampI32(value, low, high)` | `i32` | Clamps a signed 32-bit value; swapped bounds are normalized. |
|
||||
| `std.math.clampUsize(value, low, high)` | `usize` | Clamps a pointer-width unsigned value; swapped bounds are normalized. |
|
||||
| `std.math.clampI64(value, low, high)` | `i64` | Clamps a signed 64-bit value; swapped bounds are normalized. |
|
||||
| `std.math.clampU64(value, low, high)` | `u64` | Clamps an unsigned 64-bit value; swapped bounds are normalized. |
|
||||
| `std.math.absI32(value)` | `u32` | Returns the unsigned magnitude of a signed 32-bit value. |
|
||||
| `std.math.absI64(value)` | `u64` | Returns the unsigned magnitude of a signed 64-bit value. |
|
||||
| `std.math.checkedAddU32(left, right)` | `Maybe<u32>` | Adds only when the result fits in `u32`. |
|
||||
| `std.math.checkedSubU32(left, right)` | `Maybe<u32>` | Subtracts only when the result fits in `u32`. |
|
||||
| `std.math.checkedMulU32(left, right)` | `Maybe<u32>` | Multiplies only when the result fits in `u32`. |
|
||||
| `std.math.checkedAddUsize(left, right)` | `Maybe<usize>` | Adds only when the result fits in `usize`. |
|
||||
| `std.math.checkedSubUsize(left, right)` | `Maybe<usize>` | Subtracts only when the result fits in `usize`. |
|
||||
| `std.math.checkedMulUsize(left, right)` | `Maybe<usize>` | Multiplies only when the result fits in `usize`. |
|
||||
| `std.math.checkedAddI32(left, right)` | `Maybe<i32>` | Adds only when the result fits in `i32`. |
|
||||
| `std.math.checkedSubI32(left, right)` | `Maybe<i32>` | Subtracts only when the result fits in `i32`. |
|
||||
| `std.math.checkedMulI32(left, right)` | `Maybe<i32>` | Multiplies only when the result fits in `i32`. |
|
||||
| `std.math.saturatingAddU32(left, right)` | `u32` | Adds and clamps overflow to `u32` max. |
|
||||
| `std.math.saturatingSubU32(left, right)` | `u32` | Subtracts and clamps underflow to `0`. |
|
||||
| `std.math.saturatingMulU32(left, right)` | `u32` | Multiplies and clamps overflow to `u32` max. |
|
||||
| `std.math.saturatingAddUsize(left, right)` | `usize` | Adds and clamps overflow to `usize` max. |
|
||||
| `std.math.saturatingSubUsize(left, right)` | `usize` | Subtracts and clamps underflow to `0`. |
|
||||
| `std.math.saturatingMulUsize(left, right)` | `usize` | Multiplies and clamps overflow to `usize` max. |
|
||||
| `std.math.saturatingAddI32(left, right)` | `i32` | Adds and clamps overflow to the nearest `i32` bound. |
|
||||
| `std.math.saturatingSubI32(left, right)` | `i32` | Subtracts and clamps overflow to the nearest `i32` bound. |
|
||||
| `std.math.saturatingMulI32(left, right)` | `i32` | Multiplies and clamps overflow to the nearest `i32` bound. |
|
||||
| `std.math.gcdU32(left, right)` | `u32` | Euclidean greatest common divisor. |
|
||||
| `std.math.lcmU32(left, right)` | `u32` | Least common multiple; returns `0` when either input is `0`. |
|
||||
| `std.math.checkedLcmU32(left, right)` | `Maybe<u32>` | Least common multiple only when the result fits in `u32`. |
|
||||
| `std.math.powU32(base, exponent)` | `u32` | Fixed-width exponentiation by squaring. |
|
||||
| `std.math.checkedPowU32(base, exponent)` | `Maybe<u32>` | Exponentiation only when the result fits in `u32`. |
|
||||
| `std.math.modPowU32(base, exponent, modulus)` | `u32` | Modular exponentiation; returns `0` for modulus `0`. |
|
||||
| `std.math.isPrimeU32(value)` | `Bool` | Trial division primality for unsigned integers. |
|
||||
| `std.math.isEvenU32(value)` | `Bool` | Reports whether a `u32` value is even. |
|
||||
| `std.math.isOddU32(value)` | `Bool` | Reports whether a `u32` value is odd. |
|
||||
| `std.math.sqrtFloorU32(value)` | `u32` | Integer square root rounded down. |
|
||||
| `std.math.factorialU32(value)` | `Maybe<u32>` | Factorial only when the result fits in `u32`. |
|
||||
| `std.math.binomialU32(n, k)` | `Maybe<u32>` | Binomial coefficient only when the result fits in `u32`. |
|
||||
| `std.math.divisorCountU32(value)` | `u32` | Counts positive divisors; returns `0` for `0`. |
|
||||
| `std.math.properDivisorSumU32(value)` | `u32` | Sums positive divisors smaller than `value`. |
|
||||
|
||||
Current scope:
|
||||
|
||||
- Helpers are pure, target-neutral fixed-width integer operations.
|
||||
- Checked helpers return `Maybe<T>` instead of wrapping or trapping.
|
||||
- Saturating helpers clamp to documented integer bounds.
|
||||
- The module does not provide floating-point math, big integers, or arbitrary-precision number theory.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
if std.math.gcdU32(84, 30) == 6 && std.math.isPrimeU32(31) {
|
||||
check world.out.write("math helper ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.math` does not allocate and does not require a hosted runtime capability.
|
||||
Names include the integer width because Zero does not overload standard-library
|
||||
helpers by argument type.
|
||||
|
||||
Number-theory helpers are intentionally simple and deterministic. They are
|
||||
suitable for small fixed-width tasks, examples, and compiler-portable checks.
|
||||
Large-number algorithms should be added as explicit APIs with documented bounds
|
||||
instead of hidden heap allocation or implicit widening.
|
||||
@@ -0,0 +1,172 @@
|
||||
## When To Use std.mem
|
||||
|
||||
In Zerolang, use `std.mem` for spans, clamped span views, copy/fill,
|
||||
fixed-buffer allocators, explicit byte buffers, and memory-budget-visible
|
||||
collection foundations.
|
||||
|
||||
Runnable today:
|
||||
|
||||
The item-generic helpers currently support these direct-backed scalar element
|
||||
types: `Bool`, `u8`, `u16`, `usize`, `i32`, `u32`, `i64`, and `u64`.
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.mem.copy(dst, src)` | `usize` | Copies from `Span<u8>` into caller-owned `MutSpan<u8>` storage and returns the copied byte count. |
|
||||
| `std.mem.copyItems(dst, src)` | `usize` | Copies matching scalar `Span<T>` items into caller-owned mutable item storage and returns the copied item count. |
|
||||
| `std.mem.fill(dst, value)` | `usize` | Fills caller-owned `MutSpan<u8>` storage with a `u8` byte and returns the filled byte count. |
|
||||
| `std.mem.fillItems(dst, value)` | `usize` | Fills caller-owned mutable scalar item storage with a matching `T` value and returns the filled item count. |
|
||||
| `std.mem.eql(a, b)` | `Bool` | Compares string-backed byte inputs. |
|
||||
| `std.mem.span(value)` | `Span<u8>` | Builds a native `Span<u8>` view over a string literal. |
|
||||
| `std.mem.contains(items, needle)` | `Bool` | Searches readable contiguous non-owned scalar `T` storage for a matching value. |
|
||||
| `std.mem.compareI32(left, right)` | `i32` | Lexicographically compares two `Span<i32>` values and returns `-1`, `0`, or `1`. |
|
||||
| `std.mem.compareU8(left, right)` | `i32` | Lexicographically compares two `Span<u8>` values and returns `-1`, `0`, or `1`. |
|
||||
| `std.mem.compareBytes(left, right)` | `i32` | Alias-style byte lexicographic comparison for `Span<u8>` values. |
|
||||
| `std.mem.compareU32(left, right)` | `i32` | Lexicographically compares two `Span<u32>` values and returns `-1`, `0`, or `1`. |
|
||||
| `std.mem.compareUsize(left, right)` | `i32` | Lexicographically compares two `Span<usize>` values and returns `-1`, `0`, or `1`. |
|
||||
| `std.mem.startsWith(items, prefix)` | `Bool` | Checks whether a scalar span begins with a matching prefix span. |
|
||||
| `std.mem.endsWith(items, suffix)` | `Bool` | Checks whether a scalar span ends with a matching suffix span. |
|
||||
| `std.mem.splitBefore(items, delimiter)` | `Span<T>` | Returns the read-only scalar item prefix before the first delimiter, or the full span when absent. |
|
||||
| `std.mem.splitAfter(items, delimiter)` | `Span<T>` | Returns the read-only scalar item suffix after the first delimiter, or an empty span when absent. |
|
||||
| `std.mem.isEmpty(items)` | `Bool` | Reports whether readable contiguous scalar item storage has zero items. |
|
||||
| `std.mem.chunkCount(items, chunkSize)` | `usize` | Returns the number of non-overlapping chunks needed to cover the span, or `0` when `chunkSize` is zero. |
|
||||
| `std.mem.chunk(items, chunkIndex, chunkSize)` | `Span<T>` | Returns a clamped read-only scalar item chunk by zero-based chunk index. |
|
||||
| `std.mem.windowCount(items, windowSize)` | `usize` | Returns the number of overlapping fixed-size windows available in a span, or `0` when the size is zero or larger than the span. |
|
||||
| `std.mem.window(items, windowIndex, windowSize)` | `Span<T>` | Returns an overlapping read-only scalar item window by zero-based window index. |
|
||||
| `std.mem.advance(items, cursor, count)` | `usize` | Returns a clamped cursor advanced by at most `count` items. |
|
||||
| `std.mem.cursorDone(items, cursor)` | `Bool` | Reports whether a cursor is at or past the end of a span. |
|
||||
| `std.mem.remaining(items, cursor)` | `Span<T>` | Returns the clamped read-only scalar item view from `cursor` to the end. |
|
||||
| `std.mem.cursorChunk(items, cursor, count)` | `Span<T>` | Returns a clamped read-only scalar item window beginning at `cursor`. |
|
||||
| `std.mem.prefix(items, count)` | `Span<T>` | Returns a clamped read-only scalar item prefix view. |
|
||||
| `std.mem.dropPrefix(items, count)` | `Span<T>` | Returns a clamped read-only scalar item view after the first `count` items. |
|
||||
| `std.mem.suffix(items, count)` | `Span<T>` | Returns a clamped read-only scalar item suffix view. |
|
||||
| `std.mem.dropSuffix(items, count)` | `Span<T>` | Returns a clamped read-only scalar item view before the last `count` items. |
|
||||
| `std.mem.slice(items, start, count)` | `Span<T>` | Returns a clamped read-only scalar item window beginning at `start`. |
|
||||
| `std.mem.len(bytes)` | `usize` | Returns the length of a fixed array, `Span<T>`, or `MutSpan<T>`. |
|
||||
| `std.mem.get(bytes, index)` | `Maybe<T>` | Reads one indexed element from an array/span-like value when the index is in bounds. |
|
||||
| `std.mem.eqlBytes(a, b)` | `Bool` | Compares two `Span<T>`/`MutSpan<T>` values with the same element type. |
|
||||
| `std.mem.nullAlloc()` | `NullAlloc` | Creates an allocator that always returns `null`, useful for proving code does not allocate. |
|
||||
| `std.mem.fixedBufAlloc(buffer)` | `FixedBufAlloc` | Creates a mutable fixed-buffer allocator from caller-owned `MutSpan<u8>` bytes. |
|
||||
| `std.mem.arena(buffer)` | `FixedBufAlloc` | Arena-style alias over the fixed-buffer allocator model; `reset` rewinds the caller-owned storage. |
|
||||
| `std.mem.pageAlloc()` | `PageAlloc` | Explicit host allocator handle metadata; it never creates an ambient global allocator. |
|
||||
| `std.mem.generalAlloc()` | `GeneralAlloc` | Explicit general allocator handle metadata; callers still pass allocator state deliberately. |
|
||||
| `std.mem.allocBytes(alloc, len)` | `Maybe<MutSpan<u8>>` | Allocates bytes from `NullAlloc` or a mutable `FixedBufAlloc` binding. |
|
||||
| `std.mem.byteBuf(alloc, len)` | `Maybe<owned<ByteBuf>>` | Creates an owned byte buffer backed by explicit caller-provided allocator storage. |
|
||||
| `std.mem.bufBytes(&buf)` | `MutSpan<u8>` | Borrows writable bytes from an owned `ByteBuf`. |
|
||||
| `std.mem.bufLen(&buf)` | `usize` | Returns the live length of a `ByteBuf`. |
|
||||
| `std.mem.reset(&mut arena)` | `Void` | Resets caller-owned arena/fixed-buffer allocation state. |
|
||||
| `std.mem.capacity(arena)` | `usize` | Reports fixed-buffer capacity. |
|
||||
| `std.mem.vec(storage)` | `Vec` | Monomorphic byte vector over caller-owned mutable storage. |
|
||||
| `std.mem.vecPush(&mut vec, value)` | `Bool` | Appends one byte when capacity remains; returns `false` instead of growing implicitly. |
|
||||
| `std.mem.vecBytes(&vec)` | `Span<u8>` | Borrows the live bytes in the vector without copying. |
|
||||
| `std.mem.vecGet(&vec, index)` | `Maybe<u8>` | Reads one live vector byte when `index` is in bounds. |
|
||||
| `std.mem.vecSet(&mut vec, index, value)` | `Bool` | Replaces one live vector byte when `index` is in bounds; returns `false` out of bounds. |
|
||||
| `std.mem.vecClear(&mut vec)` | `usize` | Resets live length to zero and keeps caller-owned storage available for reuse. |
|
||||
| `std.mem.vecPop(&mut vec)` | `Bool` | Removes one live byte when the vector is non-empty. |
|
||||
| `std.mem.vecTruncate(&mut vec, len)` | `usize` | Shrinks the live length to at most `len` and returns the resulting length. |
|
||||
| `std.mem.vecRemoveSwap(&mut vec, index)` | `Bool` | Removes one live byte by replacing it with the last live byte. Returns `false` out of bounds. |
|
||||
| `std.mem.vecIndex(&vec, value)` | `usize` | Returns the first live index for `value`, or the current live length when absent. |
|
||||
| `std.mem.vecContains(&vec, value)` | `Bool` | Reports whether a byte value is present in the live vector prefix. |
|
||||
| `std.mem.vecInsertUnique(&mut vec, value)` | `Bool` | Appends `value` only when it is absent and capacity remains. |
|
||||
| `std.mem.vecRemoveValue(&mut vec, value)` | `Bool` | Swap-removes the first matching live byte by value. |
|
||||
| `std.mem.vecLen(&vec)` | `usize` | Reports current vector length. |
|
||||
| `std.mem.vecCapacity(&vec)` | `usize` | Reports caller-provided vector capacity. |
|
||||
| `std.mem.vecRemaining(&vec)` | `usize` | Reports remaining byte-vector capacity. Returns `0` when the vector is full. |
|
||||
| `std.mem.vecIsEmpty(&vec)` | `Bool` | Reports whether the vector has no live bytes. |
|
||||
| `std.mem.vecIsFull(&vec)` | `Bool` | Reports whether the vector has no remaining capacity. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
type SliceView {
|
||||
bytes: Span<u8>,
|
||||
values: Span<i32>,
|
||||
}
|
||||
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let bytes: Span<u8> = std.mem.span("zero-memory")
|
||||
let same: Span<u8> = std.mem.span("zero-memory")
|
||||
var scratch: [11]u8 = [0_u8; 11]
|
||||
let copied: usize = std.mem.copy(scratch, bytes)
|
||||
var ints: [3]i32 = [1, 2, 3]
|
||||
let intSpan: MutSpan<i32> = ints
|
||||
let filled: usize = std.mem.fillItems(intSpan, 7)
|
||||
let prefix: Span<i32> = std.mem.prefix(intSpan, 2)
|
||||
let suffix: Span<i32> = std.mem.suffix(intSpan, 2)
|
||||
let middle: Span<i32> = std.mem.slice(intSpan, 1, 1)
|
||||
let before: Span<i32> = std.mem.splitBefore(intSpan, 7)
|
||||
let after: Span<i32> = std.mem.splitAfter(intSpan, 7)
|
||||
let chunk: Span<i32> = std.mem.chunk(intSpan, 1_usize, 2_usize)
|
||||
let sliding: Span<i32> = std.mem.window(intSpan, 1_usize, 2_usize)
|
||||
let cursor: usize = std.mem.advance(intSpan, 0_usize, 2_usize)
|
||||
let rest: Span<i32> = std.mem.remaining(intSpan, cursor)
|
||||
let view: SliceView = SliceView { bytes: bytes, values: intSpan }
|
||||
let ordered: Bool = std.mem.compareI32(prefix, suffix) == 0
|
||||
let starts: Bool = std.mem.startsWith(view.bytes, std.mem.span("zero"))
|
||||
let ends: Bool = std.mem.endsWith(view.bytes, std.mem.span("memory"))
|
||||
if copied == 11 && filled == 3 && ordered && starts && ends && std.mem.len(view.bytes) == 11 && std.mem.eqlBytes(view.bytes, same) && std.mem.len(view.values) == 3 && std.mem.contains(view.values, 7) && std.mem.isEmpty(before) && std.mem.len(after) == 2 && std.mem.len(prefix) == 2 && std.mem.len(suffix) == 2 && std.mem.len(middle) == 1 && std.mem.len(chunk) == 1 && std.mem.len(sliding) == 2 && std.mem.len(rest) == 1 && !std.mem.cursorDone(intSpan, cursor) {
|
||||
check world.out.write("memory type forms runnable\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Allocator Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var storage: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]
|
||||
var alloc: FixedBufAlloc = std.mem.fixedBufAlloc(storage)
|
||||
let bytes: Maybe<MutSpan<u8>> = std.mem.allocBytes(alloc, 4)
|
||||
if bytes.has {
|
||||
bytes.value[0] = 90
|
||||
check world.out.write("fixed buffer allocated\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: none beyond writes performed by caller code.
|
||||
|
||||
Allocation behavior:
|
||||
|
||||
- `NullAlloc` always returns `null`.
|
||||
- `FixedBufAlloc` and `Arena` return `MutSpan<u8>` views into caller-owned
|
||||
storage.
|
||||
- `ByteBuf` owns a slice of explicit allocator storage and never reaches for a
|
||||
global heap.
|
||||
|
||||
Use `std.mem.allocBytes(alloc, capacity)` when a byte-oriented API needs
|
||||
allocator-backed mutable storage directly. `std.collections.fixedSet`,
|
||||
`std.collections.fixedDeque`, and `std.collections.fixedMap` can use those
|
||||
returned spans as caller-owned backing storage.
|
||||
|
||||
Ownership: returned spans borrow from the original fixed buffer; no heap
|
||||
ownership is created.
|
||||
|
||||
Target support: current compiler targets. Direct native builds lower
|
||||
`FixedBufAlloc` locals only; `PageAlloc`, `GeneralAlloc`, and `NullAlloc`
|
||||
locals type-check but fail `zero build` with a `BLD004` diagnostic that points
|
||||
back to `std.mem.fixedBufAlloc`.
|
||||
|
||||
## Reporting Contract
|
||||
|
||||
`zero mem --json [graph-input]` reports the allocator contract in machine-readable form:
|
||||
|
||||
- `memoryBudgets`: stack, static, heap, arena, fixed-buffer, collection-capacity, allocator-capacity, requested-allocation, and linear-memory floor budgets.
|
||||
- `allocatorFacts`: `NullAlloc`, `FixedBufAlloc`, `Arena`, `PageAlloc`, and
|
||||
`GeneralAlloc` usage, capacity, failure behavior, and
|
||||
hidden-global-allocator status.
|
||||
- `allocationInstrumentation`: pay-as-used hooks for attempts, successes, failures, requested bytes, granted bytes, and peak live bytes.
|
||||
- `collectionFacts`: fixed-capacity `Vec`, fixed-storage `std.collections`
|
||||
helpers, and owned `ByteBuf`, including growth/failure/cleanup behavior.
|
||||
- `safetyFacts`: the selected profile plus the current bounds,
|
||||
initialization, aliasing, lifetime, ownership, span, MIR, literal integer
|
||||
range-check, runtime arithmetic, and unchecked-surface facts.
|
||||
|
||||
All heap budgets are explicit. A program that only uses `std.mem` remains at
|
||||
`heapBytes: 0`, `globalHeapBytes: 0`, and `hiddenHeapAllocation: false` unless
|
||||
an allocator API documents otherwise.
|
||||
|
||||
## Design Notes
|
||||
|
||||
No standard collection may silently allocate from a global heap. Heap-owning APIs
|
||||
will require an allocator capability and document ownership, capacity, and
|
||||
cleanup.
|
||||
@@ -0,0 +1,47 @@
|
||||
## When To Use std.net
|
||||
|
||||
In Zerolang, use `std.net` for network capability metadata, local address construction,
|
||||
timeouts, and bootstrap client/listener handles.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.net.host()` | `Net` | Creates the hosted network capability. |
|
||||
| `std.net.address(host, port)` | `Address` | Builds an address value without allocation. |
|
||||
| `std.net.dnsName(address)` | `String` | Reads the address host name. |
|
||||
| `std.net.withTimeout(address, duration)` | `Address` | Returns address metadata with a timeout. |
|
||||
| `std.net.localhost(port)` | `Address` | Builds a `localhost` address with the requested port. |
|
||||
| `std.net.loopback(port)` | `Address` | Builds a `127.0.0.1` loopback address with the requested port. |
|
||||
| `std.net.connect(net, address)` | `Maybe<Conn>` | Returns a bootstrap connection handle when available. |
|
||||
| `std.net.listen(net, address)` | `Maybe<Listener>` | Returns a bootstrap listener handle when available. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: net
|
||||
- allocation behavior: no allocation
|
||||
- target support: address helpers are target-neutral; host/connect/listen require a net-capable target
|
||||
- error behavior: connection helpers return `Maybe`
|
||||
- ownership notes: no stream ownership transfer in the current handle model
|
||||
- example: `conformance/native/pass/std-net-http-breadth.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let net: Net = std.net.host()
|
||||
let addr: Address = std.net.withTimeout(std.net.localhost(8080_u16), std.time.ms(250))
|
||||
let loopback: Address = std.net.loopback(8080_u16)
|
||||
let conn: Maybe<Conn> = std.net.connect(net, addr)
|
||||
if conn.has && std.mem.eql(std.net.dnsName(addr), "localhost") && std.mem.eql(std.net.dnsName(loopback), "127.0.0.1") {
|
||||
check world.out.write("net ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.net` exposes network capability metadata and bootstrap handles. Current
|
||||
fixtures expect connection and listener handles to be absent. It does not
|
||||
provide socket read/write APIs in the current public surface. Outbound HTTP is
|
||||
exposed through `std.http.fetch(...)` rather than through raw sockets.
|
||||
@@ -0,0 +1,78 @@
|
||||
## When To Use std.parse
|
||||
|
||||
In Zerolang, use `std.parse` for allocation-free byte scanners and scalar parsers.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.parse.isAsciiDigit(value)` | `Bool` | Checks whether the first byte is `0` through `9`. |
|
||||
| `std.parse.isAsciiAlpha(value)` | `Bool` | Checks whether the first byte is ASCII alphabetic. |
|
||||
| `std.parse.isIdentifierStart(value)` | `Bool` | Checks whether the first byte can start a Zero identifier. |
|
||||
| `std.parse.isWhitespace(value)` | `Bool` | Checks ASCII whitespace. |
|
||||
| `std.parse.scanDigits(value)` | `usize` | Counts a leading run of ASCII digits. |
|
||||
| `std.parse.scanIdentifier(value)` | `usize` | Counts a leading identifier token. |
|
||||
| `std.parse.scanWhitespace(value)` | `usize` | Counts leading spaces, tabs, line feeds, and carriage returns. |
|
||||
| `std.parse.scanUntilByte(value, byte)` | `usize` | Returns the first matching byte index or the input length. |
|
||||
| `std.parse.tokenAscii(value)` | `Span<u8>` | Borrows the first non-whitespace ASCII token. |
|
||||
| `std.parse.parseBool(value)` | `Maybe<Bool>` | Parses `true` or `false`. |
|
||||
| `std.parse.parseDuration(value)` | `Maybe<Duration>` | Parses signed `ns`, `us`, `ms`, `s`, `m`, and `h` duration components such as `1h30m`. |
|
||||
| `std.parse.parseByteSize(value)` | `Maybe<usize>` | Parses bytes plus `KB`, `MB`, `GB`, `KiB`, `MiB`, and `GiB` suffixes. |
|
||||
| `std.parse.parseI32(value)` | `Maybe<i32>` | Parses a full signed 32-bit decimal value. |
|
||||
| `std.parse.parseI32Base(value, base)` | `Maybe<i32>` | Parses a full signed 32-bit value in base 2 through 36. |
|
||||
| `std.parse.parseI32Prefix(value)` | `Maybe<i32>` | Parses signed decimal, `0x`, `0o`, or `0b` input. |
|
||||
| `std.parse.parseI64(value)` | `Maybe<i64>` | Parses a full signed 64-bit decimal value. |
|
||||
| `std.parse.parseI64Base(value, base)` | `Maybe<i64>` | Parses a full signed 64-bit value in base 2 through 36. |
|
||||
| `std.parse.parseI64Prefix(value)` | `Maybe<i64>` | Parses signed decimal, `0x`, `0o`, or `0b` input. |
|
||||
| `std.parse.parseU8(value)` | `Maybe<u8>` | Parses a full decimal byte value. |
|
||||
| `std.parse.parseU16(value)` | `Maybe<u16>` | Parses a full decimal unsigned 16-bit value. |
|
||||
| `std.parse.parseU32(value)` | `Maybe<u32>` | Parses a full decimal unsigned 32-bit value. |
|
||||
| `std.parse.parseU32Base(value, base)` | `Maybe<u32>` | Parses a full unsigned 32-bit value in base 2 through 36. |
|
||||
| `std.parse.parseU32Prefix(value)` | `Maybe<u32>` | Parses unsigned decimal, `0x`, `0o`, or `0b` input. |
|
||||
| `std.parse.parseU64(value)` | `Maybe<u64>` | Parses a full decimal unsigned 64-bit value. |
|
||||
| `std.parse.parseU64Base(value, base)` | `Maybe<u64>` | Parses a full unsigned 64-bit value in base 2 through 36. |
|
||||
| `std.parse.parseU64Prefix(value)` | `Maybe<u64>` | Parses unsigned decimal, `0x`, `0o`, or `0b` input. |
|
||||
| `std.parse.parseUsize(value)` | `Maybe<usize>` | Parses a full decimal `usize` value. |
|
||||
| `std.parse.parseUsizeBase(value, base)` | `Maybe<usize>` | Parses a full `usize` value in base 2 through 36. |
|
||||
| `std.parse.parseUsizePrefix(value)` | `Maybe<usize>` | Parses `usize` decimal, `0x`, `0o`, or `0b` input. |
|
||||
|
||||
Current limits:
|
||||
|
||||
- Source position and span types.
|
||||
- Rich cursor objects beyond the current allocation-free scanner primitives.
|
||||
- Token and diagnostic data shared by language and data parsers.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
use std.parse
|
||||
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let digit: Bool = std.parse.isAsciiDigit("7")
|
||||
let ident: Bool = std.parse.isIdentifierStart("_")
|
||||
let scanned: usize = std.parse.scanDigits("123abc")
|
||||
let parsed: Maybe<u16> = std.parse.parseU16("8080")
|
||||
let signed: Maybe<i32> = std.parse.parseI32Prefix("-0x2a")
|
||||
let signed64: Maybe<i64> = std.parse.parseI64("-9223372036854775808")
|
||||
let hex: Maybe<u32> = std.parse.parseU32Base("ff", 16_u32)
|
||||
let hex64: Maybe<u64> = std.parse.parseU64Prefix("0xffffffffffffffff")
|
||||
let duration: Maybe<Duration> = std.parse.parseDuration("1h30m")
|
||||
let size: Maybe<usize> = std.parse.parseByteSize("2MiB")
|
||||
let token: Span<u8> = std.parse.tokenAscii(" zero text")
|
||||
if digit && ident && scanned == 3 && parsed.has && parsed.value == 8080 && signed.has && signed.value == -42 && signed64.has && signed64.value == 0 - 9223372036854775807 - 1 && hex.has && hex.value == 255_u32 && hex64.has && hex64.value == 18446744073709551615_u64 && duration.has && std.time.asSecondsFloor(duration.value) == 5400_i64 && size.has && size.value == 2097152 && std.mem.eql(token, "zero") {
|
||||
check world.out.write("parse primitives ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
The module stays byte-oriented so compiler, config, and codec code can parse
|
||||
without Unicode scalar handling or heap allocation. Public helpers accept byte
|
||||
spans, so callers can parse string literals, fixed arrays, and runtime buffers.
|
||||
|
||||
Integer parsers return `Maybe<T>` instead of allocating diagnostics. Base parsers
|
||||
accept bases 2 through 36, consume the full input, and reject overflow. Prefix
|
||||
parsers recognize decimal by default plus `0x`, `0o`, and `0b` for 32-bit,
|
||||
64-bit, and `usize` widths. Duration and byte-size parsers also consume the
|
||||
full input and reject overflow.
|
||||
@@ -0,0 +1,51 @@
|
||||
## When To Use std.path
|
||||
|
||||
In Zerolang, use `std.path` for lexical path operations that borrow from input
|
||||
paths or write into caller-owned buffers.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.path.basename(path)` | `String` | Borrows the final lexical component of `path`. |
|
||||
| `std.path.dirname(path)` | `String` | Borrows or returns the lexical parent portion of `path`. |
|
||||
| `std.path.extension(path)` | `String` | Borrows the suffix after the last `.` in the final component. |
|
||||
| `std.path.stem(path)` | `String` | Borrows the final component without its extension. |
|
||||
| `std.path.splitDir(path)` | `String` | Borrows the directory side of the final split. |
|
||||
| `std.path.splitBase(path)` | `String` | Borrows the basename side of the final split. |
|
||||
| `std.path.isAbs(path)` | `Bool` | Returns true for paths that begin with a path separator. |
|
||||
| `std.path.componentCount(path)` | `usize` | Counts non-empty lexical path components. |
|
||||
| `std.path.component(path, index)` | `Maybe<String>` | Borrows one non-empty lexical component by index. |
|
||||
| `std.path.abs(buffer, base, target)` | `Maybe<String>` | Copies `target` when already absolute, or joins `base` and `target` into caller storage. |
|
||||
| `std.path.join(buffer, left, right)` | `Maybe<String>` | Joins two path fragments into caller-provided fixed buffer storage. |
|
||||
| `std.path.normalize(buffer, path)` | `Maybe<String>` | Collapses repeated `/`, `.`, and lexical `..` segments into caller-provided storage. |
|
||||
| `std.path.relative(buffer, base, target)` | `Maybe<String>` | Produces a target-relative lexical path when possible, or copies `target`. |
|
||||
|
||||
Current scope:
|
||||
|
||||
- Helpers are target-neutral lexical operations over `/` and `\` separators.
|
||||
- Buffer-writing helpers return `null` when caller storage is too small.
|
||||
- The module does not implement platform-specific path rules, drive prefixes, or filesystem access.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var storage: [64]u8 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
let path: Maybe<String> = std.path.join(storage, ".zero", "example.txt")
|
||||
if path.has {
|
||||
check world.out.write(path.value)
|
||||
check world.out.write("\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.path.basename`, `dirname`, `extension`, `stem`, `splitDir`, `splitBase`,
|
||||
and `component` return borrowed views into the input path. `std.path.abs`,
|
||||
`join`, `normalize`, and `relative` write into caller storage and return `null`
|
||||
when the buffer is too small. They do not allocate.
|
||||
|
||||
The current behavior uses `/` as the portable package/example separator. These
|
||||
helpers are lexical string helpers, not target-specific filesystem resolvers.
|
||||
@@ -0,0 +1,122 @@
|
||||
## When To Use std.proc
|
||||
|
||||
In Zerolang, use `std.proc` for hosted process helpers behind explicit process
|
||||
capability boundaries. The surface is host-only and supports both status-style
|
||||
helpers and owned child handles for incremental I/O.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.proc.spawn(command)` | `ProcStatus` | Runs an argv-style command with inherited stdio through the explicit proc capability surface and returns its status. |
|
||||
| `std.proc.spawnInherit(command)` | `ProcStatus` | Runs an argv-style command while inheriting stdin, stdout, and stderr from the parent process. |
|
||||
| `std.proc.spawnInheritArgs(program, args, cwd, env)` | `ProcStatus` | Runs a program path plus newline-separated argv entries while inheriting stdio, using a working directory and newline-separated `KEY=value` environment bindings. |
|
||||
| `std.proc.exitCode(status)` | `i32` | Reads the process status code. |
|
||||
| `std.proc.succeeded(status)` | `Bool` | Reports whether the status exit code is `0`. |
|
||||
| `std.proc.failed(status)` | `Bool` | Reports whether the status exit code is nonzero. |
|
||||
| `std.proc.runOk(command)` | `Bool` | Spawns a hosted command and reports whether the resulting status succeeded. |
|
||||
| `std.proc.runCode(command)` | `i32` | Spawns a hosted command and returns its exit code. |
|
||||
| `std.proc.capture(command, buffer)` | `Maybe<usize>` | Runs an argv-style command and captures stdout into caller storage. Returns `null` on parse failure, spawn failure, nonzero exit, unsupported target, or output truncation. |
|
||||
| `std.proc.captureArgs(program, args, buffer)` | `Maybe<usize>` | Runs a program path plus newline-separated argv entries and captures stdout into caller storage. |
|
||||
| `std.proc.captureFiles(command, stdoutPath, stderrPath)` | `ProcStatus` | Runs an argv-style command and writes stdout and stderr to hosted paths. Returns `127` when the command cannot be parsed, spawned, waited on, or the output files cannot be opened. |
|
||||
| `std.proc.captureFilesArgs(program, args, stdoutPath, stderrPath)` | `ProcStatus` | Runs a program path plus newline-separated argv entries and redirects stdout and stderr to hosted paths. |
|
||||
| `std.proc.spawnChild(command)` | `ProcChild` | Starts a hosted child process with piped stdin, stdout, and stderr. Returns an invalid handle when the process cannot be created. |
|
||||
| `std.proc.spawnChildIn(command, cwd)` | `ProcChild` | Starts a hosted child process in a working directory with piped stdin, stdout, and stderr. Returns an invalid handle when the cwd is invalid or the process cannot be created. |
|
||||
| `std.proc.spawnChildInEnv(command, cwd, env)` | `ProcChild` | Starts a hosted child process in a working directory with piped stdin/stdout/stderr and explicit newline-separated `KEY=value` environment bindings. |
|
||||
| `std.proc.spawnChildArgs(program, args, cwd, env)` | `ProcChild` | Starts a hosted child process from a program path plus newline-separated argv entries, working directory, and newline-separated `KEY=value` environment bindings. |
|
||||
| `std.proc.childValid(child)` | `Bool` | Reports whether the handle currently names an open child slot. |
|
||||
| `std.proc.running(child)` | `Bool` | Polls the child process without blocking. |
|
||||
| `std.proc.wait(child)` | `ProcStatus` | Waits for process exit and returns its status. |
|
||||
| `std.proc.kill(child)` | `Bool` | Sends the child a termination signal on supported hosts. |
|
||||
| `std.proc.interrupt(child)` | `Bool` | Sends the child an interrupt signal on supported hosts. |
|
||||
| `std.proc.close(child)` | `Bool` | Closes the handle and any remaining pipes. |
|
||||
| `std.proc.closeStdin(child)` | `Bool` | Closes the child stdin pipe while keeping stdout, stderr, and status available. |
|
||||
| `std.proc.pid(child)` | `i32` | Returns the hosted process id for a child handle, or `0` when unavailable. |
|
||||
| `std.proc.pidRunning(pid)` | `Bool` | Reports whether a hosted process id appears to be running. |
|
||||
| `std.proc.killPid(pid)` | `Bool` | Sends a termination signal to a hosted process id on supported hosts. |
|
||||
| `std.proc.interruptPid(pid)` | `Bool` | Sends an interrupt signal to a hosted process id on supported hosts. |
|
||||
| `std.proc.killGroupPid(pid)` | `Bool` | Sends a termination signal to the hosted process group whose id is `pid` on supported hosts. |
|
||||
| `std.proc.interruptGroupPid(pid)` | `Bool` | Sends an interrupt signal to the hosted process group whose id is `pid` on supported hosts. |
|
||||
| `std.proc.readStdout(child, buffer)` | `Maybe<usize>` | Nonblocking read from the child's stdout pipe into caller storage. Returns `null` when no bytes are currently available or the stream is closed. |
|
||||
| `std.proc.readStderr(child, buffer)` | `Maybe<usize>` | Nonblocking read from the child's stderr pipe into caller storage. Returns `null` when no bytes are currently available or the stream is closed. |
|
||||
| `std.proc.writeStdin(child, bytes)` | `Maybe<usize>` | Nonblocking write to the child's stdin pipe. Returns `null` when the stream is not writable. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: proc
|
||||
- allocation behavior: child handles may keep runtime-owned pipe buffers while
|
||||
`wait` drains stdout, stderr, or PTY output
|
||||
- target support: hosted targets with the `proc` capability
|
||||
- error behavior: `spawn`, `spawnInherit`, `captureFiles`, and `wait` return `ProcStatus`; `exitCode` is infallible; `capture` and child I/O helpers return `null` when they cannot produce a complete result
|
||||
- ownership notes: `ProcChild` values name runtime-owned process slots; call `wait` when process status matters and `close` when the handle is no longer needed
|
||||
- example: `examples/std-platform.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let status: ProcStatus = std.proc.spawn("zero-noop")
|
||||
var storage: [64]u8 = [0_u8; 64]
|
||||
let captured: Maybe<usize> = std.proc.capture("printf proc-capture", storage)
|
||||
let files: ProcStatus = std.proc.captureFiles("sh -c 'printf proc-out; printf proc-err >&2'", ".zero/proc.out", ".zero/proc.err")
|
||||
if std.proc.succeeded(status) && std.proc.succeeded(files) && std.proc.runOk("sh -c true") && std.proc.runCode("sh -c true") == 0 && captured.has {
|
||||
check world.out.write("proc ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Incremental child I/O uses caller-owned buffers:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let child: ProcChild = std.proc.spawnChild("zero-noop")
|
||||
var stdoutStorage: [64]u8 = [0_u8; 64]
|
||||
let out: Maybe<usize> = std.proc.readStdout(child, stdoutStorage)
|
||||
if out.has {
|
||||
check world.out.write("child stdout available\n")
|
||||
}
|
||||
let pid: i32 = std.proc.pid(child)
|
||||
let status: ProcStatus = std.proc.wait(child)
|
||||
let closed: Bool = std.proc.close(child)
|
||||
if pid > 0 && std.proc.succeeded(status) && closed {
|
||||
check world.out.write("child ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.proc` requires a hosted target that advertises the `proc` capability.
|
||||
Targets without process support, including Windows hosts until their process
|
||||
runtime is implemented, must reject process helpers before code generation.
|
||||
|
||||
They should not compile a placeholder process implementation.
|
||||
|
||||
`capture` and `captureFiles` do not invoke a shell. They split simple
|
||||
argv-style command text and support quoted arguments. `capture` captures stdout
|
||||
only into caller storage. `captureFiles` redirects stdout and stderr to
|
||||
separate hosted paths.
|
||||
|
||||
`spawnInherit` uses the same argv-style parser and leaves stdin, stdout, and
|
||||
stderr connected to the parent process. `spawnInheritArgs` uses an explicit
|
||||
program plus newline-separated argv entries, working directory, and environment
|
||||
block. Use inherited stdio for editor, pager, and terminal program launches
|
||||
where captured pipes would be the wrong interface.
|
||||
|
||||
Child handles use the same command parser when created from command text. The
|
||||
`*Args` helpers avoid command-text parsing: `program` is argv[0], and each
|
||||
non-empty line in `args` becomes one following argv entry with spaces preserved.
|
||||
`captureArgs` captures stdout into caller storage, `captureFilesArgs` redirects
|
||||
stdout and stderr to hosted paths, and `spawnChildArgs` returns nonblocking
|
||||
pipes so event loops can poll process state and terminal input without owning
|
||||
threads. `wait` drains child output into the handle before reaping so post-wait
|
||||
`readStdout` and `readStderr` calls can still consume buffered data.
|
||||
|
||||
Hosted child helpers start children in their own process group where the target
|
||||
platform supports it. Use `killGroupPid` or `interruptGroupPid` when a stored
|
||||
pid should stop a command tree instead of only the direct parent process.
|
||||
|
||||
`spawnChildInEnv`, `spawnInheritArgs`, and `spawnChildArgs` accept a
|
||||
newline-separated env block such as `"TOKEN=...\nMODE=batch"`. Empty lines are
|
||||
ignored. Invalid entries or oversized env blocks make the helper fail: status
|
||||
helpers return an error status and child helpers return an invalid handle.
|
||||
@@ -0,0 +1,78 @@
|
||||
## When To Use std.pty
|
||||
|
||||
In Zerolang, use `std.pty` for hosted child processes that need terminal
|
||||
semantics instead of plain pipes. PTY children are useful for interactive CLIs,
|
||||
REPLs, shells, editors, pagers, and tools that change behavior when stdout is a
|
||||
terminal.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.pty.spawn(command)` | `ProcChild` | Starts an argv-style command attached to a pseudoterminal. |
|
||||
| `std.pty.spawnIn(command, cwd)` | `ProcChild` | Starts a PTY child in a hosted working directory. |
|
||||
| `std.pty.spawnInEnv(command, cwd, env)` | `ProcChild` | Starts a PTY child with a working directory and newline-separated `KEY=value` environment bindings. |
|
||||
| `std.pty.spawnArgs(program, args, cwd, env)` | `ProcChild` | Starts a program path plus newline-separated argv entries attached to a pseudoterminal. |
|
||||
| `std.pty.valid(child)` | `Bool` | Reports whether the handle currently names an open child slot. |
|
||||
| `std.pty.childValid(child)` | `Bool` | Alias for `std.pty.valid`. |
|
||||
| `std.pty.running(child)` | `Bool` | Polls the PTY child without blocking. |
|
||||
| `std.pty.wait(child)` | `ProcStatus` | Waits for process exit and returns its status. |
|
||||
| `std.pty.kill(child)` | `Bool` | Sends the child a termination signal on supported hosts. |
|
||||
| `std.pty.interrupt(child)` | `Bool` | Sends the child an interrupt signal on supported hosts. |
|
||||
| `std.pty.close(child)` | `Bool` | Closes the handle and remaining terminal resources. |
|
||||
| `std.pty.pid(child)` | `i32` | Returns the hosted process id for the child, or `0` when unavailable. |
|
||||
| `std.pty.read(child, buffer)` | `Maybe<usize>` | Nonblocking read from the PTY master into caller storage. |
|
||||
| `std.pty.write(child, bytes)` | `Maybe<usize>` | Nonblocking write to the PTY master. |
|
||||
| `std.pty.resize(child, columns, rows)` | `Bool` | Sets the child terminal size on supported hosts. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: proc
|
||||
- allocation behavior: child handles may keep runtime-owned PTY output buffers while `wait` drains terminal output
|
||||
- target support: hosted targets with the `proc` capability
|
||||
- error behavior: spawn helpers return an invalid `ProcChild` handle on failure; `read` and `write` return `null` when no bytes can be transferred
|
||||
- ownership notes: `ProcChild` values name runtime-owned process slots; call `wait` when process status matters and `close` when the handle is no longer needed
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let child: ProcChild = std.pty.spawnArgs("printf", "hello pty\n", ".", "")
|
||||
let resized: Bool = std.pty.resize(child, 80_usize, 24_usize)
|
||||
|
||||
var storage: [64]u8 = [0_u8; 64]
|
||||
var saw_output: Bool = false
|
||||
var attempts: usize = 0
|
||||
while attempts < 20_usize && !saw_output {
|
||||
let read: Maybe<usize> = std.pty.read(child, storage)
|
||||
if read.has {
|
||||
let bytes: Span<u8> = std.io.written(storage, read.value)
|
||||
saw_output = std.str.contains(bytes, "hello pty")
|
||||
}
|
||||
if !saw_output {
|
||||
let slept: Bool = std.time.sleep(std.time.ms(10))
|
||||
}
|
||||
attempts = attempts + 1_usize
|
||||
}
|
||||
|
||||
let status: ProcStatus = std.pty.wait(child)
|
||||
if resized && saw_output && std.proc.succeeded(status) {
|
||||
check world.out.write("pty ok\n")
|
||||
}
|
||||
let closed: Bool = std.pty.close(child)
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.pty` returns the same `ProcChild` handle shape used by `std.proc`, but the
|
||||
underlying child is connected to a pseudoterminal master. PTY output is a single
|
||||
terminal byte stream: stderr is merged by the terminal, and `std.pty.read`
|
||||
reads from that stream.
|
||||
Targets without process support, including Windows hosts until their process
|
||||
runtime is implemented, reject PTY helpers before code generation.
|
||||
For short-lived programs, drain the PTY with `std.pty.read` before `wait`; some
|
||||
hosts report the terminal as closed once the child exits.
|
||||
|
||||
Use `std.proc.spawnChild*` for programs where separate stdout and stderr pipes
|
||||
matter. Use `std.pty.spawn*` for programs where terminal behavior matters.
|
||||
@@ -0,0 +1,50 @@
|
||||
## When To Use std.rand
|
||||
|
||||
In Zerolang, use `std.rand` for deterministic random sources and target-gated entropy.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.rand.seed(value)` | `RandSource` | Creates a deterministic test source. |
|
||||
| `std.rand.nextU32(&mut source)` | `u32` | Advances an explicit random source. |
|
||||
| `std.rand.nextBool(&mut source)` | `Bool` | Advances an explicit random source and returns one random bit. |
|
||||
| `std.rand.nextBelow(&mut source, bound)` | `Maybe<u32>` | Returns a value in `[0, bound)` using rejection sampling, or null when bound is zero. |
|
||||
| `std.rand.rangeU32(&mut source, low, high)` | `Maybe<u32>` | Returns a value in `[low, high)` using rejection sampling, or null for an empty range. |
|
||||
| `std.rand.entropyU32()` | `u32` | Reads target entropy where the target provides it. |
|
||||
| `std.rand.entropySeed()` | `RandSource` | Creates a `RandSource` from target entropy where available. |
|
||||
| `std.rand.entropyHex32(buffer)` | `Maybe<Span<u8>>` | Writes an 8-byte lowercase entropy ID into caller storage. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: rand
|
||||
- allocation behavior: no allocation; `entropyHex32` writes caller-provided storage
|
||||
- target support: deterministic source is target-neutral; entropy requires a rand-capable target
|
||||
- error behavior: bounded helpers return null for invalid bounds; `entropyHex32` returns null when storage is too small
|
||||
- ownership notes: deterministic helpers mutate the caller-owned source
|
||||
- example: `examples/std-platform.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var rng: RandSource = std.rand.seed(7_u32)
|
||||
let first: u32 = std.rand.nextU32(&mut rng)
|
||||
let second: Bool = std.rand.nextBool(&mut rng)
|
||||
let bounded: Maybe<u32> = std.rand.nextBelow(&mut rng, 10_u32)
|
||||
let ranged: Maybe<u32> = std.rand.rangeU32(&mut rng, 40_u32, 50_u32)
|
||||
var id_buf: [8]u8 = [0_u8; 8]
|
||||
let entropy_id: Maybe<Span<u8>> = std.rand.entropyHex32(id_buf)
|
||||
if first == 1025555898_u32 && second && bounded.has && ranged.has && entropy_id.has {
|
||||
check world.out.write("rand ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
Zero keeps random sources explicit. Deterministic tests use `std.rand.seed`;
|
||||
bounded helpers use rejection sampling so the range is not modulo-biased.
|
||||
Caller-facing IDs use `entropyHex32` when target entropy is available.
|
||||
|
||||
Production entropy stays target-capability-gated.
|
||||
@@ -0,0 +1,100 @@
|
||||
## When To Use std.regex
|
||||
|
||||
In Zerolang, use `std.regex` to match text against a documented ECMA-262-leaning
|
||||
regular expression subset, such as JSON Schema `pattern` checks.
|
||||
|
||||
Supported syntax: literals, `.`, character classes with negation, ranges, and
|
||||
`\d \D \w \W \s \S`, anchors `^` `$` and word boundaries `\b` `\B`, greedy
|
||||
quantifiers `*` `+` `?` `{m}` `{m,}` `{m,n}`, alternation `|`, and capturing or
|
||||
`(?:...)` non-capturing groups (matching only; no capture extraction). Matching
|
||||
is by Unicode codepoint over UTF-8 text and searches anywhere in the text unless
|
||||
the pattern is anchored, like ECMAScript `RegExp.prototype.test`. When multiple
|
||||
matches start at the same byte, span-returning helpers use the longest end
|
||||
position, so `a|ab` finds `ab` in `ab`.
|
||||
|
||||
Unsupported constructs never misparse silently. Compilation fails with a
|
||||
structured status code: `1` backreference, `2` lookahead, `3` lookbehind,
|
||||
`4` named group, `5` lazy quantifier, `6` group modifier or inline flags,
|
||||
`7` unicode property escape, `8` invalid syntax, `9` invalid quantifier range,
|
||||
`10` program over the buffer or 2048-byte limit, `11` pattern is not valid
|
||||
UTF-8, `12` group nesting over depth 32.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.regex.compile(buffer, pattern)` | `Maybe<Span<u8>>` | Compiles a pattern into a caller-owned buffer; returns the compiled program span or `null` on any compile failure. |
|
||||
| `std.regex.compileStatus(buffer, pattern)` | `u32` | Compiles and returns `0` or the structured status code for diagnostics. |
|
||||
| `std.regex.compileErrorOffset(buffer, pattern)` | `Maybe<usize>` | Returns the pattern byte offset for a compile failure, or `null` when the pattern compiles. |
|
||||
| `std.regex.statusName(status)` | `String` | Names a status code, such as `unsupported backreference`. |
|
||||
| `std.regex.isMatch(program, text)` | `Bool` | Tests text against a compiled program. Compile once, then match many times. |
|
||||
| `std.regex.matches(pattern, text)` | `Maybe<Bool>` | One-shot compile and match with an internal 1024-byte program buffer; returns `null` when the pattern does not compile. |
|
||||
| `std.regex.contains(pattern, text)` | `Maybe<Bool>` | Alias-shaped one-shot search helper; returns `null` when the pattern does not compile. |
|
||||
| `std.regex.findIndex(pattern, text)` | `Maybe<usize>` | Returns the first matching byte index, the input length when absent, or `null` when the pattern does not compile. |
|
||||
| `std.regex.find(pattern, text)` | `Maybe<Span<u8>>` | Borrows the first matching span, or returns `null` when absent or invalid. |
|
||||
| `std.regex.findCount(pattern, text)` | `Maybe<usize>` | Counts non-overlapping matches, or returns `null` when the pattern does not compile. |
|
||||
| `std.regex.findNth(pattern, text, index)` | `Maybe<Span<u8>>` | Borrows the zero-based non-overlapping match at `index`, or returns `null` when absent or invalid. |
|
||||
| `std.regex.findNthIndex(pattern, text, index)` | `Maybe<usize>` | Returns the byte index of the zero-based non-overlapping match, the input length when absent, or `null` when invalid. |
|
||||
| `std.regex.replace(buffer, pattern, text, replacement)` | `Maybe<Span<u8>>` | Replaces non-overlapping matches with literal replacement bytes into caller storage. |
|
||||
| `std.regex.splitCount(pattern, text)` | `Maybe<usize>` | Counts fields separated by non-empty regex matches, or returns `null` when the pattern does not compile. |
|
||||
| `std.regex.split(pattern, text, index)` | `Maybe<Span<u8>>` | Borrows the zero-based field separated by non-empty regex matches, or returns `null` when absent or invalid. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var storage: [512]u8 = [0; 512]
|
||||
let buffer: MutSpan<u8> = storage
|
||||
let compiled: Maybe<Span<u8>> = std.regex.compile(buffer, "^[a-z]+-\\d{2,4}$")
|
||||
if !compiled.has {
|
||||
return
|
||||
}
|
||||
let program: Span<u8> = compiled.value
|
||||
let quick: Maybe<Bool> = std.regex.matches("^(cat|dog)s?$", "dogs")
|
||||
let first: Maybe<Span<u8>> = std.regex.find("\\d+", "build-2048")
|
||||
let second: Maybe<Span<u8>> = std.regex.findNth("\\d+", "a1 b22 c333", 1)
|
||||
var replaced_storage: [16]u8 = [0; 16]
|
||||
let replaced: Maybe<Span<u8>> = std.regex.replace(replaced_storage, "\\d+", "a1 b22", "#")
|
||||
let fields: Maybe<usize> = std.regex.splitCount("[,;]", "red,green;blue")
|
||||
let middle: Maybe<Span<u8>> = std.regex.split("[,;]", "red,green;blue", 1)
|
||||
if std.regex.isMatch(program, "build-2048") && !std.regex.isMatch(program, "build-1") && (quick.has && quick.value) && first.has && std.mem.eql(first.value, "2048") && second.has && std.mem.eql(second.value, "22") && replaced.has && std.mem.eql(replaced.value, "a# b#") && fields.has && fields.value == 3 && middle.has && std.mem.eql(middle.value, "green") {
|
||||
check world.out.write("regex ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Diagnosing a rejected pattern:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var storage: [128]u8 = [0; 128]
|
||||
let buffer: MutSpan<u8> = storage
|
||||
let status: u32 = std.regex.compileStatus(buffer, "(?=lookahead)")
|
||||
let offset: Maybe<usize> = std.regex.compileErrorOffset(buffer, "(?=lookahead)")
|
||||
if status != 0 {
|
||||
check world.out.write(std.regex.statusName(status))
|
||||
check world.out.write("\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: writes to caller-provided mutable storage for `compile`,
|
||||
`compileStatus`, `compileErrorOffset`, and `replace`; other helpers only borrow
|
||||
input spans or return scalar results.
|
||||
|
||||
Allocation behavior: `compile`, `compileStatus`, and `compileErrorOffset` write
|
||||
the caller program buffer. `replace` writes the caller output buffer. One-shot
|
||||
search, split, and match helpers use fixed internal program storage and allocate
|
||||
nothing on the heap.
|
||||
|
||||
Error behavior: `compile` returns `null`, `compileStatus` returns a status code
|
||||
naming the construct, and `compileErrorOffset` returns the byte offset for a
|
||||
failed compile. One-shot helpers return `null` for invalid patterns; `isMatch`
|
||||
returns `false` for malformed program spans or invalid UTF-8 text.
|
||||
|
||||
`find`, `findNth`, `replace`, `split`, and their index/count variants use the
|
||||
leftmost start and longest end for each match. `split` and `splitCount` use
|
||||
non-empty regex matches as separators. Zero-length matches are ignored as
|
||||
separators so callers get deterministic field traversal without a cursor object.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,104 @@
|
||||
## When To Use std.search
|
||||
|
||||
In Zerolang, use `std.search` for scalar span lookup and binary-search helpers
|
||||
over sorted caller-owned data.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.search.indexOf(items, needle)` | `usize` | Returns the first matching index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.lastIndexOf(items, needle)` | `usize` | Returns the last matching index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.lowerBoundI32(items, needle)` | `usize` | Returns the insertion point in sorted `Span<i32>` input. |
|
||||
| `std.search.upperBoundI32(items, needle)` | `usize` | Returns the insertion point after existing equal values in sorted `Span<i32>` input. |
|
||||
| `std.search.binaryI32(items, needle)` | `usize` | Returns the matching sorted `Span<i32>` index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.containsSortedI32(items, needle)` | `Bool` | Checks whether sorted `Span<i32>` input contains the needle. |
|
||||
| `std.search.countSortedI32(items, needle)` | `usize` | Counts equal values in sorted `Span<i32>` input. |
|
||||
| `std.search.equalRangeI32(items, needle)` | `Span<i32>` | Borrows the equal-value range in sorted `Span<i32>` input. |
|
||||
| `std.search.partitionPointI32(items, pivot)` | `usize` | Returns the split index where values stop being below `pivot`. |
|
||||
| `std.search.lowerBoundDescI32(items, needle)` | `usize` | Returns the insertion point before equal values in descending sorted `Span<i32>` input. |
|
||||
| `std.search.upperBoundDescI32(items, needle)` | `usize` | Returns the insertion point after equal values in descending sorted `Span<i32>` input. |
|
||||
| `std.search.binaryDescI32(items, needle)` | `usize` | Returns the matching descending sorted `Span<i32>` index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.containsSortedDescI32(items, needle)` | `Bool` | Checks whether descending sorted `Span<i32>` input contains the needle. |
|
||||
| `std.search.countSortedDescI32(items, needle)` | `usize` | Counts equal values in descending sorted `Span<i32>` input. |
|
||||
| `std.search.equalRangeDescI32(items, needle)` | `Span<i32>` | Borrows the equal-value range in descending sorted `Span<i32>` input. |
|
||||
| `std.search.partitionPointDescI32(items, pivot)` | `usize` | Returns the split index where descending values stop being above `pivot`. |
|
||||
| `std.search.minI32(items)` | `Maybe<i32>` | Returns the minimum value in `Span<i32>` input or `null` for an empty span. |
|
||||
| `std.search.maxI32(items)` | `Maybe<i32>` | Returns the maximum value in `Span<i32>` input or `null` for an empty span. |
|
||||
| `std.search.minIndexI32(items)` | `usize` | Returns the first minimum-value index in `Span<i32>` input or `std.mem.len(items)` for an empty span. |
|
||||
| `std.search.maxIndexI32(items)` | `usize` | Returns the first maximum-value index in `Span<i32>` input or `std.mem.len(items)` for an empty span. |
|
||||
| `std.search.lowerBoundU32(items, needle)` | `usize` | Returns the insertion point in sorted `Span<u32>` input. |
|
||||
| `std.search.upperBoundU32(items, needle)` | `usize` | Returns the insertion point after existing equal values in sorted `Span<u32>` input. |
|
||||
| `std.search.binaryU32(items, needle)` | `usize` | Returns the matching sorted `Span<u32>` index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.containsSortedU32(items, needle)` | `Bool` | Checks whether sorted `Span<u32>` input contains the needle. |
|
||||
| `std.search.countSortedU32(items, needle)` | `usize` | Counts equal values in sorted `Span<u32>` input. |
|
||||
| `std.search.equalRangeU32(items, needle)` | `Span<u32>` | Borrows the equal-value range in sorted `Span<u32>` input. |
|
||||
| `std.search.partitionPointU32(items, pivot)` | `usize` | Returns the split index where values stop being below `pivot`. |
|
||||
| `std.search.lowerBoundDescU32(items, needle)` | `usize` | Returns the insertion point before equal values in descending sorted `Span<u32>` input. |
|
||||
| `std.search.upperBoundDescU32(items, needle)` | `usize` | Returns the insertion point after equal values in descending sorted `Span<u32>` input. |
|
||||
| `std.search.binaryDescU32(items, needle)` | `usize` | Returns the matching descending sorted `Span<u32>` index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.containsSortedDescU32(items, needle)` | `Bool` | Checks whether descending sorted `Span<u32>` input contains the needle. |
|
||||
| `std.search.countSortedDescU32(items, needle)` | `usize` | Counts equal values in descending sorted `Span<u32>` input. |
|
||||
| `std.search.equalRangeDescU32(items, needle)` | `Span<u32>` | Borrows the equal-value range in descending sorted `Span<u32>` input. |
|
||||
| `std.search.partitionPointDescU32(items, pivot)` | `usize` | Returns the split index where descending values stop being above `pivot`. |
|
||||
| `std.search.minU32(items)` | `Maybe<u32>` | Returns the minimum value in `Span<u32>` input or `null` for an empty span. |
|
||||
| `std.search.maxU32(items)` | `Maybe<u32>` | Returns the maximum value in `Span<u32>` input or `null` for an empty span. |
|
||||
| `std.search.minIndexU32(items)` | `usize` | Returns the first minimum-value index in `Span<u32>` input or `std.mem.len(items)` for an empty span. |
|
||||
| `std.search.maxIndexU32(items)` | `usize` | Returns the first maximum-value index in `Span<u32>` input or `std.mem.len(items)` for an empty span. |
|
||||
| `std.search.lowerBoundUsize(items, needle)` | `usize` | Returns the insertion point in sorted `Span<usize>` input. |
|
||||
| `std.search.upperBoundUsize(items, needle)` | `usize` | Returns the insertion point after existing equal values in sorted `Span<usize>` input. |
|
||||
| `std.search.binaryUsize(items, needle)` | `usize` | Returns the matching sorted `Span<usize>` index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.containsSortedUsize(items, needle)` | `Bool` | Checks whether sorted `Span<usize>` input contains the needle. |
|
||||
| `std.search.countSortedUsize(items, needle)` | `usize` | Counts equal values in sorted `Span<usize>` input. |
|
||||
| `std.search.equalRangeUsize(items, needle)` | `Span<usize>` | Borrows the equal-value range in sorted `Span<usize>` input. |
|
||||
| `std.search.partitionPointUsize(items, pivot)` | `usize` | Returns the split index where values stop being below `pivot`. |
|
||||
| `std.search.lowerBoundDescUsize(items, needle)` | `usize` | Returns the insertion point before equal values in descending sorted `Span<usize>` input. |
|
||||
| `std.search.upperBoundDescUsize(items, needle)` | `usize` | Returns the insertion point after equal values in descending sorted `Span<usize>` input. |
|
||||
| `std.search.binaryDescUsize(items, needle)` | `usize` | Returns the matching descending sorted `Span<usize>` index or `std.mem.len(items)` when absent. |
|
||||
| `std.search.containsSortedDescUsize(items, needle)` | `Bool` | Checks whether descending sorted `Span<usize>` input contains the needle. |
|
||||
| `std.search.countSortedDescUsize(items, needle)` | `usize` | Counts equal values in descending sorted `Span<usize>` input. |
|
||||
| `std.search.equalRangeDescUsize(items, needle)` | `Span<usize>` | Borrows the equal-value range in descending sorted `Span<usize>` input. |
|
||||
| `std.search.partitionPointDescUsize(items, pivot)` | `usize` | Returns the split index where descending values stop being above `pivot`. |
|
||||
| `std.search.minUsize(items)` | `Maybe<usize>` | Returns the minimum value in `Span<usize>` input or `null` for an empty span. |
|
||||
| `std.search.maxUsize(items)` | `Maybe<usize>` | Returns the maximum value in `Span<usize>` input or `null` for an empty span. |
|
||||
| `std.search.minIndexUsize(items)` | `usize` | Returns the first minimum-value index in `Span<usize>` input or `std.mem.len(items)` for an empty span. |
|
||||
| `std.search.maxIndexUsize(items)` | `usize` | Returns the first maximum-value index in `Span<usize>` input or `std.mem.len(items)` for an empty span. |
|
||||
|
||||
Generic equality search supports the same non-owned scalar item types as
|
||||
`std.mem.contains`.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let values: [5]i32 = [1, 2, 3, 5, 8]
|
||||
let found: usize = std.search.binaryI32(values, 5)
|
||||
let present: Bool = std.search.containsSortedI32(values, 5)
|
||||
let after_five: usize = std.search.upperBoundI32(values, 5)
|
||||
let fives: usize = std.search.countSortedI32(values, 5)
|
||||
let five_range: Span<i32> = std.search.equalRangeI32(values, 5)
|
||||
let partition_point: usize = std.search.partitionPointI32(values, 5)
|
||||
let missing: usize = std.search.indexOf(values, 13)
|
||||
let minimum: Maybe<i32> = std.search.minI32(values)
|
||||
let maximum: Maybe<i32> = std.search.maxI32(values)
|
||||
let max_index: usize = std.search.maxIndexI32(values)
|
||||
let descending: [5]i32 = [9, 5, 5, 3, 1]
|
||||
let descending_found: usize = std.search.binaryDescI32(descending, 5)
|
||||
let descending_count: usize = std.search.countSortedDescI32(descending, 5)
|
||||
if found == 3 && present && after_five == 4 && fives == 1 && std.mem.len(five_range) == 1 && partition_point == 3 && missing == std.mem.len(values) && minimum.has && minimum.value == 1 && maximum.has && maximum.value == 8 && max_index == 4 && descending_found == 1 && descending_count == 2 {
|
||||
check world.out.write("search ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: none.
|
||||
|
||||
Allocation behavior: no allocation.
|
||||
|
||||
Error behavior: absent values return the input length; sorted-count helpers
|
||||
return `0`; min/max helpers return `null` for empty input; min/max index
|
||||
helpers return the input length for empty input.
|
||||
|
||||
Ownership: generic equality search rejects owned item elements.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,114 @@
|
||||
## When To Use std.sort
|
||||
|
||||
In Zerolang, use `std.sort` for in-place sorting and scalar ordering helpers
|
||||
over caller-owned storage.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.sort.insertionI32(items)` | `Void` | Sorts caller-owned mutable `i32` storage in ascending order. |
|
||||
| `std.sort.insertionDescI32(items)` | `Void` | Sorts caller-owned mutable `i32` storage in descending order. |
|
||||
| `std.sort.stableI32(items)` | `Void` | Stable ascending sort over caller-owned mutable `i32` storage. |
|
||||
| `std.sort.unstableI32(items)` | `Void` | Unstable ascending sort over caller-owned mutable `i32` storage. |
|
||||
| `std.sort.stableDescI32(items)` | `Void` | Stable descending sort over caller-owned mutable `i32` storage. |
|
||||
| `std.sort.unstableDescI32(items)` | `Void` | Unstable descending sort over caller-owned mutable `i32` storage. |
|
||||
| `std.sort.reverseI32(items)` | `Void` | Reverses caller-owned mutable `i32` storage in place. |
|
||||
| `std.sort.swapI32(items, left, right)` | `Bool` | Swaps two in-bounds `i32` elements and returns `false` for invalid indexes. |
|
||||
| `std.sort.rotateLeftI32(items, amount)` | `Void` | Rotates caller-owned mutable `i32` storage left by `amount` positions. |
|
||||
| `std.sort.rotateRightI32(items, amount)` | `Void` | Rotates caller-owned mutable `i32` storage right by `amount` positions. |
|
||||
| `std.sort.isSortedI32(items)` | `Bool` | Checks whether `Span<i32>` input is sorted ascending. |
|
||||
| `std.sort.isSortedDescI32(items)` | `Bool` | Checks whether `Span<i32>` input is sorted descending. |
|
||||
| `std.sort.partitionI32(items, pivot)` | `usize` | Moves values below `pivot` before the rest and returns the split index. |
|
||||
| `std.sort.partitionDescI32(items, pivot)` | `usize` | Moves values above `pivot` before the rest and returns the split index. |
|
||||
| `std.sort.isPartitionedI32(items, pivot)` | `Bool` | Checks whether all values below `pivot` are before the remaining `i32` values. |
|
||||
| `std.sort.isPartitionedDescI32(items, pivot)` | `Bool` | Checks whether all values above `pivot` are before the remaining `i32` values. |
|
||||
| `std.sort.dedupeSortedI32(items)` | `usize` | Compacts sorted mutable `i32` storage in place and returns the unique prefix length. |
|
||||
| `std.sort.selectNthI32(items, index)` | `Bool` | Partially reorders mutable `i32` storage so `items[index]` contains the nth ascending value. |
|
||||
| `std.sort.selectNthDescI32(items, index)` | `Bool` | Partially reorders mutable `i32` storage so `items[index]` contains the nth descending value. |
|
||||
| `std.sort.mergeSortedI32(dst, left, right)` | `usize` | Merges ascending sorted `i32` inputs into non-overlapping caller storage and returns the written count. |
|
||||
| `std.sort.mergeSortedDescI32(dst, left, right)` | `usize` | Merges descending sorted `i32` inputs into non-overlapping caller storage and returns the written count. |
|
||||
| `std.sort.insertionU32(items)` | `Void` | Sorts caller-owned mutable `u32` storage in ascending order. |
|
||||
| `std.sort.insertionDescU32(items)` | `Void` | Sorts caller-owned mutable `u32` storage in descending order. |
|
||||
| `std.sort.stableU32(items)` | `Void` | Stable ascending sort over caller-owned mutable `u32` storage. |
|
||||
| `std.sort.unstableU32(items)` | `Void` | Unstable ascending sort over caller-owned mutable `u32` storage. |
|
||||
| `std.sort.stableDescU32(items)` | `Void` | Stable descending sort over caller-owned mutable `u32` storage. |
|
||||
| `std.sort.unstableDescU32(items)` | `Void` | Unstable descending sort over caller-owned mutable `u32` storage. |
|
||||
| `std.sort.reverseU32(items)` | `Void` | Reverses caller-owned mutable `u32` storage in place. |
|
||||
| `std.sort.swapU32(items, left, right)` | `Bool` | Swaps two in-bounds `u32` elements and returns `false` for invalid indexes. |
|
||||
| `std.sort.rotateLeftU32(items, amount)` | `Void` | Rotates caller-owned mutable `u32` storage left by `amount` positions. |
|
||||
| `std.sort.rotateRightU32(items, amount)` | `Void` | Rotates caller-owned mutable `u32` storage right by `amount` positions. |
|
||||
| `std.sort.isSortedU32(items)` | `Bool` | Checks whether `Span<u32>` input is sorted ascending. |
|
||||
| `std.sort.isSortedDescU32(items)` | `Bool` | Checks whether `Span<u32>` input is sorted descending. |
|
||||
| `std.sort.partitionU32(items, pivot)` | `usize` | Moves values below `pivot` before the rest and returns the split index. |
|
||||
| `std.sort.partitionDescU32(items, pivot)` | `usize` | Moves values above `pivot` before the rest and returns the split index. |
|
||||
| `std.sort.isPartitionedU32(items, pivot)` | `Bool` | Checks whether all values below `pivot` are before the remaining `u32` values. |
|
||||
| `std.sort.isPartitionedDescU32(items, pivot)` | `Bool` | Checks whether all values above `pivot` are before the remaining `u32` values. |
|
||||
| `std.sort.dedupeSortedU32(items)` | `usize` | Compacts sorted mutable `u32` storage in place and returns the unique prefix length. |
|
||||
| `std.sort.selectNthU32(items, index)` | `Bool` | Partially reorders mutable `u32` storage so `items[index]` contains the nth ascending value. |
|
||||
| `std.sort.selectNthDescU32(items, index)` | `Bool` | Partially reorders mutable `u32` storage so `items[index]` contains the nth descending value. |
|
||||
| `std.sort.mergeSortedU32(dst, left, right)` | `usize` | Merges ascending sorted `u32` inputs into non-overlapping caller storage and returns the written count. |
|
||||
| `std.sort.mergeSortedDescU32(dst, left, right)` | `usize` | Merges descending sorted `u32` inputs into non-overlapping caller storage and returns the written count. |
|
||||
| `std.sort.insertionUsize(items)` | `Void` | Sorts caller-owned mutable `usize` storage in ascending order. |
|
||||
| `std.sort.insertionDescUsize(items)` | `Void` | Sorts caller-owned mutable `usize` storage in descending order. |
|
||||
| `std.sort.stableUsize(items)` | `Void` | Stable ascending sort over caller-owned mutable `usize` storage. |
|
||||
| `std.sort.unstableUsize(items)` | `Void` | Unstable ascending sort over caller-owned mutable `usize` storage. |
|
||||
| `std.sort.stableDescUsize(items)` | `Void` | Stable descending sort over caller-owned mutable `usize` storage. |
|
||||
| `std.sort.unstableDescUsize(items)` | `Void` | Unstable descending sort over caller-owned mutable `usize` storage. |
|
||||
| `std.sort.reverseUsize(items)` | `Void` | Reverses caller-owned mutable `usize` storage in place. |
|
||||
| `std.sort.swapUsize(items, left, right)` | `Bool` | Swaps two in-bounds `usize` elements and returns `false` for invalid indexes. |
|
||||
| `std.sort.rotateLeftUsize(items, amount)` | `Void` | Rotates caller-owned mutable `usize` storage left by `amount` positions. |
|
||||
| `std.sort.rotateRightUsize(items, amount)` | `Void` | Rotates caller-owned mutable `usize` storage right by `amount` positions. |
|
||||
| `std.sort.isSortedUsize(items)` | `Bool` | Checks whether `Span<usize>` input is sorted ascending. |
|
||||
| `std.sort.isSortedDescUsize(items)` | `Bool` | Checks whether `Span<usize>` input is sorted descending. |
|
||||
| `std.sort.partitionUsize(items, pivot)` | `usize` | Moves values below `pivot` before the rest and returns the split index. |
|
||||
| `std.sort.partitionDescUsize(items, pivot)` | `usize` | Moves values above `pivot` before the rest and returns the split index. |
|
||||
| `std.sort.isPartitionedUsize(items, pivot)` | `Bool` | Checks whether all values below `pivot` are before the remaining `usize` values. |
|
||||
| `std.sort.isPartitionedDescUsize(items, pivot)` | `Bool` | Checks whether all values above `pivot` are before the remaining `usize` values. |
|
||||
| `std.sort.dedupeSortedUsize(items)` | `usize` | Compacts sorted mutable `usize` storage in place and returns the unique prefix length. |
|
||||
| `std.sort.selectNthUsize(items, index)` | `Bool` | Partially reorders mutable `usize` storage so `items[index]` contains the nth ascending value. |
|
||||
| `std.sort.selectNthDescUsize(items, index)` | `Bool` | Partially reorders mutable `usize` storage so `items[index]` contains the nth descending value. |
|
||||
| `std.sort.mergeSortedUsize(dst, left, right)` | `usize` | Merges ascending sorted `usize` inputs into non-overlapping caller storage and returns the written count. |
|
||||
| `std.sort.mergeSortedDescUsize(dst, left, right)` | `usize` | Merges descending sorted `usize` inputs into non-overlapping caller storage and returns the written count. |
|
||||
|
||||
The first sort surface is deliberately small and typed. Generic comparator
|
||||
sorting should wait for stronger comparator contracts instead of smuggling an
|
||||
untyped callback convention into the standard library.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var values: [5]i32 = [5, 1, 4, 2, 3]
|
||||
std.sort.stableI32(values)
|
||||
let unique_len: usize = std.sort.dedupeSortedI32(values)
|
||||
std.sort.unstableI32(values)
|
||||
std.sort.reverseI32(values)
|
||||
let swapped: Bool = std.sort.swapI32(values, 0_usize, 4_usize)
|
||||
std.sort.rotateLeftI32(values, 2_usize)
|
||||
std.sort.rotateRightI32(values, 2_usize)
|
||||
let high_len: usize = std.sort.partitionDescI32(values, 2)
|
||||
let high_partitioned: Bool = std.sort.isPartitionedDescI32(values, 2)
|
||||
std.sort.stableDescI32(values)
|
||||
std.sort.unstableDescI32(values)
|
||||
var selected: [5]i32 = [9, 1, 4, 7, 2]
|
||||
let selected_ok: Bool = std.sort.selectNthI32(selected, 2_usize)
|
||||
let left_sorted: [2]i32 = [1, 3]
|
||||
let right_sorted: [3]i32 = [2, 4, 5]
|
||||
var merged: [5]i32 = [0, 0, 0, 0, 0]
|
||||
let merged_len: usize = std.sort.mergeSortedI32(merged, left_sorted, right_sorted)
|
||||
if std.sort.isSortedDescI32(values) && swapped && unique_len == 5 && high_len == 3 && high_partitioned && selected_ok && selected[2] == 4 && merged_len == 5 && values[0] == 5 && values[4] == 1 {
|
||||
check world.out.write("sort ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: writes to caller-provided mutable storage.
|
||||
|
||||
Allocation behavior: no allocation.
|
||||
|
||||
Error behavior: none.
|
||||
|
||||
Ownership: sort helpers are typed scalar helpers and do not move owned values.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,73 @@
|
||||
## When To Use std.str
|
||||
|
||||
In Zerolang, use `std.str` for allocation-free byte-string helpers over spans and
|
||||
caller-owned storage.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.str.copy(buffer, text)` | `Maybe<Span<u8>>` | Copies `text` into caller storage. |
|
||||
| `std.str.concat(buffer, left, right)` | `Maybe<Span<u8>>` | Writes `left` followed by `right`. |
|
||||
| `std.str.repeat(buffer, text, count)` | `Maybe<Span<u8>>` | Repeats `text` into caller storage. |
|
||||
| `std.str.replace(buffer, text, old, replacement)` | `Maybe<Span<u8>>` | Replaces non-overlapping `old` byte substrings into caller storage; empty `old` returns `null`. |
|
||||
| `std.str.reverse(buffer, text)` | `Maybe<Span<u8>>` | Writes reversed bytes into non-overlapping caller-provided storage. |
|
||||
| `std.str.countByte(text, byte)` | `usize` | Counts exact byte matches. |
|
||||
| `std.str.count(text, needle)` | `usize` | Counts non-overlapping byte substring matches; the empty needle returns `len + 1`. |
|
||||
| `std.str.splitCount(text, separator)` | `usize` | Counts byte-separator split parts; an empty separator returns `0`. |
|
||||
| `std.str.split(text, separator, index)` | `Maybe<Span<u8>>` | Borrows a zero-based split part. |
|
||||
| `std.str.fieldCountAscii(text)` | `usize` | Counts non-empty ASCII-whitespace separated fields. |
|
||||
| `std.str.fieldAscii(text, index)` | `Maybe<Span<u8>>` | Borrows a zero-based ASCII-whitespace field. |
|
||||
| `std.str.lineCount(text)` | `usize` | Counts LF-delimited lines; a trailing LF does not add a final empty line. |
|
||||
| `std.str.line(text, index)` | `Maybe<Span<u8>>` | Borrows a zero-based line and strips `\r` before `\n`. |
|
||||
| `std.str.indexOf(text, needle)` / `std.str.lastIndexOf(text, needle)` | `usize` | Returns a matching byte index or the input length when absent. |
|
||||
| `std.str.startsWith(text, prefix)` | `Bool` | Checks a byte prefix. |
|
||||
| `std.str.endsWith(text, suffix)` | `Bool` | Checks a byte suffix. |
|
||||
| `std.str.contains(text, needle)` | `Bool` | Checks for a byte substring; the empty needle is present. |
|
||||
| `std.str.trimAscii(text)` | `Span<u8>` | Borrows `text` without leading or trailing ASCII space bytes. |
|
||||
| `std.str.trimStartAscii(text)` / `std.str.trimEndAscii(text)` | `Span<u8>` | Borrows one-sided trimmed views. |
|
||||
| `std.str.toLowerAscii(buffer, text)` / `std.str.toUpperAscii(buffer, text)` | `Maybe<Span<u8>>` | Writes ASCII case-converted bytes into caller storage. |
|
||||
| `std.str.eqlIgnoreAsciiCase(left, right)` | `Bool` | Compares ASCII case-insensitively. |
|
||||
| `std.str.wordCountAscii(text)` | `usize` | Counts non-empty runs separated by ASCII space bytes. |
|
||||
|
||||
Current scope:
|
||||
|
||||
- Helpers operate on byte spans and ASCII delimiter rules for space, tab, line feed, and carriage return.
|
||||
- `reverse`, `repeat`, `replace`, `copy`, and `concat` write into caller storage and return `null` when the buffer is too small. The destination buffer must not overlap the input.
|
||||
- The module does not implement Unicode case mapping, grapheme segmentation, or locale-aware text rules.
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
var storage: [6]u8 = [0_u8; 6]
|
||||
let reversed: Maybe<Span<u8>> = std.str.reverse(storage, "drawer")
|
||||
var repeated_storage: [8]u8 = [0_u8; 8]
|
||||
let repeated: Maybe<Span<u8>> = std.str.repeat(repeated_storage, "ha", 3)
|
||||
var lower_storage: [4]u8 = [0_u8; 4]
|
||||
let lower: Maybe<Span<u8>> = std.str.toLowerAscii(lower_storage, "ZERO")
|
||||
let field: Maybe<Span<u8>> = std.str.fieldAscii("zero text", 1)
|
||||
if reversed.has && repeated.has && (lower.has && field.has) {
|
||||
if std.mem.eql(reversed.value, "reward") && std.mem.eql(repeated.value, "hahaha") && (std.mem.eql(lower.value, "zero") && std.mem.eql(field.value, "text")) {
|
||||
check world.out.write("string helper ok\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.str` is allocation-free. Functions that create new byte sequences use
|
||||
caller-provided storage, and functions that return spans borrow from an input or
|
||||
that caller-provided storage.
|
||||
|
||||
`reverse` is a copy helper, not an in-place reversal primitive. Pass separate
|
||||
destination storage when the source text comes from mutable bytes.
|
||||
|
||||
String literals can be passed directly to these helpers; fixed arrays and
|
||||
mutable buffers can be passed as spans when the caller needs non-literal input.
|
||||
|
||||
The current helpers are byte-string primitives. They are suitable for protocol
|
||||
tokens, Rosetta-style ASCII examples, and fixed-buffer tools. Unicode text
|
||||
algorithms should be added as explicit APIs with documented behavior instead of
|
||||
being implied by these byte-span helpers.
|
||||
@@ -0,0 +1,162 @@
|
||||
## When To Use std.term
|
||||
|
||||
In Zerolang, use `std.term` when terminal code needs ANSI output sequences,
|
||||
hosted terminal metadata, nonblocking input reads, or key decoding for bytes
|
||||
already read from input.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| Helper | Returns | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.term.reset()` | `String` | ANSI SGR reset. |
|
||||
| `std.term.bold()` | `String` | ANSI SGR bold style. |
|
||||
| `std.term.dim()` | `String` | ANSI SGR dim style. |
|
||||
| `std.term.underline()` | `String` | ANSI SGR underline style. |
|
||||
| `std.term.inverse()` | `String` | ANSI SGR inverse style. |
|
||||
| `std.term.fgDefault()` | `String` | Reset foreground color. |
|
||||
| `std.term.fgBlack()` | `String` | Black foreground color. |
|
||||
| `std.term.fgRed()` | `String` | Red foreground color. |
|
||||
| `std.term.fgGreen()` | `String` | Green foreground color. |
|
||||
| `std.term.fgYellow()` | `String` | Yellow foreground color. |
|
||||
| `std.term.fgBlue()` | `String` | Blue foreground color. |
|
||||
| `std.term.fgMagenta()` | `String` | Magenta foreground color. |
|
||||
| `std.term.fgCyan()` | `String` | Cyan foreground color. |
|
||||
| `std.term.fgWhite()` | `String` | White foreground color. |
|
||||
| `std.term.bgDefault()` | `String` | Reset background color. |
|
||||
| `std.term.bgBlack()` | `String` | Black background color. |
|
||||
| `std.term.bgRed()` | `String` | Red background color. |
|
||||
| `std.term.bgGreen()` | `String` | Green background color. |
|
||||
| `std.term.bgYellow()` | `String` | Yellow background color. |
|
||||
| `std.term.bgBlue()` | `String` | Blue background color. |
|
||||
| `std.term.bgMagenta()` | `String` | Magenta background color. |
|
||||
| `std.term.bgCyan()` | `String` | Cyan background color. |
|
||||
| `std.term.bgWhite()` | `String` | White background color. |
|
||||
| `std.term.clearScreen()` | `String` | Clear the full terminal screen. |
|
||||
| `std.term.clearScreenDown()` | `String` | Clear from the cursor through the end of the screen. |
|
||||
| `std.term.clearScreenUp()` | `String` | Clear from the cursor through the start of the screen. |
|
||||
| `std.term.clearLine()` | `String` | Clear the current terminal line. |
|
||||
| `std.term.clearLineRight()` | `String` | Clear from the cursor through the end of the line. |
|
||||
| `std.term.clearLineLeft()` | `String` | Clear from the cursor through the start of the line. |
|
||||
| `std.term.cursorHome()` | `String` | Move the cursor to row 1, column 1. |
|
||||
| `std.term.cursorTo(buffer, row, column)` | `Maybe<Span<u8>>` | Writes a 1-based ANSI cursor-position sequence into caller storage. |
|
||||
| `std.term.cursorUp(buffer, count)` | `Maybe<Span<u8>>` | Writes an ANSI cursor-up sequence into caller storage; count `0` writes an empty span. |
|
||||
| `std.term.cursorDown(buffer, count)` | `Maybe<Span<u8>>` | Writes an ANSI cursor-down sequence into caller storage; count `0` writes an empty span. |
|
||||
| `std.term.cursorRight(buffer, count)` | `Maybe<Span<u8>>` | Writes an ANSI cursor-right sequence into caller storage; count `0` writes an empty span. |
|
||||
| `std.term.cursorLeft(buffer, count)` | `Maybe<Span<u8>>` | Writes an ANSI cursor-left sequence into caller storage; count `0` writes an empty span. |
|
||||
| `std.term.saveCursor()` | `String` | Save the current cursor position. |
|
||||
| `std.term.restoreCursor()` | `String` | Restore the saved cursor position. |
|
||||
| `std.term.hideCursor()` | `String` | Hide the terminal cursor. |
|
||||
| `std.term.showCursor()` | `String` | Show the terminal cursor. |
|
||||
| `std.term.enterAltScreen()` | `String` | Enter the alternate screen buffer. |
|
||||
| `std.term.leaveAltScreen()` | `String` | Leave the alternate screen buffer. |
|
||||
| `std.term.enterBracketedPaste()` | `String` | Enable bracketed paste markers in supporting terminals. |
|
||||
| `std.term.leaveBracketedPaste()` | `String` | Disable bracketed paste markers in supporting terminals. |
|
||||
| `std.term.enterMouseCapture()` | `String` | Enable SGR mouse tracking and drag/wheel capture in supporting terminals. |
|
||||
| `std.term.leaveMouseCapture()` | `String` | Disable the SGR mouse tracking modes enabled by `enterMouseCapture`. |
|
||||
| `std.term.keyNone()` | `u32` | Sentinel returned for incomplete or unsupported key bytes. |
|
||||
| `std.term.keyEscape()` | `u32` | Escape key code. |
|
||||
| `std.term.keyEnter()` | `u32` | Enter key code for `\r` or `\n`. |
|
||||
| `std.term.keyTab()` | `u32` | Tab key code. |
|
||||
| `std.term.keyBackspace()` | `u32` | Backspace key code for `0x7f` or `0x08`. |
|
||||
| `std.term.keyCtrlA()` | `u32` | Ctrl-A key code. |
|
||||
| `std.term.keyCtrlC()` | `u32` | Ctrl-C key code. |
|
||||
| `std.term.keyCtrlD()` | `u32` | Ctrl-D key code. |
|
||||
| `std.term.keyCtrlE()` | `u32` | Ctrl-E key code. |
|
||||
| `std.term.keyCtrlK()` | `u32` | Ctrl-K key code. |
|
||||
| `std.term.keyCtrlL()` | `u32` | Ctrl-L key code. |
|
||||
| `std.term.keyCtrlN()` | `u32` | Ctrl-N key code. |
|
||||
| `std.term.keyCtrlP()` | `u32` | Ctrl-P key code. |
|
||||
| `std.term.keyCtrlR()` | `u32` | Ctrl-R key code. |
|
||||
| `std.term.keyCtrlU()` | `u32` | Ctrl-U key code. |
|
||||
| `std.term.keyCtrlW()` | `u32` | Ctrl-W key code. |
|
||||
| `std.term.keyArrowUp()` | `u32` | Up-arrow key code above the Unicode scalar range. |
|
||||
| `std.term.keyArrowDown()` | `u32` | Down-arrow key code above the Unicode scalar range. |
|
||||
| `std.term.keyArrowRight()` | `u32` | Right-arrow key code above the Unicode scalar range. |
|
||||
| `std.term.keyArrowLeft()` | `u32` | Left-arrow key code above the Unicode scalar range. |
|
||||
| `std.term.keyDelete()` | `u32` | Delete key code above the Unicode scalar range. |
|
||||
| `std.term.keyHome()` | `u32` | Home key code above the Unicode scalar range. |
|
||||
| `std.term.keyEnd()` | `u32` | End key code above the Unicode scalar range. |
|
||||
| `std.term.keyPageUp()` | `u32` | Page Up key code above the Unicode scalar range. |
|
||||
| `std.term.keyPageDown()` | `u32` | Page Down key code above the Unicode scalar range. |
|
||||
| `std.term.keyInsert()` | `u32` | Insert key code above the Unicode scalar range. |
|
||||
| `std.term.keyShiftTab()` | `u32` | Shift-Tab key code above the Unicode scalar range. |
|
||||
| `std.term.keyF1()` | `u32` | F1 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF2()` | `u32` | F2 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF3()` | `u32` | F3 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF4()` | `u32` | F4 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF5()` | `u32` | F5 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF6()` | `u32` | F6 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF7()` | `u32` | F7 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF8()` | `u32` | F8 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF9()` | `u32` | F9 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF10()` | `u32` | F10 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF11()` | `u32` | F11 key code above the Unicode scalar range. |
|
||||
| `std.term.keyF12()` | `u32` | F12 key code above the Unicode scalar range. |
|
||||
| `std.term.keyPasteStart()` | `u32` | Bracketed paste start marker code above the Unicode scalar range. |
|
||||
| `std.term.keyPasteEnd()` | `u32` | Bracketed paste end marker code above the Unicode scalar range. |
|
||||
| `std.term.keyCode(bytes)` | `u32` | Decodes one key from caller-provided bytes, returning Unicode scalar values for printable UTF-8 and named constants for control keys. |
|
||||
| `std.term.keyByteLen(bytes)` | `usize` | Returns the decoded key width in bytes, or `0` for incomplete or unsupported input. |
|
||||
| `std.term.stdinIsTty()` | `Bool` | Reports whether standard input is attached to a terminal. |
|
||||
| `std.term.stdoutIsTty()` | `Bool` | Reports whether standard output is attached to a terminal. |
|
||||
| `std.term.widthOr(fallback)` | `usize` | Returns terminal columns, or `fallback` when unavailable. |
|
||||
| `std.term.heightOr(fallback)` | `usize` | Returns terminal rows, or `fallback` when unavailable. |
|
||||
| `std.term.enterRawMode()` | `Bool` | Puts standard input into raw, nonblocking terminal mode when supported. |
|
||||
| `std.term.leaveRawMode()` | `Bool` | Restores the terminal mode saved by `enterRawMode()`. |
|
||||
| `std.term.readInput(buffer)` | `Maybe<usize>` | Fills the caller buffer with currently available stdin bytes without blocking; returns `null` when no bytes are available or the input source is unsupported. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: ANSI/key helpers are pure; TTY/size helpers read hosted terminal metadata; raw-mode helpers update the hosted terminal; `readInput` reads from hosted stdin
|
||||
- allocation behavior: no allocation
|
||||
- target support: ANSI/key helpers are target-neutral; TTY/size/raw-mode/input helpers require hosted runtime support
|
||||
- error behavior: ANSI/key helpers are infallible; hosted helpers return fallbacks, `false`, or `null` when unavailable
|
||||
- ownership notes: ANSI sequences are borrowed static byte views
|
||||
- example: `conformance/native/pass/std-term-ansi.graph`
|
||||
|
||||
Example:
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
check world.out.write(std.term.enterAltScreen())
|
||||
check world.out.write(std.term.enterMouseCapture())
|
||||
check world.out.write(std.term.clearScreen())
|
||||
check world.out.write(std.term.cursorHome())
|
||||
var cursor: [24]u8 = [0_u8; 24]
|
||||
let top: Maybe<Span<u8>> = std.term.cursorTo(cursor, 1_usize, 1_usize)
|
||||
if top.has {
|
||||
check world.out.write(top.value)
|
||||
}
|
||||
check world.out.write(std.term.bold())
|
||||
check world.out.write(std.term.fgCyan())
|
||||
let width: usize = std.term.widthOr(80_usize)
|
||||
let height: usize = std.term.heightOr(24_usize)
|
||||
let raw: Bool = std.term.enterRawMode()
|
||||
var input: [16]u8 = [0_u8; 16]
|
||||
let pending: Maybe<usize> = std.term.readInput(input)
|
||||
if pending.has {
|
||||
let bytes: Span<u8> = std.mem.prefix(input, pending.value)
|
||||
let key: u32 = std.term.keyCode(bytes)
|
||||
if key == std.term.keyCtrlC() {
|
||||
check world.out.write("cancel")
|
||||
}
|
||||
}
|
||||
check world.out.write("ready")
|
||||
if raw {
|
||||
let restored: Bool = std.term.leaveRawMode()
|
||||
if !restored {
|
||||
return
|
||||
}
|
||||
}
|
||||
check world.out.write(std.term.reset())
|
||||
check world.out.write(std.term.leaveMouseCapture())
|
||||
check world.out.write(std.term.leaveAltScreen())
|
||||
}
|
||||
```
|
||||
|
||||
Key decoding is target-neutral: it parses bytes the caller already has. TTY and
|
||||
size helpers are hosted metadata calls and return caller fallbacks when a
|
||||
terminal size is unavailable. Raw mode is a hosted terminal capability: call
|
||||
`leaveRawMode()` before returning to normal line-oriented terminal input.
|
||||
`readInput()` is nonblocking; in raw mode it can be polled by interactive
|
||||
programs, and on noninteractive stdin it returns available piped bytes when the
|
||||
host exposes them.
|
||||
@@ -0,0 +1,59 @@
|
||||
## When To Use std.testing
|
||||
|
||||
In Zerolang, use `std.testing` inside test blocks for output checks and small boolean
|
||||
assertion helpers.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.testing.isTrue(value)` | `Bool` | Passes through a `Bool` for readable `expect` statements. |
|
||||
| `std.testing.isFalse(value)` | `Bool` | Returns true when the value is false. |
|
||||
| `std.testing.equalBool(actual, expected)` | `Bool` | Compares booleans explicitly. |
|
||||
| `std.testing.equalUsize(actual, expected)` | `Bool` | Compares `usize` values explicitly. |
|
||||
| `std.testing.equalU32(actual, expected)` | `Bool` | Compares `u32` values explicitly. |
|
||||
| `std.testing.equalI32(actual, expected)` | `Bool` | Compares `i32` values explicitly. |
|
||||
| `std.testing.equalBytes(actual, expected)` | `Bool` | Compares byte spans by value. |
|
||||
| `std.testing.containsBytes(actual, needle)` | `Bool` | Checks whether a byte span contains a byte substring. |
|
||||
| `std.testing.startsWith(actual, prefix)` | `Bool` | Checks a byte prefix. |
|
||||
| `std.testing.endsWith(actual, suffix)` | `Bool` | Checks a byte suffix. |
|
||||
| `std.testing.notEqualBytes(actual, expected)` | `Bool` | Checks byte-span inequality. |
|
||||
| `std.testing.diffIndexBytes(actual, expected)` | `Maybe<usize>` | Returns the first differing byte index, or `null` when spans are equal. |
|
||||
| `std.testing.jsonFieldEquals(actual, key, expected)` | `Bool` | Compares a raw top-level JSON field value. |
|
||||
| `std.testing.jsonPathEquals(actual, path, expected)` | `Bool` | Compares a raw dotted JSON path value. |
|
||||
| `std.testing.caseName(buffer, suite, index)` | `Maybe<Span<u8>>` | Writes a stable table-case name like `suite[3]` into caller storage. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: none for scalar comparisons; memory for byte-span/case-name checks; parse for JSON checks
|
||||
- allocation behavior: no allocation
|
||||
- target support: target-neutral
|
||||
- error behavior: infallible
|
||||
- ownership notes: no ownership transfer
|
||||
- example: `examples/std-testing-log.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
test "testing helpers support direct test blocks" {
|
||||
let diff: Maybe<usize> = std.testing.diffIndexBytes("zero", "zeta")
|
||||
expect std.testing.equalU32(42_u32, 42_u32)
|
||||
expect std.testing.equalBytes("zero", "zero")
|
||||
expect std.testing.containsBytes("zerolang", "lang")
|
||||
expect diff.has && diff.value == 2
|
||||
expect std.testing.jsonPathEquals("{\"user\":{\"name\":\"zero\"}}", "user.name", "\"zero\"")
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
`std.testing` helpers return `Bool`; they do not register tests, hide failures,
|
||||
allocate output, or produce process I/O. Use them inside ordinary `expect`
|
||||
statements so the compiler and `zero test` keep one visible test model.
|
||||
|
||||
The byte helpers are byte-span predicates. They are suitable for output checks,
|
||||
protocol fixtures, and small examples where a full parser would be more complex
|
||||
than the assertion.
|
||||
|
||||
JSON helpers compare raw JSON values, so string expectations include their JSON
|
||||
quotes, such as `"\"zero\""`.
|
||||
@@ -0,0 +1,36 @@
|
||||
## When To Use std.text
|
||||
|
||||
In Zerolang, use `std.text` for ASCII and UTF-8 byte-backed validation.
|
||||
|
||||
Runnable today:
|
||||
|
||||
`std.text` is for byte-backed text validation and counting. It does not imply
|
||||
locale-aware case mapping, grapheme segmentation, normalization, or display-width
|
||||
rules.
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.text.isAscii(text)` | `Bool` | Checks that every byte is below `0x80`. |
|
||||
| `std.text.utf8Valid(text)` | `Bool` | Validates UTF-8 byte structure, rejecting overlong encodings, surrogate code points, and values above `U+10FFFF`. |
|
||||
| `std.text.utf8Len(text)` | `Maybe<usize>` | Counts Unicode scalar values when UTF-8 is valid; returns `null` on invalid input. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let valid: [2]u8 = [195_u8, 169_u8]
|
||||
let invalid: [1]u8 = [128_u8]
|
||||
let len: Maybe<usize> = std.text.utf8Len(valid)
|
||||
if !std.text.isAscii(valid) && std.text.utf8Valid(valid) && !std.text.utf8Valid(invalid) && len.has && len.value == 1 {
|
||||
check world.out.write("text ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: none.
|
||||
|
||||
Allocation behavior: no allocation.
|
||||
|
||||
Error behavior: `utf8Len` returns `null` for invalid UTF-8.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,102 @@
|
||||
## When To Use std.time
|
||||
|
||||
In Zerolang, use `std.time` for duration math, RFC 3339 date and time validation
|
||||
and parsing, and target-gated monotonic or wall-clock helpers.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.time.ns(value)` | `Duration` | Builds a nanosecond duration. |
|
||||
| `std.time.us(value)` | `Duration` | Builds a microsecond duration. |
|
||||
| `std.time.ms(value)` | `Duration` | Builds a millisecond duration. |
|
||||
| `std.time.seconds(value)` | `Duration` | Builds a second duration. |
|
||||
| `std.time.minutes(value)` | `Duration` | Builds a minute duration. |
|
||||
| `std.time.hours(value)` | `Duration` | Builds an hour duration. |
|
||||
| `std.time.zero()` | `Duration` | Returns a zero duration. |
|
||||
| `std.time.add(a, b)` | `Duration` | Adds two durations. |
|
||||
| `std.time.sub(a, b)` | `Duration` | Subtracts one duration from another. |
|
||||
| `std.time.min(a, b)` | `Duration` | Returns the smaller duration. |
|
||||
| `std.time.max(a, b)` | `Duration` | Returns the larger duration. |
|
||||
| `std.time.clamp(value, low, high)` | `Duration` | Clamps a duration between normalized bounds. |
|
||||
| `std.time.abs(value)` | `Duration` | Returns a non-negative duration magnitude. |
|
||||
| `std.time.between(start, end)` | `Duration` | Returns the non-negative duration between two values. |
|
||||
| `std.time.hasElapsed(start, now, timeout)` | `Bool` | Reports whether a timeout window has elapsed. |
|
||||
| `std.time.deadlineAfter(start, timeout)` | `Duration` | Builds a deadline by adding a timeout to a start instant. |
|
||||
| `std.time.remainingUntil(deadline, now)` | `Duration` | Returns remaining time or zero once the deadline has passed. |
|
||||
| `std.time.deadlineExpired(deadline, now)` | `Bool` | Reports whether `now` is at or past `deadline`. |
|
||||
| `std.time.sleep(duration)` | `Bool` | Sleeps for a hosted non-negative duration; returns `false` on host failure. |
|
||||
| `std.time.asNs(value)` | `i64` | Converts to nanoseconds. |
|
||||
| `std.time.asUsFloor(value)` | `i64` | Converts to whole microseconds. |
|
||||
| `std.time.asMsFloor(value)` | `i32` | Converts to whole milliseconds. |
|
||||
| `std.time.asSecondsFloor(value)` | `i64` | Converts to whole seconds. |
|
||||
| `std.time.lessThan(a, b)` | `Bool` | Compares two durations. |
|
||||
| `std.time.isZero(value)` | `Bool` | Reports whether a duration is zero. |
|
||||
| `std.time.monotonic()` | `Duration` | Reads a monotonic target clock where available. |
|
||||
| `std.time.wallSeconds()` | `i64` | Reads target wall-clock seconds where available. |
|
||||
| `std.time.isRfc3339Date(text)` | `Bool` | Validates an RFC 3339 full-date with leap years and days-in-month. |
|
||||
| `std.time.isRfc3339Time(text)` | `Bool` | Validates an RFC 3339 full-time with fractional seconds, numeric offsets, and the leap-second rule. |
|
||||
| `std.time.isRfc3339DateTime(text)` | `Bool` | Validates an RFC 3339 date-time joined by `T` or `t`. |
|
||||
| `std.time.parseRfc3339DateTimeOr(text, fallback)` | `i64` | Parses a date-time into UTC epoch seconds; returns the fallback when invalid. Fractional seconds truncate; a valid leap second maps to the same epoch second as `:59`. |
|
||||
| `std.time.isLeapYear(year)` | `Bool` | Gregorian leap-year predicate. |
|
||||
| `std.time.daysInMonth(year, month)` | `u32` | Days in a month; returns `0` for invalid months. |
|
||||
| `std.time.writeDurationNs(buffer, value)` | `Maybe<Span<u8>>` | Writes nanoseconds with an `ns` suffix into caller storage. |
|
||||
| `std.time.writeDurationMs(buffer, value)` | `Maybe<Span<u8>>` | Writes whole milliseconds with an `ms` suffix into caller storage. |
|
||||
| `std.time.writeDurationSeconds(buffer, value)` | `Maybe<Span<u8>>` | Writes whole seconds with an `s` suffix into caller storage. |
|
||||
|
||||
Current limits:
|
||||
|
||||
- Target-specific clock availability diagnostics.
|
||||
- Timer handles and fake-clock handles are not public APIs.
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: time
|
||||
- allocation behavior: no allocation
|
||||
- target support: duration math is target-neutral; clock reads and sleep require a time-capable target
|
||||
- error behavior: infallible helpers; RFC 3339 validators return `Bool` and the epoch parser returns its fallback for invalid text
|
||||
- ownership notes: no ownership transfer
|
||||
- example: `examples/std-platform.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let a: Duration = std.time.ms(250)
|
||||
let b: Duration = std.time.seconds(1)
|
||||
let total: Duration = std.time.add(a, b)
|
||||
let span: Duration = std.time.between(std.time.seconds(2), std.time.ms(250))
|
||||
let deadline: Duration = std.time.deadlineAfter(std.time.seconds(10), std.time.ms(500))
|
||||
let remaining: Duration = std.time.remainingUntil(deadline, std.time.seconds(10))
|
||||
let slept: Bool = std.time.sleep(std.time.zero())
|
||||
var text_storage: [32]u8 = [0_u8; 32]
|
||||
let text: Maybe<Span<u8>> = std.time.writeDurationMs(text_storage, total)
|
||||
if slept && std.time.asMsFloor(total) == 1250 && std.time.asMsFloor(span) == 1750 && (std.time.asMsFloor(remaining) == 500 && text.has) {
|
||||
check world.out.write("duration ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
RFC 3339 validation includes the exact leap-second rule: `seconds == 60` is
|
||||
valid only when the time normalized by its numeric offset equals `23:59:60`
|
||||
UTC, wrapping modulo 24 hours. `00:29:60+00:30` is valid because it normalizes
|
||||
to `23:59:60` UTC on the previous day, while `23:59:60-01:00` is invalid
|
||||
because it normalizes to `00:59:60` UTC.
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let wrapped: Bool = std.time.isRfc3339Time("00:29:60+00:30")
|
||||
let not_leap: Bool = std.time.isRfc3339Time("23:59:60-01:00")
|
||||
let epoch: i64 = std.time.parseRfc3339DateTimeOr("2000-01-01T00:00:00Z", -1)
|
||||
if wrapped && !not_leap && epoch == 946684800 && std.time.daysInMonth(2024, 2) == 29 {
|
||||
check world.out.write("rfc3339 ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
Time is an effect when it observes or waits on the outside world.
|
||||
|
||||
Pure duration math can stay allocation-free and target-independent.
|
||||
Timer and fake-clock APIs are not exposed in the current public surface.
|
||||
@@ -0,0 +1,67 @@
|
||||
## When To Use std.toml
|
||||
|
||||
In Zerolang, use `std.toml` for TOML validation, shallow field lookup, and typed scalar
|
||||
decode helpers.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.toml.validate(text)` | `Bool` | Checks the current TOML subset without allocation. |
|
||||
| `std.toml.validateBytes(bytes)` | `Bool` | Checks a `Span<u8>` TOML payload without allocation. |
|
||||
| `std.toml.field(bytes, key)` | `Maybe<Span<u8>>` | Returns the raw value for a direct, dotted, or shallow table field. |
|
||||
| `std.toml.stringDecode(buffer, value)` | `Maybe<Span<u8>>` | Decodes a TOML string value into caller storage. |
|
||||
| `std.toml.string(buffer, bytes, key)` | `Maybe<Span<u8>>` | Looks up and decodes a TOML string field. |
|
||||
| `std.toml.u32(bytes, key)` | `Maybe<u32>` | Looks up and decodes an unsigned integer field. |
|
||||
| `std.toml.i32(bytes, key)` | `Maybe<i32>` | Looks up and decodes a signed integer field. |
|
||||
| `std.toml.bool(bytes, key)` | `Maybe<Bool>` | Looks up and decodes a boolean field. |
|
||||
| `std.toml.arrayCount(value)` | `Maybe<usize>` | Counts items in a raw array value. |
|
||||
| `std.toml.arrayValue(value, index)` | `Maybe<Span<u8>>` | Borrows a raw array item by ordinal. |
|
||||
| `std.toml.arrayString(buffer, value, index)` | `Maybe<Span<u8>>` | Decodes a string array item into caller storage. |
|
||||
| `std.toml.arrayU32(value, index)` | `Maybe<u32>` | Decodes an unsigned integer array item. |
|
||||
| `std.toml.arrayI32(value, index)` | `Maybe<i32>` | Decodes a signed integer array item. |
|
||||
| `std.toml.arrayBool(value, index)` | `Maybe<Bool>` | Decodes a boolean array item. |
|
||||
| `std.toml.writeKeyValueString(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one string key/value line. |
|
||||
| `std.toml.writeKeyValueU32(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one unsigned integer key/value line. |
|
||||
| `std.toml.writeKeyValueBool(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one boolean key/value line. |
|
||||
| `std.toml.writeTableHeader(buffer, table)` | `Maybe<Span<u8>>` | Writes one table header line. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: parse
|
||||
- allocation behavior: allocation-free; decoded strings and writer output use caller storage
|
||||
- target support: target-neutral
|
||||
- error behavior: `Maybe` helpers return null on malformed or missing fields
|
||||
- ownership notes: returned raw fields borrow from the input span; decoded strings borrow from the caller buffer
|
||||
- examples: `conformance/native/pass/std-toml-basic.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let input: Span<u8> = "[package]\nname = \"demo\"\n\n[features]\ngraph = true\nlevels = [1, 2, 3]\n"
|
||||
var name_buffer: [16]u8 = [0_u8; 16]
|
||||
let name: Maybe<Span<u8>> = std.toml.string(name_buffer, input, "package.name")
|
||||
let graph: Maybe<Bool> = std.toml.bool(input, "features.graph")
|
||||
let levels: Maybe<Span<u8>> = std.toml.field(input, "features.levels")
|
||||
var count: Maybe<usize> = null
|
||||
if levels.has {
|
||||
count = std.toml.arrayCount(levels.value)
|
||||
}
|
||||
if std.toml.validateBytes(input) && name.has && graph.has && graph.value && count.has && count.value == 3 {
|
||||
check world.out.write("toml ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
The current TOML helper surface is deliberately narrow. It supports the package
|
||||
manifest subset used by Zero packages: tables, dotted keys, strings, booleans,
|
||||
integers, scalar arrays, and small writer helpers. Field lookup is shallow and table-aware, so
|
||||
`std.toml.string(buffer, input, "package.name")` can read `name` inside a
|
||||
`[package]` table.
|
||||
|
||||
The helpers avoid hidden allocation. Use `field` when a raw value slice is
|
||||
enough, and use `string` or `stringDecode` when escape decoding into explicit
|
||||
caller storage is required.
|
||||
@@ -0,0 +1,60 @@
|
||||
## When To Use std.unicode
|
||||
|
||||
In Zerolang, use `std.unicode` for UTF-8 codepoint decode/encode iteration and
|
||||
codepoint-class checks. For whole-span validation and codepoint counting, use
|
||||
the existing `std.text.utf8Valid` and `std.text.utf8Len` helpers; `std.unicode`
|
||||
extends them with per-codepoint access.
|
||||
|
||||
Decoding is strict UTF-8: overlong encodings, surrogate codepoints, values
|
||||
above `U+10FFFF`, and truncated sequences all return `null`.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.unicode.decodeAt(text, index)` | `Maybe<u32>` | Decodes the codepoint starting at a byte index; `null` for invalid or out-of-range positions. |
|
||||
| `std.unicode.widthAt(text, index)` | `Maybe<usize>` | Byte width of the sequence at a byte index; advance `index` by this to iterate codepoints. |
|
||||
| `std.unicode.nextIndex(text, index)` | `Maybe<usize>` | Next byte index after the codepoint at `index`; `null` for invalid input. |
|
||||
| `std.unicode.invalidIndex(text)` | `usize` | First invalid UTF-8 byte index, or the input length when valid. |
|
||||
| `std.unicode.decodeStatusAt(text, index)` | `u32` | Strict UTF-8 status code at a byte index. |
|
||||
| `std.unicode.statusName(status)` | `String` | Names a status code such as `truncated sequence`. |
|
||||
| `std.unicode.encode(buffer, cp)` | `Maybe<Span<u8>>` | Encodes a codepoint as UTF-8 into a caller buffer; `null` for surrogates, values above `U+10FFFF`, or a too-small buffer. |
|
||||
| `std.unicode.encodedWidth(cp)` | `Maybe<usize>` | UTF-8 byte width a codepoint needs (1-4); `null` for invalid codepoints. |
|
||||
| `std.unicode.isDigit(cp)` | `Bool` | ASCII digit class, matching regex `\d` semantics by codepoint. |
|
||||
| `std.unicode.isWord(cp)` | `Bool` | ASCII word class `[A-Za-z0-9_]`, matching regex `\w` semantics. |
|
||||
| `std.unicode.isSpace(cp)` | `Bool` | ECMA-262 whitespace plus line terminators, matching regex `\s` semantics. |
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let text: Span<u8> = std.mem.span("aé💯")
|
||||
var index: usize = 0
|
||||
var count: usize = 0
|
||||
while index < std.mem.len(text) {
|
||||
let next: Maybe<usize> = std.unicode.nextIndex(text, index)
|
||||
if !next.has {
|
||||
return
|
||||
}
|
||||
index = next.value
|
||||
count = count + 1
|
||||
}
|
||||
var storage: [4]u8 = [0; 4]
|
||||
let buffer: MutSpan<u8> = storage
|
||||
let encoded: Maybe<Span<u8>> = std.unicode.encode(buffer, 233)
|
||||
if count == 3 && std.unicode.invalidIndex(text) == std.mem.len(text) && (encoded.has && std.mem.len(encoded.value) == 2) {
|
||||
check world.out.write("unicode ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Effects: none.
|
||||
|
||||
Allocation behavior: `encode` writes the caller buffer; all other helpers
|
||||
allocate nothing.
|
||||
|
||||
Error behavior: decode/encode and cursor helpers return `null` for invalid
|
||||
input. `decodeStatusAt` and `statusName` provide allocation-free status details;
|
||||
class helpers are infallible.
|
||||
|
||||
Target support: current compiler targets.
|
||||
@@ -0,0 +1,71 @@
|
||||
## When To Use std.url
|
||||
|
||||
In Zerolang, use `std.url` for lexical URL splitting, percent encoding, decoded
|
||||
query lookup, form-urlencoded bodies, and query appending.
|
||||
|
||||
Runnable today:
|
||||
|
||||
| API | Return | Notes |
|
||||
| --- | --- | --- |
|
||||
| `std.url.percentEncode(buffer, bytes)` | `Maybe<Span<u8>>` | Percent-encodes bytes into caller storage. |
|
||||
| `std.url.percentDecode(buffer, bytes)` | `Maybe<Span<u8>>` | Percent-decodes bytes into caller storage. |
|
||||
| `std.url.queryEscape(buffer, bytes)` | `Maybe<Span<u8>>` | Query-escapes bytes, using `+` for spaces. |
|
||||
| `std.url.queryUnescape(buffer, bytes)` | `Maybe<Span<u8>>` | Query-unescapes bytes, converting `+` back to space. |
|
||||
| `std.url.scheme(url)` | `Maybe<Span<u8>>` | Borrows the URL scheme if present. |
|
||||
| `std.url.authority(url)` | `Maybe<Span<u8>>` | Borrows the URL authority if present. |
|
||||
| `std.url.host(url)` | `Maybe<Span<u8>>` | Borrows the host from the URL authority. |
|
||||
| `std.url.path(url)` | `Span<u8>` | Borrows the path or an empty suffix. |
|
||||
| `std.url.query(url)` | `Maybe<Span<u8>>` | Borrows the raw query string if present. |
|
||||
| `std.url.fragment(url)` | `Maybe<Span<u8>>` | Borrows the raw fragment if present. |
|
||||
| `std.url.queryValue(query, key)` | `Maybe<Span<u8>>` | Borrows a raw query parameter value by key. |
|
||||
| `std.url.queryValueDecoded(buffer, query, key)` | `Maybe<Span<u8>>` | Looks up a raw or escaped query key and writes the decoded value. |
|
||||
| `std.url.writeQueryParam(buffer, key, value)` | `Maybe<Span<u8>>` | Writes an escaped `key=value` query parameter. |
|
||||
| `std.url.writeFormField(buffer, key, value)` | `Maybe<Span<u8>>` | Writes one application/x-www-form-urlencoded field. |
|
||||
| `std.url.appendFormField(buffer, form, field)` | `Maybe<Span<u8>>` | Appends one encoded field to an existing form body. |
|
||||
| `std.url.formValue(buffer, form, key)` | `Maybe<Span<u8>>` | Looks up a form field by raw or escaped key and writes the decoded value. |
|
||||
| `std.url.appendQuery(buffer, base, query)` | `Maybe<Span<u8>>` | Writes a URL with an appended raw query segment. |
|
||||
| `std.url.writeUrl(buffer, scheme, host, path)` | `Maybe<Span<u8>>` | Writes a `scheme://host/path` URL. |
|
||||
| `std.url.appendFragment(buffer, base, fragment)` | `Maybe<Span<u8>>` | Writes a URL with an appended raw fragment. |
|
||||
|
||||
Metadata labels:
|
||||
|
||||
- effects: parse
|
||||
- allocation behavior: no allocation; writers use caller storage
|
||||
- target support: target-neutral
|
||||
- error behavior: `Maybe` helpers return null on malformed input or insufficient storage
|
||||
- ownership notes: borrowed slices point into the input; encoded output points into caller storage
|
||||
- examples: `conformance/native/pass/std-codec-json-url.graph`
|
||||
|
||||
## Example
|
||||
|
||||
```zero
|
||||
pub fn main(world: World) -> Void raises {
|
||||
let url: Span<u8> = "https://example.com/path?q=zero%20lang#part"
|
||||
let host: Maybe<Span<u8>> = std.url.host(url)
|
||||
let query: Maybe<Span<u8>> = std.url.query(url)
|
||||
let fragment: Maybe<Span<u8>> = std.url.fragment(url)
|
||||
var out: [48]u8 = [0_u8; 48]
|
||||
var param_buf: [16]u8 = [0_u8; 16]
|
||||
let param: Maybe<Span<u8>> = std.url.writeQueryParam(param_buf, "q", "zero lang")
|
||||
var decoded_buf: [16]u8 = [0_u8; 16]
|
||||
var decoded: Maybe<Span<u8>> = null
|
||||
var next: Maybe<Span<u8>> = null
|
||||
if param.has {
|
||||
next = std.url.appendQuery(out, "https://example.com/path", param.value)
|
||||
}
|
||||
if query.has {
|
||||
decoded = std.url.queryValueDecoded(decoded_buf, query.value, "q")
|
||||
}
|
||||
if host.has && query.has && fragment.has && decoded.has && next.has && std.mem.eql(host.value, "example.com") && std.mem.eql(decoded.value, "zero lang") {
|
||||
check world.out.write("url ok\n")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Design Notes
|
||||
|
||||
URL helpers are lexical and byte-oriented. They do not resolve DNS, normalize
|
||||
paths, or allocate. Decoding rejects malformed percent escapes. Form helpers use
|
||||
the same encoding as query strings: spaces become `+`, and other non-unreserved
|
||||
bytes are percent-escaped. URL builders expect path, query, and fragment bytes
|
||||
that are already escaped for their position.
|
||||
@@ -0,0 +1,77 @@
|
||||
## Make Retention Visible
|
||||
|
||||
Zerolang optimization docs should answer a concrete question: why did this graph
|
||||
input retain these bytes, helpers, allocations, or runtime shims?
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "why is this binary bigger now?"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll inspect the size report and trace what is being retained."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero size --json",
|
||||
"output": "{\"sizeBreakdown\":{},\"retentionReasons\":[\"std.http.fetch retained by handle\"]}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## What This Means
|
||||
|
||||
Use graph inputs or graph-first packages:
|
||||
|
||||
```sh
|
||||
zero size --json examples/fixed-vec.graph
|
||||
zero mem --json examples/allocator-collections.graph
|
||||
zero build --json --profile tiny --target linux-musl-x64 examples/hello.graph --out .zero/out/hello
|
||||
```
|
||||
|
||||
## Profiles
|
||||
|
||||
| Profile | Use when | Contract |
|
||||
| --- | --- | --- |
|
||||
| `debug` | You need diagnostics and local symbols. | Keeps diagnostic/debug metadata. |
|
||||
| `fast` | Throughput matters more than minimum size. | Optimizes for speed within current direct codegen limits. |
|
||||
| `small` | You want the normal release shape. | Pay-as-used helpers and deterministic artifacts. |
|
||||
| `tiny` | Artifact size is the main constraint. | Minimum runtime metadata and strict helper budget. |
|
||||
|
||||
Build JSON includes `profileSemantics`, `profileCatalog`, `profileBudget`, and
|
||||
`safetyFacts`.
|
||||
|
||||
## Size Reports
|
||||
|
||||
`zero size --json` adds:
|
||||
|
||||
- `sizeBreakdown`
|
||||
- `retentionReasons`
|
||||
- `optimizationHints`
|
||||
- retained function and helper facts
|
||||
- literal and section cost
|
||||
- target and profile facts
|
||||
|
||||
Use `retentionReasons` when an agent needs to explain why a helper, literal, or
|
||||
debug metadata block stayed in the artifact.
|
||||
|
||||
## Memory Reports
|
||||
|
||||
`zero mem --json` includes:
|
||||
|
||||
- `memoryBudgets`
|
||||
- `allocatorFacts`
|
||||
- `allocationInstrumentation`
|
||||
- `collectionFacts`
|
||||
- `safetyFacts`
|
||||
|
||||
These fields are the public way to check whether an example remains
|
||||
fixed-capacity, caller-buffered, or heap-free.
|
||||
@@ -0,0 +1,125 @@
|
||||
## Packages Are Graph-Backed
|
||||
|
||||
In Zerolang, a human usually starts with the package they want, not the manifest fields:
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "start a small crm api package here"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll start the package with zero.toml and add the first API route."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero init --template package --manifest toml --format binary",
|
||||
"output": "graph project init ok\nwrote: ./zero.toml\nwrote: ./zero.graph"
|
||||
},
|
||||
{
|
||||
"command": "zero patch /tmp/crm-routes.patch",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero run -- $'GET /health\\n\\n'",
|
||||
"output": "HTTP/1.1 200 OK\ncontent-type: application/json\nconnection: close"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## What This Means
|
||||
|
||||
A Zero package has a manifest, a graph store, and review projections:
|
||||
|
||||
```text
|
||||
zero.toml
|
||||
zero.graph
|
||||
src/main.0
|
||||
```
|
||||
|
||||
The manifest tells Zero where the human-readable target projection lives. The
|
||||
graph store is the normal compiler input. The projection is for spans, review,
|
||||
and rare manual edits.
|
||||
|
||||
## Preferred Manifest
|
||||
|
||||
Use `zero.toml` for new projects:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "crm-api"
|
||||
version = "0.1.0"
|
||||
license = "MIT"
|
||||
|
||||
[targets.cli]
|
||||
kind = "exe"
|
||||
main = "src/main.0"
|
||||
|
||||
[dependencies.local-tools]
|
||||
path = "../local-tools"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`zero.json` is still accepted as a compatibility manifest format. Use one
|
||||
manifest in normal projects. If both exist for a directory input, `zero.toml`
|
||||
takes precedence.
|
||||
|
||||
## What `main` Means
|
||||
|
||||
The `main` path names the human-readable projection associated with a target.
|
||||
It does not make the projection the normal package compile input.
|
||||
|
||||
Normal package commands compile from the checked-in `zero.graph` store:
|
||||
|
||||
```sh
|
||||
zero check
|
||||
zero test
|
||||
zero run -- help
|
||||
zero build --out .zero/out/app
|
||||
zero size --json
|
||||
```
|
||||
|
||||
These commands report projection state, but they do not rewrite `.0` files.
|
||||
|
||||
## Projection Import And Export
|
||||
|
||||
Use projection commands only when humans need them:
|
||||
|
||||
```sh
|
||||
zero export
|
||||
zero verify-projection
|
||||
zero import
|
||||
```
|
||||
|
||||
`zero export` refreshes `.0` files from the graph. `zero import` rebuilds the
|
||||
graph from projection text after a human edit. `zero verify-projection` is the
|
||||
no-write drift gate.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Package-local imports resolve from `src/` projection paths for stable human
|
||||
review:
|
||||
|
||||
- `src/foo.0` defines module `foo`
|
||||
- `src/foo/mod.0` defines directory module `foo`
|
||||
|
||||
Local path dependencies are accepted. Exact versioned registry references are
|
||||
recorded as metadata without remote fetches in the current compiler slice.
|
||||
|
||||
Package inspection and docs JSON may include `dependencies`, `package.lockfile`,
|
||||
`packageCache.cacheKeyInputs`, and `publicationGate` facts. Diagnostics such as
|
||||
`PKG001` and `PKG004` explain invalid manifests, dependency shape issues, and
|
||||
package resolution failures.
|
||||
|
||||
## Profiles
|
||||
|
||||
Profiles are declared in the manifest and reported by build/size JSON. Use
|
||||
`zero build --json` and `zero size --json` to inspect `profileSemantics`,
|
||||
`profileBudget`, retained helpers, and target readiness.
|
||||
@@ -0,0 +1,177 @@
|
||||
## The Pieces The Graph Stores
|
||||
|
||||
Zerolang exposes language pieces as graph facts and as `.0`
|
||||
projection syntax. The graph stores the type and layout facts. The projection
|
||||
lets humans read them.
|
||||
|
||||
## Scalar Values
|
||||
|
||||
| Type | Purpose |
|
||||
| --- | --- |
|
||||
| `Bool` | Conditions and logical results. |
|
||||
| `i8` `i16` `i32` `i64` | Signed fixed-width integers. |
|
||||
| `u8` `u16` `u32` `u64` | Unsigned fixed-width integers. |
|
||||
| `usize` `isize` | Pointer-sized integers. |
|
||||
| `f32` `f64` | Floating-point values. |
|
||||
| `char` | Byte-sized character value for ASCII/parser/codec work. |
|
||||
| `String` | Text value used by string literals and current I/O examples. |
|
||||
| `Void` | Return type for functions that produce no useful value. |
|
||||
|
||||
Integer literals support decimal, hexadecimal, binary, octal, `_` separators,
|
||||
and optional suffixes such as `_u8` or `_usize`. An unsuffixed integer literal
|
||||
adopts the type of a typed integer operand in arithmetic and comparisons when
|
||||
the value fits, so `index + 1` and `index < 10` work when `index` is `usize`.
|
||||
Out-of-range literals are rejected, so `byte > 300` fails for a `u8` operand.
|
||||
|
||||
```zero
|
||||
let count: u32 = 0x12c_u32
|
||||
let byte: u8 = 255
|
||||
let page: usize = 4_096
|
||||
```
|
||||
|
||||
Primitive numeric types do not implicitly narrow, widen, or change signedness.
|
||||
Use an explicit cast when the conversion is intentional.
|
||||
|
||||
```zero
|
||||
let count: u32 = 300
|
||||
let byte: u8 = count as u8
|
||||
```
|
||||
|
||||
## Absence
|
||||
|
||||
`Maybe<T>` represents an optional value:
|
||||
|
||||
```zero
|
||||
let parsed: Maybe<u32> = std.args.parseU32(1)
|
||||
if parsed.has {
|
||||
return parsed.value
|
||||
}
|
||||
return 0
|
||||
```
|
||||
|
||||
`.value` reads require a visible `.has` guard or fallible handling. That rule is
|
||||
part of the graph semantics, not a formatter convention.
|
||||
|
||||
## Fixed Storage And Views
|
||||
|
||||
| Type form | Meaning |
|
||||
| --- | --- |
|
||||
| `[N]T` | Fixed-size array with `N` elements of `T`. |
|
||||
| `Span<T>` | Read-only borrowed pointer plus length. |
|
||||
| `MutSpan<T>` | Mutable borrowed pointer plus length. |
|
||||
| `ref<T>` | Immutable reference. |
|
||||
| `mutref<T>` | Mutable reference. |
|
||||
|
||||
```zero
|
||||
var scratch: [16]u8 = [0_u8; 16]
|
||||
let bytes: Span<u8> = std.mem.span("hello")
|
||||
let copied: usize = std.mem.copy(scratch, bytes)
|
||||
```
|
||||
|
||||
These types are central to Zero's size and memory model. Helpers generally
|
||||
write into caller-owned storage so allocation behavior remains visible.
|
||||
|
||||
Fixed-size locals live in one stack frame per function, and a single function
|
||||
may declare at most 131072 bytes of locals. `zero check` reports `MEM003` when
|
||||
a frame exceeds that limit; split the buffer into smaller buffers in helper
|
||||
functions so each frame stays within the limit, or process the data in
|
||||
fixed-size chunks. `PageAlloc` and `GeneralAlloc` handles type-check but do
|
||||
not lower to the direct backends yet, so they cannot replace frame-sized
|
||||
buffers today.
|
||||
|
||||
## Ownership
|
||||
|
||||
Owned values use explicit ownership forms:
|
||||
|
||||
```zero
|
||||
fn drop(self: mutref<Self>) -> Void {
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
The canonical non-raising `fn drop(self: mutref<Self>) -> Void` shape lets the
|
||||
graph model cleanup without a hidden runtime cleanup registry. Owned resources,
|
||||
allocators, and cleanup behavior should be visible through graph inspection.
|
||||
|
||||
## User Types
|
||||
|
||||
```zero
|
||||
type Point {
|
||||
x: i32,
|
||||
y: i32,
|
||||
}
|
||||
```
|
||||
|
||||
Fields, defaults, and constructor-like projections are graph declarations and
|
||||
edges. Public type surfaces should stay explicit because agents rely on stable
|
||||
field and type facts.
|
||||
|
||||
## Enums And Choices
|
||||
|
||||
```zero
|
||||
enum Status {
|
||||
Pending,
|
||||
Ready,
|
||||
}
|
||||
```
|
||||
|
||||
Enums are named value sets. Choices and payload-bearing cases are represented
|
||||
as graph facts so `match` can be checked semantically.
|
||||
|
||||
## Fallibility
|
||||
|
||||
Fallible functions use `raises`:
|
||||
|
||||
```zero
|
||||
fn validate(ok: Bool) -> i32 raises [InvalidInput] {
|
||||
if !ok {
|
||||
raise InvalidInput
|
||||
}
|
||||
return 42
|
||||
}
|
||||
```
|
||||
|
||||
`check` propagates failure explicitly. There is no hidden exception system.
|
||||
|
||||
## Compile-Time Values
|
||||
|
||||
Compile-time facts currently cover bounded integer, `Bool`, and enum static
|
||||
values. The metadata surface includes facts such as `compileTime`,
|
||||
`target.pointerWidth`, `fieldType`, and `hasEnumCase`.
|
||||
|
||||
Use `zero inspect --json` or `zero check --json` when an agent needs those facts
|
||||
for a patch.
|
||||
|
||||
## Projection Examples
|
||||
|
||||
Projection syntax is for humans. The graph stores the same facts directly.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "what types does this helper use?"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll inspect the function facts and summarize the types."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero query --fn add",
|
||||
"output": "fn add(x: i32, y: i32) -> i32\n return x + y"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
For manual review, export the projection:
|
||||
|
||||
```sh
|
||||
zero export
|
||||
```
|
||||
@@ -0,0 +1,102 @@
|
||||
## Graph-Backed Library Surface
|
||||
|
||||
The Zerolang standard library is graph-backed. The compiler uses binary `std/*.graph`
|
||||
stores. Sibling `std/*.0` files are human-readable projections for review, not
|
||||
the normal compile path.
|
||||
|
||||
Agents should learn the callable surface from the installed compiler:
|
||||
|
||||
```sh
|
||||
zero skills get stdlib
|
||||
```
|
||||
|
||||
Humans should use this page to decide which module to ask for.
|
||||
|
||||
## Expected Usage
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "make a small json http api"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll use the HTTP and JSON helpers and run a couple route checks."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero skills get stdlib",
|
||||
"output": "stdlib helpers: std.http.writeJsonOk, std.http.requestIsGet, std.json.field, ..."
|
||||
},
|
||||
{
|
||||
"command": "zero query --find handle",
|
||||
"output": "fn handle(request: Span<u8>, response: MutSpan<u8>) -> Maybe<Span<u8>>"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Module Groups
|
||||
|
||||
Core data and memory:
|
||||
|
||||
- `std.mem`: spans, byte equality, copy/fill, allocators, byte buffers, and fixed-capacity vectors.
|
||||
- `std.collections`: fixed-capacity collection operations over caller-owned storage.
|
||||
- `std.search`: scalar span search and binary search.
|
||||
- `std.sort`: in-place sorting over caller-owned scalar storage.
|
||||
- `std.ascii`, `std.text`, `std.str`: byte-backed text helpers.
|
||||
- `std.unicode`: strict UTF-8 codepoint decode/encode iteration and codepoint classes.
|
||||
- `std.parse`, `std.fmt`, `std.codec`, `std.math`: parsers, formatters, codecs, and numeric helpers.
|
||||
- `std.regex`: compile-once regular expression matching for a documented subset.
|
||||
- `std.inet`: IPv4, IPv6, and hostname literal validation and parsing.
|
||||
|
||||
Program surfaces:
|
||||
|
||||
- `std.args`, `std.cli`, `std.env`: command-line and environment helpers.
|
||||
- `std.io`, `std.fs`, `std.path`: caller-buffer I/O, hosted filesystem helpers, and lexical paths.
|
||||
- `std.json`, `std.toml`, `std.url`, `std.csv`, `std.log`: data formats and structured output.
|
||||
- `std.testing`: test-block predicates.
|
||||
|
||||
Runtime and web:
|
||||
|
||||
- `std.time`, `std.rand`, `std.proc`, `std.term`, `std.crypto`: hosted/runtime helper surfaces, terminal sequences, key decoding, terminal metadata, and raw terminal mode.
|
||||
- `std.net`, `std.http`: network metadata, HTTP client/server metadata, request parsing, response writing, hosted fetch, and local listen support.
|
||||
|
||||
## Inspect What A Program Uses
|
||||
|
||||
```sh
|
||||
zero inspect --json examples/crm-api
|
||||
zero size --json examples/crm-api
|
||||
zero mem --json examples/allocator-collections.graph
|
||||
```
|
||||
|
||||
Useful JSON fields include:
|
||||
|
||||
- `usedStdlibHelpers`
|
||||
- `stdlibHelpers`
|
||||
- `effects`
|
||||
- `allocationBehavior`
|
||||
- `targetSupport`
|
||||
- `errorBehavior`
|
||||
- `ownershipNotes`
|
||||
- `apiStability`
|
||||
|
||||
## Allocation And Capability Rule
|
||||
|
||||
Standard library helpers should make ownership, effects, and target support
|
||||
visible. Hosted APIs such as filesystem, process, time, random, network, and
|
||||
HTTP require target capabilities. Buffer-oriented helpers should write into
|
||||
caller-owned storage rather than silently allocating.
|
||||
|
||||
## Projections In Module Pages
|
||||
|
||||
Module pages include `.0` snippets because humans need readable examples. Treat
|
||||
those snippets as projection examples. Agents should patch user programs
|
||||
through graph commands and use `zero export` only when a human asks to review
|
||||
the resulting projection.
|
||||
@@ -0,0 +1,81 @@
|
||||
## Capabilities Are Target Facts
|
||||
|
||||
In Zerolang, start with the user request. The agent should inspect target capability facts
|
||||
before patching APIs that depend on hosted runtime support.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "make this cli work on linux musl too"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll check the target facts and call out anything that blocks the port."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero check --json --target linux-musl-x64",
|
||||
"output": "{\"ok\":false,\"diagnostics\":[{\"code\":\"TAR002\",\"message\":\"target does not provide required capability\"}]}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## What This Means
|
||||
|
||||
Zero does not assume every target can do filesystem, network, process, time, or
|
||||
random operations. Those are explicit target facts. Target JSON includes host
|
||||
identity, aliases, object formats, C target mapping, capabilities, HTTP runtime
|
||||
metadata, and `targetToolchains`.
|
||||
|
||||
## Inspect Targets
|
||||
|
||||
```sh
|
||||
zero targets
|
||||
zero targets --json
|
||||
zero check --json --target linux-musl-x64 examples/memory-package
|
||||
```
|
||||
|
||||
## Hosted Capabilities
|
||||
|
||||
The current hosted capability set includes:
|
||||
|
||||
- `args`
|
||||
- `env`
|
||||
- `fs`
|
||||
- `memory`
|
||||
- `net`
|
||||
- `proc`
|
||||
- `rand`
|
||||
- `stdio`
|
||||
- `time`
|
||||
|
||||
Non-host targets expose only the capabilities listed for that target. Network
|
||||
support is intentionally target-gated. HTTP helpers that only parse or write
|
||||
request/response envelopes are target-neutral; hosted fetch and listen require
|
||||
network-capable host support.
|
||||
|
||||
## Capability Failure Is A Feature
|
||||
|
||||
If a graph input uses `std.fs` on a target that cannot provide filesystem
|
||||
support, `zero check --target ...` should report a diagnostic instead of
|
||||
silently changing behavior.
|
||||
|
||||
```sh
|
||||
zero check --json --target linux-musl-x64 conformance/common/fail/unsupported-target-feature.graph
|
||||
```
|
||||
|
||||
The diagnostic is `TAR002` and the repair id points at choosing a target with
|
||||
the required capability.
|
||||
|
||||
## What To Remember
|
||||
|
||||
Capabilities are part of the graph contract. Standard library pages document
|
||||
effects and target support so an agent can choose the right helper before it
|
||||
patches the program.
|
||||
@@ -0,0 +1,80 @@
|
||||
## Run Tests From The Graph
|
||||
|
||||
In Zerolang, `zero test` runs test blocks from graph inputs and graph-first packages. When
|
||||
this page shows `test` projection syntax, read it as the human review view of a
|
||||
graph-backed test declaration.
|
||||
|
||||
```json-render
|
||||
{
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"text": "add subtract and prove it works"
|
||||
},
|
||||
{
|
||||
"role": "assistant",
|
||||
"text": "I’ll add the function and a focused test for it."
|
||||
},
|
||||
{
|
||||
"role": "tools",
|
||||
"calls": [
|
||||
{
|
||||
"command": "zero patch /tmp/subtract.patch",
|
||||
"output": "program graph patch ok"
|
||||
},
|
||||
{
|
||||
"command": "zero test --json --filter subtract",
|
||||
"output": "{\"ok\":true,\"passedTests\":1,\"expectedFailures\":0}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## What This Means
|
||||
|
||||
```zero
|
||||
test "add works" {
|
||||
expect (add(40, 2) == 42)
|
||||
}
|
||||
```
|
||||
|
||||
Agents should add or update tests through graph patches.
|
||||
|
||||
## Daily Test Commands
|
||||
|
||||
```sh
|
||||
zero test
|
||||
zero test --json
|
||||
zero test --json --filter add
|
||||
```
|
||||
|
||||
Package tests discover test blocks across the package entry file and local
|
||||
modules. Filters use substring matching on the test name.
|
||||
|
||||
## Expected Failures
|
||||
|
||||
Expected-fail tests are named with `xfail:`, `expected fail:`, or `[xfail]`.
|
||||
|
||||
| Result | JSON effect |
|
||||
| --- | --- |
|
||||
| The test fails as expected. | `expectedFailures` increments. |
|
||||
| The test passes unexpectedly. | `unexpectedPasses` increments and the command fails. |
|
||||
|
||||
`zero test --json` includes `fixtures`, `snapshotKey`, selected/discovered
|
||||
counts, stdout/stderr, and per-test results.
|
||||
|
||||
## Repository Reliability
|
||||
|
||||
Repository-level reliability checks still live outside the docs site:
|
||||
|
||||
```sh
|
||||
pnpm run reliability:smoke
|
||||
pnpm run native:sanitize
|
||||
pnpm run conformance
|
||||
```
|
||||
|
||||
The reliability smoke covers golden output rows, structured snapshot rows,
|
||||
fuzz cases, and crasher regressions. Use those suites for compiler reliability,
|
||||
not per-page documentation assertions.
|
||||
@@ -0,0 +1,160 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
type ToolCall = { command: string; output?: string };
|
||||
|
||||
type Message =
|
||||
| { role: "user"; text: string }
|
||||
| { role: "assistant"; text: string }
|
||||
| { role: "skill"; name: string }
|
||||
| { role: "tools"; calls: ToolCall[] }
|
||||
| { role: "output"; text: string };
|
||||
|
||||
export type ChatSpec = { title?: string; messages: Message[] };
|
||||
|
||||
function CopyButton({ text }: { text: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Copy message"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1400);
|
||||
} catch {}
|
||||
}}
|
||||
className="shrink-0 cursor-pointer rounded-md p-1.5 text-muted transition hover:bg-surface-muted hover:text-fg"
|
||||
>
|
||||
{copied ? (
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none">
|
||||
<path d="M13.5 4.5 6.5 11.5 3 8" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M5 1.75A1.75 1.75 0 0 1 6.75 0h5.5A1.75 1.75 0 0 1 14 1.75v5.5A1.75 1.75 0 0 1 12.25 9H11V7.5h1.25a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-5.5a.25.25 0 0 0-.25.25V3H5V1.75Z" />
|
||||
<path d="M2 4.75A1.75 1.75 0 0 1 3.75 3h5.5A1.75 1.75 0 0 1 11 4.75v5.5A1.75 1.75 0 0 1 9.25 12h-5.5A1.75 1.75 0 0 1 2 10.25v-5.5Zm1.75-.25a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h5.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-5.5Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ChevronIcon({ open }: { open: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
className={`shrink-0 text-muted transition-transform duration-200 ${open ? "rotate-90" : ""}`}
|
||||
>
|
||||
<path
|
||||
d="M6 4l4 4-4 4"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.75"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ToolCalls({ calls }: { calls: ToolCall[] }) {
|
||||
const [open, setOpen] = useState<number[]>([]);
|
||||
const toggle = (i: number) =>
|
||||
setOpen((prev) => (prev.includes(i) ? prev.filter((n) => n !== i) : [...prev, i]));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{calls.map((call, i) => {
|
||||
const isOpen = open.includes(i);
|
||||
const hasOutput = typeof call.output === "string" && call.output.length > 0;
|
||||
return (
|
||||
<div
|
||||
key={call.command}
|
||||
className="overflow-hidden rounded-lg border border-border bg-surface"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => hasOutput && toggle(i)}
|
||||
aria-expanded={isOpen}
|
||||
disabled={!hasOutput}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 font-mono text-[0.78125rem] leading-relaxed text-muted transition-colors enabled:cursor-pointer enabled:hover:bg-surface-muted"
|
||||
>
|
||||
{hasOutput ? <ChevronIcon open={isOpen} /> : <span className="w-3 shrink-0" />}
|
||||
<span
|
||||
className={`min-w-0 flex-1 text-left text-fg/80 ${
|
||||
isOpen ? "whitespace-pre-wrap break-all" : "truncate"
|
||||
}`}
|
||||
>
|
||||
{call.command}
|
||||
</span>
|
||||
</button>
|
||||
{isOpen && hasOutput && (
|
||||
<pre className="m-0 overflow-x-auto border-t border-border/50 bg-bg px-3 py-2.5 text-[0.78125rem] leading-relaxed text-fg/70">
|
||||
{call.output}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChatMessage({ message }: { message: Message }) {
|
||||
if (message.role === "user") {
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<CopyButton text={message.text} />
|
||||
<div className="max-w-[82%] rounded-2xl rounded-br-md bg-fg px-4 py-2.5 text-[0.875rem] leading-relaxed text-bg">
|
||||
{message.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (message.role === "assistant") {
|
||||
return (
|
||||
<div className="max-w-[92%] text-[0.875rem] leading-[1.65] text-fg">{message.text}</div>
|
||||
);
|
||||
}
|
||||
if (message.role === "skill") {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-[0.78125rem] text-muted">
|
||||
<svg width="13" height="13" viewBox="0 0 16 16" fill="none" className="shrink-0">
|
||||
<path
|
||||
d="M6 2 3 8h3l-1 6 5-8H6l2-4H6Z"
|
||||
fill="currentColor"
|
||||
className="text-fg/50"
|
||||
/>
|
||||
</svg>
|
||||
<span>
|
||||
Loaded <span className="font-medium text-fg">{message.name}</span> skill
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (message.role === "tools") {
|
||||
return <ToolCalls calls={message.calls} />;
|
||||
}
|
||||
return (
|
||||
<pre className="m-0 overflow-x-auto rounded-lg border border-border bg-code-bg px-4 py-3 font-mono text-[0.8125rem] leading-relaxed text-code-fg">
|
||||
{message.text}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
export function AgentChat({ spec }: { spec: ChatSpec }) {
|
||||
return (
|
||||
<div className="not-prose my-6 overflow-hidden rounded-2xl border border-border bg-bg">
|
||||
<div className="flex flex-col gap-5 p-5 sm:p-6">
|
||||
{spec.messages.map((message, i) => (
|
||||
<ChatMessage key={i} message={message} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Link from "next/link";
|
||||
import type { ButtonHTMLAttributes, ComponentProps } from "react";
|
||||
|
||||
const SIZES = {
|
||||
sm: "h-8 px-4 text-[0.8125rem]",
|
||||
md: "h-10 px-6 text-sm",
|
||||
lg: "h-11 gap-2 px-8 text-[0.9375rem]",
|
||||
};
|
||||
|
||||
const VARIANTS = {
|
||||
default: "border-border bg-bg text-fg hover:border-fg",
|
||||
primary:
|
||||
"border-accent bg-accent text-accent-fg hover:bg-transparent hover:text-accent",
|
||||
};
|
||||
|
||||
type ButtonVariant = keyof typeof VARIANTS;
|
||||
type ButtonSize = keyof typeof SIZES;
|
||||
|
||||
type ButtonStyleProps = {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function classes({ variant = "default", size = "md", className = "" }: ButtonStyleProps): string {
|
||||
return [
|
||||
"inline-flex items-center justify-center rounded-md border font-medium no-underline transition",
|
||||
VARIANTS[variant],
|
||||
SIZES[size],
|
||||
className,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & ButtonStyleProps;
|
||||
|
||||
export function Button({ variant, size, className, ...rest }: ButtonProps) {
|
||||
return <button className={classes({ variant, size, className })} {...rest} />;
|
||||
}
|
||||
|
||||
type ButtonLinkProps = ComponentProps<typeof Link> & ButtonStyleProps;
|
||||
|
||||
export function ButtonLink({ variant, size, className, ...rest }: ButtonLinkProps) {
|
||||
return <Link className={classes({ variant, size, className })} {...rest} />;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import type { UIEvent } from "react";
|
||||
import { useMemo, useRef } from "react";
|
||||
import { highlight } from "@/lib/highlight";
|
||||
|
||||
type CodeEditorProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
language?: string;
|
||||
ariaLabel: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function CodeEditor({
|
||||
value,
|
||||
onChange,
|
||||
language = "zero",
|
||||
ariaLabel,
|
||||
className = "",
|
||||
}: CodeEditorProps) {
|
||||
const preRef = useRef<HTMLPreElement | null>(null);
|
||||
|
||||
const html = useMemo(() => {
|
||||
const trailingNewline = value.endsWith("\n") ? " " : "";
|
||||
return highlight(value + trailingNewline, language);
|
||||
}, [value, language]);
|
||||
|
||||
function handleScroll(event: UIEvent<HTMLTextAreaElement>) {
|
||||
const pre = preRef.current;
|
||||
if (!pre) return;
|
||||
pre.scrollTop = event.currentTarget.scrollTop;
|
||||
pre.scrollLeft = event.currentTarget.scrollLeft;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative h-full w-full ${className}`}>
|
||||
<pre
|
||||
ref={preRef}
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 m-0 overflow-hidden whitespace-pre p-6 font-mono text-[0.9375rem] leading-[1.7] text-code-fg"
|
||||
>
|
||||
<code dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</pre>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onScroll={handleScroll}
|
||||
spellCheck={false}
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
autoComplete="off"
|
||||
aria-label={ariaLabel}
|
||||
className="relative block h-full w-full resize-none whitespace-pre border-0 bg-transparent p-6 font-mono text-[0.9375rem] leading-[1.7] text-transparent caret-fg outline-none"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
"use client";
|
||||
|
||||
import type { MouseEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export function CopyCodeButton() {
|
||||
const [state, setState] = useState("idle");
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
async function handleClick(event: MouseEvent<HTMLButtonElement>) {
|
||||
const button = event.currentTarget;
|
||||
const block = button.closest("figure[data-rehype-pretty-code-figure], div[data-code-block]");
|
||||
const code = block?.querySelector("code");
|
||||
const text = code?.textContent ?? "";
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setState("copied");
|
||||
} catch {
|
||||
setState("failed");
|
||||
}
|
||||
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => setState("idle"), 1400);
|
||||
}
|
||||
|
||||
const color =
|
||||
state === "copied" ? "text-success" : state === "failed" ? "text-danger" : "text-muted";
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={state === "copied" ? "Code copied to clipboard" : "Copy code to clipboard"}
|
||||
onClick={handleClick}
|
||||
className={`absolute right-2 top-2 z-10 inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded border-0 bg-transparent transition hover:text-fg ${color}`}
|
||||
>
|
||||
{state === "copied" ? (
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" className="h-3.5 w-3.5 fill-current">
|
||||
<path d="M13.78 3.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 8.28a.75.75 0 0 1 1.06-1.06L6 9.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg aria-hidden="true" viewBox="0 0 16 16" className="h-3.5 w-3.5 fill-current">
|
||||
<path d="M5 1.75A1.75 1.75 0 0 1 6.75 0h5.5A1.75 1.75 0 0 1 14 1.75v5.5A1.75 1.75 0 0 1 12.25 9H11V7.5h1.25a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-5.5a.25.25 0 0 0-.25.25V3H5V1.75Z" />
|
||||
<path d="M2 4.75A1.75 1.75 0 0 1 3.75 3h5.5A1.75 1.75 0 0 1 11 4.75v5.5A1.75 1.75 0 0 1 9.25 12h-5.5A1.75 1.75 0 0 1 2 10.25v-5.5Zm1.75-.25a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h5.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-5.5Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,386 @@
|
||||
"use client";
|
||||
|
||||
import { isValidElement, useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { DefaultChatTransport } from "ai";
|
||||
import { Streamdown } from "streamdown";
|
||||
import type {
|
||||
Components as StreamdownComponents,
|
||||
ExtraProps as StreamdownExtraProps,
|
||||
} from "streamdown";
|
||||
import type {
|
||||
DynamicToolUIPart,
|
||||
UIDataTypes,
|
||||
UIMessage,
|
||||
UIMessagePart,
|
||||
UITools,
|
||||
ToolUIPart,
|
||||
} from "ai";
|
||||
import Link from "next/link";
|
||||
import { Sheet, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||
import { highlight } from "@/lib/highlight";
|
||||
|
||||
const STORAGE_KEY = "docs-chat-messages";
|
||||
const transport = new DefaultChatTransport({ api: "/api/docs-chat" });
|
||||
|
||||
const TOOL_LABELS = {
|
||||
readFile: { label: "Reading", pastLabel: "Read", argKey: "path" },
|
||||
bash: { label: "Running", pastLabel: "Ran", argKey: "command" },
|
||||
};
|
||||
|
||||
type ToolLabel = {
|
||||
label: string;
|
||||
pastLabel: string;
|
||||
argKey?: string;
|
||||
};
|
||||
|
||||
type ToolDisplayPart = ToolUIPart<UITools> | DynamicToolUIPart;
|
||||
|
||||
function extractCodeText(children: ReactNode): string {
|
||||
if (children == null || children === false) return "";
|
||||
if (typeof children === "string") return children;
|
||||
if (Array.isArray(children)) return children.map(extractCodeText).join("");
|
||||
if (isValidElement<{ children?: ReactNode }>(children)) {
|
||||
return extractCodeText(children.props.children);
|
||||
}
|
||||
return String(children);
|
||||
}
|
||||
|
||||
function ChatCodeBlock({
|
||||
children,
|
||||
}: ComponentPropsWithoutRef<"pre"> & StreamdownExtraProps) {
|
||||
const codeEl = Array.isArray(children) ? children[0] : children;
|
||||
const className = isValidElement<{ className?: string }>(codeEl)
|
||||
? codeEl.props.className ?? ""
|
||||
: "";
|
||||
const language = /language-([A-Za-z0-9_-]+)/.exec(className)?.[1] ?? "";
|
||||
const code = extractCodeText(
|
||||
isValidElement<{ children?: ReactNode }>(codeEl) ? codeEl.props.children : codeEl,
|
||||
).replace(/\n$/, "");
|
||||
const html = highlight(code, language);
|
||||
return (
|
||||
<pre className="not-prose my-3 overflow-x-auto rounded-md border border-border bg-code-bg p-3 text-[0.8125rem] leading-relaxed text-code-fg">
|
||||
<code
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
const STREAMDOWN_COMPONENTS: StreamdownComponents = {
|
||||
pre: ChatCodeBlock,
|
||||
};
|
||||
|
||||
function isToolPart(
|
||||
part: UIMessagePart<UIDataTypes, UITools>,
|
||||
): part is ToolDisplayPart {
|
||||
return part.type.startsWith("tool-") || part.type === "dynamic-tool";
|
||||
}
|
||||
|
||||
function getToolName(part: ToolDisplayPart): string {
|
||||
if (part.type === "dynamic-tool") return part.toolName ?? "tool";
|
||||
return part.type.replace(/^tool-/, "");
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function ToolCallDisplay({ part }: { part: ToolDisplayPart }) {
|
||||
const toolName = getToolName(part);
|
||||
const config: ToolLabel = TOOL_LABELS[toolName as keyof typeof TOOL_LABELS] ?? {
|
||||
label: toolName,
|
||||
pastLabel: toolName,
|
||||
};
|
||||
const isDone = part.state === "output-available";
|
||||
const isError = part.state === "output-error";
|
||||
const isRunning = !isDone && !isError;
|
||||
const displayLabel = isRunning ? config.label : config.pastLabel;
|
||||
|
||||
const args = isRecord(part.input) ? part.input : {};
|
||||
const argValue = config.argKey ? args[config.argKey] : undefined;
|
||||
const argPreview =
|
||||
argValue != null
|
||||
? String(argValue)
|
||||
.replace(/^\/workspace\//, "/")
|
||||
.replace(/\.md$/, "")
|
||||
.replace(/\/index$/, "") || "/"
|
||||
: "";
|
||||
|
||||
const docsLink = toolName === "readFile" && argPreview.startsWith("/") ? argPreview : null;
|
||||
|
||||
const argEl = argPreview ? (
|
||||
docsLink ? (
|
||||
<Link href={docsLink} className="truncate underline underline-offset-2">
|
||||
{argPreview}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="truncate">{argPreview}</span>
|
||||
)
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="min-w-0 py-0.5 text-xs">
|
||||
<span
|
||||
className={`inline-flex min-w-0 max-w-full items-center gap-1 font-mono ${
|
||||
isRunning
|
||||
? "animate-pulse text-neutral-500 dark:text-neutral-400"
|
||||
: "text-neutral-400 dark:text-neutral-500"
|
||||
}`}
|
||||
>
|
||||
<span className="shrink-0">{displayLabel}</span>
|
||||
{argEl}
|
||||
{isError && <span className="text-red-500">failed</span>}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SUGGESTIONS = [
|
||||
"What is Zero?",
|
||||
"How do I install it?",
|
||||
"What commands are available?",
|
||||
"How does cross-compilation work?",
|
||||
"Show me a hello world.",
|
||||
];
|
||||
|
||||
export function DocsChat() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const messagesScrollRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const restoredRef = useRef(false);
|
||||
|
||||
const { messages, sendMessage, status, setMessages, error } = useChat({ transport });
|
||||
|
||||
const isLoading = status === "streaming" || status === "submitted";
|
||||
const showMessages = messages.length > 0 || !!error || isLoading;
|
||||
|
||||
useEffect(() => {
|
||||
if (restoredRef.current) return;
|
||||
restoredRef.current = true;
|
||||
try {
|
||||
const stored = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) setMessages(parsed as UIMessage[]);
|
||||
}
|
||||
} catch {}
|
||||
}, [setMessages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!restoredRef.current || isLoading) return;
|
||||
if (messages.length === 0) {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(messages));
|
||||
} catch {}
|
||||
}, [messages, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e: globalThis.KeyboardEvent) {
|
||||
if (e.key === "i" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
setOpen((prev) => !prev);
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => document.removeEventListener("keydown", onKey);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const t = setTimeout(() => inputRef.current?.focus(), 200);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (error) setOpen(true);
|
||||
}, [error]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = messagesScrollRef.current;
|
||||
if (!el) return;
|
||||
requestAnimationFrame(() => {
|
||||
el.scrollTop = el.scrollHeight;
|
||||
});
|
||||
}, [messages, error]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e?: { preventDefault: () => void }) => {
|
||||
e?.preventDefault();
|
||||
if (!input.trim() || isLoading) return;
|
||||
sendMessage({ text: input });
|
||||
setInput("");
|
||||
},
|
||||
[input, isLoading, sendMessage],
|
||||
);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setMessages([]);
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
}, [setMessages]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{!open && (
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="fixed bottom-4 left-1/2 z-50 flex -translate-x-1/2 items-center gap-2 rounded-lg bg-neutral-900 px-4 py-2 text-sm font-medium text-white shadow-lg transition-opacity hover:opacity-90 dark:bg-neutral-100 dark:text-neutral-900 sm:left-auto sm:right-4 sm:translate-x-0"
|
||||
aria-label="Ask AI"
|
||||
>
|
||||
Ask AI
|
||||
<kbd className="hidden items-center gap-0.5 font-mono text-xs opacity-60 sm:inline-flex">
|
||||
<span>⌘</span>I
|
||||
</kbd>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
showCloseButton={false}
|
||||
className="flex w-full max-w-md flex-col gap-0 p-0 sm:max-w-md"
|
||||
>
|
||||
<SheetTitle className="sr-only">AI Chat</SheetTitle>
|
||||
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-neutral-200 px-4 py-3 dark:border-neutral-800">
|
||||
<span className="text-sm font-medium text-neutral-900 dark:text-neutral-100">
|
||||
Zero Docs
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{showMessages && (
|
||||
<button
|
||||
onClick={handleClear}
|
||||
className="text-xs text-neutral-500 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
aria-label="Clear conversation"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="text-neutral-500 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
aria-label="Close panel"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showMessages ? (
|
||||
<div ref={messagesScrollRef} className="flex-1 space-y-4 overflow-y-auto p-4">
|
||||
{messages.map((message) => {
|
||||
const hasVisible = message.parts?.some(
|
||||
(p) => (p.type === "text" && p.text.length > 0) || isToolPart(p),
|
||||
);
|
||||
if (!hasVisible) return null;
|
||||
return (
|
||||
<div key={message.id}>
|
||||
{message.role === "user" ? (
|
||||
<div className="whitespace-pre-wrap text-sm leading-relaxed text-neutral-500 dark:text-neutral-400">
|
||||
{message.parts.filter((p) => p.type === "text").map((p) => p.text).join("")}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{message.parts.map((part, i) => {
|
||||
if (part.type === "text" && part.text) {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="docs-chat-content max-w-none text-sm leading-relaxed text-neutral-900 dark:text-neutral-100"
|
||||
>
|
||||
<Streamdown components={STREAMDOWN_COMPONENTS}>
|
||||
{part.text}
|
||||
</Streamdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isToolPart(part)) {
|
||||
return <ToolCallDisplay key={part.toolCallId ?? i} part={part} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{error && (
|
||||
<div className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600/80 dark:bg-red-950/30 dark:text-red-400/80">
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(error.message);
|
||||
return parsed.message || parsed.error || error.message;
|
||||
} catch {
|
||||
return error.message || "Something went wrong. Please try again.";
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-1 flex-col">
|
||||
<div className="flex flex-wrap gap-2 p-4">
|
||||
{SUGGESTIONS.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => sendMessage({ text: s })}
|
||||
className="rounded-full border border-neutral-200 bg-neutral-100 px-3 py-1.5 text-xs font-medium text-neutral-600 transition-colors hover:text-neutral-900 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex shrink-0 items-end gap-2 border-t border-neutral-200 px-4 py-3 dark:border-neutral-800"
|
||||
>
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
value={input}
|
||||
onChange={(e) => {
|
||||
setInput(e.target.value);
|
||||
e.target.style.height = "auto";
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
rows={1}
|
||||
enterKeyHint="send"
|
||||
placeholder="Ask a question..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit(e);
|
||||
}
|
||||
}}
|
||||
className="max-h-32 flex-1 resize-none bg-transparent text-base leading-relaxed text-neutral-900 outline-none placeholder:text-neutral-400 disabled:opacity-50 dark:text-neutral-100 dark:placeholder:text-neutral-500 sm:text-sm"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || !input.trim()}
|
||||
className="shrink-0 rounded-full bg-neutral-900 p-1.5 text-white transition-colors hover:bg-neutral-700 disabled:opacity-30 dark:bg-neutral-100 dark:text-neutral-900 dark:hover:bg-neutral-300"
|
||||
aria-label="Send message"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="19" x2="12" y2="5" />
|
||||
<polyline points="5 12 12 5 19 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ChevronDownIcon, HamburgerIcon } from "@/components/icons";
|
||||
import { Sheet, SheetTrigger, SheetContent, SheetTitle } from "@/components/ui/sheet";
|
||||
import type { DocsGroup } from "@/lib/types";
|
||||
|
||||
const COLLAPSE_STORAGE_KEY = "docs-sidebar-collapsed";
|
||||
|
||||
type SidebarNavProps = {
|
||||
groups: DocsGroup[];
|
||||
activeSlug: string;
|
||||
onNavigate?: () => void;
|
||||
};
|
||||
|
||||
function SidebarNav({ groups, activeSlug, onNavigate }: SidebarNavProps) {
|
||||
const initialCollapsed = new Set(
|
||||
groups
|
||||
.filter((g) => g.section !== "Start Here" && !g.items.some((i) => i.slug === activeSlug))
|
||||
.map((g) => g.section),
|
||||
);
|
||||
const [collapsed, setCollapsed] = useState<Set<string>>(initialCollapsed);
|
||||
const restored = useRef(false);
|
||||
|
||||
// Restore the user's expand/collapse choices so navigating between pages
|
||||
// does not reset sections the user opened. (The active section is always
|
||||
// shown expanded via the `hasActive` check below, regardless of this set.)
|
||||
useEffect(() => {
|
||||
if (restored.current) return;
|
||||
restored.current = true;
|
||||
try {
|
||||
const stored = sessionStorage.getItem(COLLAPSE_STORAGE_KEY);
|
||||
if (stored) setCollapsed(new Set(JSON.parse(stored) as string[]));
|
||||
} catch {}
|
||||
}, []);
|
||||
|
||||
function toggle(section: string) {
|
||||
setCollapsed((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(section)) next.delete(section);
|
||||
else next.add(section);
|
||||
try {
|
||||
sessionStorage.setItem(COLLAPSE_STORAGE_KEY, JSON.stringify([...next]));
|
||||
} catch {}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className="px-3 py-4">
|
||||
{groups.map((group) => {
|
||||
const hasActive = group.items.some((item) => item.slug === activeSlug);
|
||||
const isCollapsed = collapsed.has(group.section) && !hasActive;
|
||||
return (
|
||||
<div key={group.section} className="mb-4">
|
||||
<button
|
||||
type="button"
|
||||
aria-expanded={!isCollapsed}
|
||||
onClick={() => toggle(group.section)}
|
||||
className="flex w-full cursor-pointer items-center justify-between rounded bg-transparent px-3 py-2 text-[0.6875rem] font-semibold uppercase tracking-[0.06em] text-muted transition hover:text-fg"
|
||||
>
|
||||
<span>{group.section}</span>
|
||||
<ChevronDownIcon className={`shrink-0 transition-transform ${isCollapsed ? "-rotate-90" : ""}`} />
|
||||
</button>
|
||||
<div
|
||||
className={`grid transition-[grid-template-rows] duration-200 ${
|
||||
isCollapsed ? "grid-rows-[0fr]" : "grid-rows-[1fr]"
|
||||
}`}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
{group.items.map((item) => {
|
||||
const active = item.slug === activeSlug;
|
||||
return (
|
||||
<Link
|
||||
key={item.slug}
|
||||
href={item.path}
|
||||
aria-current={active ? "page" : undefined}
|
||||
onClick={onNavigate}
|
||||
className={`block rounded px-3 py-[0.1875rem] text-[0.8125rem] leading-[1.8] no-underline transition hover:text-fg ${
|
||||
active ? "font-medium text-fg" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
type DocsSidebarShellProps = {
|
||||
groups: DocsGroup[];
|
||||
};
|
||||
|
||||
function activeDocForPath(groups: DocsGroup[], pathname: string) {
|
||||
const normalized = pathname.replace(/\/$/, "") || "/";
|
||||
return groups.flatMap((group) => group.items).find((item) => item.path === normalized);
|
||||
}
|
||||
|
||||
export function DocsSidebarShell({ groups }: DocsSidebarShellProps) {
|
||||
const pathname = usePathname();
|
||||
const activeDoc = activeDocForPath(groups, pathname);
|
||||
const activeSlug = activeDoc?.slug ?? "";
|
||||
const currentTitle = activeDoc?.title;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger
|
||||
aria-label="Open table of contents"
|
||||
className="sticky top-14 z-40 flex w-full items-center justify-between border-b border-border bg-bg/80 px-6 py-3 backdrop-blur-sm focus:outline-none md:hidden"
|
||||
>
|
||||
<span className="text-sm font-medium text-fg">{currentTitle ?? "Documentation"}</span>
|
||||
<span className="flex h-8 w-8 items-center justify-center text-muted">
|
||||
<HamburgerIcon className="h-4 w-4" />
|
||||
</span>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="overflow-y-auto p-0" showCloseButton={false}>
|
||||
<SheetTitle className="px-6 pt-6">Table of Contents</SheetTitle>
|
||||
<SidebarNav groups={groups} activeSlug={activeSlug} onNavigate={() => setOpen(false)} />
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<aside
|
||||
aria-label="Documentation"
|
||||
className="hidden md:sticky md:top-14 md:block md:h-[calc(100vh-3.5rem)] md:w-60 md:overflow-y-auto"
|
||||
>
|
||||
<SidebarNav groups={groups} activeSlug={activeSlug} />
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { Heading } from "@/lib/types";
|
||||
|
||||
export function DocsToc({ headings }: { headings: Heading[] }) {
|
||||
const [activeId, setActiveId] = useState<string | null>(headings[0]?.id ?? null);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || headings.length === 0) return;
|
||||
|
||||
const ids = headings.map((h) => h.id);
|
||||
const elements = ids
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter((el): el is HTMLElement => el !== null);
|
||||
if (elements.length === 0) return;
|
||||
|
||||
const visible = new Set();
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) visible.add(entry.target.id);
|
||||
else visible.delete(entry.target.id);
|
||||
}
|
||||
|
||||
let next = null;
|
||||
for (const h of headings) {
|
||||
if (visible.has(h.id)) {
|
||||
next = h.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!next && visible.size === 0) {
|
||||
const scrollY = window.scrollY;
|
||||
for (let i = elements.length - 1; i >= 0; i--) {
|
||||
if (elements[i].offsetTop <= scrollY + 80) {
|
||||
next = elements[i].id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (next) setActiveId(next);
|
||||
},
|
||||
{ rootMargin: "-64px 0px -60% 0px", threshold: 0 },
|
||||
);
|
||||
|
||||
elements.forEach((el) => observer.observe(el));
|
||||
observerRef.current = observer;
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [headings]);
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<aside
|
||||
aria-label="On this page"
|
||||
className="sticky top-14 hidden h-[calc(100vh-3.5rem)] overflow-y-auto py-8 pl-0 pr-4 lg:block"
|
||||
>
|
||||
<p className="mb-3 pl-4 text-xs font-semibold uppercase tracking-[0.05em] text-fg">
|
||||
On this page
|
||||
</p>
|
||||
<nav>
|
||||
{headings.map((h) => {
|
||||
const indent = h.level === 3 ? "pl-7" : h.level === 4 ? "pl-10" : "pl-4";
|
||||
return (
|
||||
<a
|
||||
key={h.id}
|
||||
href={`#${h.id}`}
|
||||
className={`block py-1 pr-4 ${indent} text-[0.8125rem] leading-snug no-underline transition hover:text-fg ${
|
||||
activeId === h.id ? "text-fg" : "text-muted"
|
||||
}`}
|
||||
>
|
||||
{h.text}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
type FlowTone = "graph" | "text" | "compiler" | "human";
|
||||
|
||||
type FlowNodeSpec = {
|
||||
id: string;
|
||||
label: string;
|
||||
x: number;
|
||||
y: number;
|
||||
tone?: FlowTone;
|
||||
};
|
||||
|
||||
type FlowEdgeSpec = {
|
||||
id?: string;
|
||||
source: string;
|
||||
target: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type FlowChartSpec = {
|
||||
type: "flow";
|
||||
title?: string;
|
||||
height?: number;
|
||||
nodes: FlowNodeSpec[];
|
||||
edges: FlowEdgeSpec[];
|
||||
};
|
||||
|
||||
// x/y in the spec are the top-left of each node, in px.
|
||||
const NODE_W = 224;
|
||||
const NODE_H = 72;
|
||||
const PAD = 80;
|
||||
|
||||
const TONE_CLASS: Record<FlowTone, string> = {
|
||||
// faint, "old" loop
|
||||
text: "border border-border bg-bg text-muted",
|
||||
// the enforcer: strong inverted
|
||||
compiler: "bg-fg text-bg",
|
||||
// the good path: clean and present
|
||||
graph: "border border-fg/40 bg-surface text-fg",
|
||||
// human review
|
||||
human: "border border-border bg-surface-muted text-fg",
|
||||
};
|
||||
|
||||
export function FlowChart({ spec }: { spec: FlowChartSpec }) {
|
||||
const byId = new Map(spec.nodes.map((n) => [n.id, n]));
|
||||
const maxX = Math.max(0, ...spec.nodes.map((n) => n.x));
|
||||
const maxY = Math.max(0, ...spec.nodes.map((n) => n.y));
|
||||
const width = PAD * 2 + maxX + NODE_W;
|
||||
const height = Math.max(spec.height ?? 0, PAD * 2 + maxY + NODE_H);
|
||||
|
||||
const cx = (n: FlowNodeSpec) => PAD + n.x + NODE_W / 2;
|
||||
const topY = (n: FlowNodeSpec) => PAD + n.y;
|
||||
const bottomY = (n: FlowNodeSpec) => PAD + n.y + NODE_H;
|
||||
|
||||
const edges = spec.edges
|
||||
.map((edge, i) => {
|
||||
const s = byId.get(edge.source);
|
||||
const t = byId.get(edge.target);
|
||||
if (!s || !t) return null;
|
||||
const loopBack = t.y <= s.y;
|
||||
let path: string;
|
||||
let labelX: number;
|
||||
let labelY: number;
|
||||
if (loopBack) {
|
||||
const leftEdge = Math.min(PAD + s.x, PAD + t.x);
|
||||
const sy = PAD + s.y + NODE_H / 2;
|
||||
const ty = PAD + t.y + NODE_H / 2;
|
||||
const bulge = leftEdge - 40;
|
||||
path = `M ${leftEdge} ${sy} C ${bulge} ${sy}, ${bulge} ${ty}, ${leftEdge} ${ty}`;
|
||||
labelX = bulge;
|
||||
labelY = (sy + ty) / 2;
|
||||
} else {
|
||||
const sx = cx(s);
|
||||
const sy = bottomY(s);
|
||||
const tx = cx(t);
|
||||
const ty = topY(t);
|
||||
const mid = (sy + ty) / 2;
|
||||
path = `M ${sx} ${sy} C ${sx} ${mid}, ${tx} ${mid}, ${tx} ${ty}`;
|
||||
labelX = (sx + tx) / 2;
|
||||
labelY = mid;
|
||||
}
|
||||
return {
|
||||
key: edge.id ?? `${edge.source}-${edge.target}-${i}`,
|
||||
path,
|
||||
label: edge.label,
|
||||
labelX,
|
||||
labelY,
|
||||
};
|
||||
})
|
||||
.filter((e): e is NonNullable<typeof e> => e !== null);
|
||||
|
||||
// Only show a connection dot where an edge actually attaches. Normal edges
|
||||
// attach to the source's bottom and the target's top; loop-backs attach to
|
||||
// the side, so they do not light up a top/bottom dot.
|
||||
const topConn = new Set<string>();
|
||||
const bottomConn = new Set<string>();
|
||||
for (const edge of spec.edges) {
|
||||
const s = byId.get(edge.source);
|
||||
const t = byId.get(edge.target);
|
||||
if (!s || !t || t.y <= s.y) continue;
|
||||
bottomConn.add(s.id);
|
||||
topConn.add(t.id);
|
||||
}
|
||||
|
||||
const stroke = "color-mix(in srgb, var(--color-fg) 28%, transparent)";
|
||||
const dot = "color-mix(in srgb, var(--color-fg) 40%, transparent)";
|
||||
|
||||
return (
|
||||
<div className="not-prose my-6 overflow-hidden rounded-2xl border border-border bg-bg">
|
||||
{spec.title ? (
|
||||
<div className="border-b border-border px-5 py-3.5 font-mono text-xs font-medium text-muted">
|
||||
{spec.title}
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className="overflow-hidden p-2"
|
||||
style={{
|
||||
backgroundImage: "radial-gradient(var(--color-border) 1px, transparent 1px)",
|
||||
backgroundSize: "22px 22px",
|
||||
backgroundPosition: "-1px -1px",
|
||||
}}
|
||||
>
|
||||
{/* Scales down to fit the column; never upscales past its natural size. */}
|
||||
<svg
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className="mx-auto block h-auto w-full"
|
||||
style={{ maxWidth: width }}
|
||||
role="img"
|
||||
aria-label={spec.title ?? "diagram"}
|
||||
>
|
||||
{edges.map((e) => (
|
||||
<path key={e.key} d={e.path} fill="none" stroke={stroke} strokeWidth={1.5} />
|
||||
))}
|
||||
{spec.nodes.map((n) => (
|
||||
<g key={`dots-${n.id}`} fill={dot}>
|
||||
{topConn.has(n.id) && <circle cx={cx(n)} cy={topY(n)} r={3} />}
|
||||
{bottomConn.has(n.id) && <circle cx={cx(n)} cy={bottomY(n)} r={3} />}
|
||||
</g>
|
||||
))}
|
||||
|
||||
{edges
|
||||
.filter((e) => e.label)
|
||||
.map((e) => (
|
||||
<foreignObject
|
||||
key={`label-${e.key}`}
|
||||
x={e.labelX - 40}
|
||||
y={e.labelY - 12}
|
||||
width={80}
|
||||
height={24}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<span className="rounded border border-border bg-bg px-1.5 py-0.5 font-mono text-[0.6875rem] text-muted">
|
||||
{e.label}
|
||||
</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
))}
|
||||
|
||||
{spec.nodes.map((n) => (
|
||||
<foreignObject key={n.id} x={PAD + n.x} y={PAD + n.y} width={NODE_W} height={NODE_H}>
|
||||
<div
|
||||
className={`flex h-full w-full items-center justify-center rounded-xl px-4 text-center text-[0.875rem] font-medium leading-snug ${
|
||||
TONE_CLASS[n.tone ?? "text"]
|
||||
}`}
|
||||
>
|
||||
{n.label}
|
||||
</div>
|
||||
</foreignObject>
|
||||
))}
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import type { MouseEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
export function HeadingAnchor({ id }: { id?: string }) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
if (!id) return null;
|
||||
|
||||
async function handleClick(event: MouseEvent<HTMLAnchorElement>) {
|
||||
event.preventDefault();
|
||||
const url = `${window.location.origin}${window.location.pathname}#${id}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
setCopied(true);
|
||||
} catch {}
|
||||
if (timer.current) clearTimeout(timer.current);
|
||||
timer.current = setTimeout(() => setCopied(false), 1400);
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`#${id}`}
|
||||
onClick={handleClick}
|
||||
aria-label={copied ? "Anchor link copied" : "Copy anchor link"}
|
||||
className="heading-anchor ml-2 inline-flex h-5 w-5 items-center justify-center align-middle text-muted no-underline opacity-0 transition-opacity hover:text-fg group-hover:opacity-100 focus-visible:opacity-100"
|
||||
>
|
||||
{copied ? (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import type { CSSProperties } from "react";
|
||||
import { useState } from "react";
|
||||
import { highlight } from "@/lib/highlight";
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
command: "zero init",
|
||||
output: 'Created zero.toml\nCreated zero.graph graph:e3b0c442\nInitialized package "hello"',
|
||||
},
|
||||
{
|
||||
command: 'zero patch --op addMain --op \'addCheckWrite fn="main" text="hello from zero\\n"\'',
|
||||
output: "Patched zero.graph\n graph hash graph:a7f7e689\n symbols main",
|
||||
},
|
||||
{
|
||||
command: "zero run",
|
||||
output: "hello from zero",
|
||||
},
|
||||
];
|
||||
|
||||
function TerminalIcon() {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="shrink-0 text-muted"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="4 17 10 11 4 5" />
|
||||
<line x1="12" y1="19" x2="20" y2="19" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChatToolRuns({ startDelayMs = 0 }: { startDelayMs?: number }) {
|
||||
const [open, setOpen] = useState<number[]>([]);
|
||||
|
||||
const toggle = (i: number) =>
|
||||
setOpen((prev) => (prev.includes(i) ? prev.filter((n) => n !== i) : [...prev, i]));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
{STEPS.map((step, i) => {
|
||||
const isOpen = open.includes(i);
|
||||
return (
|
||||
<div
|
||||
key={step.command}
|
||||
className="home-tool-row overflow-hidden rounded-lg border border-border bg-surface"
|
||||
style={{ "--chat-delay": `${startDelayMs + i * 180}ms` } as CSSProperties}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggle(i)}
|
||||
aria-expanded={isOpen}
|
||||
className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 font-mono text-[0.78125rem] leading-relaxed text-muted transition-colors hover:bg-surface-muted"
|
||||
>
|
||||
<TerminalIcon />
|
||||
<span
|
||||
className={`min-w-0 flex-1 text-left text-fg/80 ${
|
||||
isOpen ? "whitespace-pre-wrap break-all" : "truncate"
|
||||
}`}
|
||||
dangerouslySetInnerHTML={{ __html: highlight(step.command, "sh") }}
|
||||
/>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<pre className="m-0 overflow-x-auto border-t border-border/50 bg-bg px-3 py-2.5 text-[0.78125rem] leading-relaxed text-fg/70">
|
||||
{step.output}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
export function HomeChatViewport({ children }: { children: ReactNode }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = ref.current;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isReady = () => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
const fitsInViewport = rect.height <= viewportHeight;
|
||||
|
||||
if (fitsInViewport) {
|
||||
return rect.top >= 0 && rect.bottom <= viewportHeight;
|
||||
}
|
||||
|
||||
return rect.top <= viewportHeight * 0.12 && rect.bottom >= viewportHeight * 0.88;
|
||||
};
|
||||
|
||||
const update = () => {
|
||||
if (!isReady()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setVisible(true);
|
||||
window.removeEventListener("scroll", update);
|
||||
window.removeEventListener("resize", update);
|
||||
};
|
||||
|
||||
update();
|
||||
window.addEventListener("scroll", update, { passive: true });
|
||||
window.addEventListener("resize", update);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("scroll", update);
|
||||
window.removeEventListener("resize", update);
|
||||
};
|
||||
}, [visible]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="home-chat-viewport" data-chat-visible={visible ? "true" : "false"}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function LogoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="18" height="16" viewBox="0 0 76 65" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path d="M37.5274 0L75.0548 65H0L37.5274 0Z" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ArrowRightIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function GithubIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" {...props}>
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ChevronDownIcon({ className = "", ...props }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function HamburgerIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" {...props}>
|
||||
<line x1="3" y1="6" x2="21" y2="6" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function CloseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" {...props}>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
const TABS = [
|
||||
{
|
||||
id: "humans",
|
||||
label: "For humans",
|
||||
command: "curl -fsSL https://zerolang.ai/install.sh | bash",
|
||||
},
|
||||
{
|
||||
id: "agents",
|
||||
label: "For agents",
|
||||
command: "npx skills add vercel-labs/zerolang",
|
||||
},
|
||||
] as const;
|
||||
|
||||
type TabId = (typeof TABS)[number]["id"];
|
||||
|
||||
export function InstallCopy() {
|
||||
const [activeId, setActiveId] = useState<TabId>(TABS[0].id);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const active = TABS.find((t) => t.id === activeId) ?? TABS[0];
|
||||
|
||||
async function handleCopy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(active.command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1400);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-6 flex w-full max-w-[32rem] flex-col items-center gap-3">
|
||||
<div className="flex items-center gap-4 text-[0.9375rem]">
|
||||
{TABS.map((tab, i) => (
|
||||
<div key={tab.id} className="flex items-center gap-4">
|
||||
{i > 0 && <span className="h-4 w-px bg-border" />}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveId(tab.id);
|
||||
setCopied(false);
|
||||
}}
|
||||
className={`cursor-pointer font-medium transition ${
|
||||
activeId === tab.id ? "text-fg" : "text-muted hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex w-full items-center overflow-hidden rounded-md border border-border bg-surface">
|
||||
<code className="flex-1 overflow-x-auto whitespace-nowrap px-4 py-2 text-left font-mono text-[0.8125rem] tracking-tight text-muted">
|
||||
<span className="text-muted/60">$ </span>
|
||||
{active.command}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Copy install command"
|
||||
onClick={handleCopy}
|
||||
className="flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center self-stretch border-l border-border text-muted transition hover:bg-surface-muted hover:text-fg"
|
||||
>
|
||||
{copied ? (
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" className="text-success">
|
||||
<path d="M13.78 3.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 8.28a.75.75 0 0 1 1.06-1.06L6 9.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
|
||||
<path d="M5 1.75A1.75 1.75 0 0 1 6.75 0h5.5A1.75 1.75 0 0 1 14 1.75v5.5A1.75 1.75 0 0 1 12.25 9H11V7.5h1.25a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-5.5a.25.25 0 0 0-.25.25V3H5V1.75Z" />
|
||||
<path d="M2 4.75A1.75 1.75 0 0 1 3.75 3h5.5A1.75 1.75 0 0 1 11 4.75v5.5A1.75 1.75 0 0 1 9.25 12h-5.5A1.75 1.75 0 0 1 2 10.25v-5.5Zm1.75-.25a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h5.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-5.5Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/* Theme-aware, responsive version of the "agent coding loop" comparison.
|
||||
Scales to fit its container (never upscales past natural size). */
|
||||
|
||||
type Tone = "step" | "compiler" | "optional";
|
||||
type Node = { label: string; tone: Tone };
|
||||
type Loop = { from: number; to: number; side: "left" | "right" };
|
||||
type Col = { x: number; kicker: string; title: string; loop: Loop; nodes: Node[] };
|
||||
|
||||
const W = 372;
|
||||
const H = 68;
|
||||
const NSTART = 88;
|
||||
const STEP = 104;
|
||||
|
||||
const COLS: Col[] = [
|
||||
{
|
||||
x: 44,
|
||||
kicker: "Traditional",
|
||||
title: "Text is the source of truth",
|
||||
loop: { from: 5, to: 0, side: "left" },
|
||||
nodes: [
|
||||
{ label: "agent writes source text", tone: "step" },
|
||||
{ label: "format", tone: "step" },
|
||||
{ label: "check", tone: "step" },
|
||||
{ label: "build", tone: "step" },
|
||||
{ label: "test", tone: "step" },
|
||||
{ label: "inspect failures", tone: "step" },
|
||||
],
|
||||
},
|
||||
{
|
||||
x: 520,
|
||||
kicker: "Zerolang",
|
||||
title: "Graph is the program",
|
||||
loop: { from: 1, to: 0, side: "right" },
|
||||
nodes: [
|
||||
{ label: "agent writes graph patch", tone: "step" },
|
||||
{ label: "compiler checks patch", tone: "compiler" },
|
||||
{ label: "projection available for review", tone: "optional" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const TONE: Record<Tone, string> = {
|
||||
step: "h-full w-full border border-border bg-surface text-fg",
|
||||
compiler: "h-full w-full bg-fg font-semibold text-bg",
|
||||
optional: "h-full w-full border border-dashed border-border bg-transparent text-muted",
|
||||
};
|
||||
|
||||
const VB_W = 936;
|
||||
const MAX = Math.max(...COLS.map((c) => c.nodes.length));
|
||||
const VB_H = NSTART + (MAX - 1) * STEP + H;
|
||||
|
||||
const STROKE = "color-mix(in srgb, var(--color-fg) 30%, transparent)";
|
||||
const ARROW = "color-mix(in srgb, var(--color-fg) 55%, transparent)";
|
||||
const nodeTop = (i: number) => NSTART + i * STEP;
|
||||
|
||||
export function LoopDiagram() {
|
||||
const verticals: { key: string; d: string }[] = [];
|
||||
const loops: { key: string; d: string; lx: number; ly: number }[] = [];
|
||||
|
||||
for (const col of COLS) {
|
||||
const cx = col.x + W / 2;
|
||||
col.nodes.forEach((_, i) => {
|
||||
if (i < col.nodes.length - 1) {
|
||||
verticals.push({
|
||||
key: `${col.x}-${i}`,
|
||||
d: `M ${cx} ${nodeTop(i) + H + 2} L ${cx} ${nodeTop(i + 1) - 4}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
const sy = nodeTop(col.loop.from) + H / 2;
|
||||
const ty = nodeTop(col.loop.to) + H / 2;
|
||||
const r = 12;
|
||||
let d: string;
|
||||
let lx: number;
|
||||
if (col.loop.side === "right") {
|
||||
const edge = col.x + W;
|
||||
const bulge = edge + 28;
|
||||
d = `M ${edge} ${sy} H ${bulge - r} Q ${bulge} ${sy} ${bulge} ${sy - r} V ${ty + r} Q ${bulge} ${ty} ${bulge - r} ${ty} H ${edge + 3}`;
|
||||
lx = bulge;
|
||||
} else {
|
||||
const edge = col.x;
|
||||
const bulge = edge - 36;
|
||||
d = `M ${edge} ${sy} H ${bulge + r} Q ${bulge} ${sy} ${bulge} ${sy - r} V ${ty + r} Q ${bulge} ${ty} ${bulge + r} ${ty} H ${edge - 3}`;
|
||||
lx = bulge;
|
||||
}
|
||||
loops.push({ key: `loop-${col.x}`, d, lx, ly: (sy + ty) / 2 });
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
viewBox={`-40 0 ${VB_W + 80} ${VB_H}`}
|
||||
className="mx-auto block h-auto w-full"
|
||||
style={{ maxWidth: VB_W + 80 }}
|
||||
role="img"
|
||||
aria-label="Traditional source loop versus the Zerolang graph loop"
|
||||
>
|
||||
<defs>
|
||||
<marker id="loop-ah" markerUnits="userSpaceOnUse" markerWidth="10" markerHeight="10" refX="7" refY="5" orient="auto">
|
||||
<path d="M1.5 1.5 L8 5 L1.5 8.5 Z" fill={ARROW} />
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
{/* column headers */}
|
||||
{COLS.map((col) => (
|
||||
<foreignObject key={`h-${col.x}`} x={col.x} y={0} width={W} height={NSTART - 24}>
|
||||
<div className="font-mono text-[12px] font-medium uppercase tracking-[0.2em] text-muted">
|
||||
{col.kicker}
|
||||
</div>
|
||||
<div className="mt-1.5 text-[20px] font-medium tracking-[-0.02em] text-fg">{col.title}</div>
|
||||
</foreignObject>
|
||||
))}
|
||||
|
||||
{verticals.map((v) => (
|
||||
<path key={v.key} d={v.d} fill="none" stroke={STROKE} strokeWidth={1.75} markerEnd="url(#loop-ah)" />
|
||||
))}
|
||||
{loops.map((l) => (
|
||||
<path key={l.key} d={l.d} fill="none" stroke={STROKE} strokeWidth={1.75} markerEnd="url(#loop-ah)" />
|
||||
))}
|
||||
|
||||
{loops.map((l) => (
|
||||
<foreignObject key={`lbl-${l.key}`} x={l.lx - 40} y={l.ly - 16} width={80} height={32}>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<span className="rounded border border-border bg-bg px-1.5 py-0.5 font-mono text-[11px] text-muted">
|
||||
repeat
|
||||
</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
))}
|
||||
|
||||
{COLS.map((col) =>
|
||||
col.nodes.map((n, i) => (
|
||||
<foreignObject key={`${col.x}-${i}`} x={col.x} y={nodeTop(i)} width={W} height={H}>
|
||||
<div
|
||||
className={`flex items-center justify-center rounded-2xl px-4 text-center text-[16px] font-medium leading-[1.2] tracking-[-0.01em] ${TONE[n.tone]}`}
|
||||
>
|
||||
{n.label}
|
||||
</div>
|
||||
</foreignObject>
|
||||
)),
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import type { KeyboardEvent } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SearchResult } from "@/lib/types";
|
||||
|
||||
export function Search() {
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState<SearchResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const listRef = useRef<HTMLDivElement | null>(null);
|
||||
const abortRef = useRef<AbortController | null>(null);
|
||||
|
||||
const navigate = useCallback(
|
||||
(href: string) => {
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
router.push(href);
|
||||
},
|
||||
[router],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: globalThis.KeyboardEvent) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === "k") {
|
||||
e.preventDefault();
|
||||
setOpen((prev) => !prev);
|
||||
}
|
||||
}
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
return () => document.removeEventListener("keydown", onKeyDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setTimeout(() => inputRef.current?.focus(), 0);
|
||||
} else {
|
||||
setQuery("");
|
||||
setResults([]);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const q = query.trim();
|
||||
if (!q) {
|
||||
setResults([]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
abortRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal });
|
||||
if (res.ok) {
|
||||
const data = (await res.json()) as { results?: SearchResult[] };
|
||||
setResults(Array.isArray(data.results) ? data.results : []);
|
||||
}
|
||||
} catch {
|
||||
// aborted or network error
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
}
|
||||
}, 150);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
controller.abort();
|
||||
};
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(0);
|
||||
}, [results]);
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
|
||||
if (e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
setActiveIndex((i) => Math.min(i + 1, results.length - 1));
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
setActiveIndex((i) => Math.max(i - 1, 0));
|
||||
} else if (e.key === "Enter" && results[activeIndex]) {
|
||||
e.preventDefault();
|
||||
navigate(results[activeIndex].href);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const active = listRef.current?.querySelector("[data-active='true']");
|
||||
active?.scrollIntoView({ block: "nearest" });
|
||||
}, [activeIndex]);
|
||||
|
||||
const hasQuery = query.trim().length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="hidden cursor-pointer items-center gap-2 rounded-md border border-border bg-surface-muted px-3 py-1.5 text-sm text-muted transition-colors hover:border-fg/25 hover:text-fg sm:flex"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
Search docs
|
||||
<kbd className="pointer-events-none ml-1 inline-flex items-center gap-0.5 rounded border border-border bg-bg px-1.5 py-0.5 font-mono text-[10px] text-muted">
|
||||
<span>⌘</span>K
|
||||
</kbd>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="flex cursor-pointer items-center text-muted transition-colors hover:text-fg sm:hidden"
|
||||
aria-label="Search docs"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent showCloseButton={false} className="gap-0 p-0 sm:max-w-lg">
|
||||
<DialogTitle className="sr-only">Search documentation</DialogTitle>
|
||||
<div className="flex items-center gap-2 border-b border-border px-3">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0 text-muted">
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Search docs..."
|
||||
className="flex-1 bg-transparent py-3 text-sm outline-none placeholder:text-muted"
|
||||
/>
|
||||
{query && (
|
||||
<button onClick={() => setQuery("")} className="text-muted hover:text-fg">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div ref={listRef} className="max-h-[min(60vh,400px)] overflow-y-auto p-2">
|
||||
{loading && hasQuery ? (
|
||||
<div className="flex items-center justify-center py-6">
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-2 border-muted border-t-transparent" />
|
||||
</div>
|
||||
) : hasQuery && results.length === 0 ? (
|
||||
<p className="py-6 text-center text-sm text-muted">No results found.</p>
|
||||
) : !hasQuery ? (
|
||||
<p className="py-6 text-center text-sm text-muted">Type to search documentation...</p>
|
||||
) : (
|
||||
results.map((item, i) => (
|
||||
<button
|
||||
key={item.href}
|
||||
data-active={i === activeIndex}
|
||||
onClick={() => navigate(item.href)}
|
||||
onMouseEnter={() => setActiveIndex(i)}
|
||||
className={cn(
|
||||
"flex w-full flex-col gap-1 rounded-md px-3 py-2 text-left transition-colors",
|
||||
i === activeIndex ? "bg-surface-muted text-fg" : "text-fg",
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium">{item.title}</span>
|
||||
{item.snippet && (
|
||||
<span className="line-clamp-2 text-xs leading-relaxed text-muted">{item.snippet}</span>
|
||||
)}
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Link from "next/link";
|
||||
import { GeistPixelSquare } from "geist/font/pixel";
|
||||
import { Search } from "@/components/search";
|
||||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { LogoIcon } from "@/components/icons";
|
||||
|
||||
export function SiteHeader({ stars }: { stars: string }) {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 bg-white/90 backdrop-blur-sm dark:bg-neutral-950/90">
|
||||
<div className="flex h-14 items-center justify-between gap-6 px-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<LogoIcon width="16" height="14" />
|
||||
<span className={`${GeistPixelSquare.className} text-lg`}>zerolang</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link
|
||||
href="/getting-started"
|
||||
className="text-sm text-neutral-500 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
Docs
|
||||
</Link>
|
||||
<Search />
|
||||
<a
|
||||
href={`https://github.com/${process.env.NEXT_PUBLIC_GITHUB_REPO || "vercel-labs/zerolang"}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1.5 text-sm text-neutral-500 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" className="h-4 w-4" fill="currentColor" aria-hidden="true">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||
</svg>
|
||||
{stars ? <span>{stars}</span> : null}
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
|
||||
export function ThemeProvider({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<NextThemesProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
{children}
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, resolvedTheme, setTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="h-8 w-8" />;
|
||||
}
|
||||
|
||||
const isDark = (theme === "system" ? resolvedTheme : theme) === "dark";
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||
aria-label="Toggle theme"
|
||||
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-neutral-900 dark:text-neutral-400 dark:hover:bg-neutral-800 dark:hover:text-neutral-100"
|
||||
>
|
||||
{isDark ? (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog(props: ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal(props: ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({ className, ...props }: ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type DialogContentProps = ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
children?: ReactNode;
|
||||
showCloseButton?: boolean;
|
||||
};
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: DialogContentProps) {
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-lg border border-border bg-bg p-6 shadow-card outline-none duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("font-semibold text-fg", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Dialog, DialogPortal, DialogOverlay, DialogContent, DialogTitle };
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { Dialog as SheetPrimitive } from "radix-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Sheet(props: ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
}
|
||||
|
||||
function SheetPortal(props: ComponentProps<typeof SheetPrimitive.Portal>) {
|
||||
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
|
||||
}
|
||||
|
||||
function SheetOverlay({ className, ...props }: ComponentProps<typeof SheetPrimitive.Overlay>) {
|
||||
return (
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type SheetContentProps = ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
children?: ReactNode;
|
||||
side?: "left" | "right";
|
||||
showCloseButton?: boolean;
|
||||
overlayClassName?: string;
|
||||
};
|
||||
|
||||
function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
overlayClassName,
|
||||
...props
|
||||
}: SheetContentProps) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay className={overlayClassName} />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
className={cn(
|
||||
"fixed z-50 flex flex-col gap-4 bg-white shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500 dark:bg-neutral-950",
|
||||
side === "right" &&
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l border-neutral-200 data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right dark:border-neutral-800 sm:max-w-sm",
|
||||
side === "left" &&
|
||||
"inset-y-0 left-0 h-full w-3/4 border-r border-neutral-200 data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left dark:border-neutral-800 sm:max-w-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTitle({ className, ...props }: ComponentProps<typeof SheetPrimitive.Title>) {
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("font-semibold text-neutral-900 dark:text-neutral-100", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SheetTrigger({ className, ...props }: ComponentProps<typeof SheetPrimitive.Trigger>) {
|
||||
return <SheetPrimitive.Trigger data-slot="sheet-trigger" className={cn(className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Sheet, SheetTrigger, SheetContent, SheetTitle };
|
||||
@@ -0,0 +1,55 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { docs, findDocBySlug } from "./docs";
|
||||
import type { Article, Heading } from "./types";
|
||||
|
||||
const ARTICLES_ROOT = join(process.cwd(), "articles");
|
||||
|
||||
export async function readArticleBySlug(slug: string): Promise<string | null> {
|
||||
const doc = findDocBySlug(slug);
|
||||
if (!doc) return null;
|
||||
const relative = doc.sourcePath.replace(/^\/articles\//, "");
|
||||
const filePath = join(ARTICLES_ROOT, relative);
|
||||
return readFile(filePath, "utf8");
|
||||
}
|
||||
|
||||
export async function readArticleByPath(routePath: string): Promise<Article | null> {
|
||||
const doc = docs.find((d) => d.path === routePath);
|
||||
if (!doc) return null;
|
||||
const source = await readArticleBySlug(doc.slug);
|
||||
if (source === null) return null;
|
||||
return { doc, source };
|
||||
}
|
||||
|
||||
export function extractHeadings(markdown: string): Heading[] {
|
||||
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
||||
const headings: Heading[] = [];
|
||||
let inCodeBlock = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith("```")) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
if (inCodeBlock) continue;
|
||||
|
||||
const match = /^(#{2,4})\s+(.+)$/.exec(line.trim());
|
||||
if (match) {
|
||||
const text = match[2].replace(/`([^`]+)`/g, "$1");
|
||||
headings.push({
|
||||
level: match[1].length as Heading["level"],
|
||||
text,
|
||||
id: slugify(match[2]),
|
||||
});
|
||||
}
|
||||
}
|
||||
return headings;
|
||||
}
|
||||
|
||||
function slugify(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
@@ -0,0 +1,506 @@
|
||||
import type { Doc, DocsGroup } from "./types";
|
||||
|
||||
const SECTION_ORDER = [
|
||||
"Start Here",
|
||||
"Concepts",
|
||||
"Reference",
|
||||
"Agent Workflow",
|
||||
"Language Pieces",
|
||||
"Standard Library",
|
||||
"Core",
|
||||
"Text And Data",
|
||||
"Programs",
|
||||
"Runtime And Web",
|
||||
"Build And Runtime",
|
||||
];
|
||||
|
||||
export const docs: Doc[] = [
|
||||
{
|
||||
slug: "getting-started",
|
||||
title: "Getting Started",
|
||||
description: "Install Zerolang and ask an agent to create a graph-first program you can review.",
|
||||
path: "/getting-started",
|
||||
sourcePath: "/articles/getting-started.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "install",
|
||||
title: "Install",
|
||||
description: "Install the latest compiler release and validate your environment.",
|
||||
path: "/install",
|
||||
sourcePath: "/articles/install.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "learn-zero",
|
||||
title: "Learn Zerolang",
|
||||
description: "A human tour of Zerolang's graph-first workflow, projections, and agent conversations.",
|
||||
path: "/learn",
|
||||
sourcePath: "/articles/learn-zero.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "examples",
|
||||
title: "Examples",
|
||||
description: "Runnable graph examples in learning order with copyable commands.",
|
||||
path: "/examples",
|
||||
sourcePath: "/articles/examples.md",
|
||||
section: "Start Here",
|
||||
},
|
||||
{
|
||||
slug: "graph-architecture",
|
||||
title: "Graph Architecture",
|
||||
description: "How Zerolang makes the semantic graph the program instead of treating text as the primary interface.",
|
||||
path: "/concepts/graph-architecture",
|
||||
sourcePath: "/articles/concepts/graph-architecture.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "semantic-vs-text",
|
||||
title: "Semantic Graph Vs Text",
|
||||
description: "How semantic graph edits differ from source-text edits, and why that matters for agents.",
|
||||
path: "/concepts/semantic-vs-text",
|
||||
sourcePath: "/articles/concepts/semantic-vs-text.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "compile-path",
|
||||
title: "Compile Path",
|
||||
description: "How Zerolang's graph-native compile path compares with traditional parse-first compilers.",
|
||||
path: "/concepts/compile-path",
|
||||
sourcePath: "/articles/concepts/compile-path.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "projections",
|
||||
title: "Projections And Round Trips",
|
||||
description: "How .0 projections support human review, manual edits, import/export, and no silent divergence.",
|
||||
path: "/concepts/projections",
|
||||
sourcePath: "/articles/concepts/projections.md",
|
||||
section: "Concepts",
|
||||
},
|
||||
{
|
||||
slug: "cli-reference",
|
||||
title: "CLI Reference",
|
||||
description: "Commands for creating, inspecting, patching, validating, running, and building Zero programs.",
|
||||
path: "/cli",
|
||||
sourcePath: "/articles/cli-reference.md",
|
||||
section: "Reference",
|
||||
},
|
||||
{
|
||||
slug: "diagnostics",
|
||||
title: "Diagnostics And Repair",
|
||||
description: "How humans and agents read errors, structured diagnostics, and repair plans.",
|
||||
path: "/diagnostics",
|
||||
sourcePath: "/articles/diagnostics.md",
|
||||
section: "Agent Workflow",
|
||||
},
|
||||
{
|
||||
slug: "testing",
|
||||
title: "Testing And Reliability",
|
||||
description: "Graph-backed zero test workflows, package tests, snapshots, fuzzing, and hardening gates.",
|
||||
path: "/testing",
|
||||
sourcePath: "/articles/testing.md",
|
||||
section: "Agent Workflow",
|
||||
},
|
||||
{
|
||||
slug: "primitives",
|
||||
title: "Primitives And Types",
|
||||
description: "The graph-visible language pieces behind both graph facts and projection syntax.",
|
||||
path: "/primitives",
|
||||
sourcePath: "/articles/primitives.md",
|
||||
section: "Language Pieces",
|
||||
},
|
||||
{
|
||||
slug: "language-reference",
|
||||
title: "Language Model Reference",
|
||||
description: "Declarations, functions, control flow, capabilities, ownership, packages, and projections.",
|
||||
path: "/reference",
|
||||
sourcePath: "/articles/language-reference.md",
|
||||
section: "Language Pieces",
|
||||
},
|
||||
{
|
||||
slug: "package-manifest",
|
||||
title: "Package And Manifest Reference",
|
||||
description: "zero.toml package manifests, imports, targets, dependencies, and profiles.",
|
||||
path: "/package-manifest",
|
||||
sourcePath: "/articles/package-manifest.md",
|
||||
section: "Language Pieces",
|
||||
},
|
||||
{
|
||||
slug: "standard-library",
|
||||
title: "Standard Library",
|
||||
description: "Graph-backed modules, capabilities, allocation behavior, and helper metadata.",
|
||||
path: "/standard-library",
|
||||
sourcePath: "/articles/standard-library.md",
|
||||
section: "Standard Library",
|
||||
},
|
||||
{
|
||||
slug: "target-capabilities",
|
||||
title: "Target Capabilities",
|
||||
description: "Current host and target-neutral capability boundaries.",
|
||||
path: "/target-capabilities",
|
||||
sourcePath: "/articles/target-capabilities.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "cross-compilation",
|
||||
title: "Cross-Compilation",
|
||||
description: "Targets, capability denial, direct artifacts, and target facts.",
|
||||
path: "/cross-compilation",
|
||||
sourcePath: "/articles/cross-compilation.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "optimization",
|
||||
title: "Optimization And Size Profiles",
|
||||
description: "Profile contracts, size breakdowns, retention reasons, optimization hints, and benchmark trends.",
|
||||
path: "/optimization",
|
||||
sourcePath: "/articles/optimization.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "benchmarks",
|
||||
title: "Benchmarks",
|
||||
description: "Benchmark methodology, cases, and metrics.",
|
||||
path: "/benchmarks",
|
||||
sourcePath: "/articles/benchmarks.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "c-interop",
|
||||
title: "C Interop",
|
||||
description: "Graph-backed C ABI export support and target library audit facts.",
|
||||
path: "/c-interop",
|
||||
sourcePath: "/articles/c-interop.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "building-from-source",
|
||||
title: "Building From Source",
|
||||
description: "Build, use, and validate the local compiler checkout.",
|
||||
path: "/native-compiler",
|
||||
sourcePath: "/articles/building-from-source.md",
|
||||
section: "Build And Runtime",
|
||||
},
|
||||
{
|
||||
slug: "module-ascii",
|
||||
title: "std.ascii",
|
||||
description: "ASCII byte predicates, case conversion, and digit value helpers.",
|
||||
path: "/modules/ascii",
|
||||
sourcePath: "/articles/modules/ascii.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-parse",
|
||||
title: "std.parse",
|
||||
description: "Allocation-free byte scanners and integer/bool parsers.",
|
||||
path: "/modules/parse",
|
||||
sourcePath: "/articles/modules/parse.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-regex",
|
||||
title: "std.regex",
|
||||
description: "Compile-once regular expression matching for a documented ECMA-262-leaning subset.",
|
||||
path: "/modules/regex",
|
||||
sourcePath: "/articles/modules/regex.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-inet",
|
||||
title: "std.inet",
|
||||
description: "Target-neutral IPv4, IPv6, and RFC 1123 hostname literal validation and parsing.",
|
||||
path: "/modules/inet",
|
||||
sourcePath: "/articles/modules/inet.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-codec",
|
||||
title: "std.codec",
|
||||
description: "Little-endian integer helpers, unsigned varints, and CRC-32 primitives.",
|
||||
path: "/modules/codec",
|
||||
sourcePath: "/articles/modules/codec.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-csv",
|
||||
title: "std.csv",
|
||||
description: "Allocation-free CSV validation, record scanning, field decoding, and small writers.",
|
||||
path: "/modules/csv",
|
||||
sourcePath: "/articles/modules/csv.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-mem",
|
||||
title: "std.mem",
|
||||
description: "Span metadata, copy and equality helpers, and the allocator surface.",
|
||||
path: "/modules/mem",
|
||||
sourcePath: "/articles/modules/mem.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-collections",
|
||||
title: "std.collections",
|
||||
description: "Fixed-capacity collection operations over caller-owned storage.",
|
||||
path: "/modules/collections",
|
||||
sourcePath: "/articles/modules/collections.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-search",
|
||||
title: "std.search",
|
||||
description: "Scalar span search and binary-search helpers.",
|
||||
path: "/modules/search",
|
||||
sourcePath: "/articles/modules/search.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-sort",
|
||||
title: "std.sort",
|
||||
description: "In-place sort and sortedness helpers over caller-owned storage.",
|
||||
path: "/modules/sort",
|
||||
sourcePath: "/articles/modules/sort.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-args",
|
||||
title: "std.args",
|
||||
description: "Process argument count and indexed lookup for hosted command-line programs.",
|
||||
path: "/modules/args",
|
||||
sourcePath: "/articles/modules/args.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-cli",
|
||||
title: "std.cli",
|
||||
description: "Hosted flag and option helpers for command-line programs.",
|
||||
path: "/modules/cli",
|
||||
sourcePath: "/articles/modules/cli.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-diag",
|
||||
title: "std.diag",
|
||||
description: "Source offsets, line spans, and diagnostic location formatting.",
|
||||
path: "/modules/diag",
|
||||
sourcePath: "/articles/modules/diag.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-fmt",
|
||||
title: "std.fmt",
|
||||
description: "Caller-buffer formatting for booleans and integer text.",
|
||||
path: "/modules/fmt",
|
||||
sourcePath: "/articles/modules/fmt.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-math",
|
||||
title: "std.math",
|
||||
description: "Pure fixed-width integer helpers and small number-theory routines.",
|
||||
path: "/modules/math",
|
||||
sourcePath: "/articles/modules/math.md",
|
||||
section: "Core",
|
||||
},
|
||||
{
|
||||
slug: "module-path",
|
||||
title: "std.path",
|
||||
description: "Fixed-buffer path helpers with explicit storage and target-aware limits.",
|
||||
path: "/modules/path",
|
||||
sourcePath: "/articles/modules/path.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-str",
|
||||
title: "std.str",
|
||||
description: "Allocation-free byte-string helpers over spans and caller-owned storage.",
|
||||
path: "/modules/str",
|
||||
sourcePath: "/articles/modules/str.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-testing",
|
||||
title: "std.testing",
|
||||
description: "Bool-returning helpers for test blocks and output checks.",
|
||||
path: "/modules/testing",
|
||||
sourcePath: "/articles/modules/testing.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-term",
|
||||
title: "std.term",
|
||||
description: "ANSI terminal sequences, key-byte decoding, hosted terminal metadata, and raw mode.",
|
||||
path: "/modules/term",
|
||||
sourcePath: "/articles/modules/term.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-text",
|
||||
title: "std.text",
|
||||
description: "ASCII and UTF-8 byte-backed text validation.",
|
||||
path: "/modules/text",
|
||||
sourcePath: "/articles/modules/text.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
|
||||
{
|
||||
slug: "module-unicode",
|
||||
title: "std.unicode",
|
||||
description: "Strict UTF-8 codepoint decode/encode iteration and codepoint-class helpers.",
|
||||
path: "/modules/unicode",
|
||||
sourcePath: "/articles/modules/unicode.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-io",
|
||||
title: "std.io",
|
||||
description: "Buffered reader/writer helpers over caller-owned storage.",
|
||||
path: "/modules/io",
|
||||
sourcePath: "/articles/modules/io.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-fs",
|
||||
title: "std.fs",
|
||||
description: "Hosted file reads, writes, and existence checks for CLI programs.",
|
||||
path: "/modules/fs",
|
||||
sourcePath: "/articles/modules/fs.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-json",
|
||||
title: "std.json",
|
||||
description: "Validation, field lookup, explicit-allocator parsing, and caller-buffer writing.",
|
||||
path: "/modules/json",
|
||||
sourcePath: "/articles/modules/json.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-toml",
|
||||
title: "std.toml",
|
||||
description: "TOML validation, shallow field lookup, and typed scalar decode helpers.",
|
||||
path: "/modules/toml",
|
||||
sourcePath: "/articles/modules/toml.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-log",
|
||||
title: "std.log",
|
||||
description: "Explicit-buffer structured log record formatting.",
|
||||
path: "/modules/log",
|
||||
sourcePath: "/articles/modules/log.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-url",
|
||||
title: "std.url",
|
||||
description: "Lexical URL splitting, percent/query encoding, and query helpers.",
|
||||
path: "/modules/url",
|
||||
sourcePath: "/articles/modules/url.md",
|
||||
section: "Text And Data",
|
||||
},
|
||||
{
|
||||
slug: "module-env",
|
||||
title: "std.env",
|
||||
description: "Hosted environment variable lookup.",
|
||||
path: "/modules/env",
|
||||
sourcePath: "/articles/modules/env.md",
|
||||
section: "Programs",
|
||||
},
|
||||
{
|
||||
slug: "module-time",
|
||||
title: "std.time",
|
||||
description: "Duration math, hosted sleep, and target-gated monotonic and wall-clock helpers.",
|
||||
path: "/modules/time",
|
||||
sourcePath: "/articles/modules/time.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-rand",
|
||||
title: "std.rand",
|
||||
description: "Explicit deterministic random sources and target entropy helpers.",
|
||||
path: "/modules/rand",
|
||||
sourcePath: "/articles/modules/rand.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-proc",
|
||||
title: "std.proc",
|
||||
description: "Host process status helpers behind explicit process capability boundaries.",
|
||||
path: "/modules/proc",
|
||||
sourcePath: "/articles/modules/proc.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-pty",
|
||||
title: "std.pty",
|
||||
description: "Hosted pseudoterminal child processes for interactive CLIs and terminal programs.",
|
||||
path: "/modules/pty",
|
||||
sourcePath: "/articles/modules/pty.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-crypto",
|
||||
title: "std.crypto",
|
||||
description: "Hash, keyed hash, constant-time equality, and target entropy helpers.",
|
||||
path: "/modules/crypto",
|
||||
sourcePath: "/articles/modules/crypto.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-net",
|
||||
title: "std.net",
|
||||
description: "Network capability metadata, address builders, timeouts, and bootstrap handles.",
|
||||
path: "/modules/net",
|
||||
sourcePath: "/articles/modules/net.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
{
|
||||
slug: "module-http",
|
||||
title: "std.http",
|
||||
description: "HTTP envelope writing, request parsing, hosted fetch, and response metadata helpers.",
|
||||
path: "/modules/http",
|
||||
sourcePath: "/articles/modules/http.md",
|
||||
section: "Runtime And Web",
|
||||
},
|
||||
];
|
||||
|
||||
export function findDocBySlug(slug: string): Doc | null {
|
||||
return docs.find((doc) => doc.slug === slug) ?? null;
|
||||
}
|
||||
|
||||
export function findDocByPath(path: string): Doc | null {
|
||||
return docs.find((doc) => doc.path === path) ?? null;
|
||||
}
|
||||
|
||||
export function groupBySection(items: Doc[]): DocsGroup[] {
|
||||
const groups: DocsGroup[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const item of items) {
|
||||
const section = item.section ?? "Other";
|
||||
if (!seen.has(section)) {
|
||||
seen.add(section);
|
||||
groups.push({ section, items: [] });
|
||||
}
|
||||
const group = groups.find((g) => g.section === section);
|
||||
if (group) group.items.push(item);
|
||||
}
|
||||
return groups.sort((a, b) => {
|
||||
const ai = SECTION_ORDER.indexOf(a.section);
|
||||
const bi = SECTION_ORDER.indexOf(b.section);
|
||||
if (ai === -1 && bi === -1) return 0;
|
||||
if (ai === -1) return 1;
|
||||
if (bi === -1) return -1;
|
||||
return ai - bi;
|
||||
});
|
||||
}
|
||||
|
||||
export function getAdjacentDocs(slug: string): { prev: Doc | null; next: Doc | null } {
|
||||
const index = docs.findIndex((d) => d.slug === slug);
|
||||
if (index === -1) return { prev: null, next: null };
|
||||
return {
|
||||
prev: index > 0 ? docs[index - 1] : null,
|
||||
next: index < docs.length - 1 ? docs[index + 1] : null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
const REPO = process.env.NEXT_PUBLIC_GITHUB_REPO || "vercel-labs/zerolang";
|
||||
const REVALIDATE = 86400;
|
||||
|
||||
export async function getStarCount(): Promise<string> {
|
||||
try {
|
||||
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
|
||||
headers: { Accept: "application/vnd.github.v3+json" },
|
||||
next: { revalidate: REVALIDATE },
|
||||
});
|
||||
if (!res.ok) return "";
|
||||
const data = (await res.json()) as { stargazers_count?: unknown };
|
||||
const count = data.stargazers_count;
|
||||
if (typeof count !== "number") return "";
|
||||
if (count >= 1000) return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
|
||||
return String(count);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
type TokenName =
|
||||
| "comment"
|
||||
| "string"
|
||||
| "char"
|
||||
| "keyword"
|
||||
| "type"
|
||||
| "function"
|
||||
| "number"
|
||||
| "variable"
|
||||
| "id"
|
||||
| "key"
|
||||
| "boolean"
|
||||
| "punctuation"
|
||||
| "operator";
|
||||
|
||||
type Grammar = ReadonlyArray<readonly [TokenName, RegExp]>;
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
const GRAMMARS: Record<string, Grammar> = {
|
||||
bash: [
|
||||
["string", /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"/],
|
||||
["comment", /#.*/],
|
||||
["variable", /\$\{?[A-Za-z_][A-Za-z0-9_]*\}?/],
|
||||
["key", /--?[A-Za-z0-9][A-Za-z0-9-]*(?:=[^\s\\]+)?/],
|
||||
["keyword", /\b(?:if|then|else|elif|fi|for|while|do|done|case|esac|function|export|local|readonly|return|exit|set|unset)\b/],
|
||||
["function", /(?<![A-Za-z0-9_./-])(?:zero|pnpm|npm|git|curl|make|node|npx|bash|sh|zsh|cd|mkdir|rm|cp|mv|cat|sed|rg)(?=\s|$)/],
|
||||
["number", /\b\d+\b/],
|
||||
["operator", /&&|\|\||\\|[|<>;&=]/],
|
||||
["punctuation", /[{}()[\]]/],
|
||||
],
|
||||
zero: [
|
||||
["comment", /\/\/.*/],
|
||||
["string", /"(?:[^"\\]|\\.)*"/],
|
||||
["char", /'(?:[^'\\\n]|\\(?:[nrt0'"\\]|x[0-9A-Fa-f]{2}))'/],
|
||||
["keyword", /\b(?:pub|fn|let|var|return|raises|if|else|while|for|in|match|check|rescue|raise|use|interface|enum|choice|type|alias|const|as|break|continue|defer|export|extern|packed|static|meta|mut|test|and|or|not|true|false|null)\b/],
|
||||
["type", /\b(?:Void|World|Self|Bool|bool|char|u4|u8|u16|u32|u64|i8|i16|i32|i64|usize|isize|f16|f32|f64|Span|MutSpan|Maybe|Alloc|Arena|FixedBufAlloc|GeneralAlloc|NullAlloc|PageAlloc|ref|mutref|owned|Slice|Array|String|Error|Io|Fs|Net|Env|Args|Clock|Rand|Proc|Sync|Cancel|Reader|Writer|File|Path|Conn|Listener|Address|Config|Type|Field|c_int|c_long|c_size|cstr|[A-Z][A-Za-z0-9_]*)\b/],
|
||||
["function", /\b[a-z_]\w*(?=\s*\()/],
|
||||
["number", /\b(?:\d+\.\d+(?:[eE][+-]?\d+)?|0x[0-9a-fA-F_]+|0b[01_]+|0o[0-7_]+|\d[\d_]*(?:_[A-Za-z][A-Za-z0-9_]*)?)\b/],
|
||||
["variable", /\b[a-z_]\w*(?=\.)/],
|
||||
["punctuation", /[{}()\[\];,.<>]/],
|
||||
["operator", /:|[-+*/%=!&|^~@?]+/],
|
||||
],
|
||||
"zero-graph": [
|
||||
["comment", /\/\/.*/],
|
||||
["string", /"(?:[^"\\]|\\.)*"/],
|
||||
["id", /#[A-Za-z_][A-Za-z0-9_]*/],
|
||||
["keyword", /\b(?:zero-graph|zero-program-graph-patch|origin|module|hash|node|edge|expect|graphHash|set|insert|insertEdge|replace|delete|rename|addFunction|addMain|addParam|addReturnBinary|addLetLiteral|addLetBinary|addReturnValue|addCheckWrite|addCheckWriteValue|addTest|replaceFunctionBody|replaceBlockBody|end|let|var|return|if|else|while|for|in|match|check|rescue|raise|use|test|and|or|not)\b/],
|
||||
["type", /\b(?:Function|Param|Block|Identifier|Literal|MethodCall|Call|FieldAccess|Let|Var|Return|Check|If|While|For|Match|TypeRef|ArrayLiteral|Unary|Binary|Void|World|Self|Bool|bool|char|u4|u8|u16|u32|u64|i8|i16|i32|i64|usize|isize|f16|f32|f64|Span|MutSpan|Maybe|Alloc|Arena|FixedBufAlloc|GeneralAlloc|NullAlloc|PageAlloc|ref|mutref|owned|Slice|Array|String|Error|[A-Z][A-Za-z0-9_]*)\b/],
|
||||
["boolean", /\b(?:true|false|null)\b/],
|
||||
["key", /\b[A-Za-z_][A-Za-z0-9_-]*(?=\s*(?::|=))/],
|
||||
["function", /\b(?:std|[a-z_][A-Za-z0-9_]*)\.[A-Za-z_][A-Za-z0-9_.]*(?=\s|$)/],
|
||||
["number", /\b(?:v\d+|graph:[0-9a-fA-F]+|nodehash:[0-9a-fA-F]+|0x[0-9a-fA-F_]+|0b[01_]+|0o[0-7_]+|\d[\d_]*)\b/],
|
||||
["variable", /\b[a-z_][A-Za-z0-9_]*(?=\.)/],
|
||||
["punctuation", /[{}()\[\];,.<>]/],
|
||||
["operator", /:|=|[-+*/%!&|^~@?]+/],
|
||||
],
|
||||
};
|
||||
|
||||
const LANGUAGE_ALIASES: Record<string, string> = {
|
||||
sh: "bash",
|
||||
shell: "bash",
|
||||
zsh: "bash",
|
||||
graph: "zero-graph",
|
||||
"program-graph": "zero-graph",
|
||||
"zero-program-graph": "zero-graph",
|
||||
"graph-patch": "zero-graph",
|
||||
"zero-graph-patch": "zero-graph",
|
||||
"program-graph-patch": "zero-graph",
|
||||
"zero-program-graph-patch": "zero-graph",
|
||||
};
|
||||
|
||||
function normalizeLanguage(language: string): string {
|
||||
const key = language.trim().toLowerCase();
|
||||
return LANGUAGE_ALIASES[key] ?? key;
|
||||
}
|
||||
|
||||
function looksLikeGraph(code: string): boolean {
|
||||
const text = code.trimStart();
|
||||
return (
|
||||
/^zero-graph\s+v\d+\b/.test(text) ||
|
||||
/^zero-program-graph-patch\s+v\d+\b/.test(text) ||
|
||||
/^(?:node|edge)\s+#[A-Za-z_][A-Za-z0-9_]*\b/m.test(text) ||
|
||||
/^(?:expect\s+graphHash|set\s+node=|insert\s+node=|insertEdge\s+|replace\s+node=|rename\s+node=|delete\s+node=|replaceFunctionBody\s+|replaceBlockBody\s+)/m.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
export function highlightLanguage(language: string, code: string): string | null {
|
||||
const normalized = normalizeLanguage(language);
|
||||
if (GRAMMARS[normalized]) return normalized;
|
||||
if ((normalized === "" || normalized === "text" || normalized === "txt") && looksLikeGraph(code)) {
|
||||
return "zero-graph";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function highlight(code: string, language: string): string {
|
||||
const normalized = highlightLanguage(language, code);
|
||||
const grammar = normalized ? GRAMMARS[normalized] : null;
|
||||
if (!grammar) return escapeHtml(code);
|
||||
|
||||
const combined = new RegExp(
|
||||
grammar.map(([, pattern]) => `(${pattern.source})`).join("|"),
|
||||
"gm",
|
||||
);
|
||||
|
||||
const names = grammar.map(([name]) => name);
|
||||
let result = "";
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of code.matchAll(combined)) {
|
||||
const matchIndex = match.index ?? 0;
|
||||
if (matchIndex > lastIndex) {
|
||||
result += escapeHtml(code.slice(lastIndex, matchIndex));
|
||||
}
|
||||
const groupIndex = match.slice(1).findIndex((g) => g !== undefined);
|
||||
if (groupIndex === -1) continue;
|
||||
const tokenType = names[groupIndex];
|
||||
const tokenText = match[0];
|
||||
result += `<span class="hl-${tokenType} hl-${normalized}-${tokenType}">${escapeHtml(tokenText)}</span>`;
|
||||
lastIndex = matchIndex + tokenText.length;
|
||||
}
|
||||
|
||||
if (lastIndex < code.length) {
|
||||
result += escapeHtml(code.slice(lastIndex));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { Metadata } from "next";
|
||||
import { PAGE_TITLES } from "./page-titles";
|
||||
import { findDocByPath } from "./docs";
|
||||
|
||||
const SITE_NAME = "Zero";
|
||||
const DESCRIPTION =
|
||||
"zerolang is an experimental graph-first programming language where agents work with semantic program structure instead of raw source text.";
|
||||
|
||||
export function pageMetadata(slug: string): Metadata {
|
||||
const title = PAGE_TITLES[slug];
|
||||
if (title === undefined) return {};
|
||||
|
||||
const displayTitle = title.replace(/\n/g, " ");
|
||||
const fullTitle = slug === "" ? `${SITE_NAME} | ${displayTitle}` : `${displayTitle} | ${SITE_NAME}`;
|
||||
const ogImageUrl = slug ? `/og/${slug}` : "/og";
|
||||
|
||||
const doc = slug ? findDocByPath(`/${slug}`) : null;
|
||||
const description = doc?.description ?? DESCRIPTION;
|
||||
|
||||
return {
|
||||
title: slug === "" ? fullTitle : displayTitle,
|
||||
description,
|
||||
openGraph: {
|
||||
type: "website",
|
||||
locale: "en_US",
|
||||
siteName: SITE_NAME,
|
||||
title: fullTitle,
|
||||
description,
|
||||
images: [
|
||||
{
|
||||
url: ogImageUrl,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: `${displayTitle} — ${SITE_NAME}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: "summary_large_image",
|
||||
title: fullTitle,
|
||||
description,
|
||||
images: [ogImageUrl],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { docs } from "./docs";
|
||||
|
||||
const DOCS_TITLES: Record<string, string> = Object.fromEntries(
|
||||
docs.map((doc) => [doc.path.replace(/^\//, ""), doc.title]),
|
||||
);
|
||||
|
||||
export const PAGE_TITLES: Record<string, string> = {
|
||||
"": "An agent-first language experiment.",
|
||||
...DOCS_TITLES,
|
||||
};
|
||||
|
||||
export function getPageTitle(slug: string): string | null {
|
||||
return slug in PAGE_TITLES ? PAGE_TITLES[slug] : null;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { Redis } from "@upstash/redis";
|
||||
|
||||
let minuteRateLimit: Ratelimit | null = null;
|
||||
let dailyRateLimit: Ratelimit | null = null;
|
||||
|
||||
export function isDocsChatRateLimitConfigured() {
|
||||
return Boolean(process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN);
|
||||
}
|
||||
|
||||
function getRedis() {
|
||||
const url = process.env.KV_REST_API_URL;
|
||||
const token = process.env.KV_REST_API_TOKEN;
|
||||
|
||||
if (!url || !token) {
|
||||
throw new Error("Docs chat rate limiting requires KV_REST_API_URL and KV_REST_API_TOKEN");
|
||||
}
|
||||
|
||||
return new Redis({ url, token });
|
||||
}
|
||||
|
||||
function readPositiveInt(name: string, fallback: number): number {
|
||||
const value = Number(process.env[name]);
|
||||
return Number.isInteger(value) && value > 0 ? value : fallback;
|
||||
}
|
||||
|
||||
const MINUTE_LIMIT = readPositiveInt("RATE_LIMIT_PER_MINUTE", 10);
|
||||
const DAILY_LIMIT = readPositiveInt("RATE_LIMIT_PER_DAY", 100);
|
||||
|
||||
export const docsChatMinuteRateLimit = {
|
||||
limit: async (identifier: string) => {
|
||||
if (!minuteRateLimit) {
|
||||
const redis = getRedis();
|
||||
minuteRateLimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.slidingWindow(MINUTE_LIMIT, "1 m"),
|
||||
prefix: "ratelimit:docs-chat:minute",
|
||||
});
|
||||
}
|
||||
return minuteRateLimit.limit(identifier);
|
||||
},
|
||||
};
|
||||
|
||||
export const docsChatDailyRateLimit = {
|
||||
limit: async (identifier: string) => {
|
||||
if (!dailyRateLimit) {
|
||||
const redis = getRedis();
|
||||
dailyRateLimit = new Ratelimit({
|
||||
redis,
|
||||
limiter: Ratelimit.fixedWindow(DAILY_LIMIT, "1 d"),
|
||||
prefix: "ratelimit:docs-chat:daily",
|
||||
});
|
||||
}
|
||||
return dailyRateLimit.limit(identifier);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
// Rewrites ```json-render fenced code blocks into custom elements carrying
|
||||
// base64-encoded JSON, so the docs can embed structured React components
|
||||
// without switching the pipeline to MDX. Runs before rehype-pretty-code so the
|
||||
// block is never treated as a normal code fence.
|
||||
|
||||
type HastNode = {
|
||||
type?: string;
|
||||
tagName?: string;
|
||||
value?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
children?: HastNode[];
|
||||
};
|
||||
|
||||
function getNodeText(node?: HastNode): string {
|
||||
if (!node) return "";
|
||||
if (node.type === "text") return node.value ?? "";
|
||||
if (!Array.isArray(node.children)) return "";
|
||||
return node.children.map(getNodeText).join("");
|
||||
}
|
||||
|
||||
function getCodeLanguage(node: HastNode): string {
|
||||
const className = node.properties?.className;
|
||||
const classes = Array.isArray(className)
|
||||
? className
|
||||
: typeof className === "string"
|
||||
? className.split(/\s+/)
|
||||
: [];
|
||||
const languageClass = classes.find(
|
||||
(value) => typeof value === "string" && value.startsWith("language-"),
|
||||
);
|
||||
return typeof languageClass === "string" ? languageClass.slice("language-".length) : "";
|
||||
}
|
||||
|
||||
function visit(node: HastNode | undefined, callback: (node: HastNode) => void): void {
|
||||
if (!node) return;
|
||||
callback(node);
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) visit(child, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export function rehypeJsonRender(): (tree: HastNode) => void {
|
||||
return (tree: HastNode) => {
|
||||
visit(tree, (node) => {
|
||||
if (node.type !== "element" || node.tagName !== "pre") return;
|
||||
const code = node.children?.find(
|
||||
(child) => child.type === "element" && child.tagName === "code",
|
||||
);
|
||||
if (!code || getCodeLanguage(code) !== "json-render") return;
|
||||
|
||||
const raw = getNodeText(code).replace(/\n$/, "");
|
||||
let tagName = "agentchat";
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { type?: string };
|
||||
if (parsed.type === "flow") tagName = "flowchart";
|
||||
} catch {}
|
||||
node.tagName = tagName;
|
||||
node.properties = { value: Buffer.from(raw, "utf8").toString("base64") };
|
||||
node.children = [];
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { highlight, highlightLanguage } from "./highlight";
|
||||
|
||||
type HastNode = {
|
||||
type?: string;
|
||||
tagName?: string;
|
||||
value?: string;
|
||||
properties?: Record<string, unknown>;
|
||||
children?: HastNode[];
|
||||
};
|
||||
|
||||
type NodePredicate = (node: HastNode) => boolean;
|
||||
type NodeCallback = (node: HastNode) => void;
|
||||
|
||||
const ZERO_TOKEN_COLORS: Record<string, { light: string; dark: string }> = {
|
||||
comment: { light: "#6A737D", dark: "#6A737D" },
|
||||
string: { light: "#032F62", dark: "#9ECBFF" },
|
||||
char: { light: "#032F62", dark: "#9ECBFF" },
|
||||
keyword: { light: "#D73A49", dark: "#F97583" },
|
||||
type: { light: "#005CC5", dark: "#79B8FF" },
|
||||
function: { light: "#6F42C1", dark: "#B392F0" },
|
||||
number: { light: "#005CC5", dark: "#79B8FF" },
|
||||
variable: { light: "#24292E", dark: "#E1E4E8" },
|
||||
id: { light: "#6F42C1", dark: "#B392F0" },
|
||||
key: { light: "#D73A49", dark: "#F97583" },
|
||||
boolean: { light: "#005CC5", dark: "#79B8FF" },
|
||||
punctuation: { light: "#24292E", dark: "#E1E4E8" },
|
||||
operator: { light: "#D73A49", dark: "#F97583" },
|
||||
};
|
||||
|
||||
function visit(node: HastNode | undefined, predicate: NodePredicate, callback: NodeCallback): void {
|
||||
if (!node) return;
|
||||
if (predicate(node)) callback(node);
|
||||
if (Array.isArray(node.children)) {
|
||||
for (const child of node.children) visit(child, predicate, callback);
|
||||
}
|
||||
}
|
||||
|
||||
function highlightToHast(code: string, language: string): HastNode[] {
|
||||
const parts = highlight(code, language).split("\n");
|
||||
const lines = [];
|
||||
for (let li = 0; li < parts.length; li++) {
|
||||
const lineHtml = parts[li];
|
||||
const tokens = [];
|
||||
const tokenRegex = /<span class="hl-([A-Za-z0-9_-]+)(?:\s+[^"]*)?">([\s\S]*?)<\/span>|([^<]+)/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = tokenRegex.exec(lineHtml)) !== null) {
|
||||
if (match[1]) {
|
||||
const color = ZERO_TOKEN_COLORS[match[1]];
|
||||
const text = decodeEntities(match[2]);
|
||||
tokens.push({
|
||||
type: "element",
|
||||
tagName: "span",
|
||||
properties: color
|
||||
? { style: `--shiki-light:${color.light};--shiki-dark:${color.dark}` }
|
||||
: {},
|
||||
children: [{ type: "text", value: text }],
|
||||
});
|
||||
} else if (match[3]) {
|
||||
tokens.push({ type: "text", value: decodeEntities(match[3]) });
|
||||
}
|
||||
}
|
||||
lines.push({
|
||||
type: "element",
|
||||
tagName: "span",
|
||||
properties: { "data-line": "" },
|
||||
children: tokens.length > 0 ? tokens : [{ type: "text", value: "" }],
|
||||
});
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function decodeEntities(value: string): string {
|
||||
return value
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll(""", '"')
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
function getNodeText(node: HastNode | undefined): string {
|
||||
if (!node) return "";
|
||||
if (node.type === "text") return node.value ?? "";
|
||||
if (!Array.isArray(node.children)) return "";
|
||||
return node.children.map(getNodeText).join("");
|
||||
}
|
||||
|
||||
function getCodeLanguage(node: HastNode): string {
|
||||
const dataLanguage = node.properties?.dataLanguage ?? node.properties?.["data-language"];
|
||||
if (typeof dataLanguage === "string") return dataLanguage;
|
||||
const className = node.properties?.className;
|
||||
const classes = Array.isArray(className) ? className : typeof className === "string" ? className.split(/\s+/) : [];
|
||||
const languageClass = classes.find((value) => typeof value === "string" && value.startsWith("language-"));
|
||||
return typeof languageClass === "string" ? languageClass.slice("language-".length) : "";
|
||||
}
|
||||
|
||||
export function rehypeZeroHighlight(): (tree: HastNode) => void {
|
||||
return (tree: HastNode) => {
|
||||
visit(
|
||||
tree,
|
||||
(node) =>
|
||||
node.type === "element" &&
|
||||
node.tagName === "code",
|
||||
(codeNode) => {
|
||||
const text = getNodeText(codeNode).replace(/\n$/, "");
|
||||
const language = highlightLanguage(getCodeLanguage(codeNode), text);
|
||||
if (!language || language === "bash") return;
|
||||
codeNode.children = highlightToHast(text, language);
|
||||
codeNode.properties = {
|
||||
...codeNode.properties,
|
||||
style: "display:grid",
|
||||
};
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { readArticleBySlug } from "./articles";
|
||||
import { docs } from "./docs";
|
||||
import type { SearchIndexEntry } from "./types";
|
||||
|
||||
let cached: SearchIndexEntry[] | null = null;
|
||||
|
||||
function stripMarkdown(md: string): string {
|
||||
return md
|
||||
.replace(/```[\s\S]*?```/g, "")
|
||||
.replace(/`[^`]+`/g, "")
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
.replace(/\*{1,3}([^*]+)\*{1,3}/g, "$1")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export async function getSearchIndex(): Promise<SearchIndexEntry[]> {
|
||||
if (cached) return cached;
|
||||
|
||||
const entries: SearchIndexEntry[] = [];
|
||||
for (const doc of docs) {
|
||||
try {
|
||||
const raw = await readArticleBySlug(doc.slug);
|
||||
const content = raw ? stripMarkdown(raw) : "";
|
||||
entries.push({
|
||||
title: doc.title,
|
||||
href: doc.path,
|
||||
section: doc.section ?? "",
|
||||
content,
|
||||
});
|
||||
} catch {
|
||||
entries.push({
|
||||
title: doc.title,
|
||||
href: doc.path,
|
||||
section: doc.section ?? "",
|
||||
content: "",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cached = entries;
|
||||
return entries;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export type Doc = {
|
||||
slug: string;
|
||||
title: string;
|
||||
description: string;
|
||||
path: `/${string}`;
|
||||
sourcePath: `/articles/${string}.md`;
|
||||
section: string;
|
||||
};
|
||||
|
||||
export type DocsGroup = {
|
||||
section: string;
|
||||
items: Doc[];
|
||||
};
|
||||
|
||||
export type Heading = {
|
||||
level: 2 | 3 | 4;
|
||||
text: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type Article = {
|
||||
doc: Doc;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type SearchIndexEntry = {
|
||||
title: string;
|
||||
href: string;
|
||||
section: string;
|
||||
content: string;
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
title: string;
|
||||
href: string;
|
||||
section: string;
|
||||
snippet: string;
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user