Skip to content

Commit 86c2b75

Browse files
committed
Normalize legacy YSM Molang syntax
Translate one-armed ternaries, logical negation, parenthesized unary minus, scalar assignments, and trailing decimals so exported YSM expressions compile without altering their runtime meaning.
1 parent 6ab5b6d commit 86c2b75

2 files changed

Lines changed: 155 additions & 1 deletion

File tree

src/main/java/com/ysm/modelengine/molang/MolangCompilerFacade.java

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import gg.moonflower.molangcompiler.api.MolangExpression;
66
import gg.moonflower.molangcompiler.api.exception.MolangException;
77

8+
import java.util.ArrayList;
9+
import java.util.List;
810
import java.util.Locale;
911
import java.util.Map;
1012
import java.util.concurrent.ConcurrentHashMap;
@@ -18,6 +20,12 @@ final class MolangCompilerFacade {
1820
"(?i)\\bysm\\.second_order\\(\\s*'([^']*)'\\s*,");
1921
private static final Pattern BONE_ROTATION_CALL = Pattern.compile(
2022
"(?i)\\bysm\\.bone_rot\\(\\s*'([^']*)'\\s*\\)\\s*\\.\\s*([xyz])\\b");
23+
private static final Pattern LEADING_ASSIGNMENT = Pattern.compile(
24+
"(?i)^\\s*(?:v|variable|temp)\\.[a-z_][a-z0-9_]*\\s*=\\s*(?!=)");
25+
private static final Pattern LOGICAL_NOT_IDENTIFIER = Pattern.compile(
26+
"(?i)!(?!=)\\s*((?:[a-z_][a-z0-9_]*\\.)*[a-z_][a-z0-9_]*)");
27+
private static final Pattern TRAILING_DECIMAL = Pattern.compile(
28+
"(?<![a-z0-9_.])(\\d+)\\.(?!\\d)", Pattern.CASE_INSENSITIVE);
2129

2230
private final MolangCompiler compiler = GlobalMolangCompiler.get();
2331
private final Map<String, MolangExpression> cache = new ConcurrentHashMap<>();
@@ -31,7 +39,7 @@ final class MolangCompilerFacade {
3139
}
3240

