Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions apps/ppp/src/lib/runtime/zig/test-compiler-factory.ts
Original file line number Diff line number Diff line change
@@ -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<I> = (input: I) => string;
export type TransformResult<O> = (result: string) => O;

export class ZigTestCompilerFactory {
protected readonly logger: Logger;

constructor(protected readonly streams: Streams) {
this.logger = createLogger(streams.out);
}

async create<I, O>(
ctx: Context,
generateOutputContentCode: GenerateOutputContentCode<I>,
transformResult: TransformResult<O>
): Promise<TestCompiler<I, O>> {
class TestProgram extends ZigTestProgram<I, O> {
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<R>(url: string, action: (r: Response) => R): Promise<R> {
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');
}
};
}
}
14 changes: 14 additions & 0 deletions apps/ppp/src/lib/runtime/zig/test-worker.ts
Original file line number Diff line number Diff line change
@@ -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<ZigTestWorkerConfig>(createContext(), (ctx, out, factory) =>
factory(ctx, {
zigTestCompilerFactory: new ZigTestCompilerFactory(out)
})
);
Original file line number Diff line number Diff line change
@@ -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");
}
Original file line number Diff line number Diff line change
@@ -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<RemoteCompilerFactoryOptions, Input, Output> =
makeRemoteTestCompilerFactory(Worker, (ctx, { zigTestCompilerFactory }: ZigTestWorkerConfig) => {
const ZIG_PAYMENT_SYSTEM_TYPES: Record<PaymentSystemType, string> = {
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;
}
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as code } from './code.zig?raw';
export { factory } from './factory';
5 changes: 3 additions & 2 deletions packages/zig-runtime/src/create-wasi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ export function createCompilerWASI(
stdLibFiles: {
filename: string;
fileData: Uint8Array;
}[]
}[],
extraArgs: string[] = []
) {
const files: FileData[] = [];
for (const f of stdLibFiles) {
Expand All @@ -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'];
Expand Down
1 change: 1 addition & 0 deletions packages/zig-runtime/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
119 changes: 119 additions & 0 deletions packages/zig-runtime/src/zig-test-program.ts
Original file line number Diff line number Diff line change
@@ -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<I, O> implements TestProgram<I, O> {
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<O> {
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<Uint8Array> {
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<ArrayBuffer>): Promise<void> {
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));
}
}
Loading