ES6 Support Groundwork + Fixes #26

Merged
TopchetoEU merged 49 commits from ES6 into master 2024-09-05 14:26:07 +00:00
8 changed files with 390 additions and 84 deletions
Showing only changes of commit 4a5e5a71af - Show all commits

View File

@ -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<VariableDescriptor, VariableDescriptor> 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); }
}

View File

@ -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;
}
}

View File

@ -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<VariableDescriptor> all() {
return () -> locals.iterator();
}
public LocalScope(Scope parent) {
super(parent);
}
}

View File

@ -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<String> captures = new ArrayList<>();
private final ArrayList<String> 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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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; }
};
}
}

View File

@ -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<VariableDescriptor> {
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<String, ListVar> map = new HashMap<>();
private HashMap<String, VariableDescriptor> frozenMap = null;
private ArrayList<VariableDescriptor> 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<VariableDescriptor> iterator() {
if (frozen()) return frozenList.iterator();
else return new Iterator<VariableDescriptor>() {
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;
}
}