3341
MolangExpression compile(String source) {
34-
String normalized = rewriteDynamicFunctionArguments(normalize(source));
42+
String normalized = rewriteLegacySyntax(rewriteDynamicFunctionArguments(normalize(source)));
3543
if (normalized.isBlank()) {
3644
throw new IllegalArgumentException("Molang expression cannot be blank");
3745
}
@@ -98,6 +106,127 @@ static String normalize(String source) {
98106
return output.toString();
99107
}
100108

109+
private static String rewriteLegacySyntax(String source) {
110+
String rewritten = LEADING_ASSIGNMENT.matcher(source).replaceFirst("");
111+
rewritten = TRAILING_DECIMAL.matcher(rewritten).replaceAll("$1.0");
112+
rewritten = rewriteLogicalNot(rewritten);
113+
rewritten = rewriteUnaryParenthesisNegation(rewritten);
114+
return addMissingTernaryFalseBranches(rewritten);
115+
}
116+
117+
private static String rewriteLogicalNot(String source) {
118+
Matcher identifier = LOGICAL_NOT_IDENTIFIER.matcher(source);
119+
StringBuffer output = new StringBuffer();
120+
while (identifier.find()) {
121+
identifier.appendReplacement(output,
122+
Matcher.quoteReplacement("(" + identifier.group(1) + "==0)"));
123+
}
124+
identifier.appendTail(output);
125+
126+
String rewritten = output.toString();
127+
for (int index = 0; index < rewritten.length(); index++) {
128+
if (rewritten.charAt(index) != '!' || index + 1 >= rewritten.length()
129+
|| rewritten.charAt(index + 1) == '=') continue;
130+
int open = nextNonWhitespace(rewritten, index + 1);
131+
if (open < 0 || rewritten.charAt(open) != '(') continue;
132+
int close = matchingParenthesis(rewritten, open);
133+
if (close < 0) continue;
134+
String operand = rewritten.substring(open, close + 1);
135+
rewritten = rewritten.substring(0, index) + "(" + operand + "==0)"
136+
+ rewritten.substring(close + 1);
137+
}
138+
return rewritten;
139+
}
140+
141+
private static String rewriteUnaryParenthesisNegation(String source) {
142+
StringBuilder output = new StringBuilder(source.length() + 8);
143+
for (int index = 0; index < source.length(); index++) {
144+
char character = source.charAt(index);
145+
if (character == '-') {
146+
int next = nextNonWhitespace(source, index + 1);
147+
int previous = previousNonWhitespace(source, index - 1);
148+
boolean unary = previous < 0 || "([{:?,=+-*/%<>!&|;".indexOf(source.charAt(previous)) >= 0;
149+
if (unary && next >= 0 && source.charAt(next) == '(') {
150+
output.append("-1*");
151+
continue;
152+
}
153+
}
154+
output.append(character);
155+
}
156+
return output.toString();
157+
}
158+
159+
private static String addMissingTernaryFalseBranches(String source) {
160+
List<Integer> questions = new ArrayList<>();
161+
for (int index = 0; index < source.length(); index++) {
162+
if (source.charAt(index) == '?') questions.add(index);
163+
}
164+
if (questions.isEmpty()) return source;
165+
166+
StringBuilder rewritten = new StringBuilder(source);
167+
for (int questionIndex = questions.size() - 1; questionIndex >= 0; questionIndex--) {
168+
int question = questions.get(questionIndex);
169+
int nestedTernaries = 0;
170+
int depth = 0;
171+
int boundary = rewritten.length();
172+
boolean hasFalseBranch = false;
173+
for (int index = question + 1; index < rewritten.length(); index++) {
174+
char character = rewritten.charAt(index);
175+
if (character == '(' || character == '[' || character == '{') {
176+
depth++;
177+
continue;
178+
}
179+
if (character == ')' || character == ']' || character == '}') {
180+
if (depth == 0) {
181+
boundary = index;
182+
break;
183+
}
184+
depth--;
185+
continue;
186+
}
187+
if (depth != 0) continue;
188+
if (character == '?') {
189+
nestedTernaries++;
190+
} else if (character == ':') {
191+
if (nestedTernaries == 0) {
192+
hasFalseBranch = true;
193+
break;
194+
}
195+
nestedTernaries--;
196+
} else if ((character == ',' || character == ';') && nestedTernaries == 0) {
197+
boundary = index;
198+
break;
199+
}
200+
}
201+
if (!hasFalseBranch) rewritten.insert(boundary, ":0");
202+
}
203+
return rewritten.toString();
204+
}
205+
206+
private static int nextNonWhitespace(String value, int start) {
207+
for (int index = Math.max(0, start); index < value.length(); index++) {
208+
if (!Character.isWhitespace(value.charAt(index))) return index;
209+
}
210+
return -1;
211+
}
212+
213+
private static int previousNonWhitespace(String value, int start) {
214+
for (int index = Math.min(start, value.length() - 1); index >= 0; index--) {
215+
if (!Character.isWhitespace(value.charAt(index))) return index;
216+
}
217+
return -1;
218+
}
219+
220+
private static int matchingParenthesis(String value, int open) {
221+
int depth = 0;
222+
for (int index = open; index < value.length(); index++) {
223+
char character = value.charAt(index);
224+
if (character == '(') depth++;
225+
else if (character == ')' && --depth == 0) return index;
226+
}
227+
return -1;
228+
}
229+
101230
private String rewriteDynamicFunctionArguments(String source) {
102231
Matcher secondOrder = SECOND_ORDER_CALL.matcher(source);
103232
StringBuffer output = new StringBuffer();

src/test/java/com/ysm/modelengine/molang/MolangCompilerFacadeTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ void rewritesYsmStringArgumentExtensionsBeforeCompilation() {
6060
));
6161
}
6262

63+
@Test
64+
void rewritesLegacyYsmSyntaxWithoutChangingResults() throws Exception {
65+
var falseEnvironment = MolangRuntime.runtime()
66+
.setVariable("bag", MolangExpression.of(0.0F))
67+
.setVariable("qh", MolangExpression.of(0.0F))
68+
.setQuery("ground_speed", MolangExpression.of(0.0F))
69+
.setQuery("head_x_rotation", MolangExpression.of(18.0F))
70+
.create();
71+
var trueEnvironment = MolangRuntime.runtime()
72+
.setVariable("bag", MolangExpression.of(1.0F))
73+
.setVariable("qh", MolangExpression.of(1.0F))
74+
.setQuery("ground_speed", MolangExpression.of(1.0F))
75+
.setQuery("head_x_rotation", MolangExpression.of(18.0F))
76+
.create();
77+
78+
assertEquals(1.0F, compiler.compile("!v.bag").get(falseEnvironment), 0.0001F);
79+
assertEquals(0.0F, compiler.compile("!v.bag").get(trueEnvironment), 0.0001F);
80+
assertEquals(0.0F, compiler.compile("v.qh?180").get(falseEnvironment), 0.0001F);
81+
assertEquals(180.0F, compiler.compile("v.qh?180").get(trueEnvironment), 0.0001F);
82+
assertEquals(-2.0F, compiler.compile("-(query.head_x_rotation/9)").get(trueEnvironment), 0.0001F);
83+
assertEquals(0.0F, compiler.compile("v.bv=q.ground_speed>0?5").get(falseEnvironment), 0.0001F);
84+
assertEquals(5.0F, compiler.compile("v.bv=q.ground_speed>0?5").get(trueEnvironment), 0.0001F);
85+
assertDoesNotThrow(() -> compiler.compile("7.-math.sin(160+q.anim_time*240)*0.5"));
86+
}
87+
6388
@Test
6489
void rejectsInvalidExpression() {
6590
assertThrows(IllegalArgumentException.class, () -> compiler.compile("query."));

0 commit comments

Comments
 (0)