[PORT]Refine template loop detection to enable recursive functions (#2305)

* Refine template loop detection to enable recursive functions

* Reduce the recursion stack depth to pass MacOS test
This commit is contained in:
Hongyang Du (hond) 2020-06-05 08:27:57 +08:00 коммит произвёл GitHub
Родитель 69bb9563ee
Коммит c3cfaad2df
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 71 добавлений и 40 удалений

Просмотреть файл

@ -16,7 +16,6 @@ import { ExpressionFunctions } from '../expressionFunctions';
export class SimpleObjectMemory implements MemoryInterface {
private memory: any = undefined;
private versionNumber: number = 0;
public constructor(memory: any) {
this.memory = memory;
@ -142,13 +141,13 @@ export class SimpleObjectMemory implements MemoryInterface {
}
}
this.versionNumber++;
return;
}
public version(): string {
return this.versionNumber.toString();
return this.toString();
}
public toString(): string {
return JSON.stringify(this.memory, this.getCircularReplacer());
}

Просмотреть файл

@ -57,6 +57,21 @@ export class CustomizedMemory implements MemoryInterface {
}
public version(): string {
return '0';
let result = '';
if (this.globalMemory) {
const version = this.globalMemory.version();
if (version) {
result = result.concat(version);
}
}
if (this.localMemory) {
const localVersion = this.localMemory.version();
if (localVersion) {
result = result.concat(localVersion);
}
}
return result;
}
}

Просмотреть файл

