eyeprolog 0.5.13 → 0.5.15

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.
Files changed (124) hide show
  1. package/README.md +2 -0
  2. package/conformance-report.md +2 -2
  3. package/examples/auroracare.pl +26 -0
  4. package/examples/bmi.pl +20 -1
  5. package/examples/cdcl-sat-solver.pl +6 -6
  6. package/examples/composition-of-injective-functions-is-injective.pl +4 -1
  7. package/examples/deep-taxonomy-10.pl +2 -0
  8. package/examples/deep-taxonomy-100.pl +2 -0
  9. package/examples/deep-taxonomy-1000.pl +2 -0
  10. package/examples/deep-taxonomy-10000.pl +2 -0
  11. package/examples/exoplanet-validation-worlds.pl +3 -0
  12. package/examples/family-cousins.pl +2 -0
  13. package/examples/fastpow.pl +6 -6
  14. package/examples/flandor.pl +14 -0
  15. package/examples/gd-step-certified.pl +3 -0
  16. package/examples/group-inverse-uniqueness.pl +9 -9
  17. package/examples/illegitimate-reasoning.pl +6 -0
  18. package/examples/input/odrl-dpv-fpv-trust-flow-rules.pl +3 -9
  19. package/examples/list-collection.pl +6 -6
  20. package/examples/lldm.pl +2 -0
  21. package/examples/odrl-dpv-fpv-trust-flow.pl +3 -9
  22. package/examples/output/aliases-and-namespaces.pl +1 -1
  23. package/examples/output/bmi.pl +9 -0
  24. package/examples/output/dijkstra-findall-sort.pl +1 -1
  25. package/examples/output/dijkstra.pl +16 -16
  26. package/examples/output/drone-corridor-planner.pl +17 -17
  27. package/examples/output/iso-atomic-conversion.pl +1 -1
  28. package/examples/output/list-collection.pl +6 -6
  29. package/examples/pointer-analysis.pl +3 -0
  30. package/examples/proof/aliases-and-namespaces.pl +5 -5
  31. package/examples/proof/composition-of-injective-functions-is-injective.pl +80 -97
  32. package/examples/proof/graph-reachability.pl +22 -22
  33. package/examples/proof/group-inverse-uniqueness.pl +4 -4
  34. package/examples/proof/list-collection.pl +73 -29
  35. package/examples/proof/spacecraft-battery-diagnosis.pl +8 -8
  36. package/examples/proof/term-tools.pl +1 -1
  37. package/examples/relational-cube-lookup.pl +2002 -2000
  38. package/examples/riemann-hypothesis.pl +5 -0
  39. package/examples/spacecraft-battery-diagnosis.pl +8 -8
  40. package/examples/vulnerability-impact.pl +4 -0
  41. package/index.d.ts +5 -2
  42. package/package.json +1 -1
  43. package/src/cli.js +17 -9
  44. package/src/index.js +15 -5
  45. package/src/iso.js +1 -0
  46. package/src/parser.js +57 -12
  47. package/src/program.js +8 -5
  48. package/src/solver.js +11 -3
  49. package/src/term.js +41 -10
  50. package/src/write.js +6 -0
  51. package/test/conformance/README.md +1 -1
  52. package/test/conformance/cases/arithmetic/082_comparison_semantics.pl +2 -2
  53. package/test/conformance/cases/arithmetic/extra_le_equal_string.pl +1 -1
  54. package/test/conformance/cases/context/extra_context_ignores_string_parts.pl +1 -1
  55. package/test/conformance/cases/iso/double_quoted_lists.pl +27 -0
  56. package/test/conformance/cases/strings/023_quoted_escapes_readback.pl +2 -3
  57. package/test/conformance/cases/terms/003_terms_and_readback.pl +1 -1
  58. package/test/conformance/cases/terms/073_term_introspection_edges.pl +1 -1
  59. package/test/conformance/cases/terms/096_functor_scalar_edges.pl +2 -2
  60. package/test/conformance/cases/terms/extra_functor_string_scalar.pl +1 -1
  61. package/test/conformance/cases/terms/functor_number_and_string.pl +1 -1
  62. package/test/conformance/cases/unification/extra_eq_scalar_string_number.pl +1 -1
  63. package/test/conformance/cases/unification/scalar_cross_type_same_lexical_value.pl +2 -2
  64. package/test/conformance/expected/aggregation/044_findall_user_goal.pl +1 -1
  65. package/test/conformance/expected/arithmetic/082_comparison_semantics.pl +1 -1
  66. package/test/conformance/expected/arithmetic/extra_le_equal_string.pl +1 -1
  67. package/test/conformance/expected/context/extra_context_atom_parts.pl +1 -1
  68. package/test/conformance/expected/context/extra_context_ignores_string_parts.pl +1 -0
  69. package/test/conformance/expected/context/extra_context_list_parts.pl +1 -1
  70. package/test/conformance/expected/iso/atomic_term_processing.pl +1 -1
  71. package/test/conformance/expected/iso/double_quoted_lists.pl +8 -0
  72. package/test/conformance/expected/iso/exceptions_and_flags.pl +1 -1
  73. package/test/conformance/expected/iso/grouped_solutions_and_clauses.pl +1 -1
  74. package/test/conformance/expected/iso/logtalk_atom_chars.pl +1 -1
  75. package/test/conformance/expected/iso/logtalk_number_chars.pl +1 -1
  76. package/test/conformance/expected/iso/logtalk_prolog_flags.pl +1 -1
  77. package/test/conformance/expected/iso/logtalk_univ.pl +1 -1
  78. package/test/conformance/expected/iso/scryer_lexical_terms.pl +1 -1
  79. package/test/conformance/expected/lists/005_list_deconstruction.pl +1 -1
  80. package/test/conformance/expected/lists/031_lists_aggregation_ordering.pl +4 -4
  81. package/test/conformance/expected/lists/035_list_relations.pl +3 -3
  82. package/test/conformance/expected/lists/036_append_splits.pl +3 -3
  83. package/test/conformance/expected/lists/045_sort_deduplicates_atoms.pl +1 -1
  84. package/test/conformance/expected/lists/046_append_bound_prefix_suffix.pl +2 -2
  85. package/test/conformance/expected/lists/048_set_nth0_edges.pl +2 -2
  86. package/test/conformance/expected/lists/049_select_duplicate_occurrences.pl +2 -2
  87. package/test/conformance/expected/lists/067_list_edges.pl +1 -1
  88. package/test/conformance/expected/lists/068_list_generation_order.pl +6 -6
  89. package/test/conformance/expected/lists/084_append_and_select_composition.pl +9 -9
  90. package/test/conformance/expected/lists/085_nth_and_update_edges.pl +1 -1
  91. package/test/conformance/expected/lists/087_sort_reverse_length.pl +2 -2
  92. package/test/conformance/expected/lists/extra_append_all_splits_three.pl +4 -4
  93. package/test/conformance/expected/lists/extra_list_to_set_stable_order.pl +1 -1
  94. package/test/conformance/expected/lists/extra_reverse_nested_terms.pl +1 -1
  95. package/test/conformance/expected/lists/extra_slice_end.pl +1 -1
  96. package/test/conformance/expected/lists/extra_take_all.pl +1 -1
  97. package/test/conformance/expected/lists/list_pattern_last.pl +1 -1
  98. package/test/conformance/expected/lists/select_all_occurrences.pl +3 -3
  99. package/test/conformance/expected/lists/set_nth0_middle.pl +1 -1
  100. package/test/conformance/expected/rules/077_recursive_path_with_lists.pl +3 -3
  101. package/test/conformance/expected/strings/023_quoted_escapes_readback.pl +1 -1
  102. package/test/conformance/expected/strings/071_string_edges.pl +1 -1
  103. package/test/conformance/expected/strings/extra_split_empty_separator.pl +1 -1
  104. package/test/conformance/expected/terms/003_terms_and_readback.pl +3 -3
  105. package/test/conformance/expected/terms/065_reusable_term_control_builtins.pl +1 -1
  106. package/test/conformance/expected/terms/073_term_introspection_edges.pl +2 -2
  107. package/test/conformance/expected/terms/095_term_introspection_roundtrip.pl +1 -1
  108. package/test/conformance/expected/terms/096_functor_scalar_edges.pl +1 -1
  109. package/test/conformance/expected/terms/extra_compound_name_arguments_decompose_list.pl +1 -1
  110. package/test/conformance/expected/terms/extra_functor_string_scalar.pl +1 -1
  111. package/test/conformance/expected/terms/functor_number_and_string.pl +1 -1
  112. package/test/conformance/expected/unification/012_nested_compound_unification.pl +1 -1
  113. package/test/conformance/expected/unification/034_equality_and_inequality.pl +1 -1
  114. package/test/conformance/expected/unification/extra_eq_list_tail_binding.pl +1 -1
  115. package/test/conformance/expected/unification/list_tail_binding.pl +1 -1
  116. package/test/conformance/expected-proofs/proofs/bindings_nested_term.pl +5 -5
  117. package/test/conformance/expected-proofs/proofs/extra_compound_name_arguments.pl +4 -4
  118. package/test/conformance/expected-proofs/proofs/extra_findall_builtin.pl +5 -5
  119. package/test/conformance/expected-proofs/proofs/extra_nested_list_binding.pl +5 -5
  120. package/test/conformance/expected-proofs/proofs/extra_once_member.pl +2 -2
  121. package/test/conformance/expected-proofs/proofs/list_builtin.pl +1 -1
  122. package/test/run-playground.mjs +2 -2
  123. package/test/run-regression.mjs +69 -11
  124. package/the-art-of-eyeprolog.md +297 -38
