Skip to content

Commit 061131c

Browse files
authored
Merge pull request #879 from aidarkdev/590
Add `normalize` command.
2 parents d6adde8 + 09e21c0 commit 061131c

9 files changed

Lines changed: 249 additions & 19 deletions

File tree

.github/workflows/grunt-old-version.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ jobs:
2727
with:
2828
distribution: "zulu"
2929
java-version: ${{ matrix.java }}
30+
- name: Install phino
31+
run: |
32+
curl -fsSL -o phino http://phino.objectionary.com/releases/ubuntu-22.04/phino-latest
33+
chmod +x phino
34+
sudo mv phino /usr/local/bin/phino
3035
- run: npm install
3136
- run: echo '0.59.3' > eo-version.txt
3237
- run: echo '0.59.3' > home-tag.txt

.github/workflows/grunt.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,23 @@ jobs:
3030
with:
3131
distribution: "zulu"
3232
java-version: ${{ matrix.java }}
33+
- name: Install phino (Linux)
34+
if: matrix.os == 'ubuntu-24.04'
35+
run: |
36+
curl -fsSL -o phino http://phino.objectionary.com/releases/ubuntu-22.04/phino-latest
37+
chmod +x phino
38+
sudo mv phino /usr/local/bin/phino
39+
- name: Install phino (macOS)
40+
if: matrix.os == 'macos-15'
41+
run: |
42+
curl -fsSL -o phino http://phino.objectionary.com/releases/macos-15/phino-latest
43+
chmod +x phino
44+
sudo mv phino /usr/local/bin/phino
45+
- name: Install phino (Windows)
46+
if: matrix.os == 'windows-2022'
47+
run: |
48+
Invoke-WebRequest -Uri http://phino.objectionary.com/releases/windows-2022/phino-latest.exe -OutFile phino.exe
49+
Move-Item phino.exe C:\Windows\System32\phino.exe
50+
shell: powershell
3351
- run: npm install
3452
- run: npx grunt