@ -6,7 +6,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CustomizedMemory } from './customizedMemory';
import { MemoryInterface } from 'adaptive-expressions';
/**
@ -22,13 +22,13 @@ export class EvaluationTarget {
/**
* Scope.
*/
public scope: any;
public scope: MemoryInterface;
/**
* The children templates that this template has evaluated currently.
*/
public evaluatedChildren: Map<string, any>;
public constructor(templateName: string, scope: any) {
public constructor(templateName: string, scope: MemoryInterface) {
this.templateName = templateName;
this.scope = scope;
this.evaluatedChildren = new Map<string, any>();
@ -40,24 +40,7 @@ export class EvaluationTarget {
* @returns Id.
*/
public getId(): string {
const memory = this.scope as CustomizedMemory;
let result = this.templateName;
if (memory) {
if (memory.globalMemory) {
const version = memory.globalMemory.version();
if (version) {
result = result.concat(version);
}
}
if (memory.localMemory) {
const localMemoryString = memory.localMemory.toString();
if (localMemoryString) {
result = result.concat(localMemoryString);
}
}
}
return result;
const scopeVersion = this.scope ? this.scope.version() : '';
return this.templateName + scopeVersion;
}
}

Просмотреть файл

@ -73,6 +73,8 @@ export class Evaluator extends AbstractParseTreeVisitor<any> implements LGTempla
* @returns Evaluate result.
*/
public evaluateTemplate(inputTemplateName: string, scope: any): any {
const memory = scope instanceof CustomizedMemory ? scope : new CustomizedMemory(scope);
let templateName: string;
let reExecute: boolean;
({reExecute, pureTemplateName: templateName} = this.parseTemplateName(inputTemplateName));
@ -81,19 +83,15 @@ export class Evaluator extends AbstractParseTreeVisitor<any> implements LGTempla
throw new Error(TemplateErrors.templateNotExist(templateName));
}
if (this.evaluationTargetStack.some((u: EvaluationTarget): boolean => u.templateName === templateName)) {
const templateTarget: EvaluationTarget = new EvaluationTarget(templateName, memory);
const currentEvulateId: string = templateTarget.getId();
if (this.evaluationTargetStack.some((u: EvaluationTarget): boolean => u.getId() === currentEvulateId)) {
throw new Error(`${ TemplateErrors.loopDetected } ${ this.evaluationTargetStack.reverse()
.map((u: EvaluationTarget): string => u.templateName)
.join(' => ') }`);
}
if (!(scope instanceof CustomizedMemory)) {
scope = new CustomizedMemory(SimpleObjectMemory.wrap(scope));
}
const templateTarget: EvaluationTarget = new EvaluationTarget(templateName, scope);
const currentEvulateId: string = templateTarget.getId();
let previousEvaluateTarget: EvaluationTarget;
if (this.evaluationTargetStack.length !== 0) {

Просмотреть файл

@ -64,22 +64,22 @@ export class Expander extends AbstractParseTreeVisitor<string[]> implements LGTe
* @returns All possiable results.
*/
public expandTemplate(templateName: string, scope: any): any[] {
const memory = scope instanceof CustomizedMemory ? scope : new CustomizedMemory(scope);
if (!(templateName in this.templateMap)) {
throw new Error(TemplateErrors.templateNotExist(templateName));
}
if (this.evaluationTargetStack.find((u: EvaluationTarget): boolean => u.templateName === templateName)) {
const templateTarget: EvaluationTarget = new EvaluationTarget(templateName, memory);
const currentEvulateId: string = templateTarget.getId();
if (this.evaluationTargetStack.find((u: EvaluationTarget): boolean => u.getId() === currentEvulateId)) {
throw new Error(`${ TemplateErrors.loopDetected } ${ this.evaluationTargetStack.reverse()
.map((u: EvaluationTarget): string => u.templateName)
.join(' => ') }`);
}
if (!(scope instanceof CustomizedMemory)) {
scope = new CustomizedMemory(SimpleObjectMemory.wrap(scope));
}
// Using a stack to track the evalution trace
this.evaluationTargetStack.push(new EvaluationTarget(templateName, scope));
this.evaluationTargetStack.push(templateTarget);
const result: any[] = this.visit(this.templateMap[templateName].templateBodyParseTree);
this.evaluationTargetStack.pop();

Просмотреть файл

@ -645,6 +645,17 @@ describe('LG', function() {
assert.strictEqual(evaled, 18, `Evaled is ${ evaled }`);
});
it('TestRecursiveTemplate', function() {
var templates = Templates.parseFile(GetExampleFilePath('RecursiveTemplate.lg'));
var evaled = templates.evaluate('RecursiveAccumulate', {number: 10});
assert.strictEqual(evaled, 55);
var evaled = templates.evaluate('RecursiveFactorial', {number: 5});
assert.strictEqual(evaled, 1*2*3*4*5);
var evaled = templates.evaluate('RecursiveFibonacciSequence', {number: 5});
assert.strictEqual(evaled, 5);
});
it('TestLGResource', function() {
var templates = Templates.parseText(fs.readFileSync(GetExampleFilePath('2.lg'), 'utf-8'));

Просмотреть файл

@ -214,6 +214,8 @@ describe(`LGExceptionTest`, function() {
assert.throws(() => templates.evaluate(`wPhrase`), Error(`Loop detected: welcome_user => wPhrase [wPhrase] Error occurred when evaluating '-\${wPhrase()}'. [welcome_user] Error occurred when evaluating '-\${welcome_user()}'.`));
assert.throws(() => templates.analyzeTemplate(`wPhrase`), Error('Loop detected: welcome_user => wPhrase'),);
assert.throws(() => templates.analyzeTemplate(`shouldFail`), Error('Loop detected: shouldFail'),);
});
it(`AddTextWithWrongId`, function() {

Просмотреть файл

@ -0,0 +1,20 @@
> 1 + 2 + 3 +...+ number
# RecursiveAccumulate(number)
- IF:${number <= 1}
- ${1}
- ELSE:
- ${RecursiveAccumulate(number - 1) + number}
> 1 * 2 * 3 * 4 * 5 * ... * number
# RecursiveFactorial(number)
- IF: ${number <= 1}
- ${1}
- ELSE:
- ${RecursiveFactorial(number - 1) * number}
> 1, 1, 2, 3, 5, 8, 13 ... number th
# RecursiveFibonacciSequence(number)
- IF: ${number <= 2}
- ${1}
- ELSE:
- ${RecursiveFibonacciSequence(number - 1) + RecursiveFibonacciSequence(number - 2)}

Просмотреть файл

@ -8,3 +8,6 @@
# welcome_user
- ${wPhrase()}
> self loop
# shouldFail(x)
- ${shouldFail(x)}