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/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 { standardLibrarySources } from './standard-library.js';
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 };
@@ -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
- // unlike the former autoloader, no clauses are added unless use_module/1 or
4
- // use_module/2 requests their module.
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`);