.rultor.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ install: |
1313
pip install git+https://chromium.googlesource.com/external/gyp
1414
npm install --no-color
1515
npm install --no-color mocha
16+
curl -fsSL -o phino http://phino.objectionary.com/releases/ubuntu-22.04/phino-latest
17+
chmod +x phino
18+
sudo mv phino /usr/local/bin/phino
1619
pdd -f /dev/null -v
1720
release:
1821
pre: false

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ There are also commands that help manipulate with XMIR and EO sources
143143
* `docs` generates HTML documentation from `.xmir` files
144144
* `latex` generates `.tex` files from `.eo` sources
145145
* `fmt` formats `.eo` files in the source directory
146+
* `normalize` normalizes `.eo` files via phi-calculus rewriting using
147+
[phino](https://github.com/objectionary/phino) (must be installed separately);
148+
original files are saved to `.eoc/before-normalize/` for debugging
146149
* ~~`translate` converts Java/C++/Python/etc. program to EO program~~
147150
* ~~`demu` removes `cage` and `memory` objects~~
148151
* ~~`dejump` removes `goto` objects~~

src/commands/docs.js

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,8 @@ const path = require('path');
88
const SaxonJS = require('saxon-js');
99
const { marked } = require('marked');
1010
const {elapsed} = require('../elapsed');
11+
const {findFiles} = require('../files');
1112

12-
/**
13-
* Recursively reads all .xmir files from a directory.
14-
* @param {string} dir - Directory path
15-
* @return {string[]} Array of file paths
16-
*/
17-
function readXmirsRecursively(dir) {
18-
const files = [];
19-
const entries = fs.readdirSync(dir, {withFileTypes: true});
20-
for (const entry of entries) {
21-
const full = path.join(dir, entry.name);
22-
if (entry.isDirectory()) {
23-
files.push(...readXmirsRecursively(full));
24-
} else if (entry.name.endsWith('.xmir')) {
25-
files.push(full);
26-
}
27-
}
28-
return files;
29-
}
3013

3114
/**
3215
* Applies XSLT to XMIR
@@ -136,7 +119,7 @@ module.exports = function(opts) {
136119
fs.writeFileSync(css, '');
137120
const packages_info = {};
138121
const all_xmir_htmls = [];
139-
const xmirs = readXmirsRecursively(input);
122+
const xmirs = findFiles(input, '.xmir');
140123
for (const xmir of xmirs) {
141124
const relative = path.relative(input, xmir);
142125
const name = path.parse(xmir).name;

src/commands/normalize.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
const path = require('path');
7+
const {execSync} = require('child_process');
8+
const {mvnw, flags} = require('../mvnw');
9+
const {elapsed} = require('../elapsed');
10+
const {findFiles, saveFile, copyDir} = require('../files');
11+
const relative = require('relative');
12+
13+
/**
14+
* Command to normalize .EO files via phi-calculus using phino.
15+
* Runs parse, converts XMIR → .phi → normalize → .phi → XMIR → .eo.
16+
* Original .eo files are saved to .eoc/before-normalize/ for debugging.
17+
* Fails if phino is not installed.
18+
* @param {Object} opts - All options
19+
* @return {Promise} of normalize task
20+
*/
21+
module.exports = function(opts) {
22+
if (opts.sources === undefined) {
23+
throw new Error('Sources directory is not specified. Please provide it with --sources option.');
24+
}
25+
if (opts.target === undefined) {
26+
throw new Error('Target directory is not specified. Please provide it with --target option.');
27+
}
28+
try {
29+
execSync('phino --version', {stdio: 'pipe'});
30+
} catch (e) {
31+
throw new Error('phino is not installed, see https://github.com/objectionary/phino', {cause: e});
32+
}
33+
const sources = path.resolve(opts.sources);
34+
const target = path.resolve(opts.target);
35+
return elapsed(async (tracked) => {
36+
copyDir(sources, path.join(target, 'before-normalize'), '.eo');
37+
const parsed = path.join(target, '1-parse');
38+
const normed = path.join(target, 'xmir-normalized');
39+
const xmirs = findFiles(parsed, '.xmir');
40+
console.debug('Found %d XMIR file(s) to normalize', xmirs.length);
41+
for (const xmir of xmirs) {
42+
const rel = path.relative(parsed, xmir);
43+
console.debug('Normalizing %s', rel);
44+
const ts = Date.now();
45+
const out = execSync(
46+
`phino rewrite --input=xmir --output=xmir --normalize ${xmir}`,
47+
{stdio: ['pipe', 'pipe', 'pipe']}
48+
);
49+
console.debug('Normalized in %dms', Date.now() - ts);
50+
saveFile(normed, rel, out);
51+
}
52+
const r = await mvnw(
53+
['eo:print']
54+
.concat(flags(opts))
55+
.concat([
56+
`-Deo.printSourcesDir=${normed}`,
57+
`-Deo.printOutputDir=${sources}`,
58+
]),
59+
opts.target, opts.batch
60+
);
61+
tracked.print(`EO files normalized in ${relative(sources)}`);
62+
return r;
63+
});
64+
};

src/eoc.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {program} = require('commander'),
3333
jeo_disassemble: require('./commands/jeo/disassemble'),
3434
jeo_assemble: require('./commands/jeo/assemble'),
3535
latex: require('./commands/latex'),
36+
normalize: require('./commands/normalize'),
3637
},
3738
commands = {
3839
[language.java]: {
@@ -371,6 +372,15 @@ program.command('latex')
371372
await coms().latex(program.opts());
372373
});
373374

