Skip to content

Commit

Permalink
Merge pull request #334 from stanford-oval/wip/is-executable
Browse files Browse the repository at this point in the history
Improving and extending the logic for when a statement is executable
  • Loading branch information
gcampax committed Apr 16, 2021
2 parents 0a8055f + f11b477 commit 8abc692
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 37 deletions.
36 changes: 1 addition & 35 deletions lib/ast/dialogues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,41 +265,7 @@ export class DialogueHistoryItem extends AstNode {
}

isExecutable() : boolean {
let hasUndefined = false;
const visitor = new class extends NodeVisitor {
visitInvocation(invocation : Invocation) {
const schema = invocation.schema;
assert(schema instanceof FunctionDef);
const requireEither = schema.getAnnotation<string[][]>('require_either');
if (requireEither) {
const params = new Set<string>();
for (const in_param of invocation.in_params)
params.add(in_param.name);

for (const requirement of requireEither) {
let satisfied = false;
for (const option of requirement) {
if (params.has(option)) {
satisfied = true;
break;
}
}
if (!satisfied)
hasUndefined = true;
}
}

return true;
}

visitValue(value : Value) {
if (value.isUndefined)
hasUndefined = true;
return true;
}
};
this.stmt.visit(visitor);
return !hasUndefined;
return this.stmt.isExecutable();
}

