View Javadoc

1   /*
2    * #%L
3    * prolobjectlink-jpi-jtrolog
4    * %%
5    * Copyright (C) 2012 - 2018 WorkLogic Project
6    * %%
7    * This program is free software: you can redistribute it and/or modify
8    * it under the terms of the GNU Lesser General Public License as
9    * published by the Free Software Foundation, either version 2.1 of the
10   * License, or (at your option) any later version.
11   * 
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Lesser Public License for more details.
16   * 
17   * You should have received a copy of the GNU General Lesser Public
18   * License along with this program.  If not, see
19   * <http://www.gnu.org/licenses/lgpl-2.1.html>.
20   * #L%
21   */
22  package jTrolog.engine;
23  
24  import jTrolog.errors.PrologException;
25  import jTrolog.lib.BasicLibrary;
26  import jTrolog.parser.Parser;
27  import jTrolog.terms.Double;
28  import jTrolog.terms.EvaluableTerm;
29  import jTrolog.terms.Number;
30  import jTrolog.terms.Struct;
31  import jTrolog.terms.StructAtom;
32  import jTrolog.terms.Term;
33  import jTrolog.terms.Var;
34  import jTrolog.terms.WrapStruct;
35  import jTrolog.terms.WrapVar;
36  import jTrolog.terms.Wrapper;
37  
38  import java.util.Collection;
39  import java.util.HashMap;
40  import java.util.Iterator;
41  import java.util.NoSuchElementException;
42  
43  /**
44   * Keeps track of what variables are bound to what Terms and at which
45   * ChoicePoint this connection was made.
46   * 
47   * now, the BindingsTable never receives anything that is not Wrapped. All
48   * Structs sent in are wrapped as a mfck. the unifyBranch is now possible to run
49   * without generating new objects when the getArg is called.
50   * 
51   * all the methods that add vars and get vars from the bidnings table is now
52   * given both Var and context (and link term and its context) a problem is that
53   * the stackMap needs to be subsumed into the bindings table since it needs to
54   * retract the links done by a current owner. I don't think I can remove the
55   * current owner as a separate map since it provides a rollback function. What I
56   * can do is stringToStructList WrapVars for it only and then move these into
57   * and out of the bindingsMap
58   * 
59   * @author janerist@stud.ntnu.no
60   * @author ivar.orstavik@hist.no
61   */
62  @SuppressWarnings({ "rawtypes", "unchecked" })
63  public class BindingsTable {
64  
65  	private int currentExecCtx = 0;
66  
67  	private LinkTable links = new LinkTable();
68  
69  	int uniqueExecutionCtxID = 1;
70  	int[] firstInCtx = new int[10000];
71  	private GarbageCan gc = new GarbageCan(links);
72  
73  	public int getUniqueExecutionCtxID() {
74  		if (links.getWidth() == uniqueExecutionCtxID)
75  			links.doubleWidth(uniqueExecutionCtxID * 2);
76  		return uniqueExecutionCtxID++;
77  	}
78  
79  	public void expandLinkTable(int newSize) {
80  		gc.doubleTrashCanWidth(newSize);
81  	}
82  
83  	final void collectGarbage(final int execCtx) {
84  		uniqueExecutionCtxID = firstInCtx[execCtx];
85  		currentExecCtx = execCtx;
86  		gc.collectGarbageLinks(execCtx);
87  	}
88  
89  	final void collectGarbageSmall() {
90  		uniqueExecutionCtxID--;
91  		gc.collectGarbageLinks(currentExecCtx);
92  	}
93  
94  	final void setCurrentExecCtx(final int execCtx) {
95  		currentExecCtx = execCtx;
96  		if (execCtx >= firstInCtx.length)
97  			firstInCtx = LinkTable.expandArray(firstInCtx, execCtx * 2);
98  		firstInCtx[execCtx] = uniqueExecutionCtxID;
99  	}
100 
101 	// todo 1. perhaps it would be smart to keep track of the left and right
102 	// terms/ctxs that the unification process works on.
103 	// todo if this is done, then whenever a unification fails, one could
104 	// display exactly what two terms did not match.
105 	// todo but this is perhaps better done in the Engine, since one here might
106 	// like to view the entire terms..
107 	public boolean unify(Term t0, Term t1) {
108 		int oneCtx = t0 instanceof Wrapper ? ((Wrapper) t0).getContext() : 0;
109 		int twoCtx = t1 instanceof Wrapper ? ((Wrapper) t1).getContext() : 0;
110 		return unifyBranch(unWrap(t0), unWrap(t1), oneCtx, twoCtx);
111 	}
112 
113 	// ca. 500
114 
115 	/**
116 	 * @param one
117 	 * @param two
118 	 * @param oneCtx
119 	 * @param twoCtx
120 	 * @return true if the two terms are unified, false otherwise
121 	 */
122 	boolean unifyBranch(Term one, Term two, int oneCtx, int twoCtx) {
123 		while (one.type == Term.VAR) {
124 			Var vOne = (Var) one;
125 			// if the two varaiables are equal, then return true
126 			if (oneCtx == twoCtx && vOne.equals(two)) // if (oneCtx == twoCtx &&
127 														// two instanceof Var &&
128 														// vOne.nrInStruct ==
129 														// ((Var)
130 														// two).nrInStruct )
131 				return true;
132 			// else, if the left variable is not bound, bind it
133 			final int vNr = vOne.nrInStruct;
134 			Term link = links.getTerm(vNr, oneCtx);
135 			if (link == null)
136 				return setLink(vNr, oneCtx, two, twoCtx);
137 			// else, Var one is bound. If it is bound to a Var, then the loop
138 			// will continue updating the on Var, else it will continue down the
139 			// method
140 			oneCtx = links.getCtx(vNr, oneCtx);
141 			one = link;
142 		}
143 		while (two.type == Term.VAR) {
144 			final int vNr = ((Var) two).nrInStruct;
145 			Term link = links.getTerm(vNr, twoCtx);
146 			if (link == null)
147 				return setLink(vNr, twoCtx, one, oneCtx); // var two is unbound,
148 															// bind it against
149 															// one
150 			twoCtx = links.getCtx(vNr, twoCtx);
151 			two = link;
152 		}
153 		// from now on, neither branch is a Var
154 		if (one.type >= Term.STRUCT && two.type >= Term.STRUCT) {
155 			Struct a = (Struct) one;
156 			Struct b = (Struct) two;
157 			if (a.predicateIndicator != b.predicateIndicator)
158 				return false;
159 			for (int c = 0; c < a.arity; c++)
160 				if (!unifyBranch(a.getArg(c), b.getArg(c), oneCtx, twoCtx))
161 					return false;
162 			return true;
163 		}
164 		return one.type == Term.NUMBER && two.type == Term.NUMBER && BasicLibrary.number_equality_2((Number) one, (Number) two);
165 	}
166 
167 	// ca. 500
168 
169 	// this method calls an almost entirely iterative method
170 	boolean unifyBranchTwo(Term one, Term two, int oneCtx, int twoCtx) {
171 		return unifyTrees(one.pos, two.pos, one.prePost, one.tree, two.tree, two.prePost, oneCtx, twoCtx);
172 	}
173 
174 	private boolean unifyTrees(int i, int j, int[] prePostA, Term[] treeA, Term[] treeB, int[] prePostB, int oneCtx, int twoCtx) {
175 		bb: for (int stopValue = prePostA[i]; i < prePostA.length && prePostA[i] <= stopValue; i++, j++) {
176 			Term one = treeA[i], two = treeB[j];
177 			int tmpCtx1 = oneCtx;
178 			int tmpCtx2 = twoCtx;
179 			boolean mustMakeNewStackLevel = false;
180 
181 			if (one.type == Term.VAR) {// instanceof Var){
182 				while (one.type == Term.VAR) {// instanceof Var) {
183 					Var vOne = (Var) one;
184 					// if the two varaiables are equal, then return true
185 					if (tmpCtx1 == twoCtx && vOne.equals(two))
186 						continue bb;
187 					// else, if the left variable is not bound, bind it
188 					Term link = links.getTerm(vOne.nrInStruct, tmpCtx1);
189 					if (link == null) {
190 						if (!setLink(vOne.nrInStruct, tmpCtx1, two, twoCtx))
191 							return false;
192 						if (two.type == Term.STRUCT)// instanceof Struct &&
193 													// ((Struct) two).arity >0)
194 													// //we must increment j to
195 													// skip two's children
196 							for (int postStop = prePostB[j]; j < prePostB.length - 1 && prePostB[j + 1] < postStop; j++)
197 								;
198 						continue bb;
199 					}
200 					// else, Var one is bound. If it is bound to a Var, then the
201 					// loop will continue updating the on Var, else it will
202 					// continue down the method
203 					tmpCtx1 = links.getCtx(vOne.nrInStruct, tmpCtx1);
204 					one = link;
205 				}
206 				if (one.type == Term.STRUCT)// instanceof Struct && ((Struct)
207 											// one).arity > 0) //todo we don't
208 											// need to mustMakeNewStackLevel
209 											// when the node har no children.
210 					mustMakeNewStackLevel = true;
211 			}
212 
213 			if (two.type == Term.VAR) {// instanceof Var){
214 				while (two.type == Term.VAR) {// instanceof Var){
215 					Var vTwo = (Var) two;
216 					Term link = links.getTerm(vTwo.nrInStruct, tmpCtx2);
217 					if (link == null) {
218 						if (!setLink(vTwo.nrInStruct, tmpCtx2, one, tmpCtx1))
219 							return false; // var two is unbound, bind it against
220 											// on
221 						if (one.type == Term.STRUCT)// instanceof Struct &&
222 													// ((Struct) one).arity >0)
223 													// //we must increment i to
224 													// skip one's children
225 							for (int postStop = prePostA[i]; i < prePostA.length - 1 && prePostA[i + 1] < postStop; i++)
226 								;
227 						continue bb;
228 					}
229 					tmpCtx2 = links.getCtx(vTwo.nrInStruct, tmpCtx2);
230 					two = link;
231 				}
232 				if (two.type == Term.STRUCT)// instanceof Struct && ((Struct)
233 											// two).arity > 0)
234 					mustMakeNewStackLevel = true;
235 			}
236 
237 			// todo we need to mustMakeNewStackLevel here because the ctx id
238 			// changes,
239 			// todo the prePost and values arrays changes.
240 			// todo however these values only become relevant for the next
241 			// check, so Atoms and Numbers do NOT mustMakeNewStackLevel
242 			if (mustMakeNewStackLevel) {
243 				if (!unifyBranch(one, two, tmpCtx1, tmpCtx2))
244 					return false;
245 				// we must increment i to skip one's and/or two's children
246 				if (one.type == Term.STRUCT)// instanceof Struct && ((Struct)
247 											// one).arity >0)
248 					for (int postStop = prePostA[i]; i < prePostA.length - 1 && prePostA[i + 1] < postStop; i++)
249 						;
250 				if (two.type == Term.STRUCT)// instanceof Struct && ((Struct)
251 											// two).arity >0)
252 					for (int postStop = prePostB[j]; j < prePostB.length - 1 && prePostB[j + 1] < postStop; j++)
253 						;
254 			} else if (one.type >= Term.STRUCT && two.type >= Term.STRUCT) {// instanceof
255 																			// Struct
256 																			// &&
257 																			// two
258 																			// instanceof
259 																			// Struct)
260 																			// {
261 				if (((Struct) one).predicateIndicator != ((Struct) two).predicateIndicator)
262 					return false;
263 			} else if (one.type == Term.NUMBER && two.type == Term.NUMBER) {// instanceof
264 																			// Number
265 																			// &&
266 																			// two
267 																			// instanceof
268 																			// Number){
269 				if (!BasicLibrary.number_equality_2((Number) one, (Number) two))
270 					return false;
271 			} else
272 				return false;
273 		}
274 		return true;
275 	}
276 
277 	@SuppressWarnings("unused")
278 	private static int leftMostChildsPost(int i, int[] aPrePostArr) {
279 		int temp;
280 		int min = aPrePostArr[i];
281 		while (i++ < aPrePostArr.length && min > (temp = aPrePostArr[i]))
282 			min = temp;
283 		return min;
284 	}
285 
286 	public Term wrapWithID(Term t) {
287 		return wrapWithID(t, getUniqueExecutionCtxID());
288 	}
289 
290 	public static Term wrapWithID(Term t, int renameVarID) {
291 		if (t instanceof Wrapper)
292 			return t;
293 		if (t instanceof Var)
294 			return new WrapVar((Var) t, renameVarID);
295 		if (t instanceof Struct && !(t instanceof StructAtom))
296 			return new WrapStruct((Struct) t, renameVarID);
297 		return t;
298 	}
299 
300 	public static Term unWrap(Term l) {
301 		return l instanceof Wrapper ? ((Wrapper) l).getBasis() : l;
302 	}
303 
304 	/**
305 	 * Adds a link. The key of the link is an ID calculated from the Variable
306 	 * (its position in the root parent Struct) and its context The link is
307 	 * stored in three different maps: 1. a map of the link to the Term it is
308 	 * linked to 2. a map of the link to the context number of the Term it is
309 	 * linked to 3. to facilitate garbage collection of unused links, a map
310 	 * linking the context in which the binding takes place to a list of links
311 	 * that are bound at this execution context is also updated
312 	 * 
313 	 * @param vNr
314 	 * @param vCtx
315 	 * @param link
316 	 * @param linkCtx
317 	 * @return true always
318 	 */
319 	private boolean setLink(final int vNr, final int vCtx, final Term link, final int linkCtx) {
320 		// Most applications can probably turn off
321 		// checkForInternalOccurencesOfVarInTerm without any consequenses by
322 		// commenting out the if clause below.
323 		// Removing this will likely result in approx 20% speed-up. But do so
324 		// only when you know your application do not need this check.
325 //	---->	if (checkForInternalOccurencesOfVarInTerm(vNr, vCtx, link, linkCtx))
326 //	---->		return false;
327 		links.put(vNr, vCtx, link, linkCtx);
328 		gc.addToTrashCan(vNr, vCtx, currentExecCtx);
329 		return true;
330 	}
331 
332 	/**
333 	 * todo don't search, sort? this now only gets in the original terms and the
334 	 * context int as values
335 	 * 
336 	 * @param varBeingChecked
337 	 * @param beingCheckedsContext
338 	 * @param t
339 	 * @param tCtx
340 	 */
341 	@SuppressWarnings("unused")
342 	private boolean checkForInternalOccurencesOfVarInTerm(int varBeingChecked, int beingCheckedsContext, Term t, int tCtx) {
343 		while (t.type == Term.VAR) {
344 			final int vNr = ((Var) t).nrInStruct;
345 			if (beingCheckedsContext == tCtx && varBeingChecked == vNr)
346 				return true;
347 			Term link = links.getTerm(vNr, tCtx);
348 			if (link == null || link.type == Term.NUMBER || link.type == Term.ATOM)
349 				return false;
350 			t = link;
351 			tCtx = links.getCtx(vNr, tCtx);
352 			// if the Var was linked to a Var, the loop will run once more
353 			// else t is a Struct and 'if (t instanceof Struct)' below will kick
354 			// into action
355 		}
356 		if (t.type == Term.STRUCT) {
357 			Var[] varList = ((Struct) t).getVarList();
358 			if (varList == null)
359 				return false;
360 			boolean sameCtx = beingCheckedsContext == tCtx;
361 			for (int i = 0; i < varList.length; i++) {
362 				Var v = varList[i];
363 				if (sameCtx && varBeingChecked == v.nrInStruct)
364 					return true;
365 				Term link = links.getTerm(v.nrInStruct, tCtx);
366 				if (link == null || link.type == Term.NUMBER || link.type == Term.ATOM)
367 					continue;
368 				int linkCtx = links.getCtx(v.nrInStruct, tCtx);
369 				return checkForInternalOccurencesOfVarInTerm(varBeingChecked, beingCheckedsContext, link, linkCtx);
370 			}
371 		}
372 		return false;
373 	}
374 
375 	/**
376 	 * 
377 	 * @param owner
378 	 * @return // * @deprecated no longer supported. should be implemented anew
379 	 *         if needed in f.ex. GUI or debugger.
380 	 */
381 	int[][] getKeysForCp(int owner) {
382 		return gc.getGarbageLinks(owner);
383 	}
384 
385 	/**
386 	 * @param prolog
387 	 * @param expression
388 	 * @return result of the evaluation as a Number
389 	 * @throws jTrolog.errors.PrologException
390 	 *             instantiation error if one of the Variables to be evaluated
391 	 *             is not instantiated type_error(Evaluable, other) if a part in
392 	 *             the evaluable Term is not evaluable
393 	 * @throws Throwable
394 	 *             any other exception that might be thrown by parts of the
395 	 *             expression to be evaluated
396 	 */
397 	public Number evalExpression(Prolog prolog, EvaluableTerm expression) throws PrologException, Throwable {
398 		if (expression instanceof Number)
399 			return (Number) expression;
400 
401 		Term child = null;
402 		try {
403 			Struct struct = (Struct) expression;
404 			// chp 9 defines separate behavior for _evaluation_ of float/1
405 			// (casting of Int) that should only accept Numbers,
406 			// as opposed to the execution of float/1 that accepts Term.
407 			if (struct.predicateIndicator == Parser.floatSignature) {
408 				child = resolve(struct.getArg(0));
409 				Number result = evalExpression(prolog, (EvaluableTerm) child);
410 				return new Double(result.doubleValue());
411 			}
412 
413 			PrimitiveInfo prim = prolog.getPrimitiveExp(struct);
414 			if (prim == null)
415 				throw new PrologException("type_error(Evaluable, " + struct.predicateIndicator + ")");
416 
417 			// we have an expression that might be evaluable
418 			Object[] primitive_args = new Object[struct.arity + 1];
419 			primitive_args[0] = this;
420 			for (int i = 0; i < primitive_args.length - 1; i++) {
421 				child = resolve(struct.getArg(i));
422 				primitive_args[i + 1] = evalExpression(prolog, (EvaluableTerm) child);
423 			}
424 			return (Number) prim.method.invoke(prim.source, primitive_args);
425 		} catch (ClassCastException e) {
426 			if (child instanceof Var)
427 				throw new PrologException("instantiation_error");
428 			throw new PrologException("type_error(Evaluable, " + child + ")");
429 		}
430 	}
431 
432 	Object[] resolveArgs(Struct unresolved, int ctx) {
433 		Object[] primitive_args = new Object[unresolved.arity + 1];
434 		primitive_args[0] = this;
435 		for (int i = 0; i < unresolved.arity; i++) {
436 			final Term arg = unresolved.getArg(i);
437 			Term link = resolveFaster(arg, ctx);
438 			if (secondOutOfFasterResolve == Integer.MAX_VALUE)
439 				primitive_args[i + 1] = wrapWithID(arg, ctx);
440 			else
441 				primitive_args[i + 1] = wrapWithID(link, secondOutOfFasterResolve);
442 		}
443 		return primitive_args;
444 	}
445 
446 	public Term resolve(Term term) {
447 		if (term instanceof WrapVar) {
448 			final WrapVar wrapVar = ((WrapVar) term);
449 			Term link = resolveFaster(wrapVar.getBasis(), wrapVar.getContext());
450 			return secondOutOfFasterResolve == Integer.MAX_VALUE ? term : wrapWithID(link, secondOutOfFasterResolve);
451 		}
452 		return term;
453 	}
454 
455 	int secondOutOfFasterResolve;
456 
457 	public Term resolveFaster(Term term, int ctx) {
458 		secondOutOfFasterResolve = Integer.MAX_VALUE;
459 		while (term instanceof Var) {
460 			int vNr = ((Var) term).nrInStruct;
461 			Term link = links.getTerm(vNr, ctx);
462 			if (link == null)
463 				break;
464 			term = link;
465 			ctx = secondOutOfFasterResolve = links.getCtx(vNr, ctx);
466 		}
467 		return term;
468 	}
469 
470 	public Iterator structListIterator(Struct origin, boolean skipLastEmptyList) throws PrologException {
471 		if (origin.equals(Term.emptyList))
472 			return Term.iterator;
473 		if (origin.predicateIndicator != Parser.listSignature)
474 			throw new PrologException("type_error(list, " + origin + ")");
475 		return new StructListIterator(origin, skipLastEmptyList);
476 	}
477 
478 	public Struct createStructList(Collection complete) {
479 		if (complete.isEmpty())
480 			return Term.emptyList;
481 		Struct res = (WrapStruct) wrapWithID(Parser.createListContainingAnyVars(complete.size() - 1));
482 		Iterator toBeLinked = complete.iterator();
483 		try {
484 			for (Iterator it = structListIterator(res, true); it.hasNext();) {
485 				Var child = (Var) it.next();
486 				Term link = (Term) toBeLinked.next();
487 				unify(child, link);
488 			}
489 		} catch (PrologException e) {
490 			throw new RuntimeException("error in iterating a Struct list");
491 		}
492 		return res;
493 	}
494 
495 	private class StructListIterator implements Iterator {
496 
497 		Iterator currentListIterator;
498 		Term cache;
499 		boolean includeLastEmptyList;
500 
501 		StructListIterator(Struct origin, boolean skipLastEmptyList) {
502 			this.includeLastEmptyList = !skipLastEmptyList;
503 			currentListIterator = Struct.iterator(origin);
504 		}
505 
506 		public boolean hasNext() {
507 			if (cache != null)
508 				return true;
509 			if (!currentListIterator.hasNext())
510 				return false;
511 			cache = (Term) currentListIterator.next();
512 			cache = resolve(cache);
513 
514 			if (currentListIterator.hasNext())
515 				return true;
516 
517 			if (cache.equals(Term.emptyList)) {
518 				if (includeLastEmptyList)
519 					return true;
520 				cache = null;
521 				return false;
522 			}
523 
524 			if (!(cache instanceof Struct && ((Struct) cache).predicateIndicator == Parser.listSignature))
525 				return true;
526 
527 			currentListIterator = Struct.iterator((Struct) cache);
528 			cache = null;
529 			return hasNext();
530 		}
531 
532 		public Object next() {
533 			if (cache == null)
534 				hasNext();
535 			if (cache == null)
536 				throw new NoSuchElementException();
537 			Term temp = cache;
538 			cache = null;
539 			return temp;
540 		}
541 
542 		public void remove() {
543 			throw new UnsupportedOperationException();
544 		}
545 	}
546 
547 	// todo override toString();
548 
549 	/**
550 	 * See 7.1.6.1 Variants of a term in the ISO spec this method resolves all
551 	 * the terms children. Not sure if this is in line with the spec.
552 	 * 
553 	 * @param original
554 	 *            such as - f(X,Y,X) - g(A,B) - P + Q
555 	 * @return a variant of the original such as - f(_1, _2, _1) - g(_1, _2) -
556 	 *         _1 + _2
557 	 */
558 	public Term variant(Term original) {
559 		return new Copier(original).getFlatVariant();
560 	}
561 
562 	public Term flatCopy(Term original, int keepCtx) {
563 		return new Copier(BindingsTable.wrapWithID(original, keepCtx), keepCtx).getFlatCopy();
564 	}
565 
566 	private class Copier {
567 		HashMap varTable = new HashMap();
568 		Term original;
569 		Term copy;
570 		private int keepOriginals = -1;
571 
572 		public Copier(Term root) {
573 			original = root;
574 		}
575 
576 		public Copier(Term root, int ctxToKeep) {
577 			original = root;
578 			keepOriginals = ctxToKeep;
579 		}
580 
581 		private Term copyRecursive(Term original) {
582 			if (original instanceof StructAtom || original instanceof Number)
583 				return original;
584 			if (original instanceof Var) {
585 				Term link = resolve(original);
586 				if (link != original)
587 					return copyRecursive(link);
588 				if (link instanceof Wrapper && ((Wrapper) link).getContext() == keepOriginals)
589 					return ((Wrapper) link).getBasis();
590 				Var copyVar = (Var) varTable.get(link);
591 				if (copyVar == null)
592 					varTable.put(link, copyVar = new Var("_", varTable.size() + 1));
593 				return copyVar;
594 			}
595 			Struct sOrig = (Struct) original;
596 			Term[] clonedElements = new Term[sOrig.arity];
597 			for (int i = 0; i < clonedElements.length; i++)
598 				clonedElements[i] = copyRecursive(sOrig.getArg(i));
599 			return new Struct(sOrig.name, clonedElements, sOrig.getOperatorType());
600 		}
601 
602 		public Term getFlatVariant() {
603 			if (copy != null)
604 				return copy;
605 			return copy = copyRecursive(this.original);
606 		}
607 
608 		public Term getFlatCopy() {
609 			if (copy != null)
610 				return copy;
611 			return copy = copyRecursive(this.original);
612 		}
613 
614 	}
615 }