eyeprolog 1.1.7 → 1.1.8

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 (23) hide show
  1. package/package.json +1 -1
  2. package/src/dcg.js +51 -3
  3. package/src/iso.js +24 -9
  4. package/src/parser.js +13 -1
  5. package/src/quads.js +10 -1
  6. package/src/solver.js +6 -0
  7. package/test/conformance/THIRD_PARTY.md +2 -2
  8. package/test/conformance/expected-errors/iso/dcg_phrase_bad_input.txt +1 -1
  9. package/test/conformance/expected-errors/iso/dcg_phrase_bad_output.txt +1 -1
  10. package/test/conformance/expected-errors/iso/logtalk_dcg_bad_input_precedence.txt +1 -1
  11. package/test/conformance/expected-errors/iso/logtalk_dcg_bad_output_precedence.txt +1 -1
  12. package/test/conformance/expected-errors/iso/logtalk_dcg_embedded_noncallable.txt +1 -1
  13. package/test/conformance/expected-errors/iso/logtalk_dcg_improper_semicontext.txt +1 -1
  14. package/test/conformance/expected-errors/iso/logtalk_dcg_improper_terminal_rule.txt +1 -1
  15. package/test/conformance/expected-errors/iso/logtalk_dcg_invalid_semicontext.txt +1 -1
  16. package/test/conformance/expected-errors/iso/logtalk_dcg_nested_noncallable.txt +1 -1
  17. package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_failure.txt +1 -1
  18. package/test/conformance/expected-errors/iso/logtalk_dcg_noncallable_after_success.txt +1 -1
  19. package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_input.txt +1 -1
  20. package/test/conformance/expected-errors/iso/logtalk_dcg_phrase_improper_output.txt +1 -1
  21. package/test/conformance/expected-errors/syntax/extra_double_period_rejected.txt +1 -1
  22. package/test/run-regression.mjs +88 -0
  23. package/the-art-of-eyeprolog.md +6 -2
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.1.7",
6
+ "version": "1.1.8",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/dcg.js CHANGED
@@ -37,14 +37,14 @@ function terminalItems(term, env = new Env()) {
37
37
  const seen = new Set();
38
38
  let cursor = deref(term, env);
39
39
  while (cursor.type === COMPOUND && cursor.name === '.' && cursor.arity === 2) {
40
- if (seen.has(cursor)) throw new PrologError('type_error(terminal_sequence)', original);
40
+ if (seen.has(cursor)) throw new PrologError('type_error(list)', original);
41
41
  seen.add(cursor);
42
42
  items.push(cursor.args[0]);
43
43
  cursor = deref(cursor.args[1], env);
44
44
  }
45
45
  if (cursor.type === ATOM && cursor.name === '[]') return items;
46
46
  if (cursor.type === VAR) throw new PrologError('instantiation_error');
47
- throw new PrologError('type_error(terminal_sequence)', original);
47
+ throw new PrologError('type_error(list)', original);
48
48
  }
49
49
 
