added Wcfg file parser and CLI

This commit is contained in:
2023-10-30 22:30:00 +08:00
parent b7d00d9d9c
commit 9cb37790c2
45 changed files with 1445 additions and 219 deletions

View File

@@ -2,12 +2,18 @@ plugins {
id 'antlr'
}
configurations {
api {
exclude group: 'org.antlr', module: 'antlr4'
}
}
dependencies {
implementation catalog.jwo
implementation rootProject
implementation catalog.antlr.runtime
antlr catalog.antlr
antlr catalog.antlr.runtime
}
generateGrammarSource {

View File

@@ -1,15 +1,19 @@
grammar WCFG;
wcfg
: assignment*
: assignment* export?
;
export
: 'export' value ';'
;
assignment
: IDENTIFIER ':=' (expression | value) ';'
: IDENTIFIER ':=' value ';'
;
expression
: value ('<<' value)+
: (obj | IDENTIFIER) ('<<' (obj | IDENTIFIER))+
;
obj
@@ -18,7 +22,7 @@ obj
;
pair
: STRING ':' (expression | value)
: STRING ':' value
;
array
@@ -34,6 +38,7 @@ value
| IDENTIFIER
| obj
| array
| expression
;
BOOLEAN

View File

@@ -1,6 +1,6 @@
module net.woggioni.wson.wcfg {
requires static lombok;
requires static org.antlr.antlr4.runtime;
requires org.antlr.antlr4.runtime;
requires net.woggioni.jwo;
requires net.woggioni.wson;

View File

@@ -1,6 +1,7 @@
package net.woggioni.wson.wcfg;
import lombok.RequiredArgsConstructor;
import net.woggioni.jwo.LazyValue;
import net.woggioni.wson.traversal.ValueIdentity;
import net.woggioni.wson.value.ObjectValue;
import net.woggioni.wson.xface.Value;
@@ -11,53 +12,55 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import static net.woggioni.jwo.JWO.dynamicCast;
@RequiredArgsConstructor
public class CompositeObjectValue implements ObjectValue {
private final List<ObjectValue> elements;
private final List<? extends Value> elements;
private ObjectValue wrapped;
private LazyValue<ObjectValue> wrapped;
public CompositeObjectValue(List<ObjectValue> elements, Value.Configuration cfg) {
public CompositeObjectValue(List<? extends Value> elements, Value.Configuration cfg) {
this.elements = elements;
wrapped = ObjectValue.newInstance(cfg);
List<ValueIdentity> identities = new ArrayList<>();
for (ObjectValue element : elements) {
CompositeObjectValue compositeObjectValue;
if ((compositeObjectValue = dynamicCast(element, CompositeObjectValue.class)) != null) {
boolean differenceFound = false;
for (int i = 0; i < compositeObjectValue.elements.size(); i++) {
ObjectValue objectValue = compositeObjectValue.elements.get(i);
if (!differenceFound && (i >= identities.size() || !Objects.equals(
identities.get(i),
new ValueIdentity(compositeObjectValue.elements.get(i))))) {
differenceFound = true;
}
if (differenceFound) {
merge(wrapped, objectValue);
identities.add(new ValueIdentity(objectValue));
this.wrapped = LazyValue.of(() -> {
ObjectValue result = ObjectValue.newInstance(cfg);
List<ValueIdentity> identities = new ArrayList<>();
for (Value element : elements) {
if (element instanceof CompositeObjectValue compositeObjectValue) {
boolean differenceFound = false;
for (int i = 0; i < compositeObjectValue.elements.size(); i++) {
ObjectValue objectValue = (ObjectValue) compositeObjectValue.elements.get(i);
if (!differenceFound && (i >= identities.size() || !Objects.equals(
identities.get(i),
new ValueIdentity(compositeObjectValue.elements.get(i))))) {
differenceFound = true;
}
if (differenceFound) {
merge(result, objectValue);
identities.add(new ValueIdentity(objectValue));
}
}
} else {
merge(result, (ObjectValue) element);
identities.add(new ValueIdentity(element));
}
} else {
merge(wrapped, element);
identities.add(new ValueIdentity(element));
}
}
return result;
}, LazyValue.ThreadSafetyMode.NONE);
}
private static void merge(ObjectValue v1, ObjectValue v2) {
for (Map.Entry<String, Value> entry : v2) {
Value putResult = v1.getOrPut(entry.getKey(), entry.getValue());
if (putResult != entry.getValue()) {
if (putResult.type() == Value.Type.OBJECT && entry.getValue().type() == Value.Type.OBJECT) {
String key = entry.getKey();
Value value2put = entry.getValue();
Value putResult = v1.getOrPut(key, value2put);
if (putResult != value2put) {
if (putResult.type() == Value.Type.OBJECT && value2put.type() == Value.Type.OBJECT) {
ObjectValue ov = ObjectValue.newInstance();
merge(ov, (ObjectValue) putResult);
merge(ov, (ObjectValue) entry.getValue());
v1.put(entry.getKey(), ov);
merge(ov, (ObjectValue) value2put);
v1.put(key, ov);
} else {
v1.put(entry.getKey(), entry.getValue());
v1.put(key, value2put);
}
}
}
@@ -65,27 +68,27 @@ public class CompositeObjectValue implements ObjectValue {
@Override
public Iterator<Map.Entry<String, Value>> iterator() {
return wrapped.iterator();
return wrapped.get().iterator();
}
@Override
public Value get(String key) {
return wrapped.get(key);
return wrapped.get().get(key);
}
@Override
public Value getOrDefault(String key, Value defaultValue) {
return wrapped.getOrDefault(key, defaultValue);
return wrapped.get().getOrDefault(key, defaultValue);
}
@Override
public boolean has(String key) {
return wrapped.has(key);
return wrapped.get().has(key);
}
@Override
public int size() {
return wrapped.size();
return wrapped.get().size();
}
@Override

View File

@@ -0,0 +1,18 @@
package net.woggioni.wson.wcfg;
import org.antlr.v4.runtime.BaseErrorListener;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
class ErrorHandler extends BaseErrorListener {
@Override
public void syntaxError(Recognizer<?, ?> recognizer,
Object offendingSymbol,
int line, int charPositionInLine,
String msg,
RecognitionException e) {
throw new SyntaxError(msg, line, charPositionInLine);
}
}

View File

@@ -1,6 +1,9 @@
package net.woggioni.wson.wcfg;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import net.woggioni.jwo.JWO;
import net.woggioni.wson.value.ArrayValue;
import net.woggioni.wson.value.BooleanValue;
import net.woggioni.wson.value.FloatValue;
@@ -10,27 +13,45 @@ import net.woggioni.wson.value.ObjectValue;
import net.woggioni.wson.value.StringValue;
import net.woggioni.wson.xface.Value;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.tree.ErrorNode;
import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import static net.woggioni.jwo.JWO.dynamicCast;
import static net.woggioni.jwo.JWO.newThrowable;
import static net.woggioni.jwo.JWO.tail;
class ListenerImpl implements WCFGListener {
private final Value.Configuration cfg;
@Getter
private final Value result;
private Value result;
private final List<ValueHolder> holders = new ArrayList<>();
private final Map<String, ValueHolder> unresolvedReferences = new TreeMap<>();
private interface StackLevel {
Value getValue();
}
@RequiredArgsConstructor
private static class ExportStackLevel implements StackLevel {
@Setter
private Value value;
@Override
public Value getValue() {
return value;
}
}
private static class ArrayStackLevel implements StackLevel {
private final ArrayValue value = new ArrayValue();
@@ -57,7 +78,7 @@ class ListenerImpl implements WCFGListener {
private static class ExpressionStackLevel implements StackLevel {
private final Value.Configuration cfg;
private ObjectValue value = null;
public List<ObjectValue> elements = new ArrayList<>();
public List<Value> elements = new ArrayList<>();
public ExpressionStackLevel(Value.Configuration cfg) {
this.cfg = cfg;
@@ -65,8 +86,19 @@ class ListenerImpl implements WCFGListener {
@Override
public Value getValue() {
if(value == null) {
value = new CompositeObjectValue(elements, cfg);
if (value == null) {
List<ObjectValue> objects = elements.stream()
.map(it -> {
if (it instanceof ObjectValue ov)
return ov;
else if (it instanceof ValueHolder vh) {
return (ObjectValue) vh.getDelegate();
} else {
throw newThrowable(RuntimeException.class, "");
}
})
.collect(Collectors.toList());
value = new CompositeObjectValue(objects, cfg);
}
return value;
}
@@ -75,26 +107,37 @@ class ListenerImpl implements WCFGListener {
private final List<StackLevel> stack = new ArrayList<>();
private void add2Last(Value value) {
StackLevel last = stack.get(stack.size() - 1);
StackLevel last = tail(stack);
if (last instanceof ArrayStackLevel asl) {
asl.value.add(value);
if(value instanceof ValueHolder holder) {
if (value instanceof ValueHolder holder) {
Value arrayValue = asl.getValue();
int index = arrayValue.size() - 1;
holder.addDeleter(() -> arrayValue.set(index, holder.getDelegate()));
}
} else if (last instanceof ObjectStackLevel osl) {
String key = osl.currentKey;
osl.currentKey = null;
osl.value.put(key, value);
if(value instanceof ValueHolder holder) {
Value objectValue = osl.getValue();
holder.addDeleter(() -> objectValue.put(key, holder.getDelegate()));
Value objectValue = osl.getValue();
objectValue.put(key, value);
if (value instanceof ValueHolder holder) {
holder.addDeleter(() -> {
objectValue.put(key, holder.getDelegate());
});
}
} else if(last instanceof ExpressionStackLevel esl) {
esl.elements.add((ObjectValue) value);
} else if (last instanceof ExpressionStackLevel esl) {
List<Value> values = esl.elements;
int index = values.size();
values.add(value);
if (value instanceof ValueHolder holder) {
holder.addDeleter(() -> {
values.set(index, holder.getDelegate());
});
}
} else if (last instanceof ExportStackLevel esl) {
esl.setValue(value);
}
}
private static String unquote(String quoted) {
return quoted.substring(1, quoted.length() - 1);
}
@@ -121,6 +164,17 @@ class ListenerImpl implements WCFGListener {
@Override
public void exitWcfg(WCFGParser.WcfgContext ctx) {
stack.clear();
for (ValueHolder holder : unresolvedReferences.values()) {
if (holder.getDelegate() == null) {
TerminalNode node = holder.getNode();
throw new ParseError(
"Undeclared identifier '" + node.getText() + "'",
node.getSymbol().getLine(),
node.getSymbol().getCharPositionInLine() + 1
);
}
holder.replace();
}
}
@Override
@@ -128,15 +182,15 @@ class ListenerImpl implements WCFGListener {
ObjectStackLevel osl = (ObjectStackLevel) stack.get(0);
String key = ctx.IDENTIFIER().getText();
osl.currentKey = key;
ValueHolder holder = new ValueHolder();
holders.add(holder);
holder.addDeleter(() -> result.put(key, holder.getDelegate()));
result.put(key, holder);
ValueHolder holder = unresolvedReferences.computeIfAbsent(key, it -> new ValueHolder(ctx.IDENTIFIER()));
// holder.addDeleter(() -> holder.setDelegate(result.get(key)));
}
@Override
public void exitAssignment(WCFGParser.AssignmentContext ctx) {
ObjectStackLevel osl = (ObjectStackLevel) stack.get(0);
String key = osl.currentKey;
unresolvedReferences.get(key).setDelegate(osl.getValue().get(key));
osl.currentKey = null;
}
@@ -144,6 +198,16 @@ class ListenerImpl implements WCFGListener {
public void enterExpression(WCFGParser.ExpressionContext ctx) {
ExpressionStackLevel esl = new ExpressionStackLevel(cfg);
stack.add(esl);
for(TerminalNode node : ctx.IDENTIFIER()) {
String key = node.getSymbol().getText();
ValueHolder holder = unresolvedReferences
.computeIfAbsent(key, k -> new ValueHolder(node));
int index = esl.elements.size();
esl.elements.add(holder);
holder.addDeleter(() -> {
esl.elements.set(index, holder.getDelegate());
});
}
}
@Override
@@ -200,16 +264,25 @@ class ListenerImpl implements WCFGListener {
} else {
add2Last(new FloatValue(Double.parseDouble(text)));
}
} else if(ctx.IDENTIFIER() != null) {
} else if (ctx.IDENTIFIER() != null) {
String name = ctx.IDENTIFIER().getText();
Value referredValue = result.getOrDefault(name, null);
if(referredValue == null) {
throw new ParseError(
"Undeclared identifier '" + name + "'",
ctx.start.getLine(),
ctx.start.getCharPositionInLine() + 1
);
}
ValueHolder referredValue = unresolvedReferences.computeIfAbsent(name,
it -> new ValueHolder(ctx.IDENTIFIER())
);
// referredValue.addError(() -> new ParseError(
// "Undeclared identifier '" + name + "'",
// ctx.start.getLine(),
// ctx.start.getCharPositionInLine() + 1
// )
// );
// Value referredValue = result.getOrDefault(name, null);
// if(referredValue == null) {
// throw new ParseError(
// "Undeclared identifier '" + name + "'",
// ctx.start.getLine(),
// ctx.start.getCharPositionInLine() + 1
// );
// }
add2Last(referredValue);
}
}
@@ -238,9 +311,19 @@ class ListenerImpl implements WCFGListener {
}
public void replaceHolders() {
for(ValueHolder holder : holders) {
holder.replace();
@Override
public void enterExport(WCFGParser.ExportContext ctx) {
ExportStackLevel esl = new ExportStackLevel();
stack.add(esl);
}
@Override
public void exitExport(WCFGParser.ExportContext ctx) {
result = pop().getValue();
if (result instanceof ValueHolder holder) {
holder.addDeleter(() -> {
result = holder.getDelegate();
});
}
}
}

View File

@@ -0,0 +1,15 @@
package net.woggioni.wson.wcfg;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class SyntaxError extends RuntimeException {
public SyntaxError(String message, int line, int column) {
super(message + String.format(" at %d:%d", line, column));
}
@Override
public String getMessage() {
return super.getMessage();
}
}

View File

@@ -1,23 +1,32 @@
package net.woggioni.wson.wcfg;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import net.woggioni.wson.xface.Value;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.tree.TerminalNode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
@RequiredArgsConstructor
class ValueHolder implements Value {
private List<Runnable> deleters = new ArrayList<>();
@Getter
private final TerminalNode node;
private List<Runnable> deleters = new ArrayList<>();
public void addDeleter(Runnable runnable) {
deleters.add(runnable);
}
@Setter
@Getter
private Value delegate = Value.Null;
private Value delegate = null;
@Override
public Type type() {
return delegate.type();

View File

@@ -18,12 +18,14 @@ public class WConfig {
private final Value value;
@SneakyThrows
public WConfig(Reader reader, Value.Configuration cfg) {
private WConfig(Reader reader, Value.Configuration cfg) {
this.cfg = cfg;
CodePointCharStream inputStream = CharStreams.fromReader(reader);
WCFGLexer lexer = new WCFGLexer(inputStream);
CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
WCFGParser parser = new WCFGParser(commonTokenStream);
parser.removeErrorListeners();
parser.addErrorListener(new ErrorHandler());
ListenerImpl listener = new ListenerImpl(cfg);
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(listener, parser.wcfg());
@@ -44,4 +46,8 @@ public class WConfig {
public Value whole() {
return value;
}
public static WConfig parse(Reader reader, Value.Configuration cfg) {
return new WConfig(reader, cfg);
}
}

View File

@@ -2,6 +2,7 @@ package net.woggioni.wson.wcfg;
import lombok.SneakyThrows;
import net.woggioni.wson.serialization.json.JSONDumper;
import net.woggioni.wson.value.ObjectValue;
import net.woggioni.wson.xface.Value;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
@@ -23,24 +24,21 @@ public class ParseTest {
@SneakyThrows
@ParameterizedTest
@ValueSource(strings = {
// "build.wcfg",
// "test.wcfg",
// "recursive.wcfg",
"build.wcfg",
"test.wcfg",
"recursive.wcfg",
"recursive2.wcfg",
"recursive3.wcfg",
})
public void test(String resource) {
Value.Configuration cfg = Value.Configuration.builder()
.objectValueImplementation(ObjectValue.Implementation.HashMap)
.serializeReferences(true)
.build();
try(Reader reader = new InputStreamReader(getClass().getClassLoader().getResourceAsStream(resource))) {
CodePointCharStream inputStream = CharStreams.fromReader(reader);
WCFGLexer lexer = new WCFGLexer(inputStream);
CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
WCFGParser parser = new WCFGParser(commonTokenStream);
Value.Configuration cfg = Value.Configuration.builder().serializeReferences(true).build();
ListenerImpl listener = new ListenerImpl(cfg);
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(listener, parser.wcfg());
listener.replaceHolders();
Value result = listener.getResult();
new JSONDumper(cfg).dump(result, System.out);
WConfig wcfg = WConfig.parse(reader, cfg);
new JSONDumper(cfg).dump(wcfg.whole(), System.out);
System.out.println();
}
}
@@ -49,8 +47,8 @@ public class ParseTest {
public void test2() {
Value.Configuration cfg = Value.Configuration.builder().serializeReferences(true).build();
try (Reader reader = new InputStreamReader(getClass().getResourceAsStream("/build.wcfg"))) {
WConfig wConfig = new WConfig(reader, cfg);
Value result = wConfig.get("release", "dev");
WConfig wcfg = WConfig.parse(reader, cfg);
Value result = wcfg.get("release", "dev");
try (OutputStream os = new BufferedOutputStream(new FileOutputStream("/tmp/build.json"))) {
new JSONDumper(cfg).dump(result, os);
}

View File

@@ -0,0 +1,5 @@
value := [1, 2, value2, null, false];
value2 := [3, 4, value, null, true, value3];
value3 := {"key" : "Some string" };

View File

@@ -20,4 +20,6 @@ foobar := {
} << {
"enabled" : true
}
};
};
export default << foobar;