aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/workbench/native/check.mjs
blob: 3b021f1c0a58e60e3e7f3230a73b544c748dbb50 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env node
import assert from 'node:assert/strict';
import { readFile, writeFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';
import { resolve, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { compile } from '../web/compiler.mjs';
import { createRuntime } from '../web/runtime.mjs';

const root = fileURLToPath(new URL('../', import.meta.url));
const repo = resolve(root, '../..');
const connections = resolve(root, '../connections');
const args = process.argv.slice(2);
const sanitized = args.includes('--sanitize');
const fixed = args.includes('--fixed');
const zigIndex = args.indexOf('--zig');
const zig = zigIndex >= 0 ? args[zigIndex + 1] : process.env.ZIG || 'zig';
const index = args.indexOf('--output');
const output = index >= 0 ? resolve(args[index + 1]) : null;
const temporary = await mkdtemp(join(tmpdir(), 'pure-native-parity-'));
const library = join(temporary, 'general.a');
const wasm = await readFile(resolve(root, 'web/core.wasm'));
const hash = data => createHash('sha256').update(data).digest('hex');
const compiler = process.env.CC || 'cc';
const flags = ['-std=c17', sanitized ? '-O1' : '-O2', '-g', '-Wall', '-Wextra', '-Wpedantic', '-Werror',
  '-DPURE_RUNTIME_METRICS',
  ...(sanitized ? ['-fsanitize=address,undefined', '-fno-omit-frame-pointer'] : []),
  ...(fixed ? ['-DPURE_FIXED_DEFINITION=1'] : []), '-I' + resolve(root, 'native')];
function run(command, arguments_, options = {}) {
  const p = spawnSync(command, arguments_, { encoding: 'utf8', maxBuffer: 8 * 1024 * 1024, timeout: 60000, ...options });
  assert.ifError(p.error);
  return p;
}
function successful(command, arguments_, options) {
  const p = run(command, arguments_, options);
  assert.equal(p.status, 0, p.stdout + p.stderr);
  return p.stdout;
}
const units = [resolve(root, 'native/app.c'), resolve(root, 'native/plan.c'),
  ...['runtime.c', 'evaluate.c', 'store.c', 'store-port.c'].map(name => resolve(connections, name))];
const objects = [];
const cases = [];
let unitResult;
try {
  successful(zig, ['build-lib', resolve(root, 'native.zig'), '-O', 'ReleaseSafe',
    '-fno-compiler-rt', '-femit-bin=' + library, '--cache-dir', resolve(root, 'build/cache'),
    '--global-cache-dir', resolve(root, 'build/global-cache')]);
  for (const [i, path] of units.entries()) {
    const object = join(temporary, `unit-${i}.o`);
    successful(compiler, [...flags, '-c', path, '-o', object]);
    objects.push(object);
  }
  const main = join(temporary, 'main.o');
  successful(compiler, [...flags, '-c', resolve(root, 'native/main.c'), '-o', main]);
  const sizeSource = join(temporary, 'size.c');
  await writeFile(sizeSource, '#include "scalar.h"\n#include <stdio.h>\nint main(void) { printf("%zu\\n", pure_scalar_bytes()); }\n');
  const generalSizeBinary = join(temporary, 'general-size');
  successful(compiler, [...flags, sizeSource, library, '-lm', '-o', generalSizeBinary]);
  const generalInstanceBytes = Number(successful(generalSizeBinary, []).trim());
  const definition = name => readFile(resolve(root, 'examples', name + '.pure'), 'utf8');
  const repeat = (line, n) => Array(n).fill(line);
  const definitions = [
    ['counter', await definition('counter'), ['key tap down', 'key tap down', 'key tap up', 'key tap down', 'key tap up']],
    ['toggle', await definition('toggle'), ['key toggle down', 'key toggle up', ...Array.from({length: 13}, () => ['key move down', 'key move up']).flat()]],
    ['charge', await definition('charge'), ['key jump down', ...repeat('frame', 12), 'key jump up', ...repeat('frame', 90)]],
    ['direct', await definition('direct'), ['point drag down tile 90 180', 'point drag move tile 250 220', 'point drag up tile 250 220']],
    ['routed', await definition('routed-layout'), ['point drag down green_box 90 130', 'point drag down blue_box 290 170',
      'point drag move green_box 200 210', 'point drag move blue_box 500 270', 'point drag cancel green_box 200 210',
      'point drag up blue_box 500 270', 'key left down', 'key left up']],
    ['layout', await definition('layout'), ['point drag1 down tile1 90 130', 'point drag1 move tile1 230 210',
      'point drag1 up tile1 230 210', 'key right down', 'key right up',
      'point drag3 down tile3 460 230', 'point drag3 cancel tile3 510 270']],
    ['shared-layout', await definition('shared-layout'), ['point drag_green down green_box 90 130',
      'point drag_green move green_box 230 210', 'point drag_green up green_box 230 210',
      'key right down', 'key right up', 'point drag_amber down amber_box 460 230',
      'point drag_amber cancel amber_box 510 270']],
    ['document', await definition('document'), ['value split 99', 'value split -10', 'value split 61.5', 'key reset down', 'key reset up']],
    ['atomic-error', 'state x=7\nstate y=2\ninput n=value\non n.change {x=event_value; y=1/event_value}\ntext v(1,2,y,"value: ")',
      ['value n 0', 'value n 4']],
    ['blank', '', []],
    ['literal-boundary', 'state x=-0\ninput n=value\non n.change {x=event_value}\ntext v(1,2,x,"한글 \\\" \\\\ \\n")', ['value n -0']],
  ];
  for (const [name, source, commands] of definitions) {
    const sourcePath = join(temporary, name + '.pure'), prepared = join(temporary, name + '.c'), binary = join(temporary, name);
    await writeFile(sourcePath, source);
    const profile = join(temporary, name + '.zig');
    successful(process.execPath, [resolve(root, 'native/prepare.mjs'), sourcePath, prepared,
      ...(fixed ? [profile] : [])]);
    let caseLibrary = library;
    if (fixed) {
      caseLibrary = join(temporary, name + '.a');
      successful(zig, ['build-lib', '-O', 'ReleaseSafe', '-fno-compiler-rt', '--dep', 'profile',
        '-Mroot=' + resolve(root, 'native-fixed.zig'), '-Mprofile=' + profile,
        '-femit-bin=' + caseLibrary, '--cache-dir', resolve(root, 'build/cache'),
        '--global-cache-dir', resolve(root, 'build/global-cache')]);
    }
    successful(compiler, [...flags, prepared, main, ...objects, caseLibrary, '-lm', '-o', binary]);
    const sizeBinary = join(temporary, name + '-size');
    successful(compiler, [...flags, sizeSource, caseLibrary, '-lm', '-o', sizeBinary]);
    const instanceBytes = Number(successful(sizeBinary, []).trim());
    assert(Number.isSafeInteger(instanceBytes) && instanceBytes > 0 && instanceBytes <= generalInstanceBytes);
    const native = run(binary, [join(temporary, name + '-store'), 'project'], { input: commands.concat(['save', 'quit']).join('\n') + '\n' });
    const rows = native.stdout.trim().split('\n').map(line => JSON.parse(line));
    const runtime = await createRuntime(wasm);
    const plan = compile(source);
    runtime.apply(source);
    function equivalent(row) {
      const snapshot = runtime.snapshot();
      assert.deepEqual(row.states, snapshot.states, name);
      const views = Object.fromEntries(snapshot.views.map(view => [view.id, Object.fromEntries(Object.keys(plan.views.find(v => v.id === view.id).bindings).map(key => [key, view[key]]))]));
      assert.deepEqual(row.views, views, name);
    }
    equivalent(rows[0]);
    let failures = 0;
    commands.forEach((command, i) => {
      const [verb, input, phase, target, x, y] = command.split(' ');
      let code = 0;
      if (verb === 'key') code = phase === 'down' ? runtime.down(input) : runtime.up(input);
      else if (verb === 'frame') code = runtime.frame(1);
      else if (verb === 'value') code = runtime.change(input, Number(phase));
      else if (verb === 'point') {
        const routed = plan.inputs.find(p => p.name === input).routes;
        code = runtime.point(input, phase, Number(x), Number(y), target, routed ? target : undefined);
      }
      if (code) ++failures;
      assert.equal(rows[i + 1].status === 0, code === 0, command);
      if (code) assert.equal(rows[i + 1].scalar_error, code, command);
      equivalent(rows[i + 1]);
    });
    assert.equal(rows.length, commands.length + 2);
    assert.equal(rows.at(-1).status, 0);
    assert.equal(native.status, failures ? 1 : 0, native.stderr);
    const restored = JSON.parse(successful(binary, [join(temporary, name + '-store'), 'project'], {input: 'load\nquit\n'}).trim().split('\n').at(-1));
    equivalent(restored);
    assert.equal(restored.stored_revision, '1');
    if (name === 'routed') {
      if (fixed) {
        const capacityUnit = join(temporary, 'fixed-capacity');
        successful(compiler, [...flags, prepared, resolve(root, 'native/test-fixed.c'),
          resolve(root, 'native/plan.c'), caseLibrary, '-lm', '-o', capacityUnit]);
        unitResult = JSON.parse(successful(capacityUnit, []));
      } else {
        const unit = join(temporary, 'test-native');
        const authoring = join(temporary, 'app-author.o');
        successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', '-c', units[0], '-o', authoring]);
        successful(compiler, [...flags, '-DPURE_NATIVE_AUTHORING=1', prepared, resolve(root, 'native/test.c'),
          authoring, ...objects.slice(1), caseLibrary, '-lm', '-o', unit]);
        unitResult = JSON.parse(successful(unit, []));
      }
    }
    cases.push({name, commands: commands.length, failed_events: failures, states_and_views_match: true,
      instance_bytes: instanceBytes, fixed_profile_sha256: fixed ? hash(await readFile(profile)) : null,
      fresh_process_restore: true, source, source_sha256: hash(source), prepared_sha256: hash(await readFile(prepared)),
      native_binary_sha256: hash(await readFile(binary)), native_binary_bytes: (await readFile(binary)).length});
  }
  const paths = [resolve(root, 'core.zig'), resolve(root, 'engine.zig'), resolve(root, 'evaluation.zig'), resolve(root, 'native.zig'), resolve(root, 'native-fixed.zig'),
    resolve(root, 'manage.py'), ...['scalar.h','plan.h','plan.c','app.h','app.c','main.c','prepare.mjs','test.c','test-fixed.c','check.mjs'].map(name => resolve(root, 'native', name)),
    ...['compiler.mjs','runtime.mjs','project.mjs','core.wasm'].map(name => resolve(root, 'web', name)),
    ...['runtime.c','runtime.h','evaluate.c','program.h','flow.h','result.h','store.c','store.h','store-port.c','store-port.h'].map(name => resolve(connections, name))];
  const sourceHashes = Object.fromEntries(await Promise.all(paths.map(async path => [relative(repo, path), hash(await readFile(path))])));
  const result = {kind:'native-instance-and-authored-behavior-correctness-not-performance', sanitized_c_host: sanitized,
    profile: fixed ? 'fixed-definition' : 'general-authoring', general_instance_bytes: generalInstanceBytes,
    zig_mode:'ReleaseSafe; Zig object is not ASan-instrumented', compiler: successful(compiler,['--version']).split('\n')[0],
    cases, units: unitResult, source_sha256: sourceHashes, scalar_archive_sha256: hash(await readFile(library)),
    scalar_archive_size: successful('size',[library]), scalar_archive_undefined: successful('nm',['-u',library])};
  if (output) {
    await mkdir(resolve(output, '..'), {recursive: true});
    await writeFile(output, JSON.stringify(result,null,2)+'\n', {flag:'wx'});
  }
  console.log(JSON.stringify({cases:cases.length, event_comparisons:cases.reduce((n,c)=>n+c.commands,0),
    general_instance_bytes: generalInstanceBytes,
    instance_bytes: Object.fromEntries(cases.map(c => [c.name, c.instance_bytes])),
    units:unitResult, sanitized_c_host:sanitized, profile:result.profile}));
} finally {
  await rm(temporary, {recursive:true, force:true});
}