@@ -11,6 +11,11 @@
11
11
  %% goal: zero_check(X0, X1, X2)
12
12
 
13
13
 
14
+ :- discontiguous(zeta_zero/1).
15
+ :- discontiguous(real_part/2).
16
+ :- discontiguous(imaginary_part/2).
17
+ :- discontiguous(source/2).
18
+
14
19
  % --- Sample zero catalogue -------------------------------------------------
15
20
  %
16
21
  % The three z* entries stand for catalogued non-trivial zeros of the
@@ -23,14 +23,14 @@ telemetry(bp1, cell_delta_v, 0.19).
23
23
  redundant_telemetry(bp1, temperature_c, 76.0).
24
24
 
25
25
  % Illustrative engineering limits and available cooling power.
26
- limit(max_safe_temperature_c, 60.0).
27
- limit(max_temperature_rise_c_per_min, 1.5).
28
- limit(max_cell_delta_v, 0.08).
26
+ safety_limit(max_safe_temperature_c, 60.0).
27
+ safety_limit(max_temperature_rise_c_per_min, 1.5).
28
+ safety_limit(max_cell_delta_v, 0.08).
29
29
  cooling_capacity_w(bp1, 12.0).
30
30
 
31
31
  % Thermal margin is positive below the limit and negative above it.
32
32
  metric(Pack, thermal_margin_c, Margin) :-