50
50
  function terminalsGoal(terminals, input, output, env) {
@@ -155,8 +155,12 @@ export function expandDcgBody(body, input, output, options = {}) {
155
155
  // definitions and documents these choices in its conformance profile.
156
156
  if (body.type === COMPOUND && body.name === '\\+' && body.arity === 1) {
157
157
  const ignored = freshDcgVariable('negated');
158
+ const negated = body.args[0];
159
+ const expandedNegated = negated.type === ATOM || negated.type === COMPOUND || negated.type === VAR
160
+ ? expandDcgBody(negated, input, ignored, options)
161
+ : negated;
158
162
  return conjunction(
159
- compound('\\+', [expandDcgBody(body.args[0], input, ignored, options)]),
163
+ compound('\\+', [expandedNegated]),
160
164
  equality(input, output),
161
165
  );
162
166
  }
@@ -175,6 +179,50 @@ export function expandDcgBody(body, input, output, options = {}) {
175
179
  return appendStateArguments(body, input, output, module);
176
180
  }
177
181
 
182
+ // Embedded goals are validated before the translated grammar is executed.
183
+ // This keeps a non-callable goal visible even when an earlier terminal or
184
+ // branch would otherwise prevent the host goal from being reached.
185
+ export function validateDcgEmbeddedGoals(body, input, output) {
186
+ const invalidBody = invalidDcgControl(body);
187
+ if (invalidBody != null) throw new PrologError('type_error(callable)', invalidBody);
188
+
189
+ const visit = (term) => {
190
+ if (term.type !== COMPOUND) return;
191
+ if (term.name === '{}' && term.arity === 1) {
192
+ if (invalidControlGoal(term.args[0])) {
193
+ // Report the translated host-language conjunction, matching the
194
+ // convention used by Trealla and the ISO Part 3 quad corpus.
195
+ throw new PrologError('type_error(callable)', conjunction(term.args[0], equality(input, output)));
196
+ }
197
+ return;
198
+ }
199
+ if ([',', ';', '|', '->', '\\+'].includes(term.name)) {
200
+ for (const argument of term.args) visit(argument);
201
+ }
202
+ };
203
+ visit(body);
204
+ }
205
+
206
+ function invalidDcgControl(term) {
207
+ if (term.type !== ATOM && term.type !== COMPOUND && term.type !== VAR) return term;
208
+ if (term.type !== COMPOUND || ![',', ';', '|', '->'].includes(term.name)) return null;
209
+ for (const argument of term.args) {
210
+ if (argument.type === COMPOUND && argument.name === '{}' && argument.arity === 1) continue;
211
+ const invalid = invalidDcgControl(argument);
212
+ if (invalid != null) return invalid;
213
+ }
214
+ return null;
215
+ }
216
+
217
+ function invalidControlGoal(goal) {
218
+ if (goal.type === VAR) return false;
219
+ if (goal.type !== ATOM && goal.type !== COMPOUND) return true;
220
+ if (goal.type === COMPOUND && [',', ';', '->'].includes(goal.name) && goal.arity === 2) {
221
+ return goal.args.some(invalidControlGoal);
222
+ }
223
+ return false;
224
+ }
225
+
178
226
  function splitGrammarHead(head) {
179
227
  let terminals = null;
180
228
  if (head.type === COMPOUND && head.name === ',' && head.arity === 2) {
package/src/iso.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from './term.js';
8
8
  import { createParserOperatorState, parseClauses } from './parser.js';
9
9
  import { formatTermForWrite } from './write.js';
10
- import { emptyTerminalSequence, expandDcgBody, isListOrPartialList } from './dcg.js';
10
+ import { emptyTerminalSequence, expandDcgBody, isListOrPartialList, validateDcgEmbeddedGoals } from './dcg.js';
11
11
 
12
12
  let isoFresh = 0;
13
13
 
@@ -490,7 +490,7 @@ function* clauseBuiltin({ solver, goal, env }) {
490
490
  if (head.type !== ATOM && head.type !== COMPOUND) throw new PrologError('type_error(callable)', head);
491
491
  callableOrVariable(goal.args[1], env);
492
492
  const indicator = compound('/', [atom(head.name), numberTerm(head.arity)]);
493
- if (solver.registry.get(head.name, head.arity)) {
493
+ if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(head)) {
494
494
  throw new PrologError('permission_error(access, private_procedure)', indicator);
495
495
  }
496
496
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
@@ -529,9 +529,13 @@ function procedureIndicator(head) {
529
529
  return compound('/', [atom(head.name), numberTerm(head.arity)]);
530
530
  }
531
531
 
532
+ function isGrammarRuleProcedure(head) {
533
+ return head.name === '-->' && head.arity === 2;
534
+ }
535
+
532
536
  function assertModifiable(solver, head, module = 'user') {
533
537
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? module);
534
- if (solver.registry.get(head.name, head.arity) || (group && !group.dynamic)) {
538
+ if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(head) || (group && !group.dynamic)) {
535
539
  throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(head));
536
540
  }
537
541
  }
@@ -558,7 +562,7 @@ function* retractBuiltin({ solver, goal, env }) {
558
562
  const parts = clauseParts(goal.args[0], env);
559
563
  requireClauseHead(parts.head);
560
564
  const group = solver.program.findGroup(parts.head.name, parts.head.arity, parts.head.module ?? goal.module ?? 'user');
561
- if (solver.registry.get(parts.head.name, parts.head.arity) || (group && !group.dynamic)) {
565
+ if (solver.registry.get(parts.head.name, parts.head.arity) || isGrammarRuleProcedure(parts.head) || (group && !group.dynamic)) {
562
566
  throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(parts.head));
563
567
  }
564
568
  if (!group) return;
@@ -581,7 +585,7 @@ function* retractAllBuiltin({ solver, goal, env }) {
581
585
  const head = deref(goal.args[0], env);
582
586
  requireClauseHead(head);
583
587
  const group = solver.program.findGroup(head.name, head.arity, head.module ?? goal.module ?? 'user');
584
- if (solver.registry.get(head.name, head.arity) || (group && !group.dynamic)) {
588
+ if (solver.registry.get(head.name, head.arity) || isGrammarRuleProcedure(head) || (group && !group.dynamic)) {
585
589
  throw new PrologError('permission_error(modify, static_procedure)', procedureIndicator(head));
586
590
  }
587
591
  if (group) {
@@ -615,7 +619,7 @@ function* abolishBuiltin({ solver, goal, env }) {
615
619
  const target = predicateIndicatorParts(goal.args[0], env);
616
620
  const module = goal.module ?? 'user';
617
621
  const group = solver.program.findGroup(target.name, target.arity, module);
618
- if (solver.registry.get(target.name, target.arity) || (group && !group.dynamic)) {
622
+ if (solver.registry.get(target.name, target.arity) || isGrammarRuleProcedure(target) || (group && !group.dynamic)) {
619
623
  throw new PrologError('permission_error(modify, static_procedure)', target.indicator);
620
624
  }
621
625
  solver.program.abolishDynamicGroup(target.name, target.arity, module);
@@ -1519,8 +1523,19 @@ function callable(term, env) {
1519
1523
  term = resolveCallable(term, env);
1520
1524
  if (term.type === VAR) throw new PrologError('instantiation_error');
1521
1525
  if (term.type !== ATOM && term.type !== COMPOUND) throw new PrologError('type_error(callable)', term);
1526
+ validateControlCallable(term, term);
1522
1527
  return term;
1523
1528
  }
1529
+ function validateControlCallable(term, culprit) {
1530
+ if (term.type !== COMPOUND || ![',', ';', '->'].includes(term.name) || term.arity !== 2) return;
1531
+ for (const argument of term.args) {
1532
+ if (argument.type === VAR) throw new PrologError('instantiation_error');
1533
+ if (argument.type !== ATOM && argument.type !== COMPOUND) {
1534
+ throw new PrologError('type_error(callable)', culprit);
1535
+ }
1536
+ validateControlCallable(argument, culprit);
1537
+ }
1538
+ }
1524
1539
  function resolveCallable(term, env) {
1525
1540
  const resolved = deref(term, env);
1526
1541
  if (resolved.type !== COMPOUND) return resolved;
@@ -1555,14 +1570,14 @@ function* phraseBuiltin({ solver, goal, env }) {
1555
1570
  if (grammarBody.type !== ATOM && grammarBody.type !== COMPOUND) {
1556
1571
  throw new PrologError('type_error(callable)', grammarBody);
1557
1572
  }
1558
-
1559
1573
  const input = goal.args[1];
1560
1574
  const requestedOutput = goal.arity === 2 ? emptyTerminalSequence() : goal.args[2];
1575
+ validateDcgEmbeddedGoals(grammarBody, input, requestedOutput);
1561
1576
  if (!isListOrPartialList(input, env)) {
1562
- throw new PrologError('type_error(terminal_sequence)', deref(input, env));
1577
+ throw new PrologError('type_error(list)', deref(input, env));
1563
1578
  }
1564
1579
  if (!isListOrPartialList(requestedOutput, env)) {
1565
- throw new PrologError('type_error(terminal_sequence)', deref(requestedOutput, env));
1580
+ throw new PrologError('type_error(list)', deref(requestedOutput, env));
1566
1581
  }
1567
1582
 
1568
1583
  // Delay the final output unification to keep phrase/3 steadfast in its
package/src/parser.js CHANGED
@@ -33,7 +33,7 @@ function isPlainAtomStartCode(code) {
33
33
  return code >= 97 && code <= 122;
34
34
  }
35
35
 
36
- const graphicAtomChars = '#$&*+-./<=>@^~\\;:';
36
+ const graphicAtomChars = '#$&*+-./<=>@^~\\:';
37
37
 
38
38
  // ISO operator syntax is lowered to the same ordinary compound terms used by
39
39
  // canonical notation. Commas remain separators except inside parentheses.
@@ -285,10 +285,22 @@ class Parser {
285
285
  this.pos += 2;
286
286
  return { type: TOK.ATOM, text: '?-', line };
287
287
  }
288
+ if (ch === '.' && this.peek(1) &&
289
+ !isWhitespaceCode(this.peek(1).charCodeAt(0)) &&
290
+ this.peek(1) !== '%' && !(this.peek(1) === '/' && this.peek(2) === '*')) {
291
+ const start = this.pos;
292
+ this.take();
293
+ while (isGraphicAtomCode(this.peek().charCodeAt(0))) this.take();
294
+ return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
295
+ }
288
296
  if (ch === '!') {
289
297
  this.take();
290
298
  return { type: TOK.ATOM, text: '!', line };
291
299
  }
300
+ if (ch === ';') {
301
+ this.take();
302
+ return { type: TOK.ATOM, text: ';', line };
303
+ }
292
304
 
293
305
  const punct = {
294
306
  '(': TOK.LPAREN, ')': TOK.RPAREN, '[': TOK.LBRACKET, ']': TOK.RBRACKET,
package/src/quads.js CHANGED
@@ -192,12 +192,21 @@ function executeQuery(program, query, input, maxSolutions, options) {
192
192
  const solver = new Solver(program, {
193
193
  ...options,
194
194
  registry: options.registry ?? getEyePrologRegistry(),
195
- solutionLimit: Math.max(1, maxSolutions),
195
+ // The solver's counter also observes completed nested searches (for
196
+ // example each arm of a DCG disjunction). Bound the public iterator here
197
+ // instead of letting those internal completions consume the quad's answer
198
+ // allowance.
199
+ solutionLimit: Math.max(maxSolutions, options.solutionLimit ?? 10000000),
196
200
  ioOptions: {
197
201
  input,
198
202
  write: (text) => { pendingOutput += String(text); },
199
203
  },
200
204
  });
205
+ // Undefined predicates are test failures rather than silent negative
206
+ // answers unless the source explicitly selected another unknown policy.
207
+ if (!(program.prologFlagDirectives ?? []).some(([flag]) => flag.type === ATOM && flag.name === 'unknown')) {
208
+ solver.prologFlags.get('unknown').value = atom('error');
209
+ }
201
210
  const solutions = [];
202
211
  let error = null;
203
212
  let tailOutput = '';
package/src/solver.js CHANGED
@@ -281,6 +281,12 @@ export class Solver {
281
281
  this.stats.solve_one_goal_calls++;
282
282
  const group = this.program.findGroup(goal.name, goal.arity, goal.module ?? 'user');
283
283
  if (!group) {
284
+ if (goal.name === '-->' && goal.arity === 2) {
285
+ throw new PrologError(
286
+ 'existence_error(procedure)',
287
+ compound('/', [compound('-->', []), numberTerm(2)]),
288
+ );
289
+ }
284
290
  if (this.prologFlags.get('unknown')?.value?.name === 'error') {
285
291
  throw new PrologError(
286
292
  'existence_error(procedure)',
@@ -13,8 +13,8 @@ Part 3 grammar cases are additionally adapted from Logtalk's
13
13
  `tests/logtalk/methods/phrase_2_3/tests.lgt` and
14
14
  `tests/logtalk/dcgs/tests.lgt` suites. Object and unit-test scaffolding was
15
15
  removed, translator-only assertions were converted to executable grammar
16
- behavior where possible, and expected list errors follow the Part 3
17
- `terminal_sequence` terminology used by EyeProlog.
16
+ behavior where possible, and expected list errors use the portable ISO
17
+ `type_error(list)` term.
18
18
 
19
19
  Copyright 1998-2026 Paulo Moura <pmoura@logtalk.org>
20
20
 
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), not_a_terminal_sequence)
1
+ error(type_error(list), not_a_terminal_sequence)
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), not_a_terminal_sequence)
1
+ error(type_error(list), not_a_terminal_sequence)
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), not_a_terminal_sequence)
1
+ error(type_error(list), not_a_terminal_sequence)
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), not_a_terminal_sequence)
1
+ error(type_error(list), not_a_terminal_sequence)
@@ -1 +1 @@
1
- error(type_error(callable), 1)
1
+ error(type_error(callable), (1, =([], [])))
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), [a | b])
1
+ error(type_error(list), [a | b])
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), [a | b])
1
+ error(type_error(list), [a | b])
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), b)
1
+ error(type_error(list), b)
@@ -1 +1 @@
1
- error(type_error(callable), 1)
1
+ error(type_error(callable), (1, =([__anon1], [])))
@@ -1 +1 @@
1
- error(type_error(callable), 1)
1
+ error(type_error(callable), (1, =("y", [])))
@@ -1 +1 @@
1
- error(type_error(callable), 1)
1
+ error(type_error(callable), (1, =([__anon1], [])))
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), [a | b])
1
+ error(type_error(list), [a | b])
@@ -1 +1 @@
1
- error(type_error(terminal_sequence), [a | b])
1
+ error(type_error(list), [a | b])
@@ -1 +1 @@
1
- parse line 1: bad term
1
+ parse line 1: expected ., got ..
@@ -198,6 +198,19 @@ why(
198
198
  assertEqual(Boolean(program.findGroup('?-', 2)), false, 'quad is inert');
199
199
  },
200
200
  },
