diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/FunctionScope.java b/src/java/me/topchetoeu/jscript/compilation/scope/FunctionScope.java new file mode 100644 index 0000000..3318f17 --- /dev/null +++ b/src/java/me/topchetoeu/jscript/compilation/scope/FunctionScope.java @@ -0,0 +1,63 @@ +package me.topchetoeu.jscript.compilation.scope; + +import java.util.HashMap; + +import me.topchetoeu.jscript.common.parsing.Location; + +public class FunctionScope extends Scope { + private final VariableList captures = new VariableList(); + private final VariableList locals = new VariableList(captures); + private HashMap childToParent = new HashMap<>(); + + private void removeCapture(String name) { + var res = captures.remove(name); + if (res != null) childToParent.remove(res); + } + + @Override public VariableDescriptor define(String name, boolean readonly, Location loc) { + var old = locals.get(name); + if (old != null) return old; + + removeCapture(name); + return locals.add(name, readonly); + } + @Override public VariableDescriptor defineStrict(String name, boolean readonly, Location loc) { + if (locals.has(name)) throw alreadyDefinedErr(loc, name); + else if (parent == null) throw new RuntimeException("Strict variables may be defined only in local scopes"); + else return parent.defineStrict(name, readonly, loc); + } + + @Override public VariableDescriptor get(String name, boolean capture) { + if (locals.has(name)) return locals.get(name); + if (captures.has(name)) return captures.get(name); + + var parentVar = parent.get(name, true); + var childVar = captures.add(parentVar); + + childToParent.put(childVar, parentVar); + + return childVar; + } + + public int localsCount() { + return locals.size(); + } + public int offset() { + return captures.size() + locals.size(); + } + + public int[] getCaptureIndices() { + var res = new int[captures.size()]; + var i = 0; + + for (var el : captures) { + assert childToParent.containsKey(el); + res[i] = childToParent.get(el).index(); + } + + return res; + } + + public FunctionScope() { super(); } + public FunctionScope(Scope parent) { super(parent); } +} diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/GlobalScope.java b/src/java/me/topchetoeu/jscript/compilation/scope/GlobalScope.java new file mode 100644 index 0000000..75ba7d0 --- /dev/null +++ b/src/java/me/topchetoeu/jscript/compilation/scope/GlobalScope.java @@ -0,0 +1,18 @@ +package me.topchetoeu.jscript.compilation.scope; + +import me.topchetoeu.jscript.common.parsing.Location; + +public final class GlobalScope extends Scope { + @Override public VariableDescriptor define(String name, boolean readonly, Location loc) { + return null; + } + @Override public VariableDescriptor defineStrict(String name, boolean readonly, Location loc) { + return null; + } + @Override public VariableDescriptor get(String name, boolean capture) { + return null; + } + @Override public int offset() { + return 0; + } +} diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/LocalScope.java b/src/java/me/topchetoeu/jscript/compilation/scope/LocalScope.java new file mode 100644 index 0000000..0ee6630 --- /dev/null +++ b/src/java/me/topchetoeu/jscript/compilation/scope/LocalScope.java @@ -0,0 +1,46 @@ +package me.topchetoeu.jscript.compilation.scope; + +import me.topchetoeu.jscript.common.parsing.Location; + +public class LocalScope extends Scope { + private final VariableList locals = new VariableList(); + + @Override public int offset() { + if (parent != null) return parent.offset() + locals.size(); + else return locals.size(); + } + + @Override public VariableDescriptor define(String name, boolean readonly, Location loc) { + if (locals.has(name)) throw alreadyDefinedErr(loc, name); + + return parent.define(name, readonly, loc); + } + @Override public VariableDescriptor defineStrict(String name, boolean readonly, Location loc) { + if (locals.has(name)) throw alreadyDefinedErr(loc, name); + return locals.add(name, readonly); + } + + @Override public VariableDescriptor get(String name, boolean capture) { + var res = locals.get(name); + + if (res != null) return res; + if (parent != null) return parent.get(name, capture); + + return null; + } + + @Override public boolean end() { + if (!super.end()) return false; + + this.locals.freeze(); + return true; + } + + public Iterable all() { + return () -> locals.iterator(); + } + + public LocalScope(Scope parent) { + super(parent); + } +} diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/LocalScopeRecord.java b/src/java/me/topchetoeu/jscript/compilation/scope/LocalScopeRecord.java deleted file mode 100644 index 070168d..0000000 --- a/src/java/me/topchetoeu/jscript/compilation/scope/LocalScopeRecord.java +++ /dev/null @@ -1,77 +0,0 @@ -package me.topchetoeu.jscript.compilation.scope; - -import java.util.ArrayList; - -public class LocalScopeRecord implements ScopeRecord { - public final LocalScopeRecord parent; - - private final ArrayList captures = new ArrayList<>(); - private final ArrayList locals = new ArrayList<>(); - - public String[] captures() { - return captures.toArray(String[]::new); - } - public String[] locals() { - return locals.toArray(String[]::new); - } - - public LocalScopeRecord child() { - return new LocalScopeRecord(this); - } - - public int localsCount() { - return locals.size(); - } - public int capturesCount() { - return captures.size(); - } - - public int[] getCaptures() { - var buff = new int[captures.size()]; - var i = 0; - - for (var name : captures) { - var index = parent.getKey(name); - if (index instanceof Integer) buff[i++] = (int)index; - } - - var res = new int[i]; - System.arraycopy(buff, 0, res, 0, i); - - return res; - } - - public Object getKey(String name) { - var capI = captures.indexOf(name); - var locI = locals.lastIndexOf(name); - if (locI >= 0) return locI; - if (capI >= 0) return ~capI; - if (parent != null) { - var res = parent.getKey(name); - if (res != null && res instanceof Integer) { - captures.add(name); - return -captures.size(); - } - } - - return name; - } - public Object define(String name, boolean force) { - if (!force && locals.contains(name)) return locals.indexOf(name); - locals.add(name); - return locals.size() - 1; - } - public Object define(String name) { - return define(name, false); - } - public void undefine() { - locals.remove(locals.size() - 1); - } - - public LocalScopeRecord() { - this.parent = null; - } - public LocalScopeRecord(LocalScopeRecord parent) { - this.parent = parent; - } -} diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/Scope.java b/src/java/me/topchetoeu/jscript/compilation/scope/Scope.java new file mode 100644 index 0000000..6631abd --- /dev/null +++ b/src/java/me/topchetoeu/jscript/compilation/scope/Scope.java @@ -0,0 +1,68 @@ +package me.topchetoeu.jscript.compilation.scope; + +import me.topchetoeu.jscript.common.parsing.Location; +import me.topchetoeu.jscript.runtime.exceptions.SyntaxException; + +public abstract class Scope { + public final Scope parent; + private boolean active = true; + private Scope child; + + protected final SyntaxException alreadyDefinedErr(Location loc, String name) { + return new SyntaxException(loc, String.format("Identifier '%s' has already been declared", name)); + } + + /** + * Defines an ES5-style variable + * @returns The index supplier of the variable if it is a local, or null if it is a global + * @throws SyntaxException If an ES2015-style variable with the same name exists anywhere from the current function to the current scope + * @throws RuntimeException If the scope is finalized or has an active child + */ + public abstract VariableDescriptor define(String name, boolean readonly, Location loc); + /** + * Defines an ES2015-style variable + * @param readonly True if const, false if let + * @return The index supplier of the variable + * @throws SyntaxException If any variable with the same name exists in the current scope + * @throws RuntimeException If the scope is finalized or has an active child + */ + public abstract VariableDescriptor defineStrict(String name, boolean readonly, Location loc); + /** + * Gets the index supplier of the given variable name, or null if it is a global + * + * @param capture Used to signal to the scope that the variable is going to be captured. + * Not passing this could lead to a local variable being optimized out as an ES5-style variable, + * which could break the semantics of a capture + */ + public abstract VariableDescriptor get(String name, boolean capture); + /** + * Gets the index offset from this scope to its children + */ + public abstract int offset(); + + public boolean end() { + if (!active) return false; + + this.active = false; + if (this.parent != null) { + assert this.parent.child == this; + this.parent.child = this; + } + + return true; + } + + public final boolean active() { return active; } + public final Scope child() { return child; } + + public Scope() { + this.parent = null; + } + public Scope(Scope parent) { + if (!parent.active) throw new RuntimeException("Parent is not active"); + if (parent.child != null) throw new RuntimeException("Parent has an active child"); + + this.parent = parent; + this.parent.child = this; + } +} diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/ScopeRecord.java b/src/java/me/topchetoeu/jscript/compilation/scope/ScopeRecord.java deleted file mode 100644 index fe08968..0000000 --- a/src/java/me/topchetoeu/jscript/compilation/scope/ScopeRecord.java +++ /dev/null @@ -1,7 +0,0 @@ -package me.topchetoeu.jscript.compilation.scope; - -public interface ScopeRecord { - public Object getKey(String name); - public Object define(String name); - public LocalScopeRecord child(); -} diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/VariableDescriptor.java b/src/java/me/topchetoeu/jscript/compilation/scope/VariableDescriptor.java new file mode 100644 index 0000000..29b3bb5 --- /dev/null +++ b/src/java/me/topchetoeu/jscript/compilation/scope/VariableDescriptor.java @@ -0,0 +1,19 @@ +package me.topchetoeu.jscript.compilation.scope; + +public abstract class VariableDescriptor { + public final boolean readonly; + public final String name; + + public abstract int index(); + + public VariableDescriptor(String name, boolean readonly) { + this.name = name; + this.readonly = readonly; + } + + public static VariableDescriptor of(String name, boolean readonly, int i) { + return new VariableDescriptor(name, readonly) { + @Override public int index() { return i; } + }; + } +} diff --git a/src/java/me/topchetoeu/jscript/compilation/scope/VariableList.java b/src/java/me/topchetoeu/jscript/compilation/scope/VariableList.java new file mode 100644 index 0000000..2fca3a8 --- /dev/null +++ b/src/java/me/topchetoeu/jscript/compilation/scope/VariableList.java @@ -0,0 +1,176 @@ +package me.topchetoeu.jscript.compilation.scope; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.function.IntSupplier; + +public class VariableList implements Iterable { + private class ListVar extends VariableDescriptor { + private ListVar next; + private ListVar prev; + + @Override public int index() { + throw new RuntimeException("The index of a variable may not be retrieved until the scope has been finalized"); + // var res = 0; + // if (offset != null) res = offset.getAsInt(); + + // for (var it = prev; it != null; it = it.prev) { + // res++; + // } + + // return res; + } + + public ListVar(String name, boolean readonly, ListVar next, ListVar prev) { + super(name, readonly); + + this.next = next; + this.prev = prev; + } + } + + private ListVar first, last; + + private HashMap map = new HashMap<>(); + + private HashMap frozenMap = null; + private ArrayList frozenList = null; + + private final IntSupplier offset; + + public boolean frozen() { + if (frozenMap != null) { + assert frozenList != null; + assert frozenMap != null; + assert map == null; + assert first == null; + assert last == null; + + return true; + } + else { + assert frozenList == null; + assert frozenMap == null; + assert map != null; + + return false; + } + } + + public VariableDescriptor add(VariableDescriptor val) { + return add(val.name, val.readonly); + } + public VariableDescriptor add(String name, boolean readonly) { + if (frozen()) throw new RuntimeException("The scope has been frozen"); + if (map.containsKey(name)) return map.get(name); + + var res = new ListVar(name, readonly, null, last); + last.next = res; + last = res; + map.put(name, res); + + return res; + } + public VariableDescriptor remove(String name) { + if (frozen()) throw new RuntimeException("The scope has been frozen"); + + var el = map.get(name); + if (el == null) return null; + + el.prev.next = el.next; + el.next.prev = el.prev; + + el.next = null; + el.prev = null; + + return el; + } + + public VariableDescriptor get(String name) { + return map.get(name); + } + public VariableDescriptor get(int i) { + if (frozen()) { + if (i < 0 || i >= frozenList.size()) return null; + return frozenList.get(i); + } + else { + if (i < 0 || i >= map.size()) return null; + + if (i < map.size() / 2) { + var it = first; + for (var j = 0; j < i; it = it.next, j++); + return it; + } + else { + var it = last; + for (var j = map.size() - 1; j >= i; it = it.prev, j--); + return it; + } + } + } + + public boolean has(String name) { + return this.get(name) != null; + } + + public int size() { + if (frozen()) return frozenList.size(); + else return map.size(); + } + + public void freeze() { + if (frozen()) return; + + frozenMap = new HashMap<>(); + frozenList = new ArrayList<>(); + + var i = 0; + if (offset != null) i = offset.getAsInt(); + + for (var it = first; it != null; it = it.next) { + frozenMap.put(it.name, VariableDescriptor.of(it.name, it.readonly, i++)); + } + } + + @Override public Iterator iterator() { + if (frozen()) return frozenList.iterator(); + else return new Iterator() { + private ListVar curr = first; + + @Override public boolean hasNext() { + return curr != null; + } + @Override public VariableDescriptor next() { + if (curr == null) return null; + + var res = curr; + curr = curr.next; + return res; + } + }; + } + + public VariableDescriptor[] toArray() { + var res = new VariableDescriptor[size()]; + var i = 0; + + for (var el : this) res[i++] = el; + + return res; + } + + public VariableList(IntSupplier offset) { + this.offset = offset; + } + public VariableList(int offset) { + this.offset = () -> offset; + } + public VariableList(VariableList prev) { + this.offset = prev::size; + } + public VariableList() { + this.offset = null; + } +}