33
- limit(max_safe_temperature_c, Maximum),
33
+ safety_limit(max_safe_temperature_c, Maximum),
34
34
  telemetry(Pack, temperature_c, Temperature),
35
35
  (Margin is Maximum - Temperature).
36
36
 
@@ -43,17 +43,17 @@ metric(Pack, resistive_heating_w, Heating) :-
43
43
 
44
44
  over_temperature(Pack) :-
45
45
  telemetry(Pack, temperature_c, Temperature),
46
- limit(max_safe_temperature_c, Maximum),
46
+ safety_limit(max_safe_temperature_c, Maximum),
47
47
  (Temperature > Maximum).
48
48
 
49
49
  rapid_heating(Pack) :-
50
50
  telemetry(Pack, temperature_rise_c_per_min, Rate),
51
- limit(max_temperature_rise_c_per_min, Maximum),
51
+ safety_limit(max_temperature_rise_c_per_min, Maximum),
52
52
  (Rate > Maximum).
53
53
 
54
54
  cell_imbalance(Pack) :-
55
55
  telemetry(Pack, cell_delta_v, Delta),
56
- limit(max_cell_delta_v, Maximum),
56
+ safety_limit(max_cell_delta_v, Maximum),
57
57
  (Delta > Maximum).
58
58
 
59
59
  heating_exceeds_cooling(Pack) :-
@@ -65,7 +65,7 @@ heating_exceeds_cooling(Pack) :-
65
65
  corroborated_over_temperature(Pack) :-
66
66
  telemetry(Pack, temperature_c, Primary),
67
67
  redundant_telemetry(Pack, temperature_c, Redundant),
68
- limit(max_safe_temperature_c, Maximum),
68
+ safety_limit(max_safe_temperature_c, Maximum),
69
69
  (Primary > Maximum),
70
70
  (Redundant > Maximum).
71
71
 
@@ -14,6 +14,10 @@
14
14
  %% goal: urgent_patch(X0, X1)
15
15
 
16
16
 
17
+ :- discontiguous(vulnerability/2).
18
+ :- discontiguous(cvss/2).
19
+ :- discontiguous(fixed_by/2).
20
+
17
21
  service(webapp).
18
22
  service(worker).
19
23
  service(frontend).