201
+ {
202
+ name: 'parser separates compact ISO solo tokens and atom dots',
203
+ run: () => {
204
+ const program = Program.parse(
205
+ `compact ?- call((!;\\+1)).\n true.\n\n` +
206
+ `dot ?- functor([_],.,2).\n true.\n`,
207
+ );
208
+ assertEqual(program.quads.length, 2, 'quad count');
209
+ assertEqual(program.quads[0].query.args[0].name, ';', 'disjunction');
210
+ assertEqual(program.quads[0].query.args[0].args[1].name, '\\+', 'negation');
211
+ assertEqual(program.quads[1].query.args[1].name, '.', 'dot atom');
212
+ },
213
+ },
201
214
  {
202
215
  name: 'runQuads checks portable answer descriptions',
203
216
  run: () => {
@@ -221,6 +234,81 @@ why(
221
234
  assertEqual(result.stdout, 'quads: 12 run, 12 passed, 0 failed.\n', 'quad report');
222
235
  },
223
236
  },
237
+ {
238
+ name: 'runQuads matches the corrected ISO phrase quad boundaries',
239
+ run: () => {
240
+ const source = String.raw`c2 ?- call((1,fail)).
241
+ type_error(callable,(1,fail)).
242
+
243
+ c3 ?- call((fail,1)).
244
+ type_error(callable,(fail,1)).
245
+
246
+ c4 ?- call((!;1)).
247
+ type_error(callable,(!;1)).
248
+
249
+ 24 ?- asserta((a-->b)).
250
+ permission_error(modify,static_procedure,(-->)/2).
251
+
252
+ 25 ?- clause((a-->b),B).
253
+ permission_error(access,private_procedure,(-->)/2).
254
+
255
+ 26 ?- (X-->Y).
256
+ existence_error(procedure,(-->)/2).
257
+
258
+ 5 ?- phrase([a|b],L).
259
+ type_error(list,[a|b]).
260
+
261
+ 10 ?- phrase(([a],{1}),[]).
262
+ type_error(callable,(...,...)).
263
+
264
+ 37 ?- phrase((!,[a],{1}),[]).
265
+ type_error(callable,(...,...)).
266
+
267
+ 12 ?- phrase('|'([],[a]),[a]).
268
+ true.
269
+
270
+ 14 ?- phrase(([a];[]),L).
271
+ L=[a] ; L=[].
272
+
273
+ 15 ?- phrase({fail,1},L).
274
+ type_error(callable,((fail,1),...)).
275
+
276
+ 29 ?- phrase(([a],\+1),[]).
277
+ false.
278
+
279
+ 30 ?- phrase(([a],\+1;[]),[]).
280
+ true.
281
+
282
+ 31 ?- phrase(phrase(phrase,[]),L).
283
+ existence_error(procedure,phrase/4).
284
+
285
+ 32 ?- phrase(call([]),[]).
286
+ existence_error(procedure,[]/2).
287
+
288
+ 41 ?- phrase([],non_list).
289
+ type_error(list,non_list).
290
+
291
+ 42 ?- phrase([],[a|non_list]).
292
+ type_error(list,[a|non_list]).
293
+
294
+ 43 ?- phrase([],L,non_list).
295
+ type_error(list,non_list).
296
+
297
+ 44 ?- phrase([],L,[a|non_list]).
298
+ type_error(list,[a|non_list]).
299
+
300
+ 46 ?- phrase((1,{2}),[]).
301
+ type_error(callable,1).
302
+
303
+ 47 ?- phrase(({2},1),[]).
304
+ type_error(callable,1).
305
+ `;
306
+ const result = publicApi.runQuads(source);
307
+ assertEqual(result.total, 22, 'quad total');
308
+ assertEqual(result.passed, 22, 'quad passed');
309
+ assertEqual(result.stdout, 'quads: 22 run, 22 passed, 0 failed.\n', 'quad report');
310
+ },
311
+ },
224
312
  {
225
313
  name: 'runQuads rejects malformed answer substitutions',
226
314
  run: () => {
@@ -5341,8 +5341,8 @@ look_ahead(X), [X] --> [X].
5341
5341
  `phrase(+Body,?Sequence)` accepts or generates a complete sequence.
5342
5342
  `phrase(+Body,?Sequence,?Rest)` leaves `Rest` unconsumed and is steadfast in
5343
5343
  that argument. A variable body raises `instantiation_error`; a non-callable
5344
- body raises `type_error(callable)`. EyeProlog performs the optional Part 3
5345
- terminal-sequence checks and reports `type_error(terminal_sequence)`.
5344
+ body raises `type_error(callable)`. EyeProlog performs terminal-sequence checks
5345
+ and reports the portable ISO `type_error(list)` error term.
5346
5346
 
5347
5347
  Part 3 leaves `\+//1` and standalone `->//2` implementation dependent.
5348
5348
  EyeProlog uses non-consuming negation (`\+ Body` tests from the current state)
@@ -6138,6 +6138,10 @@ records its quads; it does not execute them or add their queries and answers as
6138
6138
  program clauses. A quad run prints a summary and exits with status `1` when any
6139
6139
  description fails.
6140
6140
 
6141
+ Unless the source explicitly selects another `unknown` flag, quad execution
6142
+ uses `unknown=error`, so an undefined predicate is reported rather than being
6143
+ accepted as a negative answer.
6144
+
6141
6145
  Answer descriptions support ordered answers separated by `;`, acceptable
6142
6146
  alternatives separated by `|`, `true`, `false`, standard error descriptions,
6143
6147
  and the `unexpected` annotation for an answer that must not occur (`inattendue`