WIP overhauling expression resolution
This commit is contained in:
@@ -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)
|
||||
@@ -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));
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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
75
hank/StoryTree.hx
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package hank;
|
||||
|
||||
class ViewCounts {
|
||||
public function new(ast: HankAST) {
|
||||
|
||||
}
|
||||
|
||||
public function resolve(identifier: String, scope: String): Int {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -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
54
tests/StoryTreeTest.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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()]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user