-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Expressions as Countable #13218
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Expressions as Countable #13218
Conversation
…the "engine" character of that interface
; | ||
abstract fun isOK(strict: Boolean): Boolean | ||
companion object { | ||
operator fun invoke(bool: Boolean?) = when(bool) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please just call this 'from' without operator overloading.
When you're young it's nice to experiment, it's fine to be operator-curious, but I think we're both past that phase :)
And why, indeed, NOT use Keval? |
Already mentioned some reasons. Foremost maybe: the tokenizer isn't customizable when consumed as library and won't suit our needs. Well, maybe it is by bypassing their |
For once we have a need that is actually generic, and even for that we can't use a generic solution? |
I mean with getplaceholders + placeholdertext, generating a countable() version of the user text sounds simple to me |
Except it's done and already passes quite a load of unit tests |
That doesn't super help when what I'm trying to avoid is having to maintain this in the future. At times like this I have to ask myself, how did we get here? I thought I was making a strategy game, and now I need to maintain a tokenizer/parser for generic expressions? Most of all, this sounds like solving a solved problem. And if not? We should be making a new library. Or changing an existing one - make Keval accept parse/eval distinctions, make it accept parameters. I think the root of Keval's problems is that it doesn't make the parse/eval distinction (outside - inside it exists). That's why it only allows constants (there are no runtime parameters since there is no runtime separate from parsing). The author is active so this definitely seems doable to me. |
Another thing real parsers have is "where is the problem", start to finish. I see Keval has "position" which is a start (ba-dum tish). This is important since we need to tell the user "your problem is HERE--> <--" and that requires positions. Start position will let you insert a ❌ emoji at the location, which is Good Enough ™️ |
🤷 |
It's 2 in the morning here, maybe I'm overreacting. I'll see what I think of this tomorrow. Maybe we can treat this as just a fun coding exercise, in which case we can do what we want but modders should be aware this is breakable |
No I get it. Without checking it out and playing a bit with it (e.g. setting breakpoints in the unit tests and inspecting an AST) it would be hard to get a real feel for it. And the maintainability burden is of course very real, which is why it must be readable, and once adopted it should be possible to treat is as "black box", meaning it works, period, and changes in other parts of the project shouldn't be able to break it - or at least any cross-influence should be caught by the tests. In that, I'm pretty confident, though of course you're right about the "fun coding exercise", which may have clouded my vision. As for "where is the problem", yes that was an aspect of Keval I consciously dropped - partially. The tokenizer has the full info so it can point to character indices, but the AST - why should it maintain the original input? When the AST parser needs to complain, the token string should be enough as hint, and when something goes wrong at eval time, each Node can reconstruct a logical "view" of its part of the original expression, which should likewise be enough for messages. Keval kept the original expression so the AST parser would "consume" already tokenized tokens one by one, string matching something already matched previously. I deemed it unnecessary. The integration in the validator is so far rudimentary in the sense I have refrained from patching up the validator code itself, but if more detail is needed it's certainly possible. |
Otherwise, modders can't actually debug.
When did you guys write a parser by the way. The first time I saw the find-replace code for properties parser while writing my Number formatter pr, I was honestly like: what is this. Why do we not have an parser already if the usage of regex is pushed that far. But it is also true that writing a parser is way too much work and have way to many edge cases. Remember the time when I wrote a json parser myself that was actually in production for quite a bit of time and worked in 99% of the time. (The only exception I remember when user inputted strings inside json caused some unexpected issues) But anyways, when did you guys write an actual parser for this. And maybe write up a libray now naming it UncivProperties. Lol. The description will be something like this: |
This satisfies most of my basic requirements. All errors now indicate position, we can compile prior to evaluation so we can indicate parsing errors to users. However
|
BTW 2 *** 3 doesn't show as an error, that's a bug :) |
@touhidurrr There's a lot going on in that file for sure, but each part is self contained and there's no complex sequence. Unlike here, where there IS a complex sequence: Text to tokens, tokens to AST, AST to result, so to get any value at all out of this you need to go though all stages |
complex or not why not make it a package @GGGuenni. |
That's because this PR includes commit Add a way to mark not-yet-finished Countables which was offered as not-quite-serious element in Another Countables Pass - RFC. I couldn't base it on master 'cuz I needed commits 1 and 3 from that. As you said - a cute exercise in coding, fun, a demo "we could if we wanted to", but ultimately of minor use: The idea was to later make a feature available to users as opt-in and the opt-in would automatically expire unless the experiment progresses. As in, modders would be the ones to stress-test the feature, but would need to do so knowing we still expect trouble. Or somesuch vague idea. |
Yes very much so. Needs an Eureka. I'd prefer it on GameInfo even - pass a gameInfo even at parse time just for that? That was a 'code easy way now, forget to revisit later' thing. |
? This is not about translations at all, in fact, the problem of internationalized presentation is knowingly ignored/postponed |
How do you figure? Index: tests/src/com/unciv/uniques/ExpressionTests.kt
<+>UTF-8
===================================================================
diff --git a/tests/src/com/unciv/uniques/ExpressionTests.kt b/tests/src/com/unciv/uniques/ExpressionTests.kt
--- a/tests/src/com/unciv/uniques/ExpressionTests.kt (revision 442600c53f781e4c5f2d0447ad10dd1996d2e2cf)
+++ b/tests/src/com/unciv/uniques/ExpressionTests.kt (date 1744912221824)
@@ -55,6 +55,7 @@
@Test
fun testInvalidExpressions() {
val input = listOf(
+ "2 *** 3" to Parser.MissingOperand::class,
"fake_function(2)" to Parser.UnknownIdentifier::class,
"[fake countable]" to Parser.UnknownCountable::class,
"98.234.792.374" to Parser.InvalidConstant::class, ...passes. (Did so too before your improvements). BTW, good job extending the |
Maybe - warning: a demo not production code Index: core/src/com/unciv/models/ruleset/unique/expressions/Expressions.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/core/src/com/unciv/models/ruleset/unique/expressions/Expressions.kt b/core/src/com/unciv/models/ruleset/unique/expressions/Expressions.kt
--- a/core/src/com/unciv/models/ruleset/unique/expressions/Expressions.kt (revision 442600c53f781e4c5f2d0447ad10dd1996d2e2cf)
+++ b/core/src/com/unciv/models/ruleset/unique/expressions/Expressions.kt (date 1744912139184)
@@ -1,5 +1,6 @@
package com.unciv.models.ruleset.unique.expressions
+import com.unciv.UncivGame
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.unique.ICountable
import com.unciv.models.ruleset.unique.StateForConditionals
@@ -25,18 +26,64 @@
private data class ParseResult(val severity: ICountable.MatchResult, val node: Node?, val exception: Parser.ParsingError?)
companion object {
- private val cache: MutableMap<String, ParseResult> = mutableMapOf()
+ private val cache = ExpressionCache.getInstance(ExpressionCacheType.ClearingPerGameInfo)
+
+ private fun parse(parameterText: String, ruleset: Ruleset?): ParseResult = cache.getOrPut(parameterText, ruleset)
+
+ fun getParsingError(parameterText: String, ruleset: Ruleset?): Parser.ParsingError? =
+ parse(parameterText, ruleset).exception
+
+ }
- private fun parse(parameterText: String, ruleset: Ruleset?): ParseResult = cache.getOrPut(parameterText) {
- try {
- val node = Parser.parse(parameterText, ruleset)
- ParseResult(ICountable.MatchResult.Yes, node, null)
- } catch (ex: Parser.ParsingError) {
- ParseResult(ex.severity, null, ex)
+ private enum class ExpressionCacheType { Global, PerGameInfo, ClearingPerGameInfo}
+
+ private abstract class ExpressionCache {
+ abstract fun getOrPut(parameterText: String, ruleset: Ruleset?): ParseResult
+ companion object {
+ fun getInstance(type: ExpressionCacheType): ExpressionCache = when (type) {
+ ExpressionCacheType.Global -> ExpressionCacheGlobal()
+ ExpressionCacheType.PerGameInfo -> ExpressionCachePerGameInfo()
+ ExpressionCacheType.ClearingPerGameInfo -> ExpressionCacheClearingPerGameInfo()
+ }
+ }
+ fun MutableMap<String, ParseResult>.defaultGetOrPut(parameterText: String, ruleset: Ruleset?): ParseResult {
+ return getOrPut(parameterText) {
+ try {
+ val node = Parser.parse(parameterText, ruleset)
+ ParseResult(ICountable.MatchResult.Yes, node, null)
+ } catch (ex: Parser.ParsingError) {
+ ParseResult(ex.severity, null, ex)
+ }
}
}
-
- fun getParsingError(parameterText: String, ruleset: Ruleset?): Parser.ParsingError? =
- parse(parameterText, ruleset).exception
+ }
+
+ private class ExpressionCacheGlobal : ExpressionCache() {
+ private val cache = mutableMapOf<String, ParseResult>()
+ override fun getOrPut(parameterText: String, ruleset: Ruleset?) = cache.defaultGetOrPut(parameterText, ruleset)
+ }
+
+ private class ExpressionCachePerGameInfo : ExpressionCache() {
+ private val caches = LinkedHashMap<Int, MutableMap<String, ParseResult>>(1)
+
+ override fun getOrPut(parameterText: String, ruleset: Ruleset?): ParseResult {
+ val key = UncivGame.getGameInfoOrNull().hashCode()
+ val cache = caches.getOrPut(key) { mutableMapOf() }
+ return cache.defaultGetOrPut(parameterText, ruleset)
+ }
+ }
+
+ private class ExpressionCacheClearingPerGameInfo : ExpressionCache() {
+ private val cache = mutableMapOf<String, ParseResult>()
+ private var key = 0
+
+ override fun getOrPut(parameterText: String, ruleset: Ruleset?): ParseResult {
+ val newKey = UncivGame.getGameInfoOrNull().hashCode()
+ if (newKey != key) {
+ key = newKey
+ cache.clear()
+ }
+ return cache.defaultGetOrPut(parameterText, ruleset)
+ }
}
} |
Found the problem... This is because TL;DR, in the current setup there can be no "maybe this is an expression", since all "maybe" turns into "yes", so I'm removing that option entirely so we get user visibility on expression parsing for all failed countables |
Opps. I meant unique but said translations instead. Or maybe thought both are similar. How are we handling uniques currently by the way. More regex? Always wanted to contribute uniques code in Unciv. Where to start? |
@touhidurrr @SomeTroglodyte Instead, we should return to the way it used to work - with only booleans. Either it matches or it doesn't. So when should we actually validate that? Only when checking the unique parameters, at which point we DO have a ruleset.
This means that apart from eval() which gives a double, Node also needs to have another overridable function for validity checking which accepts a ruleset. This is DFS-y in nature which is super simple since you have numeric constant (true), unary (validity = operand.validity), binary (validity = left.validity && right.validity), and countable (here's where the magic happens). |
Now that I think of it this would also resolve the reservations I have with intra-ruleset caching |
Very true 👀
Doesn't
...and I introduced it in a PR named "RFC" because I fully expected the names might need to change. I introduced it so a Countable like FilteredStuff when presented with "[crap] Stuff" could say "well YES the pattern is one of mine BUT it's either badly parameterized or I can't check parameters right now". So.... The AST parser might need to postpone the error to eval time? Or would we need 4 levels and split the enum into "NotMine, MineButBad, OKButCantCkeckWithoutRuleset, Perfect"? No, was that why I chose to already pass the ruleset into the tokenizer? NO, that was purely to accomodate that 🤬 patternless TileResources. A conundrum.
Sounds about right as I envisioned it - except the TileResources thing.
Ah yes exactly, you nailed it. I can see more clearly now the rain is gone 🎶 .
DFS as in Distributed file system? Dynamic frequency scaling? ... Anyway, good thinking. For me to implement these ideas, however, you'd have to holler and/or beg, at the moment I'm more driven to clean up my "how I want to play" branch in another project or work on my unseen movies backlog. To think I only dove into an actual implementation 'cuz some outsider (in precisely that "other project") needled me into trying ChatGPT for my first time, then half-earnestly I tried to ask it about evaluators and after a few back and forths I recognized the code it presented verbatim... I should have resisted maybe. |
This can be "pending" potentially forever, don't change your plans |
Does. The AST stores a |
I mean "currently for countables in main branch". |
This pull request has conflicts, please resolve those before we can evaluate the pull request. |
...Not ChatGPT generated...