package/index.d.ts CHANGED
@@ -16,6 +16,8 @@ export interface EyePrologRunOptions {
16
16
  sourceMetadata?: boolean;
17
17
  strictNegation?: boolean;
18
18
  analyzeNegation?: boolean;
19
+ /** Initial ISO interpretation of double-quoted list notation. Defaults to chars. */
20
+ doubleQuotes?: 'chars' | 'codes' | 'atom';
19
21
  ioOptions?: {
20
22
  input?: string;
21
23
  write?: (text: string) => void;
@@ -88,6 +90,7 @@ export class Program {
88
90
  constructor(clauses?: EyePrologClause[], options?: EyePrologRunOptions);
89
91
  clauses: EyePrologClause[];
90
92
  groups: Map<string, EyePrologPredicateGroup>;
93
+ doubleQuotes: 'chars' | 'codes' | 'atom';
91
94
  negationDependencies: Array<{ from: string; to: string; negative: boolean }>;
92
95
  negationStratificationErrors: Array<{ from: string; to: string }>;
93
96
  stratifiedNegation: boolean;
@@ -101,7 +104,7 @@ export class Program {
101
104
  assertStratifiedNegation(): true;
102
105
  isStratifiedNegation(): boolean;
103
106
  groupHasRule(group: EyePrologPredicateGroup): boolean;
104
- sourceFactLines(predicateKeys?: Set<string> | null): Set<string>;
107
+ sourceFactLines(predicateKeys?: Set<string> | null, options?: { doubleQuotes?: 'chars' | 'codes' | 'atom' }): Set<string>;
105
108
  }
106
109
 
107
110
  export interface BuiltinDefinition {
@@ -181,7 +184,7 @@ export function compareNumberText(left: string, right: string): number;
181
184
  export function makeProgram(source: string, options?: EyePrologRunOptions): Program;
182
185
  export function parseClauses(source: string, options?: EyePrologRunOptions): EyePrologClause[];
183
186
  export function parseProgramText(source: string, options?: EyePrologRunOptions): EyePrologClause[];
184
- export function parseGoalText(source: string): EyePrologTerm;
187
+ export function parseGoalText(source: string, options?: EyePrologRunOptions): EyePrologTerm;
185
188
  export function createDefaultRegistry(): BuiltinRegistry;
186
189
  export function createEyePrologRegistry(): BuiltinRegistry;
187
190
  export function getDefaultRegistry(): BuiltinRegistry;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.5.13",
6
+ "version": "0.5.15",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/cli.js CHANGED
@@ -128,22 +128,27 @@ async function loadExplanation() {
128
128
  }
129
129
 
130
130
  async function runDefault(engine, program, options) {
131
+ const registry = engine.getEyePrologRegistry();
132
+ const solver = new engine.Solver(program, {
133
+ registry,
134
+ ioOptions: { write: (text) => process.stdout.write(String(text)) },
135
+ });
136
+ program = solver.program;
131
137
  const goals = options.goals.map((text) => {
132
- const goal = engine.parseGoalText(text);
138
+ const goal = engine.parseGoalText(text, {
139
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
140
+ });
133
141
  if (goal.type === 'var') throw new engine.PrologError('instantiation_error');
134
142
  if (goal.type !== 'atom' && goal.type !== 'compound') throw new engine.PrologError('type_error(callable)', goal);
135
143
  return goal;
136
144
  });
137
145
  const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
138
- const facts = program.sourceFactLines(queriedKeys);
146
+ const writeOptions = {
147
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
148
+ };
149
+ const facts = program.sourceFactLines(queriedKeys, writeOptions);
139
150
  const lines = new Set();
140
- const registry = engine.getEyePrologRegistry();
141
151
  const explanation = options.proof ? await loadExplanation() : null;
142
- const solver = new engine.Solver(program, {
143
- registry,
144
- ioOptions: { write: (text) => process.stdout.write(String(text)) },
145
- });
146
- program = solver.program;
147
152
  try {
148
153
  solver.runInitializations();
149
154
  for (const goal of goals) {
@@ -151,7 +156,10 @@ async function runDefault(engine, program, options) {
151
156
  for (const env of solver.solve([goal], new engine.Env(), 0)) {
152
157
  if (!engine.termIsGround(goal, env)) continue;
153
158
 
154
- const line = `${engine.termToString(goal, env, true)}.\n`;
159
+ const currentWriteOptions = {
160
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
161
+ };
162
+ const line = `${engine.termToString(goal, env, true, currentWriteOptions)}.\n`;
155
163
  if (facts.has(line) || lines.has(line)) continue;
156
164
 
157
165
  lines.add(line);
package/src/index.js CHANGED
@@ -47,9 +47,12 @@ export function run(source, options = {}) {
47
47
  },
48
48
  });
49
49
  program = solver.program;
50
- const goals = normalizeGoals(options);
50
+ const goals = normalizeGoals(options, solver);
51
+ const writeOptions = {
52
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
53
+ };
51
54
  const queriedKeys = new Set(goals.map((goal) => `${goal.name}/${goal.arity}`));
52
- const facts = program.sourceFactLines(queriedKeys);
55
+ const facts = program.sourceFactLines(queriedKeys, writeOptions);
53
56
  const seen = new Set();
54
57
  let haltCode = null;
55
58
  try {
@@ -59,7 +62,10 @@ export function run(source, options = {}) {
59
62
  for (const env of solver.solve([goal], new Env(), 0)) {
60
63
  const resolved = copyResolved(goal, env);
61
64
  if (!termIsGround(resolved)) continue;
62
- const line = `${termToString(resolved, new Env(), true)}.\n`;
65
+ const currentWriteOptions = {
66
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
67
+ };
68
+ const line = `${termToString(resolved, new Env(), true, currentWriteOptions)}.\n`;
63
69
  if (facts.has(line) || seen.has(line)) continue;
64
70
  seen.add(line);
65
71
  output.push(line);
@@ -73,10 +79,14 @@ export function run(source, options = {}) {
73
79
  return { stdout: output.join(''), stats: solver.stats, haltCode };
74
80
  }
75
81
 
76
- function normalizeGoals(options) {
82
+ function normalizeGoals(options, solver) {
77
83
  const requested = options.goals ?? (options.goal == null ? [] : [options.goal]);
78
84
  return requested.map((requestedGoal) => {
79
- const goal = typeof requestedGoal === 'string' ? parseGoalText(requestedGoal) : requestedGoal;
85
+ const goal = typeof requestedGoal === 'string'
86
+ ? parseGoalText(requestedGoal, {
87
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
88
+ })
89
+ : requestedGoal;
80
90
  if (goal.type === VAR) throw new PrologError('instantiation_error');
81
91
  if (goal.type !== ATOM && goal.type !== COMPOUND) throw new PrologError('type_error(callable)', goal);
82
92
  return goal;
package/src/iso.js CHANGED
@@ -920,6 +920,7 @@ function readTermFromStream(stream, solver) {
920
920
  const clauses = parseClauses(convertedTermText(candidate.text, solver), {
921
921
  sourceMetadata: false,
922
922
  operatorState,
923
+ doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
923
924
  });
924
925
  if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
925
926
  stream.position = candidate.end;
package/src/parser.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Tokenizer and recursive-descent parser for the EyeProlog source language.
2
2
  // It preserves the compact Prolog-like syntax while producing Term objects for the solver.
3
- import { atom, compound, cons, emptyList, numberTerm, stringTerm, variable } from './term.js';
3
+ import { atom, compound, cons, emptyList, numberTerm, variable } from './term.js';
4
4
 
5
5
  const TOK = {
6
6
  EOF: 'eof', ATOM: 'atom', VAR: 'var', STRING: 'string', NUMBER: 'number',
@@ -143,6 +143,12 @@ class Parser {
143
143
  this.line = 1;
144
144
  this.anonymous = 0;
145
145
  this.sourceMetadata = options.sourceMetadata !== false;
146
+ this.parserFlagState = options.parserFlagState ?? {
147
+ doubleQuotes: options.doubleQuotes ?? 'chars',
148
+ };
149
+ if (!['chars', 'codes', 'atom'].includes(this.parserFlagState.doubleQuotes)) {
150
+ throw new Error(`invalid double_quotes parser flag: ${this.parserFlagState.doubleQuotes}`);
151
+ }
146
152
  const operatorState = options.operatorState ?? createParserOperatorState(
147
153
  options.operatorDefinitions ?? [],
148
154
  options.includeDefaultOperators !== false,
@@ -174,6 +180,14 @@ class Parser {
174
180
  for (const name of names) this.defineOperator(priority, specifierTerm.name, name);
175
181
  return true;
176
182
  }
183
+ applyParserFlagDirective(directive) {
184
+ if (directive.type !== 'compound' || directive.name !== 'set_prolog_flag' || directive.arity !== 2) return;
185
+ const [flag, value] = directive.args;
186
+ if (flag.type === 'atom' && flag.name === 'double_quotes' &&
187
+ value.type === 'atom' && ['chars', 'codes', 'atom'].includes(value.name)) {
188
+ this.parserFlagState.doubleQuotes = value.name;
189
+ }
190
+ }
177
191
  peek(offset = 0) {
178
192
  return this.source[this.pos + offset] ?? '';
179
193
  }
@@ -494,7 +508,31 @@ class Parser {
494
508
  if (this.token.type === TOK.STRING) {
495
509
  const value = this.token.text;
496
510
  this.advance();
497
- return stringTerm(value);
511
+ if (this.parserFlagState.doubleQuotes === 'atom') {
512
+ if (this.token.type === TOK.LPAREN) {
513
+ this.advance();
514
+ const args = [];
515
+ if (this.token.type === TOK.RPAREN) {
516
+ throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(value)} for arity zero data`);
517
+ }
518
+ while (true) {
519
+ args.push(this.parseTerm(0, false));
520
+ if (this.token.type !== TOK.COMMA) break;
521
+ this.advance();
522
+ }
523
+ this.expect(TOK.RPAREN, ')');
524
+ this.advance();
525
+ return compound(value, args);
526
+ }
527
+ return atom(value);
528
+ }
529
+ const items = Array.from(value, (character) =>
530
+ this.parserFlagState.doubleQuotes === 'chars'
531
+ ? atom(character)
532
+ : numberTerm(character.codePointAt(0)));
533
+ let list = emptyList();
534
+ for (let i = items.length - 1; i >= 0; i--) list = cons(items[i], list);
535
+ return list;
498
536
  }
499
537
  if (this.token.type === TOK.NUMBER) {
500
538
  const value = this.token.text;
@@ -544,6 +582,7 @@ class Parser {
544
582
  throw new Error(`parse line ${line}: bad term`);
545
583
  }
546
584
  this.expect(TOK.DOT, '.');
585
+ this.applyParserFlagDirective(directive);
547
586
  this.advance();
548
587
  const clause = { head: compound(':-', [directive]), body: [] };
549
588
  clauseNumber++;
@@ -588,11 +627,17 @@ function listAtomNames(term) {
588
627
 
589
628
 
590
629
  export function parseClauses(source, options = {}) {
630
+ const ownsParserFlagState = options.parserFlagState == null;
631
+ const initialDoubleQuotes = options.doubleQuotes ?? 'chars';
632
+ const parserOptions = ownsParserFlagState
633
+ ? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes } }
634
+ : options;
591
635
  if (options.sourceMetadata === false) {
592
- const clauses = parseClausesFastNoSource(source);
636
+ const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
593
637
  if (clauses) return clauses;
638
+ if (ownsParserFlagState) parserOptions.parserFlagState.doubleQuotes = initialDoubleQuotes;
594
639
  }
595
- return new Parser(source, options).parseProgram();
640
+ return new Parser(source, parserOptions).parseProgram();
596
641
  }
597
642
 
598
643
  // Streaming parser entry points used by ProgramBuilder. The ordinary public
@@ -602,8 +647,8 @@ export function parseClausesInto(source, options = {}, emit) {
602
647
  new Parser(source, options).parseProgram(emit);
603
648
  }
604
649
 
605
- export function tryParseClausesFastInto(source, emit, emitBinary = null) {
606
- return parseClausesFastNoSource(source, emit, emitBinary) !== null;
650
+ export function tryParseClausesFastInto(source, emit, emitBinary = null, options = {}) {
651
+ return parseClausesFastNoSource(source, emit, emitBinary, options) !== null;
607
652
  }
608
653
 
609
654
  function isSimpleName(text) {
@@ -624,7 +669,7 @@ const SIMPLE_VARIABLE = /^(?:_|[A-Z_][A-Za-z0-9_]*)$/;
624
669
  const SIMPLE_ATOM = /^[a-z][A-Za-z0-9_]*$/;
625
670
  const GRAPHIC_ATOM = /^[#$&*+\-\/<=>@^~\\]+$/;
626
671
 
627
- function parseClausesFastNoSource(source, emit = null, emitBinary = null) {
672
+ function parseClausesFastNoSource(source, emit = null, emitBinary = null, options = {}) {
628
673
  source = String(source ?? '');
629
674
  const numberCache = new Map();
630
675
  const stringCache = new Map();
@@ -896,7 +941,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null) {
896
941
  return true;
897
942
  }
898
943
  try {
899
- const parsed = new Parser(text, { sourceMetadata: false }).parseProgram();
944
+ const parsed = new Parser(text, { ...options, sourceMetadata: false }).parseProgram();
900
945
  for (const clause of parsed) accept(clause);
901
946
  return true;
902
947
  } catch (_) {
@@ -938,12 +983,12 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null) {
938
983
  return clauses ?? true;
939
984
  }
940
985
 
941
- export function parseProgramText(source) {
942
- return parseClauses(source);
986
+ export function parseProgramText(source, options = {}) {
987
+ return parseClauses(source, options);
943
988
  }
944
989
 
945
- export function parseGoalText(text) {
946
- const clauses = parseClauses(`zz_goal((${text})).`);
990
+ export function parseGoalText(text, options = {}) {
991
+ const clauses = parseClauses(`zz_goal((${text})).`, options);
947
992
  const head = clauses[0]?.head;
948
993
  if (clauses.length !== 1 || head?.type !== 'compound' ||
949
994
  head.name !== 'zz_goal' || head.arity !== 1 || clauses[0].body.length !== 0) {
package/src/program.js CHANGED
@@ -97,6 +97,7 @@ export class Program {
97
97
  this.initializations = [];
98
98
  this.prologFlagDirectives = [];
99
99
  this.charConversionDirectives = [];
100
+ this.doubleQuotes = options.doubleQuotes ?? 'chars';
100
101
  this._revisionState = { value: 0 };
101
102
  this.mutable = false;
102
103
  this._negationAnalysis = null;
@@ -372,19 +373,19 @@ export class Program {
372
373
  groupHasRule(group) {
373
374
  return group.clauses.some((clause) => clauseBodyLength(clause) > 0);
374
375
  }
375
- sourceFactLines(predicateKeys = null) {
376
+ sourceFactLines(predicateKeys = null, options = {}) {
376
377
  const lines = new Set();
377
378
  const env = new Env();
378
379
  for (const clause of this.clauses) {
379
380
  if (isCompactBinaryClause(clause)) {
380
381
  if (clause.bodyName != null) continue;
381
382
  if (predicateKeys && !predicateKeys.has(`${clause.headName}/2`)) continue;
382
- lines.add(`${termToString(clause.head, env, true)}.\n`);
383
+ lines.add(`${termToString(clause.head, env, true, options)}.\n`);
383
384
  continue;
384
385
  }
385
386
  if (clause.body.length !== 0 || (clause.head.type !== ATOM && clause.head.type !== COMPOUND)) continue;
386
387
  if (predicateKeys && !predicateKeys.has(`${clause.head.name}/${clause.head.arity}`)) continue;
387
- lines.add(`${termToString(clause.head, env, true)}.\n`);
388
+ lines.add(`${termToString(clause.head, env, true, options)}.\n`);
388
389
  }
389
390
  return lines;
390
391
  }
@@ -530,9 +531,10 @@ function buildProgramFromSources(sources, options) {
530
531
  function loadSourcesIntoBuilder(builder, sources, options, fast) {
531
532
  const ensured = new Set();
532
533
  const operatorState = createParserOperatorState();
534
+ const parserFlagState = { doubleQuotes: options.doubleQuotes ?? 'chars' };
533
535
  const prepared = sources.map((source) => ({
534
536
  source,
535
- options: { ...sourceOptionsFor(source, options), operatorState },
537
+ options: { ...sourceOptionsFor(source, options), operatorState, parserFlagState },
536
538
  }));
537
539
  for (const item of prepared) {
538
540
  const filename = sourcePath(item.options);
@@ -545,6 +547,7 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
545
547
  : item.source?.text ?? item.source?.source ?? '';
546
548
  if (!loadSourceIntoBuilder(builder, text, item.options, ensured, fast)) return false;
547
549
  }
550
+ builder.program.doubleQuotes = parserFlagState.doubleQuotes;
548
551
  return true;
549
552
  } catch (error) {
550
553
  if (error === FAST_PARSE_ABORT) return false;
@@ -600,7 +603,7 @@ function loadSourceIntoBuilder(builder, source, options, ensured, fast) {
600
603
  ));
601
604
  if (batch.length >= PROGRAM_BUILD_BATCH_SIZE) flush();
602
605
  };
603
- const parsed = tryParseClausesFastInto(source, accept, acceptBinary);
606
+ const parsed = tryParseClausesFastInto(source, accept, acceptBinary, options);
604
607
  if (parsed) flush();
605
608
  return parsed;
606
609
  }
package/src/solver.js CHANGED
@@ -27,11 +27,19 @@ export class Solver {
27
27
  this.prologFlags = options.prologFlags ?? defaultPrologFlags(this.registry?.eyePrologLibrary ? 'fail' : 'error');
28
28
  this.charConversions = options.charConversions ?? new Map();
29
29
  if (!options.prologFlags) {
30
+ if (['chars', 'codes', 'atom'].includes(program.doubleQuotes)) {
31
+ this.prologFlags.get('double_quotes').value = compound(program.doubleQuotes, []);
32
+ }
30
33
  for (const [flag, value] of program.prologFlagDirectives ?? []) {
34
+ if (flag.type === 'var' || value.type === 'var') throw new PrologError('instantiation_error');
35
+ if (flag.type !== 'atom') throw new PrologError('type_error(atom)', flag);
31
36
  const definition = this.prologFlags.get(flag.name);
32
- if (flag.type === 'atom' && value.type === 'atom' && definition?.changeable && definition.allowed.includes(value.name)) {
33
- definition.value = value;
37
+ if (!definition) throw new PrologError('domain_error(prolog_flag)', flag);
38
+ if (value.type !== 'atom' || !definition.allowed.includes(value.name)) {
39
+ throw new PrologError('domain_error(flag_value)', compound('+', [flag, value]));
34
40
  }
41
+ if (!definition.changeable) throw new PrologError('permission_error(modify, flag)', flag);
42
+ definition.value = value;
35
43
  }
36
44
  }
37
45
  if (!options.charConversions) {
@@ -383,7 +391,7 @@ function defaultPrologFlags(unknown = 'error') {
383
391
  ['debug', { value: compound('off', []), allowed: ['on', 'off'], changeable: true }],
384
392
  ['max_arity', { value: compound('unbounded', []), allowed: ['unbounded'], changeable: false }],
385
393
  ['unknown', { value: compound(unknown, []), allowed: ['error', 'fail', 'warning'], changeable: true }],
386
- ['double_quotes', { value: compound('atom', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
394
+ ['double_quotes', { value: compound('chars', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
387
395
  ]);
388
396
  }
389
397
 
package/src/term.js CHANGED
@@ -314,37 +314,68 @@ function writeString(value, quoteStrings) {
314
314
  let out = '"';
315
315
  for (const ch of value) {
316
316
  if (ch === '"' || ch === '\\') out += `\\${ch}`;
317
+ else if (ch === '\x07') out += '\\a';
318
+ else if (ch === '\b') out += '\\b';
319
+ else if (ch === '\r') out += '\\r';
320
+ else if (ch === '\f') out += '\\f';
321
+ else if (ch === '\t') out += '\\t';
317
322
  else if (ch === '\n') out += '\\n';
323
+ else if (ch === '\v') out += '\\v';
318
324
  else out += ch;
319
325
  }
320
326
  return out + '"';
321
327
  }
322
328
 
323
- function writeList(term, env) {
329
+ function quotedListText(term, env, doubleQuotes) {
330
+ if (doubleQuotes !== 'chars' && doubleQuotes !== 'codes') return null;
331
+ const characters = [];
332
+ let cursor = term;
333
+ while (true) {
334
+ cursor = deref(cursor, env);
335
+ if (isEmptyList(cursor)) return characters.length === 0 ? null : characters.join('');
336
+ if (!isCons(cursor)) return null;
337
+ const item = deref(cursor.args[0], env);
338
+ if (doubleQuotes === 'chars') {
339
+ if (item.type !== ATOM || Array.from(item.name).length !== 1) return null;
340
+ characters.push(item.name);
341
+ } else {
342
+ if (item.type !== NUMBER || !/^\d+$/.test(item.name)) return null;
343
+ const code = BigInt(item.name);
344
+ if (code < 0n || code > 0x10ffffn || (code >= 0xd800n && code <= 0xdfffn)) return null;
345
+ characters.push(String.fromCodePoint(Number(code)));
346
+ }
347
+ cursor = cursor.args[1];
348
+ }
349
+ }
350
+
351
+ function writeList(term, env, options) {
352
+ const quotedText = quotedListText(term, env, options.doubleQuotes);
353
+ if (quotedText != null) return writeString(quotedText, true);
324
354
  const parts = [];
325
355
  let cursor = term;
326
356
  while (true) {
327
357
  cursor = deref(cursor, env);
328
358
  if (isEmptyList(cursor)) return `[${parts.join(', ')}]`;
329
359
  if (!isCons(cursor)) {
330
- if (parts.length) return `[${parts.join(', ')} | ${termToString(cursor, env, true)}]`;
331
- return `[${termToString(cursor, env, true)}]`;
360
+ if (parts.length) return `[${parts.join(', ')} | ${termToString(cursor, env, true, options)}]`;
361
+ return `[${termToString(cursor, env, true, options)}]`;
332
362
  }
333
- parts.push(termToString(cursor.args[0], env, true));
363
+ parts.push(termToString(cursor.args[0], env, true, options));
334
364
  cursor = cursor.args[1];
335
365
  }
336
366
  }
337
367
 
338
- export function termToString(term, env = new Env(), quoteStrings = true) {
368
+ export function termToString(term, env = new Env(), quoteStrings = true, options = {}) {
369
+ options = { doubleQuotes: options.doubleQuotes ?? 'chars' };
339
370
  const resolved = deref(term, env);
340
371
  if (resolved.type === VAR) return writeVariable(resolved.name);
341
- if (isCons(resolved)) return writeList(resolved, env);
372
+ if (isCons(resolved)) return writeList(resolved, env, options);
342
373
  if (resolved.type === STRING) return writeString(resolved.name, quoteStrings);
343
374
  if (resolved.type === ATOM) return writeAtom(resolved.name);
344
375
  if (resolved.type === NUMBER) return resolved.name;
345
376
  if (resolved.type === COMPOUND && resolved.arity === 0) return writeAtom(resolved.name);
346
377
  if (resolved.type === COMPOUND && resolved.name === '{}' && resolved.arity === 1) {
347
- return `{${termToString(resolved.args[0], env, true)}}`;
378
+ return `{${termToString(resolved.args[0], env, true, options)}}`;
348
379
  }
349
380
  if (isConjunction(resolved)) {
350
381
  const parts = [];
@@ -352,16 +383,16 @@ export function termToString(term, env = new Env(), quoteStrings = true) {
352
383
  while (true) {
353
384
  cursor = deref(cursor, env);
354
385
  if (isConjunction(cursor)) {
355
- parts.push(termToString(cursor.args[0], env, true));
386
+ parts.push(termToString(cursor.args[0], env, true, options));
356
387
  cursor = cursor.args[1];
357
388
  } else {
358
- parts.push(termToString(cursor, env, true));
389
+ parts.push(termToString(cursor, env, true, options));
359
390
  break;
360
391
  }
361
392
  }
362
393
  return `(${parts.join(', ')})`;
363
394
  }
364
- return `${writeAtom(resolved.name)}(${resolved.args.map((arg) => termToString(arg, env, true)).join(', ')})`;
395
+ return `${writeAtom(resolved.name)}(${resolved.args.map((arg) => termToString(arg, env, true, options)).join(', ')})`;
365
396
  }
366
397
 
367
398
  export function lexicalValue(term, env) {
package/src/write.js CHANGED
@@ -52,7 +52,13 @@ function writeString(value) {
52
52
  let out = '"';
53
53
  for (const ch of value) {
54
54
  if (ch === '"' || ch === '\\') out += `\\${ch}`;
55
+ else if (ch === '\x07') out += '\\a';
56
+ else if (ch === '\b') out += '\\b';
57
+ else if (ch === '\r') out += '\\r';
58
+ else if (ch === '\f') out += '\\f';
59
+ else if (ch === '\t') out += '\\t';
55
60
  else if (ch === '\n') out += '\\n';
61
+ else if (ch === '\v') out += '\\v';
56
62
  else out += ch;
57
63
  }
58
64
  return out + '"';
@@ -90,7 +90,7 @@ Selected cases are adapted from the ISO and standard-core suites of Scryer
90
90
  Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
91
91
  are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
92
92
 
93
- The corpus has 279 cases in `iso/` and 685 file-based conformance cases in
93
+ The corpus has 280 cases in `iso/` and 686 file-based conformance cases in
94
94
  total. The generated `conformance-report.md` is the authoritative source for
95
95
  current category totals. Together with regression, documentation-sync, API,
96
96
  RDF-tool, example, and book-example checks, `npm test` is the release gate.
@@ -1,4 +1,4 @@
1
- % Reference 9.3: comparison handles numbers, durations, strings, and lexical scalars.
1
+ % Reference 9.3: comparison handles numbers, durations, character lists, and lexical scalars.
2
2
  %% goal: answer(X0, X1)
3
3
 
4
4
  answer(integer_order, ok) :- (9 < 10).
@@ -7,5 +7,5 @@ answer(numeric_not_lexical, ok) :- (10 < 100).
7
7
  answer(duration_years, ok) :- ('P1Y' @< 'P2Y').
8
8
  answer(duration_months, ok) :- ('P1Y1M' @> 'P1Y').
9
9
  answer(atom_lexical, ok) :- (alpha @< beta).
10
- answer(string_lexical, ok) :- ("z" @> "a").
10
+ answer(char_list_order, ok) :- ("z" @> "a").
11
11
  answer(compound_lexical_text, ok) :- (pair(a, 1) @< pair(b, 1)).
@@ -1,3 +1,3 @@
1
1
  %% goal: answer(X0)
2
2
 
3
- answer(le_equal_string) :- ("abc" @=< "abc").
3
+ answer(le_equal_char_list) :- ("abc" @=< "abc").
@@ -4,4 +4,4 @@ context_member((Left, _right), Member) :- context_member(Left, Member).
4
4
  context_member((_left, Right), Member) :- context_member(Right, Member).
5
5
  context_member(Member, Member) :- Member \= (_left, _right).
6
6
  context_parts(Context, Name, Args) :- context_member(Context, Statement), (Statement =.. [Name | Args]), atom(Name).
7
- answer(context_ignores_string_parts) :- context_parts("not a term", _, _).
7
+ answer(context_inspects_char_list) :- context_parts("not a term", _, _).
@@ -0,0 +1,27 @@
1
+ % ISO double-quoted list notation follows the active double_quotes flag.
2
+ %% goal: default_chars(X0)
3
+ %% goal: default_empty(X0)
4
+ %% goal: default_shape(X0)
5
+ %% goal: atom_mode(X0)
6
+ %% goal: atom_functor(X0)
7
+ %% goal: atom_shape(X0)
8
+ %% goal: codes_mode(X0)
9
+ %% goal: codes_shape(X0)
10
+
11
+ stored_chars("ab").
12
+ stored_empty("").
13
+ default_chars(Value) :- stored_chars(Value).
14
+ default_empty(Value) :- stored_empty(Value).
15
+ default_shape(compound) :- compound("a"), \+ atomic("a").
16
+
17
+ :- set_prolog_flag(double_quotes, atom).
18
+ stored_atom("ab").
19
+ stored_compound("pair"(a, b)).
20
+ atom_mode(Value) :- stored_atom(Value).
21
+ atom_functor(Value) :- stored_compound(Value).
22
+ atom_shape(atomic) :- atom("a"), atomic("a").
23
+
24
+ :- set_prolog_flag(double_quotes, codes).
25
+ stored_codes("ab").
26
+ codes_mode(Value) :- stored_codes(Value).
27
+ codes_shape(compound) :- compound("a"), \+ atomic("a").
@@ -1,6 +1,5 @@
1
- % Reference 3.5, 11: quoted strings and atoms preserve escape sequences at read-back.
2
- raw(string, "line\nnext\t\\slash").
1
+ % Reference 3.5, 11: double-quoted lists and quoted atoms preserve escapes at read-back.
2
+ raw(char_list, "line\nnext\t\\slash").
3
3
  raw(atom, 'line\nnext\t\\slash').
4
4
  answer(K, V) :- raw(K, V).
5
5
  %% goal: answer(X0, X1)
6
-
@@ -5,7 +5,7 @@ raw_value(atom, pat).
5
5
  raw_value(quoted_atom, 'atom with spaces').
6
6
  raw_value(quoted_quote, 'needs''quote').
7
7
  raw_value(empty_atom, '').
8
- raw_value(string, "line\nquote: \"ok\"").
8
+ raw_value(char_list, "line\nquote: \"ok\"").
9
9
  raw_value(integer, -42).
10
10
  raw_value(decimal, 0.25).
11
11
  raw_value(scientific, 1.25e-3).
@@ -3,7 +3,7 @@
3
3
 
4
4
  answer(functor_atom, pair(Name, Arity)) :- functor(alpha, Name, Arity).
5
5
  answer(functor_number, pair(Name, Arity)) :- functor(42, Name, Arity).
6
- answer(functor_string, pair(Name, Arity)) :- functor("hi", Name, Arity).
6
+ answer(functor_char_list, pair(Name, Arity)) :- functor("hi", Name, Arity).
7
7
  answer(arg_nested, X) :- arg(1, path(edge(a, b), c), X).
8
8
  answer(compose_nested, X) :- (X =.. [outer | [inner(a), [b, c]]]).
9
9
  answer(compose_atom_empty_args, X) :- (X =.. [z | []]).