eyeprolog 1.2.44 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/examples/book/chapter-06/03-square.pl +0 -1
- package/examples/fast-fourier-transform.pl +2 -0
- package/examples/output/fast-fourier-transform.pl +1 -1
- package/examples/sudoku.pl +4 -4
- package/index.d.ts +8 -0
- package/package.json +2 -1
- package/src/cli.js +26 -1
- package/src/index.js +9 -1
- package/src/iso.js +0 -9
- package/src/lib/lists.pl +132 -15
- package/src/program.js +233 -1
- package/src/standard-library.js +60 -5
- package/test/run-interop.mjs +77 -0
- package/test/run-regression.mjs +212 -104
- package/the-art-of-eyeprolog.md +48 -11
package/src/program.js
CHANGED
|
@@ -10,11 +10,17 @@ import {
|
|
|
10
10
|
createParserOperatorState,
|
|
11
11
|
parseClauses,
|
|
12
12
|
parseClausesInto,
|
|
13
|
+
parseGoalText,
|
|
13
14
|
tryParseClausesFastInto,
|
|
14
15
|
} from './parser.js';
|
|
15
16
|
import { PrologError, getStrictIsoRegistry } from './iso.js';
|
|
16
17
|
import { currentWorkingDirectory, fs, path } from './platform.js';
|
|
17
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
standardLibrarySources,
|
|
20
|
+
eyePrologInteropAutoload,
|
|
21
|
+
eyePrologInteropLibraryIndicators,
|
|
22
|
+
eyePrologInteropLibraryModules,
|
|
23
|
+
} from './standard-library.js';
|
|
18
24
|
import { expandDcgRuleClause } from './dcg.js';
|
|
19
25
|
|
|
20
26
|
const DEFER_PROGRAM_BUILD = Symbol('deferProgramBuild');
|
|
@@ -102,6 +108,9 @@ export class Program {
|
|
|
102
108
|
this.modules = new Map([['user', { name: 'user', exports: new Map(), filename: '<input>' }]]);
|
|
103
109
|
this.moduleImports = new Map();
|
|
104
110
|
this.moduleMetaPredicates = new Map();
|
|
111
|
+
this.autoloadedPredicates = [];
|
|
112
|
+
this.libraryImports = [];
|
|
113
|
+
this.interopPortabilityWarnings = [];
|
|
105
114
|
this.dynamicPredicates = new Set();
|
|
106
115
|
this.strictIso = options.isoStrict === true;
|
|
107
116
|
this.operators = new Map();
|
|
@@ -677,6 +686,19 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
|
|
|
677
686
|
const context = { module: 'user' };
|
|
678
687
|
if (!loadSourceIntoBuilder(builder, text, item.options, ensured, loadedModules, fast, context)) return false;
|
|
679
688
|
}
|
|
689
|
+
const autoloadGoals = parseInteropGoalInputs(options.autoloadGoals, {
|
|
690
|
+
...options,
|
|
691
|
+
operatorState,
|
|
692
|
+
parserFlagState,
|
|
693
|
+
}, builder.program);
|
|
694
|
+
if (options.isoStrict !== true && options.autoload !== false) {
|
|
695
|
+
autoloadInteropDependencies(builder, {
|
|
696
|
+
...options,
|
|
697
|
+
operatorState,
|
|
698
|
+
parserFlagState,
|
|
699
|
+
}, ensured, loadedModules, autoloadGoals);
|
|
700
|
+
}
|
|
701
|
+
analyzeInteropPortability(builder.program, autoloadGoals);
|
|
680
702
|
builder.program.doubleQuotes = parserFlagState.doubleQuotes;
|
|
681
703
|
return true;
|
|
682
704
|
} catch (error) {
|
|
@@ -685,6 +707,208 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
|
|
|
685
707
|
}
|
|
686
708
|
}
|
|
687
709
|
|
|
710
|
+
|
|
711
|
+
const interopIndicatorSet = new Set(eyePrologInteropLibraryIndicators);
|
|
712
|
+
const interopLibraryModuleSet = new Set(eyePrologInteropLibraryModules);
|
|
713
|
+
|
|
714
|
+
function libraryDesignationName(designation) {
|
|
715
|
+
return designation?.type === COMPOUND && designation.name === 'library' && designation.arity === 1 &&
|
|
716
|
+
designation.args[0]?.type === ATOM
|
|
717
|
+
? designation.args[0].name
|
|
718
|
+
: null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function bundledLibraryModule(program, module) {
|
|
722
|
+
return module !== 'user' && program.modules.get(module)?.filename?.startsWith('src/lib/');
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function groupDependencies(group) {
|
|
726
|
+
const dependencies = [];
|
|
727
|
+
for (const clause of group.clauses) {
|
|
728
|
+
if (isCompactBinaryClause(clause)) {
|
|
729
|
+
if (clause.bodyName != null) {
|
|
730
|
+
dependencies.push({
|
|
731
|
+
key: `${clause.bodyName}/2`,
|
|
732
|
+
name: clause.bodyName,
|
|
733
|
+
arity: 2,
|
|
734
|
+
module: clause.module ?? group.module,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
for (const goal of clause.body) {
|
|
740
|
+
dependencies.push(...collectGoalDependencies(goal, false, true));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return dependencies;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function parseInteropGoalInputs(inputs, options, program) {
|
|
747
|
+
if (inputs == null) return [];
|
|
748
|
+
const values = Array.isArray(inputs) ? inputs : [inputs];
|
|
749
|
+
return values.filter((value) => value != null).map((value) => {
|
|
750
|
+
if (typeof value !== 'string') return value;
|
|
751
|
+
return parseGoalText(value, {
|
|
752
|
+
doubleQuotes: options.parserFlagState?.doubleQuotes ?? options.doubleQuotes ?? program.doubleQuotes ?? 'chars',
|
|
753
|
+
operatorDefinitions: [...program.operators.values()],
|
|
754
|
+
isoStrict: options.isoStrict === true,
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function extraGoalDependencies(goals) {
|
|
760
|
+
const dependencies = [];
|
|
761
|
+
for (const goal of goals) dependencies.push(...collectGoalDependencies(goal, false, true));
|
|
762
|
+
return dependencies;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function interopAutoloadRequests(program, extraGoals = []) {
|
|
766
|
+
const requests = new Map();
|
|
767
|
+
for (const group of program.groups.values()) {
|
|
768
|
+
if (bundledLibraryModule(program, group.module)) continue;
|
|
769
|
+
for (const dependency of groupDependencies(group)) {
|
|
770
|
+
const targetModule = dependency.module ?? group.module;
|
|
771
|
+
if (program.findGroup(dependency.name, dependency.arity, targetModule)) continue;
|
|
772
|
+
const library = eyePrologInteropAutoload[dependency.key];
|
|
773
|
+
if (library == null) continue;
|
|
774
|
+
const requestKey = `${targetModule}\u0000${dependency.key}`;
|
|
775
|
+
requests.set(requestKey, {
|
|
776
|
+
targetModule,
|
|
777
|
+
library,
|
|
778
|
+
name: dependency.name,
|
|
779
|
+
arity: dependency.arity,
|
|
780
|
+
key: dependency.key,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
for (const goal of program.initializations) {
|
|
785
|
+
for (const dependency of collectGoalDependencies(goal, false, true)) {
|
|
786
|
+
const targetModule = dependency.module ?? 'user';
|
|
787
|
+
if (program.findGroup(dependency.name, dependency.arity, targetModule)) continue;
|
|
788
|
+
const library = eyePrologInteropAutoload[dependency.key];
|
|
789
|
+
if (library == null) continue;
|
|
790
|
+
const requestKey = `${targetModule}\u0000${dependency.key}`;
|
|
791
|
+
requests.set(requestKey, {
|
|
792
|
+
targetModule,
|
|
793
|
+
library,
|
|
794
|
+
name: dependency.name,
|
|
795
|
+
arity: dependency.arity,
|
|
796
|
+
key: dependency.key,
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
for (const dependency of extraGoalDependencies(extraGoals)) {
|
|
801
|
+
const targetModule = dependency.module ?? 'user';
|
|
802
|
+
if (program.findGroup(dependency.name, dependency.arity, targetModule)) continue;
|
|
803
|
+
const library = eyePrologInteropAutoload[dependency.key];
|
|
804
|
+
if (library == null) continue;
|
|
805
|
+
const requestKey = `${targetModule}\u0000${dependency.key}`;
|
|
806
|
+
requests.set(requestKey, {
|
|
807
|
+
targetModule,
|
|
808
|
+
library,
|
|
809
|
+
name: dependency.name,
|
|
810
|
+
arity: dependency.arity,
|
|
811
|
+
key: dependency.key,
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
return [...requests.values()];
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function autoloadInteropDependencies(builder, options, ensured, loadedModules, extraGoals = []) {
|
|
818
|
+
const program = builder.program;
|
|
819
|
+
while (true) {
|
|
820
|
+
const requests = interopAutoloadRequests(program, extraGoals);
|
|
821
|
+
if (requests.length === 0) return;
|
|
822
|
+
let changed = false;
|
|
823
|
+
for (const request of requests) {
|
|
824
|
+
if (program.findGroup(request.name, request.arity, request.targetModule)) continue;
|
|
825
|
+
if (!loadedModules.has(request.library)) {
|
|
826
|
+
const designation = compound('library', [atom(request.library)]);
|
|
827
|
+
const loaded = readModuleSource(designation, options);
|
|
828
|
+
const childContext = { module: loaded.name };
|
|
829
|
+
// Bundled library sources are small and directive-heavy. Parse them
|
|
830
|
+
// with the general parser even when the user source took the compact
|
|
831
|
+
// fast path; this avoids rebuilding otherwise-fast user programs.
|
|
832
|
+
if (!loadSourceIntoBuilder(
|
|
833
|
+
builder, loaded.text, loaded.options, ensured, loadedModules, false, childContext,
|
|
834
|
+
)) {
|
|
835
|
+
throw new Error(`could not autoload library(${request.library})`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
program.importModule(request.targetModule, request.library, [request]);
|
|
839
|
+
program.autoloadedPredicates.push({
|
|
840
|
+
targetModule: request.targetModule,
|
|
841
|
+
library: request.library,
|
|
842
|
+
indicator: request.key,
|
|
843
|
+
});
|
|
844
|
+
changed = true;
|
|
845
|
+
}
|
|
846
|
+
if (!changed) return;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function analyzeInteropPortability(program, extraGoals = []) {
|
|
851
|
+
const warnings = new Map();
|
|
852
|
+
const nonInteropImports = new Map();
|
|
853
|
+
for (const entry of program.libraryImports) {
|
|
854
|
+
if (bundledLibraryModule(program, entry.targetModule)) continue;
|
|
855
|
+
if (!interopLibraryModuleSet.has(entry.library)) {
|
|
856
|
+
warnings.set(`library:${entry.targetModule}:${entry.library}`, {
|
|
857
|
+
kind: 'library',
|
|
858
|
+
library: entry.library,
|
|
859
|
+
targetModule: entry.targetModule,
|
|
860
|
+
});
|
|
861
|
+
const libraries = nonInteropImports.get(entry.targetModule) ?? new Set();
|
|
862
|
+
libraries.add(entry.library);
|
|
863
|
+
nonInteropImports.set(entry.targetModule, libraries);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (entry.imports != null) {
|
|
867
|
+
for (const indicator of entry.imports) {
|
|
868
|
+
if (interopIndicatorSet.has(indicator.key)) continue;
|
|
869
|
+
warnings.set(`predicate:${entry.targetModule}:${entry.library}:${indicator.key}`, {
|
|
870
|
+
kind: 'predicate',
|
|
871
|
+
library: entry.library,
|
|
872
|
+
indicator: indicator.key,
|
|
873
|
+
targetModule: entry.targetModule,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
for (const group of program.groups.values()) {
|
|
880
|
+
if (bundledLibraryModule(program, group.module)) continue;
|
|
881
|
+
for (const dependency of groupDependencies(group)) {
|
|
882
|
+
const targetModule = dependency.module ?? group.module;
|
|
883
|
+
const resolved = program.findGroup(dependency.name, dependency.arity, targetModule);
|
|
884
|
+
if (!resolved || !bundledLibraryModule(program, resolved.module)) continue;
|
|
885
|
+
if (interopIndicatorSet.has(dependency.key)) continue;
|
|
886
|
+
if (nonInteropImports.get(targetModule)?.has(resolved.module)) continue;
|
|
887
|
+
warnings.set(`predicate:${targetModule}:${resolved.module}:${dependency.key}`, {
|
|
888
|
+
kind: 'predicate',
|
|
889
|
+
library: resolved.module,
|
|
890
|
+
indicator: dependency.key,
|
|
891
|
+
targetModule,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
for (const dependency of extraGoalDependencies(extraGoals)) {
|
|
897
|
+
const targetModule = dependency.module ?? 'user';
|
|
898
|
+
const resolved = program.findGroup(dependency.name, dependency.arity, targetModule);
|
|
899
|
+
if (!resolved || !bundledLibraryModule(program, resolved.module)) continue;
|
|
900
|
+
if (interopIndicatorSet.has(dependency.key)) continue;
|
|
901
|
+
if (nonInteropImports.get(targetModule)?.has(resolved.module)) continue;
|
|
902
|
+
warnings.set(`predicate:${targetModule}:${resolved.module}:${dependency.key}`, {
|
|
903
|
+
kind: 'predicate',
|
|
904
|
+
library: resolved.module,
|
|
905
|
+
indicator: dependency.key,
|
|
906
|
+
targetModule,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
program.interopPortabilityWarnings = [...warnings.values()];
|
|
910
|
+
}
|
|
911
|
+
|
|
688
912
|
function sourceOptionsFor(source, options) {
|
|
689
913
|
if (typeof source === 'string') return options;
|
|
690
914
|
return {
|
|
@@ -736,6 +960,14 @@ function loadSourceIntoBuilder(builder, source, options, ensured, loadedModules,
|
|
|
736
960
|
flush();
|
|
737
961
|
clause.module = context.module;
|
|
738
962
|
builder.addClauses([clause]);
|
|
963
|
+
const libraryName = libraryDesignationName(use.designation);
|
|
964
|
+
if (libraryName != null) {
|
|
965
|
+
builder.program.libraryImports.push({
|
|
966
|
+
targetModule: context.module,
|
|
967
|
+
library: libraryName,
|
|
968
|
+
imports: use.imports == null ? null : use.imports.map((indicator) => ({ ...indicator })),
|
|
969
|
+
});
|
|
970
|
+
}
|
|
739
971
|
const loaded = readModuleSource(use.designation, options);
|
|
740
972
|
if (!loadedModules.has(loaded.name)) {
|
|
741
973
|
const childContext = { module: loaded.name };
|
package/src/standard-library.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// ISO/IEC 13211-2 module sources shipped with EyeProlog.
|
|
2
|
-
// The sources are registered here so library(Name) works in Node and browsers
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// The sources are registered here so library(Name) works in Node and browsers.
|
|
3
|
+
// Modules are loaded on demand by explicit use_module/1-2 or by the conservative
|
|
4
|
+
// source-level interop autoloader declared below.
|
|
5
5
|
import { PrologError, createDefaultRegistry, eyePrologLibraryBuiltins } from './iso.js';
|
|
6
6
|
import { clpzBuiltins } from './clpz.js';
|
|
7
7
|
import { fs, isNode, memoryStatistics } from './platform.js';
|
|
@@ -62,8 +62,8 @@ export const eyePrologPortableLibraryIndicators = Object.freeze([
|
|
|
62
62
|
'maplist/6', 'maplist/7', 'maplist/8', 'between/3',
|
|
63
63
|
'smallest_divisor_from/3', 'random/3', 'matches/3', 'split/3',
|
|
64
64
|
'replace/4', 'lowercase/2', 'uppercase/2', 'trim/2', 'number_string/2',
|
|
65
|
-
'atom_string/2', 'term_string/2', 'append/3', 'string_concat/3', 'contains/2',
|
|
66
|
-
'matches/2', 'join/3', 'substring/4', 'member/2', 'select/3', 'last/2',
|
|
65
|
+
'atom_string/2', 'term_string/2', 'append/2', 'append/3', 'string_concat/3', 'contains/2',
|
|
66
|
+
'matches/2', 'join/3', 'substring/4', 'member/2', 'memberchk/2', 'select/3', 'last/2', 'same_length/2',
|
|
67
67
|
'nth0/3', 'nth0/4', 'nth1/3', 'nth1/4', 'set_nth0/4', 'take/3', 'drop/3', 'slice/4', 'reverse/2',
|
|
68
68
|
'length/2', 'sum_list/2', 'min_list/2', 'max_list/2', 'list_to_set/2',
|
|
69
69
|
'succ/2', 'foldl/4', 'foldl/5', 'foldl/6',
|
|
@@ -74,6 +74,61 @@ export const eyePrologLibraryIndicators = Object.freeze([
|
|
|
74
74
|
...eyePrologNativeLibraryIndicators,
|
|
75
75
|
]);
|
|
76
76
|
|
|
77
|
+
|
|
78
|
+
// Conservative cross-implementation profile. These predicate indicators are
|
|
79
|
+
// intentionally limited to interfaces that EyeProlog wants to keep source-
|
|
80
|
+
// compatible with the Trealla/Scryer ecosystem. The value is the canonical
|
|
81
|
+
// EyeProlog library used when an otherwise undefined call is autoloaded.
|
|
82
|
+
//
|
|
83
|
+
// This is deliberately separate from eyePrologPortableLibraryIndicators:
|
|
84
|
+
// "portable" there means implemented in Prolog rather than as a host builtin;
|
|
85
|
+
// this table means interoperable source-level API.
|
|
86
|
+
export const eyePrologInteropAutoload = Object.freeze({
|
|
87
|
+
'member/2': 'lists',
|
|
88
|
+
'memberchk/2': 'lists',
|
|
89
|
+
'select/3': 'lists',
|
|
90
|
+
'append/2': 'lists',
|
|
91
|
+
'append/3': 'lists',
|
|
92
|
+
'last/2': 'lists',
|
|
93
|
+
'same_length/2': 'lists',
|
|
94
|
+
'nth0/3': 'lists',
|
|
95
|
+
'nth0/4': 'lists',
|
|
96
|
+
'nth1/3': 'lists',
|
|
97
|
+
'nth1/4': 'lists',
|
|
98
|
+
'reverse/2': 'lists',
|
|
99
|
+
'length/2': 'lists',
|
|
100
|
+
'maplist/2': 'lists',
|
|
101
|
+
'maplist/3': 'lists',
|
|
102
|
+
'maplist/4': 'lists',
|
|
103
|
+
'maplist/5': 'lists',
|
|
104
|
+
'maplist/6': 'lists',
|
|
105
|
+
'maplist/7': 'lists',
|
|
106
|
+
'maplist/8': 'lists',
|
|
107
|
+
'foldl/4': 'lists',
|
|
108
|
+
'foldl/5': 'lists',
|
|
109
|
+
'foldl/6': 'lists',
|
|
110
|
+
'sum_list/2': 'lists',
|
|
111
|
+
'list_to_set/2': 'lists',
|
|
112
|
+
// call_nth/2 is available in both Trealla and Scryer (the latter via
|
|
113
|
+
// library(iso_ext)). EyeProlog keeps its adapter in library(prologue), so
|
|
114
|
+
// autoloading hides that implementation-specific location from source.
|
|
115
|
+
'call_nth/2': 'prologue',
|
|
116
|
+
// Trealla and Scryer expose between/3 without an EyeProlog-style
|
|
117
|
+
// library(prologue) dependency. EyeProlog keeps its implementation in the
|
|
118
|
+
// Prologue module but autoloads it so portable source need not name that
|
|
119
|
+
// implementation-specific library.
|
|
120
|
+
'between/3': 'prologue',
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export const eyePrologInteropLibraryIndicators = Object.freeze(
|
|
124
|
+
Object.keys(eyePrologInteropAutoload),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Libraries whose *name* is part of the current interop profile. A program
|
|
128
|
+
// may freely use_module(library(lists)); predicates in that module which are
|
|
129
|
+
// outside eyePrologInteropLibraryIndicators are still diagnosed when used.
|
|
130
|
+
export const eyePrologInteropLibraryModules = Object.freeze(['lists']);
|
|
131
|
+
|
|
77
132
|
function runtimeStatistics(solver) {
|
|
78
133
|
return { ...solver.stats, ...memoryStatistics() };
|
|
79
134
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Cross-implementation smoke tests for source-level Prolog interoperability.
|
|
3
|
+
//
|
|
4
|
+
// By default this runner requires Trealla (`tpl`) and Scryer (`scryer-prolog`)
|
|
5
|
+
// to be installed. Pass --allow-missing for local development when only the
|
|
6
|
+
// EyeProlog leg is available.
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import process from 'node:process';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const allowMissing = process.argv.includes('--allow-missing');
|
|
14
|
+
const sudoku = path.join(root, 'examples', 'sudoku.pl');
|
|
15
|
+
const goal = 'sudoku_solution(S), write(S), nl, fail; halt';
|
|
16
|
+
const expected = '[[5,3,4,6,7,8,9,1,2],[6,7,2,1,9,5,3,4,8],[1,9,8,3,4,2,5,6,7],[8,5,9,7,6,1,4,2,3],[4,2,6,8,5,3,7,9,1],[7,1,3,9,2,4,8,5,6],[9,6,1,5,3,7,2,8,4],[2,8,7,4,1,9,6,3,5],[3,4,5,2,8,6,1,7,9]]';
|
|
17
|
+
|
|
18
|
+
const engines = [
|
|
19
|
+
{
|
|
20
|
+
name: 'EyeProlog',
|
|
21
|
+
command: process.execPath,
|
|
22
|
+
args: [path.join(root, 'bin', 'eyeprolog.js'), '--portable', '-g', goal, sudoku],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'Trealla',
|
|
26
|
+
command: process.env.TPL ?? 'tpl',
|
|
27
|
+
args: ['-q', '-g', goal, sudoku],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'Scryer',
|
|
31
|
+
command: process.env.SCRYER_PROLOG ?? 'scryer-prolog',
|
|
32
|
+
args: ['-f', '-g', goal, sudoku],
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
let failed = false;
|
|
37
|
+
let executed = 0;
|
|
38
|
+
for (const engine of engines) {
|
|
39
|
+
const result = spawnSync(engine.command, engine.args, {
|
|
40
|
+
cwd: root,
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
timeout: 120000,
|
|
43
|
+
});
|
|
44
|
+
if (result.error?.code === 'ENOENT') {
|
|
45
|
+
if (allowMissing && engine.name !== 'EyeProlog') {
|
|
46
|
+
console.log(`SKIP ${engine.name}: ${engine.command} not installed`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
console.error(`FAIL ${engine.name}: ${engine.command} not installed`);
|
|
50
|
+
failed = true;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
executed++;
|
|
54
|
+
if (result.error) {
|
|
55
|
+
console.error(`FAIL ${engine.name}: ${result.error.message}`);
|
|
56
|
+
failed = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
console.error(`FAIL ${engine.name}: exit ${result.status}`);
|
|
61
|
+
if (result.stderr) console.error(result.stderr.trim());
|
|
62
|
+
failed = true;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const lines = String(result.stdout ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
66
|
+
if (!lines.includes(expected)) {
|
|
67
|
+
console.error(`FAIL ${engine.name}: Sudoku output mismatch`);
|
|
68
|
+
console.error(`stdout: ${JSON.stringify(result.stdout)}`);
|
|
69
|
+
if (result.stderr) console.error(`stderr: ${result.stderr.trim()}`);
|
|
70
|
+
failed = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
console.log(`OK ${engine.name}: portable Sudoku`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (failed) process.exitCode = 1;
|
|
77
|
+
else console.log(`OK ${executed}/${allowMissing ? engines.length : executed} available interop engines passed`);
|