WIP overhauling expression resolution

This commit is contained in:
2019-03-25 18:19:54 -06:00
parent 139f2acd9f
commit 39bca77bd1
11 changed files with 191 additions and 23 deletions

View File

@@ -5,6 +5,6 @@
// The first subsection should be the default divert target
Fifth output.
-> start__end
-> start.end
// TODO test labeled gathers within sections (although that is covered by The Intercept as well)

View File

@@ -1,9 +1,13 @@
package hank;
using Reflect;
import hscript.Parser;
import hscript.Interp;
import hscript.Expr;
import hank.StoryTree;
/**
Interface between a Hank story, and its embedded hscript interpreter
**/
@@ -14,8 +18,28 @@ class HInterface {
var parser: Parser = new Parser();
var interp: Interp = new Interp();
public function new(viewCounts: ViewCounts) {
public function new(storyTree: StoryNode, viewCounts: Map<StoryNode, Int>) {
this.interp.variables['_isTruthy'] = isTruthy;
this.interp.variables['_resolve'] = resolve.bind(this.interp.variables, viewCounts, storyTree);
this.interp.variables['_resolveField'] = resolve.bind(this.interp.variables, viewCounts);
}
static function resolve(variables: Map<String, Dynamic>, viewCounts: Map<StoryNode, Int>,container: Dynamic, name: String): Dynamic {
if (variables.exists(name)) {
return variables[name];
} else {
if (Type.typeof(container).getName() == 'StoryNode') {
var node: StoryNode = cast(container, StoryNode);
switch (node.resolve(name)) {
case Some(node):
return viewCounts[node];
case None:
throw 'Cannot resolve ${name}';
}
} else {
return container.field(name);
}
}
}
static function isTruthy(v: Dynamic) {
@@ -69,9 +93,8 @@ class HInterface {
}
return switch (expr) {
case EIdent(name):
// TODO if the name is a root-level view count, return EArray(view_counts, ...)
// Or if it's a nested view count of the current scope, also need to resolve that
EIdent(name);
// Identifiers need to be resolved
ECall(EIdent('_resolve'), [EConst(CString(name))]);
case EVar(name, _, nested):
// Declare all variables in embedded global context
interp.variables[name] = null;
@@ -81,8 +104,7 @@ class HInterface {
case EBlock(nestedExpressions):
EBlock([for(nested in nestedExpressions) transmute(nested)]);
case EField(nested, f):
// TODO this is where . access on view counts will be handled by transmuting to an EArray
EField(transmute(nested), f);
ECall(EIdent('_resolveField'), [transmute(nested), EConst(CString(f))]);
case EBinop(op, e1, e2):
if (BOOLEAN_OPS.indexOf(op) != -1) {
EBinop(op, boolify(e1), boolify(e2));

View File

@@ -1,6 +1,7 @@
package hank;
import utest.Assert;
import haxe.ds.Option;
class HankAssert {
/**
@@ -23,4 +24,22 @@ class HankAssert {
public static function notContains(unexpected: String, actual: String) {
Assert.equals(-1, actual.indexOf(unexpected));
}
public static function isSome<T>(option: Option<T>) {
switch (option) {
case Some(_):
case None:
Assert.fail();
}
}
public static function isNone<T>(option: Option<T>) {
switch (option) {
case Some(_):
Assert.fail();
case None:
}
}
}

View File

@@ -2,6 +2,7 @@ package hank;
using HankAST.ASTExtension;
import hank.HankAST.ExprType;
import hank.StoryTree;
/**
Possible states of the story being executed.
@@ -23,17 +24,25 @@ class Story {
var ast: HankAST;
var exprIndex: Int;
var storyTree: StoryNode;
var viewCounts: Map<StoryNode, Int>;
var parser: Parser;
public function new(script: String, ?randomSeed: Int) {
random = new Random(randomSeed);
parser = new Parser();
ast = parser.parseFile(script);
var viewCounts = new ViewCounts(ast);
storyTree = StoryNode.FromAST(ast);
viewCounts = new Map();
for (node in storyTree.traverseAll()) {
viewCounts[node] = 0;
}
hInterface = new HInterface(viewCounts);
hInterface = new HInterface(storyTree, viewCounts);
hInterface.addVariable('story', this);
exprIndex = ast.findFile(script);

75
hank/StoryTree.hx Normal file
View File

@@ -0,0 +1,75 @@
package hank;
import haxe.ds.Option;
@:allow(tests.StoryTreeTest)
class StoryNode {
public var astIndex(default, null): Int;
var children: Map<String, StoryNode> = new Map();
public function new(astIndex: Int) {
this.astIndex = astIndex;
}
public function addChild(name: String, child: StoryNode) {
this.children[name] = child;
}
public function resolve(name: String): Option<StoryNode> {
if (!children.exists(name)) {
return None;
}
return Some(children[name]);
}
public function traverseAll(?nodes: Array<StoryNode>) {
if (nodes == null) {
nodes = [];
}
nodes.push(this);
for (c in children) {
nodes = nodes.concat(c.traverseAll());
}
return nodes;
}
public static function FromAST(ast: HankAST) {
var exprIndex = 0;
var root = new StoryNode(-1);
var lastKnot = null;
var lastStitch = null;
while (exprIndex < ast.length) {
switch (ast[exprIndex].expr) {
case EKnot(name):
var node = new StoryNode(exprIndex);
root.addChild(name, node);
lastKnot = node;
lastStitch = null;
case EStitch(name):
if (lastKnot == null) {
throw 'stitch declared outside of knot';
}
var node = new StoryNode(exprIndex);
lastKnot.addChild(name, node);
lastStitch = node;
case EGather(Some(name), _, _) | EChoice(_, _, Some(name), _, _, _):
var node = new StoryNode(exprIndex);
if (lastKnot == null) {
root.addChild(name, node);
} else if (lastStitch == null) {
lastKnot.addChild(name, node);
} else {
lastStitch.addChild(name, node);
}
default:
}
exprIndex++;
}
return root;
}
}

View File

@@ -1,11 +0,0 @@
package hank;
class ViewCounts {
public function new(ast: HankAST) {
}
public function resolve(identifier: String, scope: String): Int {
return 0;
}
}

View File

@@ -4,14 +4,14 @@ import utest.Test;
import utest.Assert;
import hank.HInterface;
import hank.ViewCounts;
import hank.StoryTree;
class HInterfaceTest extends utest.Test {
var hInterface: HInterface;
public function setup() {
hInterface = new HInterface(new ViewCounts([]));
hInterface = new HInterface(new StoryNode(0), new Map());
}
function assertVar(name: String, value: Dynamic) {

54
tests/StoryTreeTest.hx Normal file
View File

@@ -0,0 +1,54 @@
package tests;
import utest.Assert;
import hank.HankAssert;
import hank.StoryTree;
import hank.Parser;
using hank.Extensions;
class StoryTreeTest extends utest.Test {
var tree: StoryNode;
function setupClass() {
tree = new StoryNode(0);
var section = new StoryNode(1);
var subsection = new StoryNode(2);
var globalVar = new StoryNode(3);
var sectionVar = new StoryNode(4);
var subsectionVar = new StoryNode(5);
tree.addChild("section", section);
tree.addChild("ambiguous", globalVar);
section.addChild("ambiguous", sectionVar);
section.addChild("subsection", subsection);
subsection.addChild("ambiguous", subsectionVar);
}
function testTraverseAll() {
var allNodes = tree.traverseAll();
HankAssert.equals(6, allNodes.length);
}
function testResolve() {
var global = tree.resolve("ambiguous");
HankAssert.equals(3, global.unwrap().astIndex);
}
function testParse() {
var tree = StoryNode.FromAST(new Parser().parseFile("examples/subsections/main.hank"));
HankAssert.isSome(tree.resolve("start"));
HankAssert.isSome(tree.resolve("three"));
HankAssert.isSome(tree.resolve("other_section"));
// Resolving a nonexistent name should throw
HankAssert.isNone(tree.resolve("one"));
HankAssert.isSome(tree.resolve("start").unwrap().resolve("end"));
HankAssert.isNone(tree.resolve("start").unwrap().resolve("three"));
HankAssert.isSome(tree.resolve("three").unwrap().resolve("three"));
Assert.pass();
}
}

View File

@@ -3,6 +3,6 @@ import utest.Test;
class TestMain extends Test {
public static function main() {
utest.UTest.run([new HInterfaceTest(), new HankBufferTest(), new ParserTest(), new StoryTest()]);
utest.UTest.run([new HInterfaceTest(), new HankBufferTest(), new ParserTest(), new StoryTest(), new StoryTreeTest()]);
}
}