equals(other : DialogueHistoryItem) : boolean {
Expand Down
66 changes: 64 additions & 2 deletions lib/ast/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ import Node, {
nlAnnotationsToSource,
} from './base';
import NodeVisitor from './visitor';
import { Value, VarRefValue } from './values';
import { DeviceSelector, InputParam, BooleanExpression } from './expression';
import { Value, VarRefValue, EnumValue, BooleanValue } from './values';
import { Invocation, DeviceSelector, InputParam, BooleanExpression } from './expression';
import {
Stream,
Table,
Expand Down Expand Up @@ -484,6 +484,62 @@ export class Command extends Statement {
}
}

class IsExecutableVisitor extends NodeVisitor {
isExecutable = true;

visitInvocation(invocation : Invocation) {
const schema = invocation.schema;
assert(schema instanceof FunctionDef);

const params = new Map<string, Value>();
for (const in_param of invocation.in_params)
params.set(in_param.name, in_param.value);

const requireEither = schema.getImplementationAnnotation<string[][]>('require_either');
if (requireEither) {
for (const requirement of requireEither) {
let satisfied = false;
for (const option of requirement) {
if (params.has(option)) {
satisfied = true;
break;
}
}
if (!satisfied)
this.isExecutable = false;
}
}

for (const arg of schema.iterateArguments()) {
const requiredIf = arg.getImplementationAnnotation<string[]>('required_if');
if (requiredIf && !params.has(arg.name)) {
let required = false;
for (const requirement of requiredIf) {
const [param, value] = requirement.split('=');
const current = params.get(param);
if (!current)
continue;
if ((current instanceof EnumValue && current.value === value) ||
(current instanceof BooleanValue && current.value === (value === 'true'))) {
required = true;
break;
}
}
if (required)
this.isExecutable = false;
}
}

return true;
}

visitValue(value : Value) {
if (value.isUndefined || !value.isConcrete())
this.isExecutable = false;
return true;
}
}

/**
* A statement that evaluates an expression and presents the results
* to the user.
Expand Down Expand Up @@ -572,6 +628,12 @@ export class ExpressionStatement extends Statement {
clone() : ExpressionStatement {
return new ExpressionStatement(this.location, this.expression.clone());
}

isExecutable() {
const visitor = new IsExecutableVisitor;
this.visit(visitor);
return visitor.isExecutable;
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions test/test_all.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ seq([
('./test_ast'),
('./test_schema_retriever'),
('./test_compound_type'),
('./test_is_executable'),

// test syntax (first test that the parser we generated is good, then use it)
('./test_generated_parser'),
Expand Down
205 changes: 205 additions & 0 deletions test/test_is_executable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// -*- mode: js; indent-tabs-mode: nil; js-basic-offset: 4 -*-
//
// This file is part of ThingTalk
//
// Copyright 2021 The Board of Trustees of the Leland Stanford Junior University
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import SchemaRetriever from '../lib/schema';
import * as Syntax from '../lib/syntax_api';

import _mockSchemaDelegate from './mock_schema_delegate';
import _mockMemoryClient from './mock_memory_client';
const schemaRetriever = new SchemaRetriever(_mockSchemaDelegate, _mockMemoryClient, true);

const TEST_CASES = [
// 1 simple
[`class @foo {
query q1(in req p1 : String);
}
@foo.q1();
`, false],

// 2 simple
[`class @foo {
query q1(in req p1 : String);
}
@foo.q1(p1="lol");
`, true],

// 3 explicit
[`class @foo {
query q1(in req p1 : String);
}
@foo.q1(p1=$?);
`, false],

// 4 optional
[`class @foo {
query q1(in opt p1 : String);
}
@foo.q1();
`, true],

// 5 optional explicit
[`class @foo {
query q1(in opt p1 : String);
}
@foo.q1(p1=$?);
`, false],

// 6 require either
[`class @foo {
query q1(in opt p1 : String,
in opt p2 : String)
#[require_either=[["p1", "p2"]]];
}
@foo.q1();
`, false],

// 7 require either, provided
[`class @foo {
query q1(in opt p1 : String,
in opt p2 : String)
#[require_either=[["p1", "p2"]]];
}
@foo.q1(p1="lol");
`, true],

// 8 require either, two requirements
[`class @foo {
query q1(in opt p1 : String,
in opt p2 : String,
in opt p3 : String,
in opt p4 : String)
#[require_either=[["p1", "p2"], ["p3", "p4"]] ];
}
@foo.q1(p1="lol");
`, false],

// 9 require either, two requirements, fulfilled
[`class @foo {
query q1(in opt p1 : String,
in opt p2 : String,
in opt p3 : String,
in opt p4 : String)
#[require_either=[["p1", "p2"], ["p3", "p4"]] ];
}
@foo.q1(p1="lol", p3="lal");
`, true],

// 10 required if
[`class @foo {
query q1(in opt p1 : Enum(a, b),
in opt p2 : String #[required_if=["p1=a"]]);
}
@foo.q1();
`, true],

// 11 required if, param matches
[`class @foo {
query q1(in opt p1 : Enum(a, b),
in opt p2 : String #[required_if=["p1=a"]]);
}
@foo.q1(p1=enum a);
`, false],

// 12 required if, param matches, provided
[`class @foo {
query q1(in opt p1 : Enum(a, b),
in opt p2 : String #[required_if=["p1=a"]]);
}
@foo.q1(p1=enum a, p2="lol");
`, true],

// 13 required if, param matches, undefined
[`class @foo {
query q1(in opt p1 : Enum(a, b),
in opt p2 : String #[required_if=["p1=a"]]);
}
@foo.q1(p1=enum a, p2=$?);
`, false],

// 14 required if, param does not match
[`class @foo {
query q1(in opt p1 : Enum(a, b),
in opt p2 : String #[required_if=["p1=a"]]);
}
@foo.q1(p1=enum b);
`, true],

// 15 required if, boolean
[`class @foo {
query q1(in opt p1 : Boolean,
in opt p2 : String #[required_if=["p1=true"]]);
}
@foo.q1(p1=true);
`, false],

// 16 required if, boolean
[`class @foo {
query q1(in opt p1 : Boolean,
in opt p2 : String #[required_if=["p1=true"]]);
}
@foo.q1(p1=false);
`, true],

// 17 non-concrete values
[`class @foo {
query q1(in opt p1 : Location);
}
@foo.q1(p1=new Location("somewhere"));
`, false],

// 18 non-concrete values
[`class @foo {
query q1(in opt p1 : Location);
}
@foo.q1(p1=$location.home);
`, false],
];

async function test(i) {
console.log('Test Case #' + (i+1));

let [code, expected] = TEST_CASES[i];

try {
const parsed = Syntax.parse(code);
await parsed.typecheck(schemaRetriever);

const stmt = parsed.statements[0];

const isExecutable = stmt.isExecutable();
if (expected !== isExecutable) {
console.error('Test Case #' + (i+1) + ': failed');
if (process.env.TEST_MODE)
throw new Error(`testIsExecutable ${i+1} FAILED`);
}
} catch(e) {
console.error('Test Case #' + (i+1) + ': failed with exception');
console.error('Code: ' + code);
console.error('Error: ' + e.message);
console.error(e.stack);
if (process.env.TEST_MODE)
throw e;
}
}

export default async function main() {
for (let i = 0; i < TEST_CASES.length; i++)
await test(i);
}
if (!module.parent)
main();

0 comments on commit 8abc692

Please sign in to comment.