InputReader.java
package org.sterling.source.scanner;
import static java.lang.Character.isWhitespace;
import static java.util.regex.Pattern.compile;
import static org.sterling.source.Location.at;
import static org.sterling.source.LocationRange.between;
import static org.sterling.source.syntax.Token.token;
import static org.sterling.util.BufferUtil.buffer;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.sterling.source.Location;
import org.sterling.source.exception.InputException;
import org.sterling.source.exception.NoAcceptedInputException;
import org.sterling.source.exception.NoMoreInputException;
import org.sterling.source.syntax.NodeKind;
import org.sterling.source.syntax.Token;
public class InputReader implements AutoCloseable {
private final String source;
private final BufferedReader reader;
private final Deque<InputReference> readBuffer;
private final Deque<StoredReference> storeBuffer;
private final StateManager<InputReference> stateManager;
private final Pattern whitespace;
private String input;
private int line;
private int column;
public InputReader(String source, InputStream inputStream) {
this.source = source;
this.reader = buffer(inputStream);
this.readBuffer = new ArrayDeque<>();
this.storeBuffer = new ArrayDeque<>();
this.stateManager = new StateManager<>(readBuffer);
this.whitespace = compile("^\\s+");
this.line = -1;
this.column = 0;
}
public Token accept(NodeKind kind) {
Deque<StoredReference> acceptedReferences = acceptReferences();
Location start = acceptedReferences.peek().getLocation();
Location end = acceptedReferences.peekLast().getLocation();
StringBuilder builder = new StringBuilder();
while (!acceptedReferences.isEmpty()) {
builder.append(acceptedReferences.pop().getValue());
}
return token(kind, builder.toString(), between(start, at(
end.getSource(),
end.getLine(),
end.getColumn() + 1
)));
}
public void begin() {
stateManager.begin();
}
@Override
public void close() {
try {
stateManager.close();
reader.close();
} catch (IOException exception) {
// intentionally empty
}
}
public void end() {
stateManager.end();
}
public boolean expect(char value) {
return peek() == value;
}
public boolean expect(Pattern... patterns) {
if (hasMore()) {
for (Pattern pattern : patterns) {
if (match(pattern).find()) {
return true;
}
}
}
return false;
}
public Location getLocation() {
hasMore();
InputReference reference = peekReference();
if (reference == null) {
return at(source, line, column);
} else {
return reference.getLocation();
}
}
public char peek() {
if (hasMore()) {
return peekReference().getValue();
} else {
return '\0';
}
}
public void reject() {
while (!storeBuffer.isEmpty()) {
backward();
}
}
public void rollback() {
stateManager.restore();
}
public void skip() {
forward(true);
}
public void skip(Pattern pattern) {
Matcher matcher = match(pattern);
if (matcher.find()) {
int length = matcher.group(0).length();
for (int i = 0; i < length; i++) {
skip();
}
}
}
public void skipWhitespace() {
while (isWhitespace(peek())) {
skip(whitespace);
}
}
public void store() {
forward();
}
public void store(int thisMany) {
for (int i = 0; i < thisMany; i++) {
store();
}
}
public boolean store(Pattern... patterns) {
for (Pattern pattern : patterns) {
Matcher matcher = match(pattern);
if (matcher.find()) {
store(matcher.group(0).length());
return true;
}
}
return false;
}
private Deque<StoredReference> acceptReferences() {
Deque<StoredReference> acceptedInput = new ArrayDeque<>();
for (StoredReference reference : storeBuffer) {
if (reference.isKeep()) {
acceptedInput.push(reference);
}
}
if (acceptedInput.isEmpty()) {
throw new NoAcceptedInputException();
}
storeBuffer.clear();
return acceptedInput;
}
private void backward() {
readBuffer.push(storeBuffer.pop().getReference());
}
private void forward() {
forward(false);
}
private void forward(boolean skip) {
if (hasMore()) {
InputReference next = readBuffer.pop();
stateManager.push(next);
storeBuffer.push(new StoredReference(next, skip));
} else {
throw new NoMoreInputException("No more input [" + getLocation() + "]");
}
}
private String getCurrentLine() {
StringBuilder builder = new StringBuilder();
int offset = getLocation().getColumn();
for (InputReference reference : readBuffer) {
builder.append(reference.getValue());
offset = reference.getLocation().getColumn();
}
if (input != null) {
builder.append(input.substring(offset + 1));
}
return builder.toString();
}
private String getLine() {
if (hasMore()) {
if (getLocation().getLine() == line) {
return getCurrentLine();
} else {
return getPreviousLine();
}
} else {
return "\0";
}
}
private String getPreviousLine() {
StringBuilder builder = new StringBuilder();
for (InputReference reference : readBuffer) {
if (reference.getLocation().getLine() != line) {
builder.append(reference.getValue());
}
}
return builder.toString();
}
private boolean hasLine() {
if (input == null || column >= input.length()) {
try {
input = reader.readLine();
} catch (IOException exception) {
throw new InputException(exception);
}
if (input != null) {
input = input + "\n";
column = 0;
line++;
}
}
return input != null;
}
private boolean hasMore() {
if (readBuffer.isEmpty()) {
if (hasLine()) {
readBuffer.push(new InputReference(at(source, line, column), input.charAt(column++)));
} else {
return false;
}
}
return true;
}
private Matcher match(Pattern pattern) {
return pattern.matcher(getLine());
}
private InputReference peekReference() {
return readBuffer.peek();
}
private static final class InputReference {
private final Location location;
private final char value;
public InputReference(Location location, char value) {
this.location = location;
this.value = value;
}
public Location getLocation() {
return location;
}
public char getValue() {
return value;
}
}
private static final class StoredReference {
private final InputReference reference;
private final boolean discard;
public StoredReference(InputReference reference, boolean discard) {
this.reference = reference;
this.discard = discard;
}
public Location getLocation() {
return reference.getLocation();
}
public InputReference getReference() {
return reference;
}
public char getValue() {
return reference.getValue();
}
public boolean isKeep() {
return !discard;
}
}
}