From 64ca3df4770ef2f36539be950a231890a98d4218 Mon Sep 17 00:00:00 2001 From: Roman Krasilnikov Date: Sat, 22 Aug 2026 02:48:04 +0300 Subject: [PATCH] [zig] Implement test program --- .../lib/runtime/zig/test-compiler-factory.ts | 76 +++++++++++ apps/ppp/src/lib/runtime/zig/test-worker.ts | 14 +++ .../payment-system/runtimes/zig/code.zig | 10 ++ .../payment-system/runtimes/zig/factory.ts | 35 ++++++ .../payment-system/runtimes/zig/index.ts | 2 + packages/zig-runtime/src/create-wasi.ts | 5 +- packages/zig-runtime/src/index.ts | 1 + packages/zig-runtime/src/zig-test-program.ts | 119 ++++++++++++++++++ 8 files changed, 260 insertions(+), 2 deletions(-) create mode 100644 apps/ppp/src/lib/runtime/zig/test-compiler-factory.ts create mode 100644 apps/ppp/src/lib/runtime/zig/test-worker.ts create mode 100644 apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/code.zig create mode 100644 apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/factory.ts create mode 100644 apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/index.ts create mode 100644 packages/zig-runtime/src/zig-test-program.ts diff --git a/apps/ppp/src/lib/runtime/zig/test-compiler-factory.ts b/apps/ppp/src/lib/runtime/zig/test-compiler-factory.ts new file mode 100644 index 00000000..e5de1d88 --- /dev/null +++ b/apps/ppp/src/lib/runtime/zig/test-compiler-factory.ts @@ -0,0 +1,76 @@ +import { untar } from '@andrewbranch/untar.js'; +import type { Context } from 'libs/context'; +import type { Streams } from 'libs/io'; +import { createLogger, type Logger } from 'libs/logger'; +import type { TestCompiler } from 'libs/testing'; +import { ZigTestProgram, createCompilerWASI } from 'zig-runtime'; + +import zigWasmUrl from 'zig-runtime/zig.wasm?url'; +import compilerRtUrl from 'zig-runtime/lib/libcompiler_rt.a?url'; +import stdLibUrl from 'zig-runtime/lib/zig.tar.gz?url'; + +import { createCachedFetch } from '$lib/fetch'; + +export type GenerateOutputContentCode = (input: I) => string; +export type TransformResult = (result: string) => O; + +export class ZigTestCompilerFactory { + protected readonly logger: Logger; + + constructor(protected readonly streams: Streams) { + this.logger = createLogger(streams.out); + } + + async create( + ctx: Context, + generateOutputContentCode: GenerateOutputContentCode, + transformResult: TransformResult + ): Promise> { + class TestProgram extends ZigTestProgram { + protected override generateOutputContentCode(input: I): string { + return generateOutputContentCode(input); + } + protected override transformResult(data: string): O { + return transformResult(data); + } + } + const fetcher = await createCachedFetch( + 'zig-cache@', + `${zigWasmUrl}|${compilerRtUrl}|${stdLibUrl}` + ); + const logger = this.logger; + async function fetch(url: string, action: (r: Response) => R): Promise { + const response = await fetcher(url, { signal: ctx.signal }); + const result = await action(response); + logger.info(`Loaded ${url}`); + return result; + } + const [zigWasmModule, compilerRtArrayBuffer, stdLibFiles] = await Promise.all([ + fetch(zigWasmUrl, (r) => WebAssembly.compileStreaming(r)), + fetch(compilerRtUrl, (r) => r.arrayBuffer()), + fetch(stdLibUrl, async (r) => { + let arrayBuffer = await r.arrayBuffer(); + const magicNumber = new Uint8Array(arrayBuffer).slice(0, 2); + if (magicNumber[0] == 0x1f && magicNumber[1] == 0x8b) { + const ds = new DecompressionStream('gzip'); + const response = new Response(new Response(arrayBuffer).body!.pipeThrough(ds)); + arrayBuffer = await response.arrayBuffer(); + } else { + // already decompressed + } + return untar(arrayBuffer); + }) + ]); + const wasi = createCompilerWASI(this.streams, compilerRtArrayBuffer, stdLibFiles, [ + '--export=_start' + ]); + return { + async compile(_, files) { + if (files.length !== 1) { + throw new Error('Compilation of multiple files is not implemented'); + } + return new TestProgram(files[0].content, wasi, zigWasmModule, 'case_output'); + } + }; + } +} diff --git a/apps/ppp/src/lib/runtime/zig/test-worker.ts b/apps/ppp/src/lib/runtime/zig/test-worker.ts new file mode 100644 index 00000000..a5b3e171 --- /dev/null +++ b/apps/ppp/src/lib/runtime/zig/test-worker.ts @@ -0,0 +1,14 @@ +import { startTestCompilerActor } from 'libs/testing/actor'; +import { createContext } from 'libs/context'; + +import { ZigTestCompilerFactory } from './test-compiler-factory'; + +export interface ZigTestWorkerConfig { + zigTestCompilerFactory: ZigTestCompilerFactory; +} + +startTestCompilerActor(createContext(), (ctx, out, factory) => + factory(ctx, { + zigTestCompilerFactory: new ZigTestCompilerFactory(out) + }) +); diff --git a/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/code.zig b/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/code.zig new file mode 100644 index 00000000..eb914cf3 --- /dev/null +++ b/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/code.zig @@ -0,0 +1,10 @@ +const std = @import("std"); + +const PaymentSystemType = enum { paypal, webmoney, cat_bank }; + +fn payment(tp: PaymentSystemType, base: i64, amount: i64) i64 { + _ = tp; + _ = base; + _ = amount; + @panic("Not implemented"); +} diff --git a/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/factory.ts b/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/factory.ts new file mode 100644 index 00000000..ea35e461 --- /dev/null +++ b/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/factory.ts @@ -0,0 +1,35 @@ +import { makeRemoteTestCompilerFactory } from 'libs/testing/actor'; + +import Worker from '$lib/runtime/zig/test-worker?worker'; + +// Only type imports are allowed + +import type { RemoteCompilerFactoryOptions } from 'libs/compiler/actor'; +import type { TestCompilerFactory } from 'libs/testing'; + +import type { ZigTestWorkerConfig } from '$lib/runtime/zig/test-worker'; + +import type { Input, Output } from '../../tests-data'; +import type { PaymentSystemType } from '../../reference'; + +export const factory: TestCompilerFactory = + makeRemoteTestCompilerFactory(Worker, (ctx, { zigTestCompilerFactory }: ZigTestWorkerConfig) => { + const ZIG_PAYMENT_SYSTEM_TYPES: Record = { + paypal: '.paypal', + webmoney: '.webmoney', + 'cat-bank': '.cat_bank' + }; + return zigTestCompilerFactory.create( + ctx, + ({ paymentSystem, amount, base }) => + `var buf: [64]u8 = undefined; + const output_content = try std.fmt.bufPrint(&buf, "{d}", .{payment(${ZIG_PAYMENT_SYSTEM_TYPES[paymentSystem]}, ${base}, ${amount})});`, + (result) => { + const r = parseInt(result, 10); + if (isNaN(r)) { + throw new Error(`Invalid result type: ${result}, expected number`); + } + return r; + } + ); + }); diff --git a/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/index.ts b/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/index.ts new file mode 100644 index 00000000..92b2f945 --- /dev/null +++ b/apps/ppp/src/routes/(app)/problems/[problem]/design-patterns/factory/payment-system/runtimes/zig/index.ts @@ -0,0 +1,2 @@ +export { default as code } from './code.zig?raw'; +export { factory } from './factory'; diff --git a/packages/zig-runtime/src/create-wasi.ts b/packages/zig-runtime/src/create-wasi.ts index 1b5fc081..7879bf44 100644 --- a/packages/zig-runtime/src/create-wasi.ts +++ b/packages/zig-runtime/src/create-wasi.ts @@ -59,7 +59,8 @@ export function createCompilerWASI( stdLibFiles: { filename: string; fileData: Uint8Array; - }[] + }[], + extraArgs: string[] = [] ) { const files: FileData[] = []; for (const f of stdLibFiles) { @@ -83,7 +84,7 @@ export function createCompilerWASI( new PreopenDirectory('/lib', convert(files).contents), new PreopenDirectory('/cache', new Map()) ]; - return new WASI(compilerArgs, compilerEnv, descriptors, { debug: false }); + return new WASI([...compilerArgs, ...extraArgs], compilerEnv, descriptors, { debug: false }); } const programArgs = ['main.wasm']; diff --git a/packages/zig-runtime/src/index.ts b/packages/zig-runtime/src/index.ts index 0a6e5ed3..caca7239 100644 --- a/packages/zig-runtime/src/index.ts +++ b/packages/zig-runtime/src/index.ts @@ -1,3 +1,4 @@ export * from './create-wasi.js'; export * from './zig-compiler.js'; export * from './zig-program.js'; +export * from './zig-test-program.js'; diff --git a/packages/zig-runtime/src/zig-test-program.ts b/packages/zig-runtime/src/zig-test-program.ts new file mode 100644 index 00000000..743393dc --- /dev/null +++ b/packages/zig-runtime/src/zig-test-program.ts @@ -0,0 +1,119 @@ +import type { OpenDirectory, WASI } from '@bjorn3/browser_wasi_shim'; +import type { TestProgram } from 'libs/testing'; +import { inContext, type Context } from 'libs/context'; +import { isErr } from 'libs/result'; +import { assertOpenDir, lookupFile } from 'libs/wasi'; + +export abstract class ZigTestProgram implements TestProgram { + protected textEncoder = new TextEncoder(); + protected textDecoder = new TextDecoder(); + + constructor( + protected readonly code: string, + protected readonly wasi: WASI, + protected readonly zigModule: WebAssembly.Module, + protected readonly outputPath: string + ) {} + + async run(ctx: Context, input: I): Promise { + this.writeCaseExecutionCode(this.generateCaseExecutionCode(input)); + const program = new Uint8Array(await this.compile(ctx)); + await this.execute(ctx, program); + return this.readResult(); + } + + protected async compile(ctx: Context): Promise { + const instance = await inContext( + ctx, + WebAssembly.instantiate(this.zigModule, { + wasi_snapshot_preview1: this.wasi.wasiImport + }) + ); + // @ts-expect-error lack of type information + const exitCode = this.wasi.start(instance); + if (exitCode !== 0) { + throw new Error(`Compilation failed with exit code ${exitCode}`); + } + return this.getWasmFile().data; + } + + protected async execute(ctx: Context, program: Uint8Array): Promise { + const module = await inContext(ctx, WebAssembly.compile(program)); + const instance = await inContext( + ctx, + WebAssembly.instantiate(module, { + wasi_snapshot_preview1: this.wasi.wasiImport + }) + ); + // @ts-expect-error lack of type information + const exitCode = this.wasi.start(instance); + if (exitCode !== 0) { + throw new Error(`Code execution failed with exit code ${exitCode}`); + } + } + + /** + * Should generate code that produces a variable `output_content: []const u8`. + * Requires the user code to declare `const std = @import("std");` + */ + protected abstract generateOutputContentCode(input: I): string; + + protected generateCaseExecutionCode(input: I): string { + return `${this.code} + +fn writeCaseOutput(io: std.Io) !void { + ${this.generateOutputContentCode(input)} + try std.Io.Dir.writeFile(std.Io.Dir.cwd(), io, .{ + .sub_path = "${this.outputPath}", + .data = output_content + }); +} + +export fn _start() void { + const io = std.Io.Threaded.global_single_threaded.io(); + writeCaseOutput(io) catch |err| { + std.Io.Dir.writeFile(std.Io.Dir.cwd(), io, .{ + .sub_path = "${this.outputPath}", + .data = @errorName(err) + }) catch {}; + }; +} +`; + } + + protected writeCaseExecutionCode(code: string) { + const file = lookupFile(this.rootDir, 'main.zig'); + if (isErr(file)) { + throw new Error(`Failed to read main file: ${file.error}`); + } + file.value.data = this.textEncoder.encode(code); + } + + protected get rootDir(): OpenDirectory { + const dir = this.wasi.fds[3]; + assertOpenDir(dir); + return dir; + } + + protected getWasmFile() { + const file = lookupFile(this.rootDir, 'main.wasm'); + if (isErr(file)) { + throw new Error(`Failed to read compiled file: ${file.error}`); + } + return file.value; + } + + protected readOutputFile() { + const file = lookupFile(this.rootDir, this.outputPath); + if (isErr(file)) { + throw new Error(`Failed to read output file: ${file.error}`); + } + return file.value; + } + + protected abstract transformResult(data: string): O; + + protected readResult(): O { + return this.transformResult(this.textDecoder.decode(this.readOutputFile().data)); + } +}