diff --git a/pom.xml b/pom.xml
index 848b3615..79763f1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -144,9 +144,9 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.11.0
+ 3.12.1
- 17
+ 21
diff --git a/praxiscore-script/pom.xml b/praxiscore-script/pom.xml
index 106d1bbf..11629de7 100644
--- a/praxiscore-script/pom.xml
+++ b/praxiscore-script/pom.xml
@@ -12,8 +12,18 @@
- org.junit.vintage
- junit-vintage-engine
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
test
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java b/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java
index 4f9cb8b2..f88f0192 100644
--- a/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java
+++ b/praxiscore-script/src/main/java/org/praxislive/script/ScriptExecutor.java
@@ -31,7 +31,6 @@
import org.praxislive.core.Lookup;
import org.praxislive.core.types.PError;
import org.praxislive.script.commands.CoreCommandsInstaller;
-import org.praxislive.script.commands.ScriptCmds;
import static java.lang.System.Logger.Level;
@@ -42,36 +41,27 @@ class ScriptExecutor {
private static final System.Logger log = System.getLogger(ScriptExecutor.class.getName());
- private List stack;
- private Queue queue;
- private Env env;
- private Command evaluator;
- private Map commandMap;
- private Namespace rootNS;
+ private final List stack;
+ private final Queue queue;
+ private final Env env;
+ private final Map commandMap;
+ private final Namespace rootNS;
- public ScriptExecutor(Env env, boolean inline) {
- this.env = env;
+ ScriptExecutor(Env context, final ComponentAddress ctxt) {
+ this.env = context;
stack = new LinkedList<>();
queue = new LinkedList<>();
- if (inline) {
- evaluator = ScriptCmds.INLINE_EVAL;
- } else {
- evaluator = ScriptCmds.EVAL;
- }
+ commandMap = buildCommandMap();
rootNS = new NS();
- buildCommandMap();
- }
-
- public ScriptExecutor(Env context, final ComponentAddress ctxt) {
- this(context, true);
rootNS.addVariable(Env.CONTEXT, new ConstantImpl(ctxt));
}
- private void buildCommandMap() {
- commandMap = new HashMap<>();
+ private Map buildCommandMap() {
+ Map map = new HashMap<>();
CommandInstaller installer = new CoreCommandsInstaller();
- installer.install(commandMap);
- Lookup.SYSTEM.findAll(CommandInstaller.class).forEach(cmds -> cmds.install(commandMap));
+ installer.install(map);
+ Lookup.SYSTEM.findAll(CommandInstaller.class).forEach(cmds -> cmds.install(map));
+ return map;
}
public void queueEvalCall(Call call) {
@@ -150,7 +140,11 @@ private void checkAndStartEval() {
Call call = queue.peek();
var args = call.args();
try {
- stack.add(0, evaluator.createStackFrame(rootNS, args));
+ var script = args.get(0).toString();
+ var stackFrame = ScriptStackFrame.forScript(rootNS, script)
+ .inline()
+ .build();
+ stack.add(0, stackFrame);
processStack();
break;
} catch (Exception ex) {
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java
new file mode 100644
index 00000000..666b477c
--- /dev/null
+++ b/praxiscore-script/src/main/java/org/praxislive/script/ScriptStackFrame.java
@@ -0,0 +1,400 @@
+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2024 Neil C Smith.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License version 3 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+ * version 3 for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License version 3
+ * along with this work; if not, see http://www.gnu.org/licenses/
+ *
+ *
+ * Please visit https://www.praxislive.org if you need additional information or
+ * have any questions.
+ */
+package org.praxislive.script;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.praxislive.core.Value;
+import org.praxislive.core.Call;
+import org.praxislive.core.ControlAddress;
+import org.praxislive.core.syntax.InvalidSyntaxException;
+import org.praxislive.core.types.PArray;
+import org.praxislive.core.types.PError;
+import org.praxislive.core.types.PString;
+import org.praxislive.script.ast.RootNode;
+import org.praxislive.script.ast.ScriptParser;
+
+import static java.lang.System.Logger.Level;
+
+/**
+ * A stackframe implementation that supports parsing and running of Pcl scripts.
+ */
+public final class ScriptStackFrame implements StackFrame {
+
+ private static final String TRAP = "_TRAP";
+
+ private static final System.Logger log = System.getLogger(ScriptStackFrame.class.getName());
+
+ private final Namespace namespace;
+ private final RootNode rootNode;
+ private final boolean trapErrors;
+ private final List scratchList;
+
+ private State state;
+ private String activeCommand;
+ private Call pending;
+ private List result;
+ private boolean doProcess;
+
+ private ScriptStackFrame(Namespace namespace,
+ RootNode rootNode,
+ boolean trapErrors) {
+ this.namespace = namespace;
+ this.rootNode = rootNode;
+ this.state = State.Incomplete;
+ this.trapErrors = trapErrors;
+ this.scratchList = new ArrayList<>();
+ rootNode.reset();
+ if (trapErrors) {
+ namespace.createVariable(TRAP, PArray.EMPTY);
+ }
+ rootNode.init(namespace);
+ doProcess = true;
+ }
+
+ @Override
+ public State getState() {
+ return state;
+ }
+
+ @Override
+ public StackFrame process(Env context) {
+ if (state != State.Incomplete) {
+ throw new IllegalStateException();
+ }
+ if (!doProcess) {
+ return null;
+ }
+ while (!rootNode.isDone() && state == State.Incomplete) {
+ try {
+ return processNextCommand(context);
+ } catch (Exception ex) {
+ postError(List.of(PError.of(ex)));
+ }
+ }
+ if (rootNode.isDone() && state == State.Incomplete) {
+
+ try {
+ processResultFromNode();
+ } catch (Exception ex) {
+ result = List.of(PError.of(ex));
+ state = State.Error;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void postResponse(Call call) {
+ if (pending != null && pending.matchID() == call.matchID()) {
+ pending = null;
+ if (call.isReply()) {
+ log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Return call : \n" + call);
+ postResponse(call.args());
+ } else {
+ log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Error call : \n" + call);
+ postError(call.args());
+ }
+ doProcess = true;
+ } else {
+ log.log(Level.TRACE, () -> "EvalStackFrame - Received invalid call : \n" + call);
+ }
+
+ }
+
+ @Override
+ public void postResponse(State state, List args) {
+ if (this.state != State.Incomplete) {
+ throw new IllegalStateException();
+ }
+ switch (state) {
+ case Incomplete ->
+ throw new IllegalArgumentException();
+ case OK ->
+ postResponse(args);
+ case Error ->
+ postError(args);
+ default -> {
+ this.state = state;
+ this.result = List.copyOf(args);
+ }
+ }
+ doProcess = true;
+ }
+
+ @Override
+ public List result() {
+ if (state == State.Incomplete) {
+ throw new IllegalStateException();
+ }
+ if (result == null) {
+ return List.of();
+ } else {
+ return result;
+ }
+ }
+
+ private void postResponse(List args) {
+ try {
+ scratchList.clear();
+ scratchList.addAll(args);
+ rootNode.postResponse(scratchList);
+ } catch (Exception ex) {
+ state = State.Error;//@TODO proper error reporting
+ }
+ }
+
+ private void postError(List args) {
+ Variable trap = namespace.getVariable(TRAP);
+ if (trap != null) {
+ rootNode.skipCurrentLine();
+ PArray existing = PArray.from(trap.getValue()).orElse(PArray.EMPTY);
+ Value response = args.isEmpty() ? null : args.getFirst();
+ trap.setValue(addErrorToTrap(existing, response));
+ } else {
+ result = List.copyOf(args);
+ state = State.Error;
+ }
+ }
+
+ private PArray addErrorToTrap(PArray trap, Value response) {
+ String msg;
+ if (response == null) {
+ msg = activeCommand + " : Error";
+ } else {
+ msg = PError.from(response)
+ .map(err -> activeCommand + " : " + err.exceptionType().getSimpleName()
+ + " : " + err.message())
+ .orElse(activeCommand + " : Error : " + response);
+ }
+ return Stream.concat(trap.stream(), Stream.of(PString.of(msg)))
+ .collect(PArray.collector());
+ }
+
+ private void processResultFromNode() throws Exception {
+ scratchList.clear();
+ Variable trap = namespace.getVariable(TRAP);
+ if (trapErrors && trap != null && !trap.getValue().isEmpty()) {
+ String errors = PArray.from(trap.getValue())
+ .orElse(PArray.EMPTY)
+ .asListOf(String.class)
+ .stream()
+ .collect(Collectors.joining("\n"));
+ result = List.of(PString.of(errors));
+ state = State.Error;
+ } else {
+ rootNode.writeResult(scratchList);
+ result = List.copyOf(scratchList);
+ state = State.OK;
+ }
+ }
+
+ private StackFrame processNextCommand(Env context)
+ throws Exception {
+
+ scratchList.clear();
+ rootNode.writeNextCommand(scratchList);
+ if (scratchList.size() < 1) {
+ throw new Exception();
+ }
+ Value cmdArg = scratchList.get(0);
+ activeCommand = cmdArg.toString();
+ if (cmdArg instanceof ControlAddress) {
+ routeCall(context, scratchList);
+ return null;
+ }
+ String cmdStr = cmdArg.toString();
+ if (cmdStr.isEmpty()) {
+ throw new IllegalArgumentException("Empty command");
+ }
+ Command cmd = namespace.getCommand(cmdStr);
+ if (cmd != null) {
+ scratchList.remove(0);
+ return cmd.createStackFrame(namespace, List.copyOf(scratchList));
+ }
+ if (cmdStr.charAt(0) == '/' && cmdStr.lastIndexOf('.') > -1) {
+ routeCall(context, scratchList);
+ return null;
+ }
+
+ throw new IllegalArgumentException("Command not found");
+
+ }
+
+ private void routeCall(Env context, List argList)
+ throws Exception {
+ ControlAddress ad = ControlAddress.from(argList.get(0))
+ .orElseThrow(Exception::new);
+ argList.remove(0);
+ Call call = Call.create(ad, context.getAddress(), context.getTime(), List.copyOf(argList));
+ log.log(Level.TRACE, () -> "Sending Call" + call);
+ pending = call;
+ context.getPacketRouter().route(call);
+ }
+
+ /**
+ * Create a {@link ScriptStackFrame.Builder} for the provided namespace and
+ * script. By default the script will be evaluated in a dedicated child
+ * namespace. Neither the builder or the stack frame are reusable.
+ *
+ * @param namespace namespace to run script in
+ * @param script script to parse and run
+ * @return builder
+ * @throws InvalidSyntaxException if the script cannot be parsed
+ */
+ public static Builder forScript(Namespace namespace, String script) {
+ RootNode root = ScriptParser.getInstance().parse(script);
+ return new Builder(namespace, root);
+ }
+
+ /**
+ * A builder for {@link ScriptStackFrame}.
+ *
+ * @see #forScript(org.praxislive.script.Namespace, java.lang.String)
+ */
+ public static class Builder {
+
+ private final Namespace namespace;
+ private final RootNode root;
+
+ private boolean inline;
+ private List allowedCommands;
+ private boolean trapErrors;
+
+ private Builder(Namespace namespace, RootNode root) {
+ this.namespace = namespace;
+ this.root = root;
+ }
+
+ /**
+ * Run the script directly in the provided namespace rather than a
+ * child.
+ *
+ * @return this for chaining
+ */
+ public Builder inline() {
+ if (trapErrors) {
+ throw new IllegalStateException("Inline and trap errors cannot be used together");
+ }
+ this.inline = true;
+ return this;
+ }
+
+ /**
+ * Trap errors. Error messages will be aggregated and script execution
+ * will attempt to continue. If no allowed commands have been specified,
+ * an empty list of allowed commands will be set.
+ *
+ * @return this for chaining
+ */
+ public Builder trapErrors() {
+ if (inline) {
+ throw new IllegalStateException("Inline and trap errors cannot be used together");
+ }
+ this.trapErrors = true;
+ if (this.allowedCommands == null) {
+ this.allowedCommands = List.of();
+ }
+ return this;
+ }
+
+ /**
+ * Specify a list of allowed commands to filter those available from the
+ * provided namespace.
+ *
+ * @param commands list of allowed commands
+ * @return this for chaining
+ */
+ public Builder allowedCommands(List commands) {
+ this.allowedCommands = List.copyOf(commands);
+ return this;
+ }
+
+ /**
+ * Build the ScriptStackFrame.
+ *
+ * @return script stackframe
+ */
+ public ScriptStackFrame build() {
+ Namespace ns;
+ if (inline) {
+ ns = namespace;
+ } else {
+ ns = namespace.createChild();
+ }
+ if (allowedCommands != null) {
+ ns = new FilteredNamespace(ns, allowedCommands);
+ }
+ return new ScriptStackFrame(ns, root, trapErrors);
+ }
+
+ }
+
+ private static class FilteredNamespace implements Namespace {
+
+ private final Namespace delegate;
+ private final List allowed;
+
+ private FilteredNamespace(Namespace delegate, List allowed) {
+ this.delegate = Objects.requireNonNull(delegate);
+ this.allowed = List.copyOf(allowed);
+ }
+
+ @Override
+ public void addCommand(String id, Command cmd) {
+ if (allowed.contains(id)) {
+ delegate.addCommand(id, cmd);
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public void addVariable(String id, Variable var) {
+ delegate.addVariable(id, var);
+ }
+
+ @Override
+ public Namespace createChild() {
+ return new FilteredNamespace(delegate.createChild(), allowed);
+ }
+
+ @Override
+ public Command getCommand(String id) {
+ if (allowed.contains(id)) {
+ return delegate.getCommand(id);
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public Variable getVariable(String id) {
+ return delegate.getVariable(id);
+ }
+
+ }
+
+}
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java
index 50f59598..ec0267ba 100644
--- a/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java
+++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/CompositeNode.java
@@ -1,7 +1,7 @@
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
- * Copyright 2018 Neil C Smith.
+ * Copyright 2024 Neil C Smith.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version 3 only, as
@@ -19,7 +19,6 @@
* Please visit https://www.praxislive.org if you need additional information or
* have any questions.
*/
-
package org.praxislive.script.ast;
import java.util.List;
@@ -28,19 +27,20 @@
/**
*
- *
+ *
*/
-public abstract class CompositeNode extends Node {
-
- private Node[] children;
+abstract class CompositeNode extends Node {
+
+ private final List children;
+
private int active;
private Namespace namespace;
public CompositeNode(List extends Node> children) {
- this.children = children.toArray(new Node[children.size()]);
+ this.children = List.copyOf(children);
}
- @Override
+ @Override
public void init(Namespace namespace) {
if (namespace == null) {
throw new NullPointerException();
@@ -50,9 +50,7 @@ public void init(Namespace namespace) {
}
this.namespace = namespace;
active = 0;
- for (Node child : children) {
- child.init(namespace);
- }
+ children.forEach(child -> child.init(namespace));
}
@Override
@@ -63,8 +61,8 @@ public boolean isDone() {
if (active < 0) {
return isThisDone();
} else {
- while (active < children.length) {
- if (!children[active].isDone()) {
+ while (active < children.size()) {
+ if (!children.get(active).isDone()) {
return false;
}
active++;
@@ -77,13 +75,13 @@ public boolean isDone() {
protected abstract boolean isThisDone();
@Override
- public void writeNextCommand(List args)
+ public void writeNextCommand(List args)
throws Exception {
if (namespace == null) {
throw new IllegalStateException();
}
if (active >= 0) {
- children[active].writeNextCommand(args);
+ children.get(active).writeNextCommand(args);
} else {
writeThisNextCommand(args);
}
@@ -92,15 +90,14 @@ public void writeNextCommand(List args)
protected abstract void writeThisNextCommand(List args)
throws Exception;
-
@Override
- public void postResponse(List args)
+ public void postResponse(List args)
throws Exception {
if (namespace == null) {
throw new IllegalStateException();
}
if (active >= 0) {
- children[active].postResponse(args);
+ children.get(active).postResponse(args);
} else {
postThisResponse(args);
}
@@ -116,11 +113,19 @@ public void reset() {
}
namespace = null;
}
-
- protected Node[] getChildren() {
+
+ protected List getChildren() {
return children;
}
-
+ protected void skipActive() {
+ if (active < 0) {
+ return;
+ }
+ active++;
+ if (active >= children.size()) {
+ active = -1;
+ }
+ }
}
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java
index afd29eab..794a6f2e 100644
--- a/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java
+++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/RootNode.java
@@ -19,7 +19,6 @@
* Please visit https://www.praxislive.org if you need additional information or
* have any questions.
*/
-
package org.praxislive.script.ast;
import java.util.List;
@@ -27,7 +26,7 @@
/**
*
- *
+ *
*/
public class RootNode extends CompositeNode {
@@ -52,13 +51,17 @@ protected void postThisResponse(List args) {
@Override
public void writeResult(List args) throws Exception {
- Node[] children = getChildren();
- if (children.length > 0) {
- children[children.length - 1].writeResult(args);
+ List children = getChildren();
+ if (!children.isEmpty()) {
+ Node last = children.getLast();
+ if (last.isDone()) {
+ last.writeResult(args);
+ }
}
-
}
-
+ public void skipCurrentLine() {
+ skipActive();
+ }
}
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java b/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java
index 6fc9660a..0c2829c6 100644
--- a/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java
+++ b/praxiscore-script/src/main/java/org/praxislive/script/ast/SubcommandNode.java
@@ -26,7 +26,7 @@
/**
*
- *
+ *
*/
public class SubcommandNode extends CompositeNode {
@@ -52,7 +52,7 @@ protected void postThisResponse(List args) {
@Override
public void writeResult(List args)
throws Exception {
- Node[] children = getChildren();
- children[children.length - 1].writeResult(args);
+ List children = getChildren();
+ children.get(children.size() - 1).writeResult(args);
}
}
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java
index f6a9ee83..f1270eeb 100644
--- a/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java
+++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/AtCmds.java
@@ -39,6 +39,7 @@
import org.praxislive.script.CommandInstaller;
import org.praxislive.script.Env;
import org.praxislive.script.Namespace;
+import org.praxislive.script.ScriptStackFrame;
import org.praxislive.script.StackFrame;
/**
@@ -171,7 +172,7 @@ public StackFrame process(Env env) {
try {
Namespace child = namespace.createChild();
child.createConstant(Env.CONTEXT, ctxt);
- return ScriptCmds.INLINE_EVAL.createStackFrame(child, List.of(script));
+ return ScriptStackFrame.forScript(child, script.toString()).inline().build();
} catch (Exception ex) {
state = State.Error;
result = List.of(PError.of(ex));
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java
deleted file mode 100644
index 9769f552..00000000
--- a/praxiscore-script/src/main/java/org/praxislive/script/commands/EvalStackFrame.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
- *
- * Copyright 2024 Neil C Smith.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU Lesser General Public License version 3 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
- * version 3 for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License version 3
- * along with this work; if not, see http://www.gnu.org/licenses/
- *
- *
- * Please visit https://www.praxislive.org if you need additional information or
- * have any questions.
- */
-package org.praxislive.script.commands;
-
-import java.util.LinkedList;
-import java.util.List;
-import org.praxislive.core.Value;
-import org.praxislive.core.Call;
-import org.praxislive.core.ControlAddress;
-import org.praxislive.core.types.PError;
-import org.praxislive.script.Command;
-import org.praxislive.script.Env;
-import org.praxislive.script.Namespace;
-import org.praxislive.script.StackFrame;
-import org.praxislive.script.ast.RootNode;
-
-import static java.lang.System.Logger.Level;
-
-/**
- *
- */
-class EvalStackFrame implements StackFrame {
-
- private static final System.Logger log = System.getLogger(EvalStackFrame.class.getName());
-
- private Namespace namespace;
- private RootNode rootNode;
- private State state;
- private Call pending;
- private List result;
- private List argList;
- private boolean doProcess;
-
- public EvalStackFrame(Namespace namespace, RootNode rootNode) {
- this.namespace = namespace;
- this.rootNode = rootNode;
- this.state = State.Incomplete;
- this.argList = new LinkedList<>();
- rootNode.reset();
- rootNode.init(namespace);
- doProcess = true;
- }
-
- @Override
- public State getState() {
- return state;
- }
-
- @Override
- public StackFrame process(Env context) {
- if (state != State.Incomplete) {
- throw new IllegalStateException();
- }
- if (!doProcess) {
- return null;
- }
- try {
- if (rootNode.isDone()) {
- processResultFromNode();
- return null;
- } else {
- return processNextCommand(context);
- }
- } catch (Exception ex) {
- result = List.of(PError.of(ex));
- state = State.Error;
- return null;
- } finally {
- doProcess = false;
- }
-
- }
-
- @Override
- public void postResponse(Call call) {
- if (pending != null && pending.matchID() == call.matchID()) {
- pending = null;
- if (call.isReply()) {
- log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Return call : \n" + call);
- postResponse(call.args());
- } else {
- log.log(Level.TRACE, () -> "EvalStackFrame - Received valid Error call : \n" + call);
- this.state = State.Error;
- this.result = call.args();
- }
- doProcess = true;
- } else {
- log.log(Level.TRACE, () -> "EvalStackFrame - Received invalid call : \n" + call);
- }
-
- }
-
- @Override
- public void postResponse(State state, List args) {
- if (this.state != State.Incomplete) {
- throw new IllegalStateException();
- }
- switch (state) {
- case Incomplete:
- throw new IllegalArgumentException();
- case OK:
- postResponse(args);
- break;
- default:
- this.state = state;
- this.result = args;
- }
- doProcess = true;
- }
-
- @Override
- public List result() {
- if (state == State.Incomplete) {
- throw new IllegalStateException();
- }
- if (result == null) {
- return List.of();
- } else {
- return result;
- }
- }
-
- private void postResponse(List args) {
- try {
- argList.clear();
- argList.addAll(args);
- rootNode.postResponse(argList);
- } catch (Exception ex) {
- state = State.Error;//@TODO proper error reporting
- }
- }
-
- private void processResultFromNode() throws Exception {
- argList.clear();
- rootNode.writeResult(argList);
- result = List.copyOf(argList);
- state = State.OK;
-
- }
-
- private StackFrame processNextCommand(Env context)
- throws Exception {
-
- argList.clear();
- rootNode.writeNextCommand(argList);
- if (argList.size() < 1) {
- throw new Exception();
- }
- Value cmdArg = argList.get(0);
- if (cmdArg instanceof ControlAddress) {
- routeCall(context, argList);
- return null;
- }
- String cmdStr = cmdArg.toString();
- if (cmdStr.isEmpty()) {
- throw new Exception();
- }
- Command cmd = namespace.getCommand(cmdStr);
- if (cmd != null) {
- argList.remove(0);
- return cmd.createStackFrame(namespace, List.copyOf(argList));
- }
- if (cmdStr.charAt(0) == '/' && cmdStr.lastIndexOf('.') > -1) {
- routeCall(context, argList);
- return null;
- }
-
- throw new Exception();
-
- }
-
- private void routeCall(Env context, List argList)
- throws Exception {
- ControlAddress ad = ControlAddress.from(argList.get(0))
- .orElseThrow(Exception::new);
- argList.remove(0);
- Call call = Call.create(ad, context.getAddress(), context.getTime(), List.copyOf(argList));
- log.log(Level.TRACE, () -> "Sending Call" + call);
- pending = call;
- context.getPacketRouter().route(call);
- }
-
-}
diff --git a/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java b/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java
index c5c168cc..9f6cc67c 100644
--- a/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java
+++ b/praxiscore-script/src/main/java/org/praxislive/script/commands/ScriptCmds.java
@@ -22,17 +22,18 @@
package org.praxislive.script.commands;
import java.io.File;
+import java.util.ArrayDeque;
import java.util.List;
import java.util.Map;
+import java.util.Queue;
import org.praxislive.core.Value;
-import org.praxislive.core.syntax.InvalidSyntaxException;
+import org.praxislive.core.types.PArray;
import org.praxislive.core.types.PResource;
import org.praxislive.script.Command;
import org.praxislive.script.CommandInstaller;
import org.praxislive.script.Namespace;
+import org.praxislive.script.ScriptStackFrame;
import org.praxislive.script.StackFrame;
-import org.praxislive.script.ast.RootNode;
-import org.praxislive.script.ast.ScriptParser;
/**
*
@@ -41,7 +42,6 @@ public class ScriptCmds implements CommandInstaller {
private final static ScriptCmds instance = new ScriptCmds();
public final static Command EVAL = new Eval();
- public final static Command INLINE_EVAL = new InlineEval();
public final static Command INCLUDE = new Include();
private ScriptCmds() {
@@ -62,34 +62,44 @@ private static class Eval implements Command {
@Override
public StackFrame createStackFrame(Namespace namespace, List args)
throws Exception {
- if (args.size() != 1) {
- throw new Exception();
+ if (args.isEmpty()) {
+ throw new IllegalArgumentException("No script passed to eval");
}
- String script = args.get(0).toString();
- try {
- RootNode astRoot = ScriptParser.getInstance().parse(script);
- return new EvalStackFrame(namespace.createChild(), astRoot);
- } catch (InvalidSyntaxException ex) {
- throw new Exception(ex);
+ Queue queue = new ArrayDeque<>(args);
+ boolean inline = false;
+ boolean trap = false;
+ List allowed = null;
+ String script = null;
+ while (!queue.isEmpty()) {
+ String arg = queue.poll().toString();
+ if ("--inline".equals(arg)) {
+ inline = true;
+ } else if ("--trap-errors".equals(arg)) {
+ trap = true;
+ if (allowed == null) {
+ allowed = List.of();
+ }
+ } else if ("--allowed-commands".equals(arg)) {
+ allowed = PArray.from(queue.poll()).orElseThrow().asListOf(String.class);
+ } else {
+ script = arg;
+ break;
+ }
}
- }
- }
-
- private static class InlineEval implements Command {
-
- @Override
- public StackFrame createStackFrame(Namespace namespace, List args)
- throws Exception {
- if (args.size() != 1) {
- throw new Exception();
+ if (!queue.isEmpty()) {
+ throw new IllegalArgumentException("Additional arguments after script");
}
- String script = args.get(0).toString();
- try {
- RootNode astRoot = ScriptParser.getInstance().parse(script);
- return new EvalStackFrame(namespace, astRoot);
- } catch (InvalidSyntaxException ex) {
- throw new Exception(ex);
+ var bld = ScriptStackFrame.forScript(namespace, script);
+ if (inline) {
+ bld.inline();
+ }
+ if (trap) {
+ bld.trapErrors();
+ }
+ if (allowed != null) {
+ bld.allowedCommands(allowed);
}
+ return bld.build();
}
}
@@ -105,8 +115,7 @@ public StackFrame createStackFrame(Namespace namespace, List args) throws
PResource res = PResource.from(args.get(0)).orElseThrow();
File file = new File(res.value());
String script = Utils.loadStringFromFile(file);
- RootNode astRoot = ScriptParser.getInstance().parse(script);
- return new EvalStackFrame(namespace.createChild(), astRoot);
+ return ScriptStackFrame.forScript(namespace, script).build();
} catch (Exception ex) {
throw new Exception(ex);
}
diff --git a/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java b/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java
new file mode 100644
index 00000000..1a5f473d
--- /dev/null
+++ b/praxiscore-script/src/test/java/org/praxislive/script/DefaultScriptServiceTest.java
@@ -0,0 +1,308 @@
+package org.praxislive.script;
+
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.praxislive.core.Call;
+import org.praxislive.core.Clock;
+import org.praxislive.core.ControlAddress;
+import org.praxislive.core.Lookup;
+import org.praxislive.core.Packet;
+import org.praxislive.core.Root;
+import org.praxislive.core.RootHub;
+import org.praxislive.core.Value;
+import org.praxislive.core.types.PArray;
+import org.praxislive.core.types.PError;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ *
+ */
+public class DefaultScriptServiceTest {
+
+ private static final boolean VERBOSE = Boolean.getBoolean("praxis.test.verbose");
+ private static final int TIMEOUT = Integer.getInteger("praxis.test.timeout", 1000000);
+
+ public DefaultScriptServiceTest() {
+ }
+
+ @Test
+ public void testInlineScript() throws Exception {
+ logTest("testInlineScript");
+ String script = """
+ set V1 "One"
+ set V2 "Two"
+ set V3 $V1
+ set RET [array $V1 $V2 $V3]
+ """;
+ var root = new DefaultScriptService();
+ try (var hub = new RootHubImpl("script", root)) {
+ hub.start();
+ hub.send("/script.eval", "/hub.result", script);
+ var result = hub.poll();
+ logCall("Result received", result);
+ assertTrue(result.isReply());
+ assertEquals(1, result.args().size());
+ var expected = Stream.of("One", "Two", "One")
+ .map(Value::ofObject)
+ .collect(PArray.collector());
+ assertEquals(expected, result.args().get(0));
+ }
+
+ }
+
+ @Test
+ public void testAtScript() throws Exception {
+ logTest("testAtScript");
+ String script = """
+ .foo
+ @ /bar {
+ set V 42
+ @ ./baz test:component {
+ .value $V
+ }
+ }
+ """;
+ var root = new DefaultScriptService();
+ try (var hub = new RootHubImpl("script", root)) {
+ hub.start();
+ hub.send("/script.eval", "/hub.result", script);
+ Call call = hub.poll();
+ logCall("Call to /hub.foo received", call);
+ assertTrue(call.isRequest());
+ assertEquals("/hub.foo", call.to().toString());
+ assertTrue(call.args().isEmpty());
+ hub.dispatch(call.reply());
+
+ call = hub.poll();
+ logCall("Call to /bar.add-child received", call);
+ assertTrue(call.isRequest());
+ assertEquals("/bar.add-child", call.to().toString());
+ hub.dispatch(call.reply());
+
+ call = hub.poll();
+ logCall("Call to /bar/baz.value received", call);
+ assertTrue(call.isRequest());
+ assertEquals("/bar/baz.value", call.to().toString());
+ assertEquals(1, call.args().size());
+ assertEquals("42", call.args().get(0).toString());
+ hub.dispatch(call.reply());
+
+ call = hub.poll();
+ logCall("Result received", call);
+
+ assertTrue(call.isReply());
+
+ }
+
+ }
+
+ @Test
+ public void testEvalInline() throws Exception {
+ logTest("testEvalInline");
+ String script = """
+ eval --inline {
+ set X 42
+ /hub.value $X
+ }
+ /hub.value $X
+ eval {
+ set Y 84
+ /hub.value $Y
+ }
+ /hub.value $Y
+ """;
+ var root = new DefaultScriptService();
+ try (var hub = new RootHubImpl("script", root)) {
+ hub.start();
+ hub.send("/script.eval", "/hub.result", script);
+
+ for (String expected : new String[]{"42", "42", "84"}) {
+ Call call = hub.poll();
+ logCall("Value received", call);
+ assertEquals("/hub.value", call.to().toString());
+ assertEquals(expected, call.args().get(0).toString());
+ hub.dispatch(call.reply());
+ }
+
+ Call call = hub.poll();
+ logCall("Expected execution failure", call);
+ assertTrue(call.isError());
+
+ }
+ }
+
+ @Test
+ public void testEvalTrapErrors() throws Exception {
+ logTest("testEvalTrapErrors");
+ String script = """
+ eval --trap-errors {
+ set X 42
+ /hub.value "FOO"
+ }
+ """;
+ var root = new DefaultScriptService();
+ try (var hub = new RootHubImpl("script", root)) {
+ hub.start();
+ hub.send("/script.eval", "/hub.result", script);
+ Call call = hub.poll();
+ logCall("Value received", call);
+ assertEquals("/hub.value", call.to().toString());
+ assertEquals("FOO", call.args().get(0).toString());
+ hub.dispatch(call.reply());
+ call = hub.poll();
+ logCall("Error result received", call);
+ assertTrue(call.isError());
+ }
+
+ }
+
+ @Test
+ public void testEvalTrapErrorsNested() throws Exception {
+ logTest("testEvalTrapErrorsNested");
+ String script = """
+ set allowed [array "@"]
+ eval --trap-errors --allowed-commands $allowed {
+ @ /bar {
+ set V 42
+ @ ./baz test:component {
+ .error
+ .value "FOO"
+ }
+ }
+ set Y [set X]
+ }
+ """;
+ var root = new DefaultScriptService();
+ try (var hub = new RootHubImpl("script", root)) {
+ hub.start();
+ hub.send("/script.eval", "/hub.result", script);
+ Call call = hub.poll();
+ logCall("Call to /bar.add-child received", call);
+ assertTrue(call.isRequest());
+ assertEquals("/bar.add-child", call.to().toString());
+ hub.dispatch(call.reply());
+
+ call = hub.poll();
+ logCall("Call to /bar/baz.error received", call);
+ assertTrue(call.isRequest());
+ assertEquals("/bar/baz.error", call.to().toString());
+ hub.dispatch(call.error(PError.of("BAZ ERROR")));
+
+ call = hub.poll();
+ logCall("Call to /bar/baz.value received", call);
+ assertTrue(call.isRequest());
+ assertEquals("/bar/baz.value", call.to().toString());
+ assertEquals(1, call.args().size());
+ assertEquals("FOO", call.args().get(0).toString());
+ hub.dispatch(call.reply());
+
+ call = hub.poll();
+ logCall("Result received", call);
+
+ assertTrue(call.isError());
+ String errors = call.args().get(0).toString();
+ if (VERBOSE) {
+ System.out.println("");
+ System.out.println("Completed with errors");
+ System.out.println("=====================");
+ System.out.println(errors);
+ }
+ assertTrue(errors.contains("set"));
+ assertTrue(errors.contains("BAZ ERROR"));
+ }
+
+ }
+
+ private static void logTest(String testName) {
+ if (VERBOSE) {
+ System.out.println("");
+ System.out.println("");
+ System.out.println(testName);
+ System.out.println("====================");
+ }
+ }
+
+ private static void logCall(String msg, Call call) {
+ if (VERBOSE) {
+ System.out.println(msg);
+ System.out.println(call);
+ }
+ }
+
+ private static class RootHubImpl implements RootHub, AutoCloseable {
+
+ private final String rootID;
+ private final Root root;
+ private final BlockingQueue queue;
+
+ private Root.Controller ctrl;
+
+ private RootHubImpl(String rootID, Root root) {
+ this.rootID = rootID;
+ this.root = root;
+ this.queue = new LinkedBlockingQueue<>();
+ }
+
+ @Override
+ public boolean dispatch(Packet packet) {
+ if (rootID.equals(packet.rootID())) {
+ return ctrl.submitPacket(packet);
+ } else {
+ return queue.offer(packet);
+ }
+ }
+
+ @Override
+ public Clock getClock() {
+ return System::nanoTime;
+ }
+
+ @Override
+ public Lookup getLookup() {
+ return Lookup.EMPTY;
+ }
+
+ public void send(String to, String from, Object arg) {
+ send(to, from, List.of(Value.ofObject(arg)));
+ }
+
+ public void send(String to, String from, List args) {
+ dispatch(Call.create(ControlAddress.of(to),
+ ControlAddress.of(from),
+ getClock().getTime(),
+ args
+ ));
+ }
+
+ public Call poll() throws InterruptedException, TimeoutException {
+ Packet p = queue.poll(TIMEOUT, TimeUnit.MILLISECONDS);
+ if (p instanceof Call c) {
+ return c;
+ }
+ throw new TimeoutException("Call poll timed out");
+ }
+
+ public void start() {
+ ctrl = root.initialize(rootID, this);
+ ctrl.start();
+ }
+
+ @Override
+ public void close() {
+ ctrl.shutdown();
+ try {
+ ctrl.awaitTermination(10, TimeUnit.SECONDS);
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ }
+
+ }
+
+}
diff --git a/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java b/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java
index c96eff8f..2b7f9c4b 100644
--- a/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java
+++ b/praxiscore-script/src/test/java/org/praxislive/script/ast/AddressNodeTest.java
@@ -1,95 +1,51 @@
-/*
- * To change this template, choose Tools | Templates
- * and open the template in the editor.
- */
-
package org.praxislive.script.ast;
-import org.praxislive.script.ast.AddressNode;
import java.util.ArrayList;
import java.util.List;
+import org.junit.jupiter.api.Test;
import org.praxislive.core.Value;
import org.praxislive.core.ComponentAddress;
import org.praxislive.script.Command;
import org.praxislive.script.Namespace;
import org.praxislive.script.Variable;
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import static org.junit.Assert.*;
-
-/**
- *
- *
- */
-public class AddressNodeTest {
- public AddressNodeTest() {
- }
+import static org.junit.jupiter.api.Assertions.*;
- @BeforeClass
- public static void setUpClass() throws Exception {
- }
- @AfterClass
- public static void tearDownClass() throws Exception {
- }
-
- @Before
- public void setUp() {
- }
-
- @After
- public void tearDown() {
- }
-
-// /**
-// * Test of init method, of class AddressNode.
-// */
-// @Test
-// public void testInit() {
-// System.out.println("init");
-// Namespace namespace = null;
-// AddressNode instance = null;
-// instance.init(namespace);
-// // TODO review the generated test code and remove the default call to fail.
-// fail("The test case is a prototype.");
-// }
-//
-// /**
-// * Test of reset method, of class AddressNode.
-// */
-// @Test
-// public void testReset() {
-// System.out.println("reset");
-// AddressNode instance = null;
-// instance.reset();
-// // TODO review the generated test code and remove the default call to fail.
-// fail("The test case is a prototype.");
-// }
+public class AddressNodeTest {
/**
* Test of writeResult method, of class AddressNode.
*/
@Test
public void testWriteResult() {
- System.out.println("writeResult");
- List args = new ArrayList();
Namespace ns = new NS();
- String[] ads = {"./to/here", "./to/here.control", "./to/here!port", ".control2"};
- for (String ad : ads) {
- AddressNode instance = new AddressNode(ad);
- instance.init(ns);
- instance.writeResult(args);
- System.out.println(ad + " : " + args.get(0));
- args.clear();
- }
+ List scratch = new ArrayList<>();
+ List relative = List.of(
+ "./to/here",
+ "./to/here.control",
+ "./to/here!port",
+ ".control2");
+ List absolute = List.of(
+ "/test/address/to/here",
+ "/test/address/to/here.control",
+ "/test/address/to/here!port",
+ "/test/address.control2");
+ List result = relative.stream()
+ .map(address -> {
+ var addressNode = new AddressNode(address);
+ addressNode.init(ns);
+ scratch.clear();
+ addressNode.writeResult(scratch);
+ return scratch.get(0).toString();
+ }).toList();
+ assertEquals(absolute, result);
+
}
private class NS implements Namespace, Variable {
+ @Override
public Variable getVariable(String id) {
if ("_CTXT".equals(id)) {
return this;
@@ -97,30 +53,36 @@ public Variable getVariable(String id) {
return null;
}
+ @Override
public void addVariable(String id, Variable var) {
throw new UnsupportedOperationException("Not supported yet.");
}
+ @Override
public Namespace createChild() {
throw new UnsupportedOperationException("Not supported yet.");
}
+ @Override
public void setValue(Value value) {
throw new UnsupportedOperationException("Not supported yet.");
}
+ @Override
public Value getValue() {
return ComponentAddress.of("/test/address");
}
+ @Override
public Command getCommand(String id) {
throw new UnsupportedOperationException("Not supported yet.");
}
+ @Override
public void addCommand(String id, Command cmd) {
throw new UnsupportedOperationException("Not supported yet.");
}
}
-}
\ No newline at end of file
+}