375+
program.command('normalize')
376+
.description('Normalize EO files using phi-calculus normalization via phino')
377+
.action(async (str, opts) => {
378+
pin(program.opts());
379+
clear(str);
380+
await pipe()(coms(), ['register', 'parse'], program.opts());
381+
await coms().normalize(program.opts());
382+
});
383+
374384
program.command('fmt')
375385
.description('Format EO files in the source directory')
376386
.action(async (str, opts) => {

src/files.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
const fs = require('fs');
7+
const path = require('path');
8+
9+
/**
10+
* Recursively find all files with given extension in a directory.
11+
* @param {string} dir - Directory to search
12+
* @param {string} ext - File extension including dot (e.g. '.xmir')
13+
* @return {Array.<string>} List of absolute file paths
14+
*/
15+
function findFiles(dir, ext) {
16+
if (!fs.existsSync(dir)) {return [];}
17+
const result = [];
18+
for (const entry of fs.readdirSync(dir, {withFileTypes: true})) {
19+
const full = path.join(dir, entry.name);
20+
if (entry.isDirectory()) {
21+
result.push(...findFiles(full, ext));
22+
} else if (entry.name.endsWith(ext)) {
23+
result.push(full);
24+
}
25+
}
26+
return result;
27+
}
28+
29+
/**
30+
* Write content to a file, creating parent directories as needed.
31+
* @param {string} dir - Parent directory
32+
* @param {string} name - File name relative to dir
33+
* @param {Buffer} content - File content
34+
*/
35+
function saveFile(dir, name, content) {
36+
const file = path.join(dir, name);
37+
fs.mkdirSync(path.dirname(file), {recursive: true});
38+
fs.writeFileSync(file, content);
39+
}
40+
41+
/**
42+
* Recursively copy files with given extension from src to dst directory.
43+
* @param {string} src - Source directory
44+
* @param {string} dst - Destination directory
45+
* @param {string} ext - File extension filter (e.g. '.eo'), or empty for all files
46+
*/
47+
function copyDir(src, dst, ext) {
48+
if (!fs.existsSync(src)) {return;}
49+
fs.mkdirSync(dst, {recursive: true});
50+
for (const entry of fs.readdirSync(src, {withFileTypes: true})) {
51+
const source = path.join(src, entry.name);
52+
const dest = path.join(dst, entry.name);
53+
if (entry.isDirectory()) {
54+
copyDir(source, dest, ext);
55+
} else if (!ext || entry.name.endsWith(ext)) {
56+
fs.copyFileSync(source, dest);
57+
}
58+
}
59+
}
60+
61+
module.exports = {findFiles, saveFile, copyDir};

test/commands/test_normalize.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com
3+
* SPDX-License-Identifier: MIT
4+
*/
5+
6+
const assert = require('assert');
7+
const fs = require('fs');
8+
const path = require('path');
9+
const {execSync} = require('child_process');
10+
const {runSync, parserVersion, homeTag, weAreOnline} = require('../helpers');
11+
12+
const eo = '# sample\n[] > simple\n';
13+
14+
function setup(name, content = eo) {
15+
const home = path.resolve('temp/test-normalize', name);
16+
const source = path.resolve(home, 'src');
17+
const target = path.resolve(home, 'target');
18+
fs.rmSync(home, {recursive: true, force: true});
19+
fs.mkdirSync(source, {recursive: true});
20+
fs.mkdirSync(target, {recursive: true});
21+
fs.writeFileSync(path.resolve(source, 'simple.eo'), content);
22+
runSync([
23+
'normalize',
24+
'--verbose',
25+
`--parser=${parserVersion}`,
26+
`--home-tag=${homeTag}`,
27+
'-s', source,
28+
'-t', target,
29+
]);
30+
return {home, source, target};
31+
}
32+
33+
describe('normalize', () => {
34+
before(weAreOnline);
35+
before(() => {
36+
try {
37+
execSync('phino --version', {stdio: 'pipe'});
38+
} catch (e) {
39+
throw new Error(
40+
'phino is required to run normalize tests, see https://github.com/objectionary/phino',
41+
{cause: e}
42+
);
43+
}
44+
});
45+
it('normalizes EO files and saves originals in before-normalize/', done => {
46+
const {source, target} = setup('simple');
47+
assert(
48+
fs.existsSync(path.resolve(target, 'before-normalize/simple.eo')),
49+
'Original .eo file should be saved in before-normalize/'
50+
);
51+
assert(
52+
fs.existsSync(path.resolve(source, 'simple.eo')),
53+
'Normalized .eo file should exist in sources directory'
54+
);
55+
done();
56+
});
57+
it('normalized output matches expected content', done => {
58+
const {source} = setup('content');
59+
const actual = fs.readFileSync(path.resolve(source, 'simple.eo'), 'utf8');
60+
const expected = '# No comments.\n[] > simple\n';
61+
assert.strictEqual(
62+
actual, expected,
63+
`Normalized output must equal expected.\nExpected:\n${expected}\nActual:\n${actual}`
64+
);
65+
done();
66+
});
67+
it('backup matches original input and all pipeline files are produced', done => {
68+
const {source, target} = setup('pipeline');
69+
const backup = fs.readFileSync(
70+
path.resolve(target, 'before-normalize/simple.eo'), 'utf8'
71+
);
72+
assert.strictEqual(
73+
backup, eo,
74+
'Backup in before-normalize/ must exactly match the original input'
75+
);
76+
const xmir = path.resolve(target, 'xmir-normalized/simple.xmir');
77+
assert(fs.existsSync(xmir), 'Normalized XMIR file must exist');
78+
assert(fs.readFileSync(xmir).length > 0, 'Normalized XMIR file must not be empty');
79+
const normalized = fs.readFileSync(path.resolve(source, 'simple.eo'), 'utf8');
80+
assert(normalized.length > 0, 'Normalized .eo file must not be empty');
81+
done();
82+
});
83+
});

0 commit comments

Comments
 (0)